File Coverage

blib/lib/Catalyst/Stats.pm
Criterion Covered Total %
statement 86 87 98.8
branch 35 40 87.5
condition 10 11 90.9
subroutine 16 16 100.0
pod 7 7 100.0
total 154 161 95.6


line stmt bran cond sub pod time code
1             package Catalyst::Stats;
2              
3 156     156   78303 use Moose;
  156         475628  
  156         1386  
4 156     156   1179093 use Time::HiRes qw/gettimeofday tv_interval/;
  156         261680  
  156         955  
5 156     156   71917 use Text::SimpleTable ();
  156         5468  
  156         5914  
6 156     156   3867 use Catalyst::Utils;
  156         2548  
  156         7231  
7 156     156   3280 use Tree::Simple qw/use_weak_refs/;
  156         2728  
  156         6043  
8 156     156   9596 use Tree::Simple::Visitor::FindByUID;
  156         8903  
  156         8939  
9              
10 156     156   5898 use namespace::clean -except => 'meta';
  156         586  
  156         5805  
11              
12             has enable => (is => 'rw', required => 1, default => sub{ 1 });
13             has tree => (
14             is => 'ro',
15             required => 1,
16             default => sub{ Tree::Simple->new({t => [gettimeofday]}) },
17             handles => [qw/ accept traverse /],
18             );
19             has stack => (
20             is => 'ro',
21             required => 1,
22             lazy => 1,
23             default => sub { [ shift->tree ] }
24             );
25              
26             sub profile {
27 76     76 1 1155 my $self = shift;
28              
29 76 100       2079 return unless $self->enable;
30              
31 75         132 my %params;
32 75 100       298 if (@_ <= 1) {
    50          
33 4   50     22 $params{comment} = shift || "";
34             }
35             elsif (@_ % 2 != 0) {
36 0         0 die "profile() requires a single comment parameter or a list of name-value pairs; found "
37             . (scalar @_) . " values: " . join(", ", @_);
38             }
39             else {
40 71         276 (%params) = @_;
41 71   100     299 $params{comment} ||= "";
42             }
43              
44 75         132 my $parent;
45             my $prev;
46 75         253 my $t = [ gettimeofday ];
47 75         2020 my $stack = $self->stack;
48              
49 75 100       234 if ($params{end}) {
50             # parent is on stack; search for matching block and splice out
51 33         71 for (my $i = $#{$stack}; $i > 0; $i--) {
  33         127  
52 37 100       138 if ($stack->[$i]->getNodeValue->{action} eq $params{end}) {
53 33         190 my ($node) = splice(@{$stack}, $i, 1);
  33         100  
54             # Adjust elapsed on partner node
55 33         90 my $v = $node->getNodeValue;
56 33         175 $v->{elapsed} = tv_interval($v->{t}, $t);
57 33         467 return $node->getUID;
58             }
59             }
60             # if partner not found, fall through to treat as non-closing call
61             }
62 42 100       141 if ($params{parent}) {
63             # parent is explicitly defined
64 5         21 $prev = $parent = $self->_get_uid($params{parent});
65             }
66 42 100       206 if (!$parent) {
67             # Find previous node, which is either previous sibling or parent, for ref time.
68 37 50       121 $prev = $parent = $stack->[-1] or return undef;
69 37         156 my $n = $parent->getChildCount;
70 37 100       326 $prev = $parent->getChild($n - 1) if $n > 0;
71             }
72              
73             my $node = Tree::Simple->new({
74             action => $params{begin} || "",
75             t => $t,
76             elapsed => tv_interval($prev->getNodeValue->{t}, $t),
77             comment => $params{comment},
78 42   100     265 });
79 42 100       2533 $node->setUID($params{uid}) if $params{uid};
80              
81 42         322 $parent->addChild($node);
82 42 100       4683 push(@{$stack}, $node) if $params{begin};
  34         81  
83              
84 42         132 return $node->getUID;
85             }
86              
87             sub created {
88 1     1 1 739 return @{ shift->{tree}->getNodeValue->{t} };
  1         5  
89             }
90              
91             sub elapsed {
92 16     16 1 2631 return tv_interval(shift->{tree}->getNodeValue->{t});
93             }
94              
95             sub report {
96 16     16 1 59 my $self = shift;
97              
98 16         41 my $t;
99             my @results;
100              
101 16 100       57 if (!wantarray) {
102 15         78 $t = Text::SimpleTable->new(
103             [ Catalyst::Utils::term_width() - 9 - 13, 'Action' ],
104             [ 9, 'Time' ],
105             );
106             }
107              
108             $self->traverse(sub {
109 39     39   2334 my $action = shift;
110 39         115 my $stat = $action->getNodeValue;
111             my @r = ( $action->getDepth,
112             ($stat->{action} || "") .
113             ($stat->{action} && $stat->{comment} ? " " : "") . ($stat->{comment} ? '- ' . $stat->{comment} : ""),
114             $stat->{elapsed},
115 39 100 100     162 $stat->{action} ? 1 : 0,
    100 100        
    100          
116             );
117             # Trim down any times >= 10 to avoid ugly Text::Simple line wrapping
118 39         779 my $elapsed = substr(sprintf("%f", $stat->{elapsed}), 0, 8) . "s";
119 39 100       135 if ($t) {
120 28 50       187 $t->row( ( q{ } x $r[0] ) . $r[1],
121             defined $r[2] ? $elapsed : '??');
122             }
123             else {
124 11         48 push @results, \@r;
125             }
126 16         1805 });
127 16 100       2086 return wantarray ? @results : $t->draw;
128             }
129              
130             sub _get_uid {
131 5     5   14 my ($self, $uid) = @_;
132              
133 5         27 my $visitor = Tree::Simple::Visitor::FindByUID->new;
134 5         189 $visitor->searchForUID($uid);
135 5         49 $self->accept($visitor);
136 5         619 return $visitor->getResult;
137             }
138              
139             sub addChild {
140 1     1 1 46 my $self = shift;
141 1         19 my $node = $_[ 0 ];
142              
143 1         4 my $stat = $node->getNodeValue;
144              
145             # do we need to fake $stat->{ t } ?
146 1 50       8 if( $stat->{ elapsed } ) {
147             # remove the "s" from elapsed time
148 1         6 $stat->{ elapsed } =~ s{s$}{};
149             }
150              
151 1         29 $self->tree->addChild( @_ );
152             }
153              
154             sub setNodeValue {
155 1     1 1 9 my $self = shift;
156 1         3 my $stat = $_[ 0 ];
157              
158             # do we need to fake $stat->{ t } ?
159 1 50       4 if( $stat->{ elapsed } ) {
160             # remove the "s" from elapsed time
161 1         5 $stat->{ elapsed } =~ s{s$}{};
162             }
163              
164 1         29 $self->tree->setNodeValue( @_ );
165             }
166              
167             sub getNodeValue {
168 1     1 1 17 my $self = shift;
169 1         26 $self->tree->getNodeValue( @_ )->{ t };
170             }
171              
172             __PACKAGE__->meta->make_immutable();
173              
174             1;
175              
176             __END__
177              
178             =for stopwords addChild getNodeValue mysub rollup setNodeValue
179              
180             =head1 NAME
181              
182             Catalyst::Stats - Catalyst Timing Statistics Class
183              
184             =head1 SYNOPSIS
185              
186             $stats = $c->stats;
187             $stats->enable(1);
188             $stats->profile($comment);
189             $stats->profile(begin => $block_name, comment =>$comment);
190             $stats->profile(end => $block_name);
191             $elapsed = $stats->elapsed;
192             $report = $stats->report;
193              
194             See L<Catalyst>.
195              
196             =head1 DESCRIPTION
197              
198             This module provides the default, simple timing stats collection functionality for Catalyst.
199             If you want something different set C<< MyApp->stats_class >> in your application module,
200             e.g.:
201              
202             __PACKAGE__->stats_class( "My::Stats" );
203              
204             If you write your own, your stats object is expected to provide the interface described here.
205              
206             Catalyst uses this class to report timings of component actions. You can add
207             profiling points into your own code to get deeper insight. Typical usage might
208             be like this:
209              
210             sub mysub {
211             my ($c, ...) = @_;
212             $c->stats->profile(begin => "mysub");
213             # code goes here
214             ...
215             $c->stats->profile("starting critical bit");
216             # code here too
217             ...
218             $c->stats->profile("completed first part of critical bit");
219             # more code
220             ...
221             $c->stats->profile("completed second part of critical bit");
222             # more code
223             ...
224             $c->stats->profile(end => "mysub");
225             }
226              
227             Supposing mysub was called from the action "process" inside a Catalyst
228             Controller called "service", then the reported timings for the above example
229             might look something like this:
230              
231             .----------------------------------------------------------------+-----------.
232             | Action | Time |
233             +----------------------------------------------------------------+-----------+
234             | /service/process | 1.327702s |
235             | mysub | 0.555555s |
236             | - starting critical bit | 0.111111s |
237             | - completed first part of critical bit | 0.333333s |
238             | - completed second part of critical bit | 0.111000s |
239             | /end | 0.000160s |
240             '----------------------------------------------------------------+-----------'
241              
242             which means mysub took 0.555555s overall, it took 0.111111s to reach the
243             critical bit, the first part of the critical bit took 0.333333s, and the second
244             part 0.111s.
245              
246              
247             =head1 METHODS
248              
249             =head2 new
250              
251             Constructor.
252              
253             $stats = Catalyst::Stats->new;
254              
255             =head2 enable
256              
257             $stats->enable(0);
258             $stats->enable(1);
259              
260             Enable or disable stats collection. By default, stats are enabled after object creation.
261              
262             =head2 profile
263              
264             $stats->profile($comment);
265             $stats->profile(begin => $block_name, comment =>$comment);
266             $stats->profile(end => $block_name);
267              
268             Marks a profiling point. These can appear in pairs, to time the block of code
269             between the begin/end pairs, or by themselves, in which case the time of
270             execution to the previous profiling point will be reported.
271              
272             The argument may be either a single comment string or a list of name-value
273             pairs. Thus the following are equivalent:
274              
275             $stats->profile($comment);
276             $stats->profile(comment => $comment);
277              
278             The following key names/values may be used:
279              
280             =over 4
281              
282             =item * begin => ACTION
283              
284             Marks the beginning of a block. The value is used in the description in the
285             timing report.
286              
287             =item * end => ACTION
288              
289             Marks the end of the block. The name given must match a previous 'begin'.
290             Correct nesting is recommended, although this module is tolerant of blocks that
291             are not correctly nested, and the reported timings should accurately reflect the
292             time taken to execute the block whether properly nested or not.
293              
294             =item * comment => COMMENT
295              
296             Comment string; use this to describe the profiling point. It is combined with
297             the block action (if any) in the timing report description field.
298              
299             =item * uid => UID
300              
301             Assign a predefined unique ID. This is useful if, for whatever reason, you wish
302             to relate a profiling point to a different parent than in the natural execution
303             sequence.
304              
305             =item * parent => UID
306              
307             Explicitly relate the profiling point back to the parent with the specified UID.
308             The profiling point will be ignored if the UID has not been previously defined.
309              
310             =back
311              
312             Returns the UID of the current point in the profile tree. The UID is
313             automatically assigned if not explicitly given.
314              
315             =head2 created
316              
317             ($seconds, $microseconds) = $stats->created;
318              
319             Returns the time the object was created, in C<gettimeofday> format, with
320             Unix epoch seconds followed by microseconds.
321              
322             =head2 elapsed
323              
324             $elapsed = $stats->elapsed
325              
326             Get the total elapsed time (in seconds) since the object was created.
327              
328             =head2 report
329              
330             print $stats->report ."\n";
331             $report = $stats->report;
332             @report = $stats->report;
333              
334             In scalar context, generates a textual report. In array context, returns the
335             array of results where each row comprises:
336              
337             [ depth, description, time, rollup ]
338              
339             The depth is the calling stack level of the profiling point.
340              
341             The description is a combination of the block name and comment.
342              
343             The time reported for each block is the total execution time for the block, and
344             the time associated with each intermediate profiling point is the elapsed time
345             from the previous profiling point.
346              
347             The 'rollup' flag indicates whether the reported time is the rolled up time for
348             the block, or the elapsed time from the previous profiling point.
349              
350             =head1 COMPATIBILITY METHODS
351              
352             Some components might expect the stats object to be a regular Tree::Simple object.
353             We've added some compatibility methods to handle this scenario:
354              
355             =head2 accept
356              
357             =head2 addChild
358              
359             =head2 setNodeValue
360              
361             =head2 getNodeValue
362              
363             =head2 traverse
364              
365             =head1 SEE ALSO
366              
367             L<Catalyst>
368              
369             =head1 AUTHORS
370              
371             Catalyst Contributors, see Catalyst.pm
372              
373             =head1 COPYRIGHT
374              
375             This library is free software. You can redistribute it and/or modify
376             it under the same terms as Perl itself.
377              
378             =cut
379              
380             __PACKAGE__->meta->make_immutable;
381              
382             1;