File Coverage

blib/lib/Timer/Milestones.pm
Criterion Covered Total %
statement 282 287 98.2
branch 97 100 97.0
condition 29 40 72.5
subroutine 42 43 97.6
pod 7 7 100.0
total 457 477 95.8


line stmt bran cond sub pod time code
1             package Timer::Milestones;
2              
3 5     5   938450 use strict;
  5         40  
  5         150  
4 5     5   27 use warnings;
  5         11  
  5         138  
5 5     5   28 no warnings 'uninitialized';
  5         10  
  5         187  
6              
7 5     5   2333 use parent 'Exporter';
  5         1620  
  5         31  
8              
9 5     5   303 use Carp;
  5         11  
  5         327  
10 5     5   2439 use PerlX::Maybe;
  5         12629  
  5         21  
11 5     5   210 use Scalar::Util qw(blessed refaddr);
  5         11  
  5         12557  
12              
13             our @EXPORT_OK = qw(start_timing add_milestone stop_timing
14             generate_intermediate_report generate_final_report
15             time_function);
16             our %EXPORT_TAGS = (all => \@EXPORT_OK);
17              
18             # Have you updated the version number in the POD below?
19             our $VERSION = '0.003';
20             $VERSION = eval $VERSION;
21              
22             =head1 NAME
23              
24             Timer::Milestones - measure code execution time succinctly by setting milestones
25              
26             =head1 VERSION
27              
28             This is version 0.003.
29              
30             =head1 SYNOPSIS
31              
32             use Timer::Milestones qw(:all);
33              
34             start_timing();
35             time_function('_my_own_function_elsewhere');
36             time_function('Some::ThirdParty::Module::do_slow_thing');
37              
38             my @objects = _set_up_objects();
39              
40             add_milestone('Everything set up');
41              
42             for my $object (@objects) {
43             _do_something_potentially_slow($object);
44            
45             # This code depends on a whole bunch of variables being set in the
46             # calling context, so don't bother refactoring it away into a separate
47             # function if it turns out that it's not actually slow.
48             state $code_to_time_individually = time_function(
49             sub {
50             ...
51             },
52             report_name_as => 'Possibly slow inlined code',
53             summarise_calls => 1,
54             summarise_arguments => sub {
55             my ($object) = @_;
56             $object->type,
57             },
58             );
59             $code_to_time_individually->($object);
60             }
61              
62             add_milestone('Telling the user')
63              
64             for my $object (@objects) {
65             _inform_user($object);
66             }
67             ...
68              
69             stop_timing();
70              
71             Spits out to STDERR e.g.
72              
73             START: Tue Feb 4 16:03:08 2020
74             3 s ( 0.28%)
75             Everything set up
76             5 min 30 s ( 31.22%)
77             3 min 7 s Some::ThirdParty::Module::do_slow_thing
78             15 s Possibly slow inlined code (x10)
79             Outgoing (x7)
80             Incoming (x3)
81             Telling the user
82             12 min 7 s ( 68.78%)
83             8 min 30 s Some::ThirdParty::Module::do_slow_thing (x3)
84             40 s _my_own_function_elsewhere
85             END: Tue Feb 4 16:20:48 2020
86              
87             =head1 DESCRIPTION
88              
89             At its simplest, Timer::Milestones is yet another timer module. It is designed
90             to have the smallest possible interface, so adding timing calls to your code
91             doesn't make it look unreadable.
92              
93             It can also time execution time of functions in other modules, as a more
94             informative (and quicker!) alternative to running everything under
95             Devel::NYTProf.
96              
97             =head2 Functional vs OO interface
98              
99             You can use Timer::Milestones via a functional interface:
100              
101             use Timer::Milestones qw(start_timing add_milestone stop_timing);
102             start_timing();
103             ...;
104             add_milestone('Half-way through');
105             ...;
106             stop_timing();
107              
108             Or via an OO interface:
109              
110             use Timer::Milestones;
111             {
112             my $timer = Timer::Milestones->new;
113             # $timer->start_timing automatically called
114             ...;
115             $timer->add_milestone('Half-way through');
116             ...;
117             }
118             # $timer->stop_timing automatically called when $timer is destroyed
119              
120             The OO interface is simpler if you're timing a monolithic block of code. If you
121             need to add timing calls throughout code scattered across multiple files, you're
122             better off with the functional interface as you don't need to pass a
123             Timer::Milestone object around.
124              
125             =head2 Milestones and reports
126              
127             At its simplest, the report you get will include when timing started and when
128             timing ended. These are displayed in your locale's local time, on the assumption
129             that that's what makes sense to you.
130              
131             As soon as you add milestones, you will also be told how much time passed
132             between the start, each milestone, and the end. Times are specified in both
133             human-friendly periods (a number of milliseconds; seconds; minutes and seconds;
134             or hours and minutes), and percentages of the total elapsed time.
135              
136             If you decide that you want to L
137             well|/time_function>, they'll be mentioned with the milestones they follow. If
138             you provide a coderef to summarise the arguments passed to them that will be
139             included; if you decide that you don't need to see individual timings for each
140             function call, just an overall time, you'll get a shorter list of function
141             calls and an overall time.
142              
143             =head2 Basic functionality
144              
145             =head3 new
146              
147             Out: $timer
148              
149             Creates a new Timer::Milestones object, and calls L on it.
150              
151             =cut
152              
153             sub new {
154             # Start off with an empty hashref
155 25     25 1 3555 my $invocant = shift;
156 25   33     123 my $self = bless {} => ref($invocant) || $invocant;
157              
158             # Accept replacement coderefs for get_time and notify_report (mostly
159             # used in tests), and otherwise populate them with default values.
160 25 100       86 if (my %params = @_) {
161 9         22 for my $coderef_param (qw(get_time notify_report)) {
162 18 100       54 if (ref($params{$coderef_param}) eq 'CODE') {
163 9         31 $self->{$coderef_param} = $params{$coderef_param};
164             }
165             }
166             }
167 25   66     114 $self->{get_time} ||= $self->_default_get_time;
168 25   66     128 $self->{notify_report} ||= $self->_default_notify_report;
169              
170             # Start timing, and return this object.
171 25         152 $self->start_timing;
172 25         70 return $self;
173             }
174              
175             # Passed a list of arguments, returns a similar list of arguments beginning
176             # with a Timer::Milestones object - the first argument, if it was such an
177             # object, or otherwise a singleton, followed by the other arguments.
178             {
179             my $singleton;
180              
181             sub _object_and_arguments {
182 164     164   5940 my (@arguments) = @_;
183            
184 164 100 100     1550 unless (blessed($arguments[0])
185             && $arguments[0]->isa('Timer::Milestones'))
186             {
187 14   66     62 $singleton ||= __PACKAGE__->new;
188 14         27 unshift @arguments, $singleton
189             }
190 164         431 return @arguments;
191             }
192             }
193              
194              
195             =head3 start_timing
196              
197             If timing hadn't already been started, starts timing. Otherwise does nothing.
198             Automatically called by L, but you'll need to call it explicitly when
199             using the functional interface.
200              
201             =cut
202              
203             sub start_timing {
204 28     28 1 2370 my ($self) = _object_and_arguments(@_);
205 28 100       81 unless (exists $self->{milestones}) {
206 25         59 $self->add_milestone('START');
207             }
208             }
209              
210             =head3 add_milestone
211              
212             In: $name (optional)
213              
214             Adds another milestone. If supplied with a name, uses that name for the
215             milestone; otherwise, generates a name from the place it was called from
216             (package, function, line number).
217              
218             Throws an exception if a timing report has already been generated by
219             L.
220              
221             =cut
222              
223             sub add_milestone {
224 41     41 1 321 my ($self, $milestone_name) = _object_and_arguments(@_);
225              
226             # Can't add milestones if we've decided that we're finished.
227 41 100       101 if ($self->{timing_stopped}) {
228 2         406 croak 'Stopped timing already';
229             }
230              
231             # Build up a milestone structure with the name provided, or a suitable
232             # default.
233 39   66     144 my $milestone = { name => $milestone_name || $self->_milestone_name };
234              
235             # End the previous milestone, if there was one; reuse the time it ended
236             # if we can.
237 39 100       104 if (my $previous_milestone = $self->_end_previous_milestone) {
238 14         80 $milestone->{started} = $previous_milestone->{ended};
239             } else {
240 25         53 $milestone->{started} = $self->_now
241             }
242              
243             # Remember this new milestone.
244 39         90 push @{ $self->{milestones} }, $milestone;
  39         80  
245              
246             # We can now usefully generate a new report.
247 39         67 delete $self->{generated_report};
248              
249 39         76 return $milestone;
250             }
251              
252             sub _milestone_name {
253             # Where we were called from (skipping over add_milestone which called *us*):
254 1     1   7 my ($package, $filename, $line) = caller(1);
255             # The subroutine the calling code was called from, if any. It will be
256             # fully-qualified, so no need to mention the package.
257 1 50       8 if (my $calling_subroutine = (caller(2))[3]) {
258 1         8 return "$calling_subroutine (line $line of $filename)";
259             } else {
260 0         0 return "Line $line of $package ($filename)";
261             }
262             }
263              
264             =head3 generate_intermediate_report
265              
266             Out: $report
267              
268             Returns a report on the milestones that have elapsed so far, or undef if a
269             report has previously been generated and no new milestones have been
270             reached since then.
271              
272             =cut
273              
274             sub generate_intermediate_report {
275 5     5 1 577 my ($self) = _object_and_arguments(@_);
276            
277 5         15 $self->_generate_report;
278             }
279              
280             sub _generate_report {
281 42     42   86 my ($self) = @_;
282              
283             # If we've got nothing new since the last time since we said anything,
284             # don't say anything.
285 42 100       114 return if $self->{generated_report};
286              
287             # There's also nothing to say if we don't have any milestones.
288 28 50       63 return if !$self->{milestones};
289              
290             # Build up a report.
291 28         54 my ($previous_milestone, @elements, @function_calls);
292 28         41 for my $milestone (@{ $self->{milestones} }) {
  28         67  
293             # If this is the first milestone, mention when this milestone started,
294             # as it's the start of it all.
295 45 100       88 if (!$previous_milestone) {
296             push @elements,
297             {
298             type => 'time',
299             name => $milestone->{name},
300             time => $milestone->{started},
301 27         126 };
302             }
303              
304             # But if we *do* have a previous milestone, we can now report how long
305             # it took to get to this one.
306 45 100       93 if ($previous_milestone) {
307             my $elapsed_time = $previous_milestone->{ended}
308 18         38 - $previous_milestone->{started};
309 18         47 push @elements,
310             { type => 'interval', elapsed_time => $elapsed_time };
311 18 100       46 push @elements, @function_calls if @function_calls;
312             push @elements,
313 18         45 { type => 'milestone', name => $milestone->{name} };
314             }
315            
316             # Remember this milestone for when we reach the next one.
317 45         69 $previous_milestone = $milestone;
318              
319             # If there were any function calls in this milestone, remember them.
320 45         69 @function_calls = ();
321 45 100       101 if ($milestone->{function_calls}) {
322 12         29 for my $function_call (@{ $milestone->{function_calls} }) {
  12         51  
323 57         111 $self->_add_function_call_to_list($function_call,
324             \@function_calls);
325             }
326             }
327             }
328              
329             # If we've ended, also remember that.
330 28 100       66 if ($self->{timing_stopped}) {
331             push @elements,
332             {
333             type => 'interval',
334             elapsed_time => $previous_milestone->{ended}
335             - $previous_milestone->{started}
336 23         85 };
337 23 100       63 push @elements, @function_calls if @function_calls;
338             push @elements,
339             {
340             type => 'time',
341             name => 'END',
342             time => $self->{timing_stopped}
343 23         71 };
344             }
345              
346             # Now that we've got all the elements, generate a report from them.
347 28         85 my $report = $self->_generate_report_from_elements(@elements);
348              
349             # Remember that we generated a report, so we don't produce it again.
350 27         65 $self->{generated_report} = 1;
351              
352             # And return the report we generated.
353 27         139 return $report;
354             }
355              
356             # Provided with a function call hashref and an arrayref of function call
357             # elements, adds or combines the function call with what we have already.
358              
359             sub _add_function_call_to_list {
360 57     57   94 my ($self, $function_call, $call_elements) = @_;
361              
362 57         103 my $elapsed_time = $function_call->{ended} - $function_call->{started};
363 57         77 my $function_name = $function_call->{report_name_as};
364 57 100       110 if (!defined $function_name) {
365 41         57 $function_name = $function_call->{function_name};
366             }
367              
368             # If we're not summarising calls, we're going to add another element,
369             # so just do that.
370 57 100       132 if (!$function_call->{summarise_calls}) {
371 25         84 my $element = {
372             type => 'function_call',
373             function_name => $function_name,
374             elapsed_time => $elapsed_time,
375             };
376 25 100       69 if (exists $function_call->{argument_summary}) {
377             $element->{arguments_seen} = [
378             {
379             call_count => 1,
380             argument_summary => $function_call->{argument_summary},
381             }
382 8         22 ];
383             }
384 25         40 push @$call_elements, $element;
385 25         56 return;
386             }
387              
388             # OK, find out which element we're going to use.
389             my ($element)
390 32         50 = grep { $_->{function_name} eq $function_name } @$call_elements;
  127         231  
391 32 100       65 if (!$element) {
392 5         10 push @{$call_elements},
  5         15  
393             {
394             type => 'function_call',
395             function_name => $function_name,
396             elapsed_time => 0,
397             };
398 5         11 $element = $call_elements->[-1];
399             }
400 32         45 $element->{elapsed_time} += $elapsed_time;
401              
402             # If we're summarising arguments as well, store that information inside
403             # this element, once for each argument summary we see.
404 32 100       63 if (exists $function_call->{argument_summary}) {
405             my ($matching_arguments) = grep {
406             $_->{argument_summary} eq $function_call->{argument_summary}
407 13   100     17 } @{ $element->{arguments_seen} ||= [] };
  14         28  
  13         32  
408 13 100       37 if (!$matching_arguments) {
409 4         11 push @{ $element->{arguments_seen} },
410 4         8 { argument_summary => $function_call->{argument_summary} };
411 4         7 $matching_arguments = $element->{arguments_seen}[-1];
412             }
413 13         20 $matching_arguments->{call_count}++;
414             }
415              
416             # No matter what, remember that this function was called again.
417 32         66 $element->{call_count}++;
418             }
419              
420              
421             sub _generate_report_from_elements {
422 28     28   83 my ($self, @elements) = @_;
423              
424             # Work out how much time passed between all intervals so far.
425 28         45 my $total_elapsed_time;
426 28         52 for my $element (grep { $_->{type} eq 'interval' } @elements) {
  139         302  
427 41         93 $total_elapsed_time += $element->{elapsed_time};
428             }
429              
430             # In case all our timestamps are equal (which *can* happen if you're
431             # testing very, very fast code, or Time::HiRes isn't working), tweak the
432             # total elapsed time to merely be *very small*, to avoid a divide by zero
433             # error later on when we work out percentages.
434 28   100     79 $total_elapsed_time ||= 0.000_001;
435              
436             # Now we can report all of this: static times, and intervals between them.
437             # Build up a set of lines that will go into the report: mostly static
438             # strings, but some lines that involve times may need to be adjusted to
439             # align together, and we'll only be able to do that when we have all of
440             # them.
441 28         42 my @report_lines;
442 28         50 for my $element (@elements) {
443 139 100       466 if ($element->{type} eq 'time') {
    100          
    100          
    50          
444             push @report_lines,
445 50         1450 $element->{name} . ': ' . localtime($element->{time});
446             } elsif ($element->{type} eq 'milestone') {
447 18         38 push @report_lines, $element->{name};
448             } elsif ($element->{type} eq 'interval') {
449             push @report_lines,
450             {
451             indent_level => 1,
452             elapsed_time => $element->{elapsed_time},
453             suffix => sprintf('(%6.2f%%)',
454 41         411 $element->{elapsed_time} / $total_elapsed_time * 100)
455             };
456             } elsif ($element->{type} eq 'function_call') {
457 30         46 my $function_name = $element->{function_name};
458 30 100       60 if ($element->{call_count}) {
459 5         18 $function_name .= ' (x' . $element->{call_count} . ')';
460             }
461             push @report_lines,
462             {
463             indent_level => 2,
464             elapsed_time => $element->{elapsed_time},
465 30         156 suffix => $function_name
466             };
467 30 100       83 if ($element->{arguments_seen}) {
468 10         13 for my $arguments (@{ $element->{arguments_seen}}) {
  10         22  
469 12         32 my $line = (' ' x 12) . $arguments->{argument_summary};
470 12 100       29 if ($arguments->{call_count} > 1) {
471 3         8 $line .= ' (x' . $arguments->{call_count} . ')';
472             }
473 12         28 push @report_lines, $line;
474             }
475             }
476             } else {
477             croak 'What the hell is an element of type '
478 0         0 . $element->{type} . '?';
479             }
480             }
481              
482             # Now that we know about all the times we're going to report, we can work
483             # on lining them up.
484 28         76 my %time_refs_by_indent_level;
485 28         48 for my $line (@report_lines) {
486 151 100       351 if (ref($line) eq 'HASH') {
487             $line->{human_elapsed_time}
488 71         180 = $self->_human_elapsed_time($line->{elapsed_time});
489 71         215 push @{ $time_refs_by_indent_level{ $line->{indent_level} } },
490 71         127 \($line->{human_elapsed_time});
491             }
492             }
493 28         74 for my $indent_level (keys %time_refs_by_indent_level) {
494             $self->_format_times_to_fit(
495 34         53 @{ $time_refs_by_indent_level{$indent_level} }
  34         112  
496             );
497             }
498              
499             # Now that that's done, unpack all of this.
500 27         56 for my $line (@report_lines) {
501 148 100       389 if (ref($line) eq 'HASH') {
502             $line
503             = (' ' x (4 * $line->{indent_level}))
504             . $line->{human_elapsed_time} . ' '
505 70         319 . $line->{suffix};
506             }
507             }
508              
509             # And return it all as a string.
510 27         186 return join("\n", @report_lines) . "\n";
511             }
512              
513             # Supplied with an elapsed time, as a number of seconds, returns a
514             # human-friendly description of the elapsed time.
515              
516             sub _human_elapsed_time {
517 88     88   6926 my ($self, $elapsed_time) = @_;
518              
519 88         225 my @unit_specs = $self->_unit_specs;
520             unit_spec:
521 88         190 for my $unit_spec (@unit_specs) {
522             next unit_spec
523 144 100 100     530 if $unit_spec->{max} && $elapsed_time >= $unit_spec->{max};
524             return sprintf(
525             $unit_spec->{label_format},
526             $unit_spec->{transform}
527 88 100       317 ? $unit_spec->{transform}->($elapsed_time)
528             : $elapsed_time
529             );
530             }
531             }
532              
533             sub _unit_specs {
534             (
535             {
536             max => 1,
537             label_format => '%.0f ms',
538 59     59   517 transform => sub { (shift) * 1_000 },
539             },
540             {
541             max => 60,
542             label_format => '%.0f s',
543             },
544             {
545             max => 60 * 60,
546             label_format => '%d min %2.0f s',
547             transform => sub {
548 15     15   33 my $seconds = shift;
549 15         216 ($seconds / 60, $seconds % 60)
550             },
551             },
552             {
553             label_format => '%d h %2.0f min',
554             transform => sub {
555 6     6   8 my $seconds = shift;
556 6         14 my $minutes = $seconds / 60;
557 6         94 ($minutes / 60, $minutes % 60)
558             },
559             }
560 88     88   906 );
561             }
562              
563             # Supplied with a list of scalar references (!), works out how they could all
564             # be formatted so they line up, and updates the references.
565             # Specifically, it makes sure that:
566             # * The primary units always lines up
567             # * The primary units have as much space as they need
568             # * After that, all strings are the same length, with trailing spaces added
569             # to pad
570             # Any secondary units were already padded, as there can only ever be two
571             # digits used by a secondary unit; any spillover goes into the primary unit.
572              
573             sub _format_times_to_fit {
574 35     35   90 my ($self, @time_refs) = @_;
575              
576             # First up: find out how many characters we're using for the spaces
577             # (if any) and digits before the primary unit, and how many characters
578             # we're using for the primary unit.
579 35         123 my $re_primary_quantity_and_unit = qr{ ^ (\s* \d+ \s* ) ([a-z]+) }x;
580 35         60 my %max_length;
581 35         66 for my $time (map { $$_ } @time_refs) {
  87         173  
582 87 100       486 if ($time =~ $re_primary_quantity_and_unit) {
583 86         314 for ([unit_value => $1], [unit_name => $2]) {
584 172         349 my ($label, $value) = @$_;
585 172         278 my $value_length = length($value);
586             $max_length{$label} = $value_length
587 172 100       544 if $value_length > $max_length{$label};
588             }
589             } else {
590 1         214 croak "Wasn't expecting $time";
591             }
592             }
593 34         121 my $value_format = '%' . $max_length{unit_value} . 's';
594 34         69 my $name_format = '%-' . $max_length{unit_name} . 's';
595 34         59 for my $time_ref (@time_refs) {
596 86         624 $$time_ref =~ s{^ $re_primary_quantity_and_unit }{
597 86         402 sprintf($value_format, $1) . sprintf($name_format, $2)}xe;
598             }
599              
600             # Now we've done that, pad all times to be the same length.
601 34         74 for my $time (map { $$_ } @time_refs) {
  86         191  
602             $max_length{total} = length($time)
603 86 100       246 if length($time) > $max_length{total};
604             }
605 34         112 my $total_format = '%-' . $max_length{total} . 's';
606 34         64 for my $time_ref (@time_refs) {
607 86         205 $$time_ref = sprintf($total_format, $$time_ref);
608             }
609 34         134 1;
610             }
611              
612             =head3 generate_final_report
613              
614             Out: $report
615              
616             Stops timing, and returns a report for all of the milestones.
617              
618             =cut
619              
620             sub generate_final_report {
621 36     36 1 998 my ($self) = _object_and_arguments(@_);
622              
623 36 100       92 if (!$self->{timing_stopped}) {
624 23         57 my $milestone = $self->_end_previous_milestone;
625 23         44 $self->{timing_stopped} = $milestone->{ended};
626 23         44 delete $self->{generated_report};
627             }
628              
629 36         91 return $self->_generate_report;
630             }
631              
632             =head3 stop_timing
633              
634             Stops timing, and spits out the result of L to
635             STDERR. This is called automatically in OO mode when the object goes out of
636             scope. This does nothing if you've already called L.
637             Also stops wrapping functions handed to L.
638              
639             =cut
640              
641             sub stop_timing {
642 29     29 1 4336 my ($self) = _object_and_arguments(@_);
643              
644 29         68 my $report = $self->generate_final_report;
645 28 100       72 if ($self->{wrapped_functions}) {
646 5         8 for my $function_data (@{ $self->{wrapped_functions} }) {
  5         12  
  0         0  
647 5     5   47 no strict 'refs';
  5         11  
  5         198  
648 5     5   30 no warnings 'redefine';
  5         29  
  5         334  
649 5         8 *{ $function_data->{function_name} } = $function_data->{orig_code};
  5         47  
650 5     5   53 use warnings 'redefine';
  5         11  
  5         242  
651 5     5   30 use strict 'refs';
  5         10  
  5         2473  
652             }
653             }
654 28 100       132 if ($report) {
655 16   66     54 $self->{notify_report} ||= $self->_default_notify_report;
656 16         57 return $self->{notify_report}->($report);
657             }
658             }
659              
660             sub _default_notify_report {
661 0     0   0 sub { my $report = shift; print STDERR $report }
  0         0  
662 4     4   21 }
663              
664             # Makes sure that we have a list of milestones; if we also had a previous
665             # milestone, marks it as having ended now.
666              
667             sub _end_previous_milestone {
668 62     62   101 my ($self) = @_;
669 62   100     188 $self->{milestones} ||= [];
670 62 100       168 if (my $previous_milestone = $self->{milestones}[-1]) {
671 36         73 $previous_milestone->{ended} = $self->_now;
672 36         121 return $previous_milestone;
673             }
674 26         65 return;
675             }
676              
677             # Returns the current time, via the get_time coderef. The main use for this
678             # level of indirection is (a) supporting Time::HiRes if it's installed, and
679             # otherwise falling back to the standard time function, and (b) making it
680             # possible to mock time, which we'll need in the tests.
681              
682             sub _now {
683 177     177   723 my ($self) = @_;
684              
685 177   33     334 $self->{get_time} ||= $self->_default_get_time;
686 177         539 return $self->{get_time}->();
687             }
688              
689             sub _default_get_time {
690 21         2392 eval { require Time::HiRes; Time::HiRes::time() }
  21         5709  
691             ? \&Time::HiRes::time
692 3 100   3   7 : sub { time };
  21     21   37  
693             }
694              
695             sub DESTROY {
696 19     19   20501 my ($self) = @_;
697              
698 19         68 $self->stop_timing;
699             }
700              
701             =head2 Timing other people's code
702              
703             Adding calls to L throughout your code is all very well, but
704             sometimes you want to time a small handful of methods deep in someone else's
705             code (or deep in I code - same difference). By carefully targeting only
706             a few methods to time, you can avoid the pitfalls of profiling with
707             L, where code that does zillions of fast method calls will
708             appear to be much slower than it is when not profiling.
709              
710             =head3 time_function
711              
712             In: $function_name_or_coderef
713             In: %args (optional)
714             Out: \&wrapped_function
715              
716             Supplied with a function name or a coderef, and an optional hash of arguments,
717             wraps it with a temporary shim that records the time spent inside this
718             function. Details of the functions called are included between milestones in
719             the resulting report.
720              
721             Returns the resulting wrapped function. This is useful if you supplied a coderef
722             to be wrapped with timing code.
723              
724             Optional arguments are as follows:
725              
726             =over
727              
728             =item summarise_arguments
729              
730             A coderef, which will be passed the arguments passed to the function,
731             and which should return a scalar that will be included in the report.
732              
733             =item summarise_calls
734              
735             If set to a true value, repeated calls to this function will be summarised
736             rather than listed individually: the first time a function call is found, it
737             will also mention all subsequent calls.
738              
739             This can combine with C: calls which result in an
740             identical return value from that coderef will be combined.
741              
742             =item report_name_as
743              
744             If specified, the name to use in reports instead of the default name.
745              
746             =back
747              
748             The exact behaviour of this function depends on the nature of the first
749             argument, $function_name_or_coderef.
750              
751             =over
752              
753             =item *
754              
755             If you specify a fully-qualified function name (e.g.
756             C), time_function will look for that exact
757             function, insert a wrapper into the symbol table, and use that full name in
758             reports by default.
759              
760             =item *
761              
762             If you specify an unqualified function name (e.g. C), it is assumed
763             to be a function in the calling package, insert a wrapper into the symbol table,
764             and the unqualified name will be used in reports by default.
765              
766             =item *
767              
768             If you specify a coderef, the symbol table will not be modified, and the default
769             name in reports will be of the form C - so if you're passing
770             more than one coderef to time_function, strongly consider overriding the
771             default name.
772              
773             =back
774              
775             When L is called, all wrapping in the symbol table will be undone;
776             but note that this means "what was in the symbol table when time_function was
777             called will be put back". So if you decide that you want to time the same
778             function with two separate Timer::Milestones objects, it is very important to
779             stop timing in the reverse order that you started timing.
780              
781             =cut
782              
783             sub time_function {
784 18     18 1 1776 my ($self, $function_name_or_coderef, %args) = _object_and_arguments(@_);
785              
786 18         37 my ($function_name, $orig_code, $modify_symbol_table);
787              
788             # If we were passed a coderef, that's simple enough.
789 18 100       39 if (ref($function_name_or_coderef) eq 'CODE') {
790 2         4 $orig_code = $function_name_or_coderef;
791 2         14 $function_name = sprintf('CODE(0x%x)', refaddr($orig_code));
792             } else {
793             # OK, find the code corresponding to this function name.
794 16         55 $function_name = $function_name_or_coderef;
795 16         23 $modify_symbol_table = 1;
796              
797             # If the function is unqualified, look for it in the calling package.
798 16 100       55 if ($function_name !~ /::/) {
799 4   33     23 $args{report_name_as} ||= $function_name;
800 4         9 my ($package) = caller();
801 4         17 $function_name = $package . '::' . $function_name;
802             }
803              
804             # There had better be a function of this name.
805 5     5   41 no strict 'refs';
  5         19  
  5         241  
806 16         22 $orig_code = \&{$function_name};
  16         59  
807 5     5   33 use strict 'refs';
  5         41  
  5         1536  
808 16 100       39 if (!defined &$orig_code) {
809 2         22 die "No such function as $function_name";
810             }
811             }
812              
813             # OK, generate a wrapper.
814             my $wrapper = sub {
815             # Remember how this function was called.
816 56     56   2979 my @args = @_;
817 56         95 my $wantarray = wantarray;
818              
819             # Take a snapshot before we called it.
820 56   100     199 push @{ $self->{milestones}[-1]{function_calls} ||= [] },
821             my $function_call = {
822             function_name => $function_name,
823             started => $self->_now,
824             maybe report_name_as => $args{report_name_as},
825 56         74 };
826              
827             # Remember that we want to summarise these calls if necessary.
828 56 100       211 if ($args{summarise_calls}) {
829 29         49 $function_call->{summarise_calls} = 1;
830             }
831              
832             # Include a summary of the arguments provided if necessary.
833 56 100       116 if ($args{summarise_arguments}) {
834             $function_call->{argument_summary}
835 21         43 = $args{summarise_arguments}->(@args);
836             }
837              
838             # Call it.
839 56         161 my ($scalar_return, @list_return);
840 56 100       122 if ($wantarray) {
    100          
841 1         3 @list_return = $orig_code->(@args);
842             } elsif (defined $wantarray) {
843 8         19 $scalar_return = $orig_code->(@args);
844             } else {
845 47         90 $orig_code->(@args);
846             }
847              
848             # Take a snapshot at the end.
849 56         763 $function_call->{ended} = $self->_now;
850              
851             # And return the original return values.
852 56 100       140 if ($wantarray) {
    100          
853 1         6 return @list_return;
854             } elsif (defined $wantarray) {
855 8         35 return $scalar_return;
856             } else {
857 47         104 return;
858             }
859 16         109 };
860              
861             # Maybe install that wrapper (and remember to uninstall it later).
862 16 100       54 if ($modify_symbol_table) {
863 5     5   37 no strict 'refs';
  5         10  
  5         154  
864 5     5   26 no warnings 'redefine';
  5         17  
  5         265  
865 14         20 *{$function_name} = $wrapper;
  14         46  
866 5     5   31 use warnings 'redefine';
  5         28  
  5         222  
867 5     5   79 use strict 'refs';
  5         13  
  5         491  
868              
869 14   100     26 push @{ $self->{wrapped_functions} ||= [] },
  14         91  
870             {
871             function_name => $function_name,
872             orig_code => $orig_code,
873             };
874             }
875              
876             # And return the wrapper, in case the calling code wants to use it
877             # (most obviously if it was a coderef that wasn't added to the symbol
878             # table)
879 16         50 return $wrapper;
880             }
881              
882             =head1 SEE ALSO
883              
884             L, which is simpler but more verbose.
885              
886             L, which does some similar things.
887              
888             L, which is probably worth using as a first pass, even if you
889             don't necessarily trust its idea of what's I slow.
890              
891             =head1 AUTHOR
892              
893             Sam Kington
894              
895             The source code for this module is hosted on GitHub
896             L - this is probably the
897             best place to look for suggestions and feedback.
898              
899             =head1 COPYRIGHT
900              
901             Copyright (c) 2020 Sam Kington.
902              
903             =head1 LICENSE
904              
905             This library is free software and may be distributed under the same terms as
906             perl itself.
907              
908             =cut
909              
910             1;