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