Files
percona-toolkit/lib/Progress.pm
2016-02-05 13:35:44 -03:00

217 lines
8.2 KiB
Perl

# This program is copyright 2010-2011 Percona Ireland Ltd.
# Feedback and improvements are welcome.
#
# THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
# MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, version 2; OR the Perl Artistic License. On UNIX and similar
# systems, you can issue `man perlgpl' or `man perlartistic' to read these
# licenses.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# Place, Suite 330, Boston, MA 02111-1307 USA.
# ###########################################################################
# Progress package
# ###########################################################################
{
# Package: Progress
# Progress encapsulates a progress report.
package Progress;
use strict;
use warnings FATAL => 'all';
use English qw(-no_match_vars);
use constant PTDEBUG => $ENV{PTDEBUG} || 0;
# This module encapsulates a progress report. To create a new object, pass in
# the following:
# jobsize Must be a number; defines the job's completion condition
# report How and when to report progress. Possible values:
# percentage: based on the percentage complete.
# time: based on how much time elapsed.
# iterations: based on how many progress updates have happened.
# interval How many of whatever's specified in 'report' to wait before
# reporting progress: report each X%, each X seconds, or each X
# iterations.
#
# The 'report' and 'interval' can also be omitted, as long the following option
# is passed:
# spec An arrayref of [report,interval]. This is convenient to use from a
# --progress command-line option that is an array.
#
# Optional arguments:
# start The start time of the job; can also be set by calling start()
# fraction How complete the job is, as a number between 0 and 1. Updated by
# calling update(). Normally don't specify this.
# name If you want to use the default progress indicator, by default it
# just prints out "Progress: ..." but you can replace "Progress" with
# whatever you specify here.
sub new {
my ( $class, %args ) = @_;
foreach my $arg (qw(jobsize)) {
die "I need a $arg argument" unless defined $args{$arg};
}
if ( (!$args{report} || !$args{interval}) ) {
if ( $args{spec} && @{$args{spec}} == 2 ) {
@args{qw(report interval)} = @{$args{spec}};
}
else {
die "I need either report and interval arguments, or a spec";
}
}
my $name = $args{name} || "Progress";
$args{start} ||= time();
my $self;
$self = {
last_reported => $args{start},
fraction => 0, # How complete the job is
callback => sub {
my ($fraction, $elapsed, $remaining) = @_;
printf STDERR "$name: %3d%% %s remain\n",
$fraction * 100,
Transformers::secs_to_time($remaining);
},
%args,
};
return bless $self, $class;
}
# Validates the 'spec' argument passed in from --progress command-line option.
# It calls die with a trailing newline to avoid auto-adding the file/line.
sub validate_spec {
shift @_ if $_[0] eq 'Progress'; # Permit calling as Progress-> or Progress::
my ( $spec ) = @_;
if ( @$spec != 2 ) {
die "spec array requires a two-part argument\n";
}
if ( $spec->[0] !~ m/^(?:percentage|time|iterations)$/ ) {
die "spec array's first element must be one of "
. "percentage,time,iterations\n";
}
if ( $spec->[1] !~ m/^\d+$/ ) {
die "spec array's second element must be an integer\n";
}
}
# Specify your own custom way to report the progress. The default is to print
# the percentage to STDERR. This is created in the call to new(). The
# callback is a subroutine that will receive the fraction complete from 0 to
# 1, seconds elapsed, seconds remaining, and the Unix timestamp of when we
# expect to be complete.
sub set_callback {
my ( $self, $callback ) = @_;
$self->{callback} = $callback;
}
# Set the start timer of when work began. You can either set it to time() which
# is the default, or pass in a value.
sub start {
my ( $self, $start ) = @_;
$self->{start} = $self->{last_reported} = $start || time();
$self->{first_report} = 0;
}
# Provide a progress update. Pass in a callback subroutine which this code can
# use to ask how complete the job is. This callback will be called as
# appropriate. For example, in time-lapse updating, it won't be called unless
# it's time to report the progress. The callback has to return a number that's
# of the same dimensions as the jobsize. For example, if a text file has 800
# lines to process, that's a jobsize of 800; the callback should return how
# many lines we're done processing -- a number between 0 and 800. You can also
# optionally pass in the current time, but this is only for testing.
sub update {
my ( $self, $callback, %args ) = @_;
my $jobsize = $self->{jobsize};
my $now ||= $args{now} || time;
$self->{iterations}++; # How many updates have happened;
# The caller may want to report something special before the actual
# first report ($callback) if, for example, they know that the wait
# could be long. This is called only once; subsequent reports will
# come from $callback after 30s, or whatever the interval is.
if ( !$self->{first_report} && $args{first_report} ) {
$args{first_report}->();
$self->{first_report} = 1;
}
# Determine whether to just quit and return...
if ( $self->{report} eq 'time'
&& $self->{interval} > $now - $self->{last_reported}
) {
return;
}
elsif ( $self->{report} eq 'iterations'
&& ($self->{iterations} - 1) % $self->{interval} > 0
) {
return;
}
$self->{last_reported} = $now;
# Get the updated status of the job
my $completed = $callback->();
$self->{updates}++; # How many times we have run the update callback
# Sanity check: can't go beyond 100%
return if $completed > $jobsize;
# Compute the fraction complete, between 0 and 1.
my $fraction = $completed > 0 ? $completed / $jobsize : 0;
# Now that we know the fraction completed, we can decide whether to continue
# on and report, for percentage-based reporting. Have we crossed an
# interval-percent boundary since the last update?
if ( $self->{report} eq 'percentage'
&& $self->fraction_modulo($self->{fraction})
>= $self->fraction_modulo($fraction)
) {
# We're done; we haven't advanced progress enough to report.
$self->{fraction} = $fraction;
return;
}
$self->{fraction} = $fraction;
# Continue computing the metrics, and call the callback with them.
my $elapsed = $now - $self->{start};
my $remaining = 0;
my $eta = $now;
if ( $completed > 0 && $completed <= $jobsize && $elapsed > 0 ) {
my $rate = $completed / $elapsed;
if ( $rate > 0 ) {
$remaining = ($jobsize - $completed) / $rate;
$eta = $now + int($remaining);
}
}
$self->{callback}->($fraction, $elapsed, $remaining, $eta, $completed);
}
# Returns the number rounded to the nearest lower $self->{interval}, for use
# with interval-based reporting. For example, when you want to report every 5%,
# then 0% through 4% all return 0%; 5% through 9% return 5%; and so on. The
# number needs to be passed as a fraction from 0 to 1.
sub fraction_modulo {
my ( $self, $num ) = @_;
$num *= 100; # Convert from fraction to percentage
return sprintf('%d',
sprintf('%d', $num / $self->{interval}) * $self->{interval});
}
sub _d {
my ($package, undef, $line) = caller 0;
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
map { defined $_ ? $_ : 'undef' }
@_;
print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
}
1;
}
# ###########################################################################
# End Progress package
# ###########################################################################