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