resolve conflict in the correct direction this time :)
[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 {
15 enabled => 1,
16 stack => [ $root ],
17 tree => $root,
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) {
34 $params{comment} = shift || "";
35 }
36 elsif (@_ % 2 != 0) {
37 die "profile() requires a single comment parameter or a list of name-value pairs; found "
38 . (scalar @_) . " values: " . join(", ", @_);
39 }
40 else {
41 (%params) = @_;
42 $params{comment} ||= "";
43 }
44
45 my $parent;
46 my $prev;
47 my $t = [ gettimeofday ];
48
49 if ($params{end}) {
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
62 }
63 if ($params{parent}) {
64 # parent is explicitly defined
65 $prev = $parent = $self->_get_uid($params{parent}) or return undef;
66 }
67 if (!$parent) {
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;
72 }
73
74 my $node = Tree::Simple->new({
75 action => $params{begin} || "",
76 t => $t,
77 elapsed => tv_interval($prev->getNodeValue->{t}, $t),
78 comment => $params{comment},
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--) {
97 $self->profile(end => $self->{stack}->[$i]->getNodeValue->{action});
98 }
99
100 my $t = Text::SimpleTable->new( [ 62, 'Action' ], [ 9, 'Time' ] );
101 my @results;
102 $self->{tree}->traverse(
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 );
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
1291;
130
131__END__
132
133=head1 NAME
134
135Catalyst::Stats - Catalyst Timing Statistics Class
136
137=head1 SYNOPSIS
138
139 $stats = $c->stats;
140 $stats->enable(1);
141 $stats->profile($comment);
142 $stats->profile(begin => $block_name, comment =>$comment);
143 $stats->profile(end => $block_name);
144 $elapsed = $stats->elapsed;
145 $report = $stats->report;
146
147See L<Catalyst>.
148
149=head1 DESCRIPTION
150
151This module provides the default, simple timing stats collection functionality for Catalyst.
152If you want something different set C<< MyApp->stats_class >> in your application module,
153e.g.:
154
155 __PACKAGE__->stats_class( "My::Stats" );
156
157If you write your own, your stats object is expected to provide the interface described here.
158
159Catalyst uses this class to report timings of component actions. You can add
160profiling points into your own code to get deeper insight. Typical usage might
161be like this:
162
163 sub mysub {
164 my ($c, ...) = @_;
165 $c->stats->profile(begin => "mysub");
166 # code goes here
167 ...
168 $c->stats->profile("starting critical bit");
169 # code here too
170 ...
171 $c->stats->profile("completed first part of critical bit");
172 # more code
173 ...
174 $c->stats->profile("completed second part of critical bit");
175 # more code
176 ...
177 $c->stats->profile(end => "mysub");
178 }
179
180Supposing mysub was called from the action "process" inside a Catalyst
181Controller called "service", then the reported timings for the above example
182might look something like this:
183
184 .----------------------------------------------------------------+-----------.
185 | Action | Time |
186 +----------------------------------------------------------------+-----------+
187 | /service/process | 1.327702s |
188 | mysub | 0.555555s |
189 | - starting critical bit | 0.111111s |
190 | - completed first part of critical bit | 0.333333s |
191 | - completed second part of critical bit | 0.111000s |
192 | /end | 0.000160s |
193 '----------------------------------------------------------------+-----------'
194
195which means mysub took 0.555555s overall, it took 0.111111s to reach the
196critical bit, the first part of the critical bit took 0.333333s, and the second
197part 0.111s.
198
199
200=head1 METHODS
201
202=head2 new
203
204Constructor.
205
206 $stats = Catalyst::Stats->new;
207
208=head2 enable
209
210 $stats->enable(0);
211 $stats->enable(1);
212
213Enable or disable stats collection. By default, stats are enabled after object creation.
214
215=head2 profile
216
217 $stats->profile($comment);
218 $stats->profile(begin => $block_name, comment =>$comment);
219 $stats->profile(end => $block_name);
220
221Marks a profiling point. These can appear in pairs, to time the block of code
222between the begin/end pairs, or by themselves, in which case the time of
223execution to the previous profiling point will be reported.
224
225The argument may be either a single comment string or a list of name-value
226pairs. Thus the following are equivalent:
227
228 $stats->profile($comment);
229 $stats->profile(comment => $comment);
230
231The following key names/values may be used:
232
233=over 4
234
235=item * begin => ACTION
236
237Marks the beginning of a block. The value is used in the description in the
238timing report.
239
240=item * end => ACTION
241
242Marks the end of the block. The name given must match a previous 'begin'.
243Correct nesting is recommended, although this module is tolerant of blocks that
244are not correctly nested, and the reported timings should accurately reflect the
245time taken to execute the block whether properly nested or not.
246
247=item * comment => COMMENT
248
249Comment string; use this to describe the profiling point. It is combined with
250the block action (if any) in the timing report description field.
251
252=item * uid => UID
253
254Assign a predefined unique ID. This is useful if, for whatever reason, you wish
255to relate a profiling point to a different parent than in the natural execution
256sequence.
257
258=item * parent => UID
259
260Explicitly relate the profiling point back to the parent with the specified UID.
261The profiling point will be ignored if the UID has not been previously defined.
262
263=back
264
265Returns the UID of the current point in the profile tree. The UID is
266automatically assigned if not explicitly given.
267
268=head2 elapsed
269
270 $elapsed = $stats->elapsed
271
272Get the total elapsed time (in seconds) since the object was created.
273
274=head2 report
275
276 print $stats->report ."\n";
277 $report = $stats->report;
278 @report = $stats->report;
279
280In scalar context, generates a textual report. In array context, returns the
281array of results where each row comprises:
282
283 [ depth, description, time, rollup ]
284
285The depth is the calling stack level of the profiling point.
286
287The description is a combination of the block name and comment.
288
289The time reported for each block is the total execution time for the block, and
290the time associated with each intermediate profiling point is the elapsed time
291from the previous profiling point.
292
293The 'rollup' flag indicates whether the reported time is the rolled up time for
294the block, or the elapsed time from the previous profiling point.
295
296
297=head1 SEE ALSO
298
299L<Catalyst>.
300
301=head1 AUTHOR
302
303Jon Schutz
304
305=head1 COPYRIGHT
306
307This program is free software, you can redistribute it and/or modify
308it under the same terms as Perl itself.
309
310=cut
311
3121;