瀏覽代碼

add more advanced server-timing goodies and actually stream ouptut

George Baugh 1 年之前
父節點
當前提交
d96178010a
共有 1 個文件被更改,包括 93 次插入 和 24 次删除
  1. 93 24
      lib/TPSGI.pm

+ 93 - 24
lib/TPSGI.pm

@@ -14,7 +14,7 @@ use POSIX();
 use Mojo::File;
 use Plack::MIME;
 use IO::Compress::Gzip;
-use Time::HiRes qw{tv_interval};
+use Time::HiRes qw{usleep gettimeofday tv_interval};
 
 # For CGI features
 use HTTP::Parser::XS qw{HEADERS_AS_HASHREF};
@@ -26,7 +26,6 @@ use File::Find;
 use Sys::Hostname();
 use Plack::MIME  ();
 use DateTime::Format::HTTP();
-use Time::HiRes      qw{gettimeofday tv_interval};
 
 use File::Touch;
 use File::Path;
@@ -559,7 +558,7 @@ sub route {
     my $content_type = $env->{CONTENT_TYPE};
     # It is the responsibility of each route to respond to HEAD requests correctly
     return $self->badrequest($self->{cur_query}) unless List::Util::any { $_ eq $env->{REQUEST_METHOD} } ('HEAD', $route->{method});
-    
+
     my $callback;
     if (exists $route->{callbacks}{'*'}) {
         $callback = $route->{callbacks}{'*'};
@@ -584,6 +583,7 @@ sub route {
     my @ranges = parse_ranges($env);
     $query->{ranges}       = \@ranges;
     $query->{start}        = $start;
+    # Put things to tv_interval in here for Server-Timing
     $query->{fullpath}     = $fullpath;
     $query->{method}       = $route->{method};
 
@@ -668,27 +668,73 @@ sub cgi {
 }
 
 sub stream_raw_http {
-    my ($self, $output, $last_fetch, $callback) = @_;
+    my ($self, $data, $output, $last_fetch, $to_fork, $callback) = @_;
+
+    open(my $out, '+<', undef) or die "I couldn't open anon filehandle, which can only happen if you don't compile perl with PerlIO";
+    $out->autoflush(1);
+    print $out $output;
+    my $pid = _fork($to_fork, $out);
+
+    # If we can't emit headers within ~250ms something's wrong
+    my $timeout = 250 * 1000;
+
+    my ($code,%headers);
+
+    for (0..$timeout) {
+        seek($out, 0,0);
+        ($code, %headers) = extract_headers($out, $last_fetch, 1);
+        usleep 1 unless %headers;
+        last if %headers;
+    }
+    die "Failed to emit headers within timeout" unless %headers;
+
+    # Append server-timing headers
+    $self->checkpoint($data, 'emitHeader');
+    $headers{'Server-Timing'} = $self->build_server_timing($data);
 
-    open(my $out, '<', \$output) or die "I couldn't open a string as a filehandle, which can only happen if you don't compile perl with PerlIO";
-    my ($code, %headers) = extract_headers($out, $last_fetch, 1);
     $code //= 500;
 
     $self->INFO("$self->{cur_query}{method} $code $self->{cur_query}{fullpath}");
+
+    # If the page doesn't load within 30 seconds, don't load it
+    $timeout = 3000000;
     return sub {
         my $responder = shift;
 
         my $writer = $responder->( [ $code, [%headers] ] );
-        while ( $out->read( my $buf, $CHUNK_SIZE ) ) {
+        my $SEEK_SET = $out->tell();
+        for (0..$timeout) {
+            $out->seek($SEEK_SET, 0);
+            $out->read( my $buf, $CHUNK_SIZE );
             $writer->write($buf);
+            #If we read anything, advance the pointer
+            $SEEK_SET = $out->tell() if $buf;
+            last if waitpid($pid, 1) > 0;
+            usleep 1000;
         }
         $writer->close;
         close($out);
+        waitpid($pid, 0);
+        $self->INFO("Response written in ".(tv_interval($data->{start}))." s");
         # Wait on the script to do whatever it's doing after closing stdout
         $callback->() if ref $callback eq 'CODE';
     };
 }
 
+sub _fork {
+    my ($callback, $fh) = @_;
+    select $fh;
+    $|=1;
+    my $pid = fork();
+    die "Could not fork child" unless defined $pid;
+    if ($pid == 0) {
+        $callback->();
+        exit 0;
+    }
+    select STDOUT;
+    return $pid;
+}
+
 =head2 extract_headers($fh, $last_fetch)
 
 Extract the headers from a filehandle.  Set the 'Last Modified' header appropriately based on the output of `stat` on the filehandle.
@@ -770,24 +816,47 @@ sub get_config {
         user       => '',
     );
     $ENV{HOME} ||= Cwd::getcwd();
-   	my $config_file = "$ENV{HOME}/.tpsgi.ini";
-	if (-f $config_file) {
-		my $conf = Config::Simple->new($config_file);
-		my %config;
-		%config = %{$conf->param(-block => 'default')} if $conf;
-
-		# Merge the configuration with the options
-		foreach my $opt (keys(%options)) {
-			if ( ref $options{$opt} eq 'ARRAY' ) {
-				next unless exists $config{$opt};
-				my @arrayed = ref $config{$opt} eq 'ARRAY' ? @{$config{$opt}} : ($config{$opt});
-				push(@{$options{$opt}}, @arrayed);
-				next;
-			}
-			$options{$opt} = $config{$opt} if exists $config{$opt};
-		}
-	}
+    my $config_file = "$ENV{HOME}/.tpsgi.ini";
+    if (-f $config_file) {
+        my $conf = Config::Simple->new($config_file);
+        my %config;
+        %config = %{$conf->param(-block => 'default')} if $conf;
+
+        # Merge the configuration with the options
+        foreach my $opt (keys(%options)) {
+            if ( ref $options{$opt} eq 'ARRAY' ) {
+                next unless exists $config{$opt};
+                my @arrayed = ref $config{$opt} eq 'ARRAY' ? @{$config{$opt}} : ($config{$opt});
+                push(@{$options{$opt}}, @arrayed);
+                next;
+            }
+            $options{$opt} = $config{$opt} if exists $config{$opt};
+        }
+    }
     return %options;
 }
 
+# Convenience method to keep track of server-timing
+sub checkpoint {
+    my ($self, $data, $name) = @_;
+    $data->{checkpoints} //= [[ start => $data->{start} ]];
+    push(@{$data->{checkpoints}}, [ $name => [gettimeofday] ]);
+}
+
+sub build_server_timing {
+    my ($self, $data) = @_;
+    my @st;
+    my $last_interval;
+    foreach my $checkpoint (@{$data->{checkpoints}}) {
+        if ($last_interval) {
+            my $dur = tv_interval($last_interval, $checkpoint->[1]) * 1000;
+            push(@st, "$checkpoint->[0];dur=$dur");
+        }
+        $last_interval = $checkpoint->[1];
+    }
+    my $tot = tv_interval($data->{start}) * 1000;
+    push(@st, "tot;dur=$tot");
+    return join(', ', @st);
+}
+
 1;