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