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