File Coverage

blib/lib/Timer/Milestones.pm
Criterion Covered Total %
statement 243 248 97.9
branch 85 88 96.5
condition 30 43 69.7
subroutine 40 41 97.5
pod 7 7 100.0
total 405 427 94.8


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