File Coverage

blib/lib/Log/Any/Progress.pm
Criterion Covered Total %
statement 17 87 19.5
branch 0 36 0.0
condition 0 18 0.0
subroutine 6 10 60.0
pod 4 4 100.0
total 27 155 17.4


line stmt bran cond sub pod time code
1             package Log::Any::Progress;
2              
3 1     1   68227 use 5.006;
  1         5  
4 1     1   5 use strict;
  1         2  
  1         33  
5 1     1   5 use warnings;
  1         2  
  1         55  
6              
7             =head1 NAME
8              
9             Log::Any::Progress - log incremental progress using Log::Any
10              
11             =head1 VERSION
12              
13             Version 1.00
14              
15             =cut
16              
17             our $VERSION = '1.00';
18              
19             =head1 SYNOPSIS
20              
21             use Log::Any::Progress;
22              
23             use Log::Any::Adapter 'Stderr';
24              
25             my $progress = Log::Any::Progress->new(
26             count => $num_things_to_do,
27             prefix => 'Processing widgets',
28             );
29              
30             foreach my $thing_to_do (@things_to_do) {
31             do_the_thing($thing_to_do);
32             $progress->update;
33             }
34              
35             =head1 DESCRIPTION
36              
37             This module makes it easy to use L to log incremental
38             progress, similar in concept to L. It can be
39             useful for monitoring the progress of a long-running process and to
40             get an idea of how long that process might take to finish.
41              
42             It is generally applied to a processing loop. In the typical case
43             where the expected number of iterations is known in advance, it
44             produces output containing the iteration count, percent completion,
45             elapsed time, average time per iteration, and estimated time remaining.
46             For example:
47              
48             Progress: Iteration:0/5 0% STARTING
49             Progress: Iteration:1/5 20% Elapsed:2.000s Avg:2.000s Remaining:8.001s
50             Progress: Iteration:2/5 40% Elapsed:4.001s Avg:2.000s Remaining:6.001s
51             Progress: Iteration:3/5 60% Elapsed:6.001s Avg:2.000s Remaining:4.001s
52             Progress: Iteration:4/5 80% Elapsed:8.001s Avg:2.000s Remaining:2.000s
53             Progress: Iteration:5/5 100% FINISHED Elapsed:10.002s Avg:2.000s
54              
55             The remaining time estimate as of any particular iteration is a
56             simple linear calculation based on the average time per iteration up
57             to that point, and the number of remaining iterations.
58              
59             If the expected number of iterations is not known in advance, it still
60             reports on incremental progress, but cannot compute either percent
61             completion or estimated remaining time. For example:
62              
63             Progress: Iteration:0 STARTING
64             Progress: Iteration:1 Elapsed:2.000s Avg:2.000s
65             Progress: Iteration:2 Elapsed:4.001s Avg:2.000s
66             Progress: Iteration:3 Elapsed:6.001s Avg:2.000s
67             Progress: Iteration:4 Elapsed:8.001s Avg:2.000s
68             Progress: Iteration:5 Elapsed:10.001s Avg:2.000s
69              
70             =cut
71              
72 1     1   492 use Log::Any ();
  1         10426  
  1         29  
73 1     1   605 use Time::HiRes qw( gettimeofday tv_interval );
  1         1404  
  1         4  
74              
75             use constant {
76 1         924 DEFAULT_LOG_LEVEL => 'infof',
77             DEFAULT_LOG_LEVEL_START_FINISH => 'noticef',
78             DEFAULT_MIN_SEC_BETWEEN_MESSAGES => 10,
79             DEFAULT_PREFIX => 'Progress',
80 1     1   241 };
  1         2  
81              
82             =head1 METHODS
83              
84             =head2 new
85              
86             my $progress = Log::Any::Progress->new(
87              
88             count => $num_things_to_do, # mandatory
89              
90             delayed_start => 1,
91             logger => $logger,
92             log_level => 'info',
93             log_level_start_finish => 'notice',
94             min_sec_between_messages => 10,
95             prefix => 'Processing widgets',
96             );
97              
98             Create a new object for logging incremental progress. Options include:
99              
100             =over 4
101              
102             =item count
103              
104             A mandatory non-zero count of the expected number of iterations for
105             progress tracking.
106              
107             Specifying C<-1> indicates that the expected number of iterations is
108             unknown, in which case abbreviated statistics will be logged for each
109             iteration (percent completion and estimated finish time cannot be
110             computed without knowing the expected number of iterations in advance).
111              
112             =item delayed_start
113              
114             An optional boolean value controlling whether or not L should
115             be automatically called at time of object construction. It defaults
116             to false, in which case L is automatically called, assuming
117             that progress tracking will commence immediately after.
118              
119             Specifying a true value for C will prevent L
120             from being automatically called, in which case it should be explicitly
121             called just before progress iteration begins.
122              
123             =item logger
124              
125             An optional L logger object to use for logging.
126              
127             If not specified, a logger object will be obtained via
128             C<< Log::Any->get_logger() >>, which will in turn use whatever
129             L might be configured.
130              
131             If not specifying a logger object, you will want to make sure that
132             some adapter other than the default L
133             adapter is configured (for example, L),
134             otherwise no log messages will be emitted.
135              
136             =item log_level
137              
138             An optional L log level for the incremental progress lines.
139             It defaults to C.
140              
141             Valid log levels include:
142              
143             trace
144             debug
145             info (inform)
146             notice
147             warning (warn)
148             error (err)
149             critical (crit, fatal)
150             alert
151             emergency
152              
153             =item log_level_start_finish
154              
155             An optional L log level for the start and finish progress
156             lines. It defaults to C.
157              
158             Valid log levels include:
159              
160             trace
161             debug
162             info (inform)
163             notice
164             warning (warn)
165             error (err)
166             critical (crit, fatal)
167             alert
168             emergency
169              
170             =item min_sec_between_messages
171              
172             An optional value for the minimum number of seconds to wait before
173             emitting the next incremental progress log message (as a result of
174             calling L). Values specifying fractional seconds are allowed
175             (e.g. C<0.5>). It defaults to C<10> seconds.
176              
177             Setting C appropriately can be used to
178             control log verbosity in cases where many hundreds or thousands of
179             iterations are being processed and it's not necessary to report after
180             each iteration. Setting it to C<0> will result in every incremental
181             progress message will be emitted.
182              
183             =item prefix
184              
185             An optional string which will be used to prefix each logged message.
186             It defaults to C.
187              
188             =back
189              
190             =cut
191              
192             sub new
193             {
194 0     0 1   my $class_or_instance = shift;
195              
196 0 0 0       my %args = @_ == 1 && ref $_[0] eq 'HASH' ? %{$_[0]} : @_;
  0            
197              
198 0   0       my $logger = $args{logger} || Log::Any->get_logger;
199              
200 0 0         die $logger->fatal('No "count" specified') unless $args{count};
201              
202 0   0       my $class = ref $class_or_instance || $class_or_instance;
203 0           my $self = bless {
204             delayed_start => 0,
205             logger => $logger,
206             log_level => DEFAULT_LOG_LEVEL,
207             log_level_start_finish => DEFAULT_LOG_LEVEL_START_FINISH,
208             min_sec_between_messages => DEFAULT_MIN_SEC_BETWEEN_MESSAGES,
209             prefix => DEFAULT_PREFIX,
210             %args,
211             _initialized => 0,
212             _finished => 0,
213             _current_iteration => 0,
214             }, $class;
215              
216 0           for ( qw( log_level log_level_start_finish ) ) {
217 0 0         $self->{$_} .= "f" unless $self->{$_} =~ /f\z/;
218             }
219              
220 0           $self->{_format} = $self->{prefix} . ': Iteration:%d/%d %.0f%% Elapsed:%s Avg:%s Remaining:%s';
221 0           $self->{_format_nocount} = $self->{prefix} . ': Iteration:%d Elapsed:%s Avg:%s';
222              
223 0 0         $self->start unless $self->{delayed_start};
224              
225 0           return $self;
226             }
227              
228             =head2 start
229              
230             my $progress = Log::Any::Progress->new(
231             count => $num_things_to_do,
232             delayed_start => 1, # don't start the timer yet
233             );
234              
235             # Do some other work here that might take some time...
236              
237             $progress->start;
238              
239             foreach my $thing_to_do (@things_to_do) {
240             do_the_thing($thing_to_do);
241             $progress->update;
242             }
243              
244             Initialize (or reinitialize) the progress object by resetting the
245             start time, elapsed time, etc.
246              
247             This is normally called automatically at object construction time
248             unless L is specified, in which case it should be
249             called explicitly at the appropriate time.
250              
251             Initializing the progress object (whether done automatically or
252             manually) causes the first log message to be emitted.
253              
254             =cut
255              
256 0     0 1   sub start { shift->update(0) }
257              
258             =head2 update
259              
260             my $progress = Log::Any::Progress->new(
261             count => $num_things_to_do,
262             );
263              
264             foreach my $thing_to_do (@things_to_do) {
265             do_the_thing($thing_to_do);
266             $progress->update;
267             }
268              
269             Update the iteration count within the progress object and maybe emit
270             a corresponding log message showing the current progress statistics
271             (depending on timing and the value of L).
272              
273             Calling C with no arguments increments the internal iteration
274             count by one. A positive interger may be passed as an argument to
275             explicitly update the iteration count to a particular value.
276              
277             Once the iteration count reaches the specified L value, the
278             progress is considered to be complete and a final log message is
279             emitted with summary statistics, and subsequent calls to C
280             will have no effect.
281              
282             =cut
283              
284             sub update
285             {
286 0     0 1   my ($self, $current_iteration) = @_;
287              
288 0 0         if (defined $current_iteration) {
289 0           $self->{_current_iteration} = $current_iteration;
290             }
291             else {
292 0           $current_iteration = ++$self->{_current_iteration};
293             }
294              
295 0 0         return if $current_iteration < 0;
296              
297             # Allow for reinitialization even if finished:
298 0 0 0       return if $self->{_finished} && $current_iteration != 0;
299              
300 0           my $now = [ gettimeofday ];
301              
302 0           my $have_count = $self->{count} > 0;
303              
304 0 0 0       if ($current_iteration == 0 || !$self->{_initialized}) {
305 0           $self->{_time_elapsed} = 0;
306 0           $self->{_time_start} = $now;
307 0           $self->{_time_last_log} = $now;
308 0           $self->{_current_iteration} = $current_iteration;
309 0           $self->{_finished} = 0;
310 0           $self->{_initialized} = 1;
311              
312 0           my $level = $self->{log_level_start_finish};
313             my $format = $have_count
314             ? $self->{prefix} . ': Iteration:0/%d 0%% STARTING'
315 0 0         : $self->{prefix} . ': Iteration:0 STARTING';
316             $self->{logger}->$level(
317             $format,
318             $self->{count},
319 0           );
320             }
321              
322 0 0         return if $current_iteration == 0;
323              
324 0           $self->{_time_elapsed} = tv_interval $self->{_time_start}, $now;
325              
326 0           my $elapsed_sec = $self->{_time_elapsed};
327 0           my $elapsed = $self->format_seconds($elapsed_sec);
328              
329 0           my $avg_sec = $self->{_time_elapsed} / $current_iteration;
330 0           my $avg = $self->format_seconds($avg_sec);
331              
332 0 0 0       if ($have_count && $current_iteration >= $self->{count}) {
333              
334 0           $self->{_current_iteration} = $current_iteration = $self->{count};
335 0           $self->{_finished} = 1;
336              
337 0           my $level = $self->{log_level_start_finish};
338             $self->{logger}->$level(
339             $self->{prefix} . ': Iteration:%d/%d 100%% FINISHED Elapsed:%s Avg:%s',
340             $current_iteration,
341             $self->{count},
342 0           $elapsed,
343             $avg,
344             );
345             }
346             else {
347              
348 0           my $elapsed_since_last_log = tv_interval $self->{_time_last_log}, $now;
349 0 0         return if $elapsed_since_last_log < $self->{min_sec_between_messages};
350 0           $self->{_time_last_log} = $now;
351              
352 0           my $remaining; # unused if no count
353 0 0         if ($have_count) {
354 0           my $remaining_sec = ($self->{count}-$current_iteration) * $avg_sec;
355 0           $remaining = $self->format_seconds($remaining_sec);
356             }
357              
358 0           my $level = $self->{log_level};
359 0 0         my $format = $have_count ? $self->{_format} : $self->{_format_nocount};
360             $self->{logger}->$level(
361             $format,
362             $current_iteration,
363             $have_count ? (
364             $self->{count},
365             100 * $current_iteration / $self->{count},
366 0 0         ) : (),
367             $elapsed,
368             $avg,
369             $remaining,
370             );
371             }
372              
373 0           return;
374             }
375              
376             =head2 format_seconds
377              
378             my $string = $progress->format_seconds($elapsed_seconds);
379              
380             This methods formats the elapsed time, average time, and remaining time
381             values from seconds into something more easily readable. For example,
382             C<10000> seconds is formatted as C<2h46m40.000s>.
383              
384             It can be overridden in a subclass if desired.
385              
386             =cut
387              
388             sub format_seconds
389             {
390 0     0 1   my ($self, $sec) = @_;
391              
392 0           my $formatted = '';
393              
394 0 0         if ($sec >= 60) {
395 0           my $min = int($sec / 60);
396 0           $sec = $sec - ($min * 60);
397              
398 0 0         if ($min >= 60) {
399 0           my $hours = int($min / 60);
400 0           $min = $min - ($hours * 60);
401              
402 0 0         if ($hours >= 24) {
403 0           my $days = int($hours / 24);
404 0           $hours = $hours - ($days * 24);
405              
406 0           $formatted .= sprintf '%dd', $days;
407             }
408              
409 0           $formatted .= sprintf '%dh', $hours;
410             }
411              
412 0           $formatted .= sprintf '%dm', $min;
413             }
414              
415 0           $formatted .= sprintf '%.3fs', $sec;
416              
417 0           return $formatted;
418             }
419              
420             =head1 AUTHOR
421              
422             Larry Leszczynski, C<< >>
423              
424             =head1 BUGS
425              
426             Please report any bugs or feature requests through the web interface at
427             L.
428              
429             I will be notified, and then you'll automatically be notified of
430             progress on your bug as I make changes.
431              
432             =head1 SUPPORT
433              
434             You can find documentation for this module with the perldoc command.
435              
436             perldoc Log::Any::Progress
437              
438             You can also look for information at:
439              
440             =over 4
441              
442             =item * GitHub (report bugs or suggest features here)
443              
444             L.
445              
446             =item * CPAN Ratings
447              
448             L
449              
450             =item * Search CPAN
451              
452             L
453              
454             =back
455              
456             =head1 LICENSE AND COPYRIGHT
457              
458             This software is Copyright (c) 2023 by Larry Leszczynski.
459              
460             This is free software, licensed under:
461              
462             The Artistic License 2.0 (GPL Compatible)
463              
464             =cut
465              
466             1; # End of Log::Any::Progress
467