merge the stats patch against .10 into trunk
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Stats.pm
diff --git a/lib/Catalyst/Stats.pm b/lib/Catalyst/Stats.pm
new file mode 100644 (file)
index 0000000..e47954f
--- /dev/null
@@ -0,0 +1,312 @@
+package Catalyst::Stats;
+
+use strict;
+use warnings;
+use Time::HiRes qw/gettimeofday tv_interval/;
+use Text::SimpleTable ();
+use Tree::Simple qw/use_weak_refs/;
+use Tree::Simple::Visitor::FindByUID;
+
+sub new {
+    my $class = shift;
+
+    my $root = Tree::Simple->new({t => [gettimeofday]});
+    bless { 
+       enabled => 1,
+       stack => [ $root ],
+       tree => $root,
+    }, ref $class || $class;
+}
+
+sub enable {
+    my ($self, $enable) = @_;
+
+    $self->{enabled} = $enable;
+}
+
+sub profile {
+    my $self = shift;
+
+    return unless $self->{enabled};
+
+    my %params;
+    if (@_ <= 1) {
+       $params{comment} = shift || "";
+    }
+    elsif (@_ % 2 != 0) {
+       die "profile() requires a single comment parameter or a list of name-value pairs; found " 
+           . (scalar @_) . " values: " . join(", ", @_);
+    }
+    else {
+       (%params) = @_;
+       $params{comment} ||= "";
+    }
+
+    my $parent;
+    my $prev;
+    my $t = [ gettimeofday ];
+
+    if ($params{end}) {
+       # parent is on stack; search for matching block and splice out
+       for (my $i = $#{$self->{stack}}; $i > 0; $i--) {
+           if ($self->{stack}->[$i]->getNodeValue->{action} eq $params{end}) {
+               my $node = $self->{stack}->[$i];
+               splice(@{$self->{stack}}, $i, 1);
+               # Adjust elapsed on partner node
+               my $v = $node->getNodeValue;
+               $v->{elapsed} =  tv_interval($v->{t}, $t);
+               return $node->getUID;
+           }
+       }
+       # if partner not found, fall through to treat as non-closing call
+    }
+    if ($params{parent}) {
+       # parent is explicitly defined
+       $prev = $parent = $self->_get_uid($params{parent}) or return undef;
+    }
+    if (!$parent) {
+       # Find previous node, which is either previous sibling or parent, for ref time.
+       $prev = $parent = $self->{stack}->[-1] or return undef;
+       my $n = $parent->getChildCount;
+       $prev = $parent->getChild($n - 1) if $n > 0;
+    }
+
+    my $node = Tree::Simple->new({
+       action  => $params{begin} || "",
+       t => $t, 
+       elapsed => tv_interval($prev->getNodeValue->{t}, $t),
+       comment => $params{comment},
+    });
+    $node->setUID($params{uid}) if $params{uid};
+
+    $parent->addChild($node);
+    push(@{$self->{stack}}, $node) if $params{begin};
+
+    return $node->getUID;
+}
+
+sub elapsed {
+    return tv_interval(shift->{tree}->getNodeValue->{t});
+}
+
+sub report {
+    my $self = shift;
+
+# close any remaining open nodes
+    for (my $i = $#{$self->{stack}}; $i > 0; $i--) {
+       $self->profile(end => $self->{stack}->[$i]->getNodeValue->{action});
+    }
+
+    my $t = Text::SimpleTable->new( [ 62, 'Action' ], [ 9, 'Time' ] );
+    my @results;
+    $self->{tree}->traverse(
+                           sub {
+                               my $action = shift;
+                               my $stat   = $action->getNodeValue;
+                               my @r = ( $action->getDepth,
+                                         ($stat->{action} || "") .
+                                         ($stat->{action} && $stat->{comment} ? " " : "") . ($stat->{comment} ? '- ' . $stat->{comment} : ""),
+                                         $stat->{elapsed},
+                                         $stat->{action} ? 1 : 0,
+                                         );
+                               $t->row( ( q{ } x $r[0] ) . $r[1], 
+                                        defined $r[2] ? sprintf("%fs", $r[2]) : '??');
+                               push(@results, \@r);
+                           }
+                           );
+    return wantarray ? @results : $t->draw;
+}
+
+sub _get_uid {
+    my ($self, $uid) = @_;
+
+    my $visitor = Tree::Simple::Visitor::FindByUID->new;
+    $visitor->searchForUID($uid);
+    $self->{tree}->accept($visitor);
+    return $visitor->getResult;
+} 
+
+1;
+
+__END__
+
+=head1 NAME
+
+Catalyst::Stats - Catalyst Timing Statistics Class
+
+=head1 SYNOPSIS
+
+    $stats = $c->stats;
+    $stats->enable(1);
+    $stats->profile($comment);
+    $stats->profile(begin => $block_name, comment =>$comment);
+    $stats->profile(end => $block_name);
+    $elapsed = $stats->elapsed;
+    $report = $stats->report;
+
+See L<Catalyst>.
+
+=head1 DESCRIPTION
+
+This module provides the default, simple timing stats collection functionality for Catalyst.
+If you want something different set C<< MyApp->stats_class >> in your application module,
+e.g.:
+
+    __PACKAGE__->stats_class( "My::Stats" );
+
+If you write your own, your stats object is expected to provide the interface described here.
+
+Catalyst uses this class to report timings of component actions.  You can add
+profiling points into your own code to get deeper insight. Typical usage might
+be like this:
+
+  sub mysub {
+    my ($c, ...) = @_;
+    $c->stats->profile(begin => "mysub");
+    # code goes here
+    ...
+    $c->stats->profile("starting critical bit");
+    # code here too
+    ...
+    $c->stats->profile("completed first part of critical bit");
+    # more code
+    ...
+    $c->stats->profile("completed second part of critical bit");
+    # more code
+    ...
+    $c->stats->profile(end => "mysub"); 
+  }
+
+Supposing mysub was called from the action "process" inside a Catalyst
+Controller called "service", then the reported timings for the above example
+might look something like this:
+
+  .----------------------------------------------------------------+-----------.
+  | Action                                                         | Time      |
+  +----------------------------------------------------------------+-----------+
+  | /service/process                                               | 1.327702s |
+  |  mysub                                                         | 0.555555s |
+  |   - starting critical bit                                      | 0.111111s |
+  |   - completed first part of critical bit                       | 0.333333s |
+  |   - completed second part of critical bit                      | 0.111000s |
+  | /end                                                           | 0.000160s |
+  '----------------------------------------------------------------+-----------'
+
+which means mysub took 0.555555s overall, it took 0.111111s to reach the
+critical bit, the first part of the critical bit took 0.333333s, and the second
+part 0.111s.
+
+
+=head1 METHODS
+
+=head2 new
+
+Constructor. 
+
+    $stats = Catalyst::Stats->new;
+
+=head2 enable
+
+    $stats->enable(0);
+    $stats->enable(1);
+
+Enable or disable stats collection.  By default, stats are enabled after object creation.
+
+=head2 profile
+
+    $stats->profile($comment);
+    $stats->profile(begin => $block_name, comment =>$comment);
+    $stats->profile(end => $block_name);
+
+Marks a profiling point.  These can appear in pairs, to time the block of code
+between the begin/end pairs, or by themselves, in which case the time of
+execution to the previous profiling point will be reported.  
+
+The argument may be either a single comment string or a list of name-value
+pairs.  Thus the following are equivalent:
+
+    $stats->profile($comment);
+    $stats->profile(comment => $comment);
+
+The following key names/values may be used:
+
+=over 4
+
+=item * begin => ACTION
+
+Marks the beginning of a block.  The value is used in the description in the
+timing report.
+
+=item * end => ACTION
+
+Marks the end of the block.  The name given must match a previous 'begin'.
+Correct nesting is recommended, although this module is tolerant of blocks that
+are not correctly nested, and the reported timings should accurately reflect the
+time taken to execute the block whether properly nested or not.
+
+=item * comment => COMMENT
+
+Comment string; use this to describe the profiling point.  It is combined with
+the block action (if any) in the timing report description field.
+
+=item * uid => UID
+
+Assign a predefined unique ID.  This is useful if, for whatever reason, you wish
+to relate a profiling point to a different parent than in the natural execution
+sequence.
+
+=item * parent => UID
+
+Explicitly relate the profiling point back to the parent with the specified UID.
+The profiling point will be ignored if the UID has not been previously defined.
+
+=back
+
+Returns the UID of the current point in the profile tree.  The UID is
+automatically assigned if not explicitly given.
+
+=head2 elapsed
+
+    $elapsed = $stats->elapsed
+
+Get the total elapsed time (in seconds) since the object was created.
+
+=head2 report
+
+    print $stats->report ."\n";
+    $report = $stats->report;
+    @report = $stats->report;
+
+In scalar context, generates a textual report.  In array context, returns the
+array of results where each row comprises:
+
+    [ depth, description, time, rollup ]
+
+The depth is the calling stack level of the profiling point.
+
+The description is a combination of the block name and comment.
+
+The time reported for each block is the total execution time for the block, and
+the time associated with each intermediate profiling point is the elapsed time
+from the previous profiling point.
+
+The 'rollup' flag indicates whether the reported time is the rolled up time for
+the block, or the elapsed time from the previous profiling point.
+
+
+=head1 SEE ALSO
+
+L<Catalyst>.
+
+=head1 AUTHOR
+
+Jon Schutz
+
+=head1 COPYRIGHT
+
+This program is free software, you can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut
+
+1;