File Coverage

blib/lib/LWPx/TimedHTTP.pm
Criterion Covered Total %
statement 41 243 16.8
branch 2 144 1.3
condition 1 56 1.7
subroutine 12 16 75.0
pod 1 1 100.0
total 57 460 12.3


line stmt bran cond sub pod time code
1             package LWPx::TimedHTTP;
2              
3 1     1   90826 use strict;
  1         3  
  1         41  
4 1     1   8 use Carp;
  1         2  
  1         105  
5              
6             require LWP::Debug;
7             require HTTP::Response;
8             require HTTP::Status;
9             require Net::HTTP;
10 1     1   1212 use Time::HiRes qw(gettimeofday tv_interval);
  1         2058  
  1         5  
11              
12 1     1   206 use vars qw(@ISA @EXTRA_SOCK_OPTS $VERSION);
  1         2  
  1         1093  
13              
14             $VERSION = "1.8";
15              
16             =pod
17              
18             =head1 NAME
19              
20             LWPx::TimedHTTP - time the different stages of an HTTP request
21              
22             =head1 SYNOPSIS
23              
24             # do the work for you
25             use LWP::UserAgent;
26             use LWPx::TimedHTTP qw(:autoinstall);
27              
28             # now just continue as normal
29             my $ua = LWP::UserAgent->new;
30             my $response = $ua->get("http://thegestalt.org");
31             # ... with optional retrieving of metrics (in seconds)
32             printf "%f\n", $response->header('Client-Request-Connect-Time');
33              
34              
35             # or if you don't like magic going on in the background
36             use LWP::UserAgent;
37             use LWP::Protocol;
38             use LWPx::TimedHTTP;
39              
40             LWP::Protocol::implementor('http', 'LWPx::TimedHTTP');
41              
42             # or for https ....
43             LWP::Protocol::implementor('https', 'LWPx::TimedHTTP::https');
44              
45             my $ua = LWP::UserAgent->new;
46             my $response = $ua->get("http://thegestalt.org");
47             printf "%f\n", $response->header('Client-Request-Connect-Time');
48              
49             =head1 DESCRIPTION
50              
51             This module performs an HTTP request exactly the same
52             as B does normally except for the fact that it
53             times each stage of the request and then inserts the
54             results as header.
55              
56             It's useful for debugging where abouts in a connection slow downs
57             are occuring.
58              
59             =head1 METRICS
60              
61             All times returned are in seconds
62              
63             =head2 Client-Request-Dns-Time
64              
65             The time it took to do a DNS lookup on the host.
66              
67             B The value of this timing is NOT thread safe since it
68             has to smuggle the data back via a global variable.
69              
70             =head2 Client-Request-Connect-Time
71              
72             The time it took to connect to the remote server
73              
74             =head2 Client-Request-Transmit-Time
75              
76             The time it took to transmit the request
77              
78             =head2 Client-Response-Server-Time
79              
80             Time it took to respond to the request
81              
82             =head2 Client-Response-Receive-Time
83              
84             Time it took to get the data back
85              
86             =head1 AUTHOR
87              
88             Simon Wistow
89              
90             Based entirely on work by David Carter -
91             this module is a little light frobbing and some packaging of
92             code he posted to the libwww-perl mailing list in response to
93             one of my questions.
94              
95             His code was, in turn, based on B by
96             Gisle Aas which is distributed as part of the B package.
97              
98             =head1 COPYING
99              
100             (c)opyright 2002, Simon Wistow
101              
102             Distributed under the same terms as Perl itself.
103              
104             This software is under no warranty and will probably ruin your life, kill your friends, burn your house and bring about the apocalypse
105              
106             =head1 BUGS
107              
108             None known
109              
110             =head1 SEE ALSO
111              
112             L, L
113              
114             =cut
115              
116              
117             sub import {
118 1     1   12 my $class = shift;
119 1   50     5 my $command = shift || return;
120              
121 1 50       5 croak "No such option '$command'\n" unless $command eq ':autoinstall';
122 1         2 eval { require LWP::Protocol };
  1         4  
123 1 50       4 croak "Requiring of LWP::Protocol failed - $@" if $@;
124              
125 1         81 LWP::Protocol::implementor('http', __PACKAGE__);
126 1         14 LWP::Protocol::implementor('https', "LWPx::TimedHTTP::https");
127              
128             }
129              
130              
131             require LWP::Protocol::http;
132             @ISA = qw(LWP::Protocol::http);
133              
134             my $CRLF = "\015\012";
135              
136             sub request
137             {
138 0     0 1   my($self, $request, $proxy, $arg, $size, $timeout) = @_;
139              
140 0   0       $size ||= 4096;
141              
142             # check method
143 0           my $method = $request->method;
144 0 0         unless ($method =~ /^[A-Za-z0-9_!\#\$%&\'*+\-.^\`|~]+$/) { # HTTP token
145 0           return HTTP::Response->new( &HTTP::Status::RC_BAD_REQUEST,
146             'Library does not allow method ' .
147             "$method for 'http:' URLs");
148             }
149              
150 0           my $url = $request->uri;
151 0           my($host, $port, $fullpath);
152              
153             # Check if we're proxy'ing
154 0 0         if (defined $proxy) {
155             # $proxy is an URL to an HTTP server which will proxy this request
156 0           $host = $proxy->host;
157 0           $port = $proxy->port;
158 0 0         $fullpath = $method eq "CONNECT" ?
159             ($url->host . ":" . $url->port) :
160             $url->as_string;
161             }
162             else {
163 0           $host = $url->host;
164 0           $port = $url->port;
165 0           $fullpath = $url->path_query;
166 0 0         $fullpath = "/$fullpath" unless $fullpath =~ m,^/,;
167             }
168            
169 0           my $prev_time = [gettimeofday];
170 0           my $this_time;
171              
172             # connect to remote site
173 0           my $socket = $self->_new_socket($host, $port, $timeout);
174              
175 0           $this_time = [gettimeofday];
176              
177 0           my $http_version = "";
178 0 0         if (my $proto = $request->protocol) {
179 0 0         if ($proto =~ /^(?:HTTP\/)?(1.\d+)$/) {
180 0           $http_version = $1;
181 0           $socket->http_version($http_version);
182 0 0         $socket->send_te(0) if $http_version eq "1.0";
183             }
184             }
185              
186 0           $self->_check_sock($request, $socket);
187              
188 0           my @h;
189 0           my $request_headers = $request->headers->clone;
190 0           $self->_fixup_header($request_headers, $url, $proxy);
191              
192             $request_headers->scan(sub {
193 0     0     my($k, $v) = @_;
194 0           $k =~ s/^://;
195 0           $v =~ s/\n/ /g;
196 0           push(@h, $k, $v);
197 0           });
198              
199 0           my $content_ref = $request->content_ref;
200 0 0         $content_ref = $$content_ref if ref($$content_ref);
201 0           my $chunked;
202             my $has_content;
203              
204 0 0         if (ref($content_ref) eq 'CODE') {
205 0           my $clen = $request_headers->header('Content-Length');
206 0 0         $has_content++ if $clen;
207 0 0         unless (defined $clen) {
208 0           push(@h, "Transfer-Encoding" => "chunked");
209 0           $has_content++;
210 0           $chunked++;
211             }
212             }
213             else {
214             # Set (or override) Content-Length header
215 0           my $clen = $request_headers->header('Content-Length');
216 0 0 0       if (defined($$content_ref) && length($$content_ref)) {
    0          
217 0           $has_content = length($$content_ref);
218 0 0 0       if (!defined($clen) || $clen ne $has_content) {
219 0 0         if (defined $clen) {
220 0           warn "Content-Length header value was wrong, fixed";
221 0           hlist_remove(\@h, 'Content-Length');
222             }
223 0           push(@h, 'Content-Length' => $has_content);
224             }
225             }
226             elsif ($clen) {
227 0           warn "Content-Length set when there is no content, fixed";
228 0           hlist_remove(\@h, 'Content-Length');
229             }
230             }
231              
232 0           my $write_wait = 0;
233 0 0 0       $write_wait = 2
234             if ($request_headers->header("Expect") || "") =~ /100-continue/;
235              
236 0           my $req_buf = $socket->format_request($method, $fullpath, @h);
237             #print "------\n$req_buf\n------\n";
238              
239 0 0 0       if (!$has_content || $write_wait || $has_content > 8*1024) {
      0        
240 0           WRITE:
241             {
242             # Since this just writes out the header block it should almost
243             # always succeed to send the whole buffer in a single write call.
244 0           my $n = $socket->syswrite($req_buf, length($req_buf));
245 0 0         unless (defined $n) {
246 1 0   1   1214 redo WRITE if $!{EINTR};
  1         1850  
  1         2144  
  0            
247 0 0         if ($!{EAGAIN}) {
248 0           select(undef, undef, undef, 0.1);
249 0           redo WRITE;
250             }
251 0           die "write failed: $!";
252             }
253 0 0         if ($n) {
254 0           substr($req_buf, 0, $n, "");
255             }
256             else {
257 0           select(undef, undef, undef, 0.5);
258             }
259 0 0         redo WRITE if length $req_buf;
260             }
261             }
262              
263 0           my($code, $mess, @junk);
264 0           my $drop_connection;
265              
266 0 0         if ($has_content) {
267 0           my $eof;
268             my $wbuf;
269 0           my $woffset = 0;
270             INITIAL_READ:
271 0 0         if ($write_wait) {
    0          
272             # skip filling $wbuf when waiting for 100-continue
273             # because if the response is a redirect or auth required
274             # the request will be cloned and there is no way
275             # to reset the input stream
276             # return here via the label after the 100-continue is read
277             }
278             elsif (ref($content_ref) eq 'CODE') {
279 0           my $buf = &$content_ref();
280 0 0         $buf = "" unless defined($buf);
281 0 0         $buf = sprintf "%x%s%s%s", length($buf), $CRLF, $buf, $CRLF
282             if $chunked;
283 0 0         substr($buf, 0, 0) = $req_buf if $req_buf;
284 0           $wbuf = \$buf;
285             }
286             else {
287 0 0         if ($req_buf) {
288 0           my $buf = $req_buf . $$content_ref;
289 0           $wbuf = \$buf;
290             }
291             else {
292 0           $wbuf = $content_ref;
293             }
294 0           $eof = 1;
295             }
296              
297 0           my $fbits = '';
298 0           vec($fbits, fileno($socket), 1) = 1;
299              
300             WRITE:
301 0   0       while ($write_wait || $woffset < length($$wbuf)) {
302              
303 0           my $sel_timeout = $timeout;
304 0 0         if ($write_wait) {
305 0 0         $sel_timeout = $write_wait if $write_wait < $sel_timeout;
306             }
307 0           my $time_before;
308 0 0         $time_before = time if $sel_timeout;
309              
310 0           my $rbits = $fbits;
311 0 0         my $wbits = $write_wait ? undef : $fbits;
312 0           my $sel_timeout_before = $sel_timeout;
313 0           SELECT:
314             {
315 0           my $nfound = select($rbits, $wbits, undef, $sel_timeout);
316 0 0         if ($nfound < 0) {
317 0 0 0       if ($!{EINTR} || $!{EAGAIN}) {
318 0 0         if ($time_before) {
319 0           $sel_timeout = $sel_timeout_before - (time - $time_before);
320 0 0         $sel_timeout = 0 if $sel_timeout < 0;
321             }
322 0           redo SELECT;
323             }
324 0           die "select failed: $!";
325             }
326             }
327              
328 0 0         if ($write_wait) {
329 0           $write_wait -= time - $time_before;
330 0 0         $write_wait = 0 if $write_wait < 0;
331             }
332              
333 0 0 0       if (defined($rbits) && $rbits =~ /[^\0]/) {
334             # readable
335 0           my $buf = $socket->_rbuf;
336 0           my $n = $socket->sysread($buf, 1024, length($buf));
337 0 0         unless (defined $n) {
    0          
338 0 0 0       die "read failed: $!" unless $!{EINTR} || $!{EAGAIN};
339             # if we get here the rest of the block will do nothing
340             # and we will retry the read on the next round
341             }
342             elsif ($n == 0) {
343             # the server closed the connection before we finished
344             # writing all the request content. No need to write any more.
345 0           $drop_connection++;
346 0           last WRITE;
347             }
348 0           $socket->_rbuf($buf);
349 0 0 0       if (!$code && $buf =~ /\015?\012\015?\012/) {
350             # a whole response header is present, so we can read it without blocking
351 0           ($code, $mess, @h) = $socket->read_response_headers(laxed => 1,
352             junk_out => \@junk,
353             );
354 0 0         if ($code eq "100") {
355 0           $write_wait = 0;
356 0           undef($code);
357 0           goto INITIAL_READ;
358             }
359             else {
360 0           $drop_connection++;
361 0           last WRITE;
362             # XXX should perhaps try to abort write in a nice way too
363             }
364             }
365             }
366 0 0 0       if (defined($wbits) && $wbits =~ /[^\0]/) {
367 0           my $n = $socket->syswrite($$wbuf, length($$wbuf), $woffset);
368 0 0         unless (defined $n) {
    0          
369 0 0 0       die "write failed: $!" unless $!{EINTR} || $!{EAGAIN};
370 0           $n = 0; # will retry write on the next round
371             }
372             elsif ($n == 0) {
373 0           die "write failed: no bytes written";
374             }
375 0           $woffset += $n;
376              
377 0 0 0       if (!$eof && $woffset >= length($$wbuf)) {
378             # need to refill buffer from $content_ref code
379 0           my $buf = &$content_ref();
380 0 0         $buf = "" unless defined($buf);
381 0 0         $eof++ unless length($buf);
382 0 0         $buf = sprintf "%x%s%s%s", length($buf), $CRLF, $buf, $CRLF
383             if $chunked;
384 0           $wbuf = \$buf;
385 0           $woffset = 0;
386             }
387             }
388             } # WRITE
389             }
390              
391 0 0         ($code, $mess, @h) = $socket->read_response_headers(laxed => 1, junk_out => \@junk)
392             unless $code;
393 0 0         ($code, $mess, @h) = $socket->read_response_headers(laxed => 1, junk_out => \@junk)
394             if $code eq "100";
395              
396 0           my $response = HTTP::Response->new($code, $mess);
397 0           my $peer_http_version = $socket->peer_http_version;
398 0           $response->protocol("HTTP/$peer_http_version");
399             {
400 0           local $HTTP::Headers::TRANSLATE_UNDERSCORE;
  0            
401 0           $response->push_header(@h);
402             }
403 0 0         $response->push_header("Client-Junk" => \@junk) if @junk;
404            
405             # store the leftover info from the connect (had to wait until we had a response. . .)
406 0           $response->push_header($_, $LWPx::TimedHTTP::Socket::timings{$_}) for keys %LWPx::TimedHTTP::Socket::timings;
407 0           $response->push_header('Client-Request-Connect-Time', tv_interval($prev_time, $this_time));
408 0           $prev_time = $this_time;
409 0           $this_time = [gettimeofday];
410 0           $response->push_header('Client-Request-Transmit-Time', tv_interval($prev_time, $this_time));
411 0           $prev_time = $this_time;
412              
413 0           $response->request($request);
414 0           $self->_get_sock_info($response, $socket);
415              
416 0 0         if ($method eq "CONNECT") {
417 0           $response->{client_socket} = $socket; # so it can be picked up
418 0           return $response;
419             }
420              
421 0 0         if (my @te = $response->remove_header('Transfer-Encoding')) {
422 0           $response->push_header('Client-Transfer-Encoding', \@te);
423             }
424 0           $response->push_header('Client-Response-Num', scalar $socket->increment_response_count);
425              
426 0           my $complete;
427             $response = $self->collect($arg, $response, sub {
428 0     0     my $buf = ""; #prevent use of uninitialized value in SSLeay.xs
429 0           my $n;
430 0           READ:
431             {
432 0           $n = $socket->read_entity_body($buf, $size);
433 0 0         unless (defined $n) {
434 0 0 0       redo READ if $!{EINTR} || $!{EAGAIN};
435 0           die "read failed: $!";
436             }
437 0 0         if (! defined $response->header('Client-Response-Server-Time') ) {
438 0           $this_time = [gettimeofday];
439 0           $response->push_header('Client-Response-Server-Time', tv_interval($prev_time, $this_time));
440 0           $prev_time = $this_time;
441             }
442 0 0         redo READ if $n == -1;
443             }
444 0 0         $complete++ if !$n;
445 0           return \$buf;
446 0           } );
447              
448 0           $this_time = [gettimeofday];
449 0           $response->push_header('Client-Response-Receive-Time', tv_interval($prev_time, $this_time));
450 0 0         $drop_connection++ unless $complete;
451            
452 0           @h = $socket->get_trailers;
453 0 0         if (@h) {
454 0           local $HTTP::Headers::TRANSLATE_UNDERSCORE;
455 0           $response->push_header(@h);
456             }
457              
458             # keep-alive support
459 0 0         unless ($drop_connection) {
460 0 0         if (my $conn_cache = $self->{ua}{conn_cache}) {
461 0   0       my %connection = map { (lc($_) => 1) }
  0            
462             split(/\s*,\s*/, ($response->header("Connection") || ""));
463 0 0 0       if (($peer_http_version eq "1.1" && !$connection{close}) ||
      0        
464             $connection{"keep-alive"})
465             {
466 0           $conn_cache->deposit($self->socket_type, "$host:$port", $socket);
467             }
468             }
469             }
470              
471 0           $response;
472             }
473              
474             #-----------------------------------------------------------
475             package LWPx::TimedHTTP::Socket;
476 1     1   10 use vars qw(@ISA);
  1         2  
  1         77  
477             @ISA = qw(LWP::Protocol::http::SocketMethods Net::HTTP);
478 1     1   1077 use IO::Socket;
  1         25026  
  1         5  
479 1     1   684 use Socket;
  1         2  
  1         1105  
480 1     1   6 use Time::HiRes qw(gettimeofday tv_interval);
  1         2  
  1         10  
481              
482             our %timings;
483              
484             sub _get_addr {
485 0     0     my($sock,$addr_str, $multi) = @_;
486 0           my @addr;
487 0           my $prev_time = [gettimeofday];
488 0 0 0       if ($multi && $addr_str !~ /^\d+(?:\.\d+){3}$/) {
489 0           (undef, undef, undef, undef, @addr) = gethostbyname($addr_str);
490             } else {
491 0           my $h = inet_aton($addr_str);
492 0 0         push(@addr, $h) if defined $h;
493             }
494 0           my $this_time = [gettimeofday];
495 0           $timings{'Client-Request-Dns-Time'} = tv_interval($prev_time, $this_time);
496 0           @addr;
497             }
498              
499             package LWPx::TimedHTTP::https;
500             eval { require LWP::Protocol::https };
501 1     1   327 use vars qw(@ISA);
  1         1  
  1         65  
502             @ISA = qw(LWPx::TimedHTTP);
503              
504             package LWPx::TimedHTTP::https::Socket;
505 1     1   4 use vars qw(@ISA);
  1         9  
  1         62  
506             @ISA = qw(LWP::Protocol::https::Socket);
507              
508              
509              
510             1;