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