Went with hard/skip/drift in the end.
--
Paul Evans
=== modified file 'lib/IO/Async/Timer/Periodic.pm'
--- lib/IO/Async/Timer/Periodic.pm 2012-03-24 21:08:34 +0000
+++ lib/IO/Async/Timer/Periodic.pm 2012-05-18 22:04:04 +0000
@@ -1,7 +1,7 @@
# You may distribute under the terms of either the GNU General Public License
# or the Artistic License (the same terms as Perl itself)
#
-# (C) Paul Evans, 2009-2011 -- leonerd@leonerd.org.uk
+# (C) Paul Evans, 2009-2012 -- leonerd@leonerd.org.uk
package IO::Async::Timer::Periodic;
@@ -41,9 +41,10 @@
=head1 DESCRIPTION
This subclass of L<IO::Async::Timer> implements repeating events at regular
-clock intervals. The timing is not subject to how long it takes the callback
-to execute, but runs at regular intervals beginning at the time the timer was
-started, then adding each interval thereafter.
+clock intervals. The timing may or may not be subject to how long it takes the
+callback to execute. Iterations may be rescheduled runs at fixed regular
+intervals beginning at the time the timer was started, or by a fixed delay
+after the previous code has finished executing.
For a C<Timer> object that only runs a callback once, after a given delay, see
instead L<IO::Async::Timer::Countdown>. A Countdown timer can also be used to
@@ -90,6 +91,24 @@
by the containing C<Loop> object, and not synchronously by the C<start> method
itself.
+=item reschedule => STRING
+
+Optional. Must be one of C<hard>, C<skip> or C<drift>. Defines the algorithm
+used to reschedule the next invocation.
+
+C<hard> schedules each iteration at the fixed interval from the previous
+iteration's schedule time, ensuring a regular repeating event.
+
+C<skip> schedules similarly to C<hard>, but skips over times that have already
+passed. This matters if the duration is particularly short and there's a
+possibility that times may be missed, or if the entire process is stopped and
+resumed by C<SIGSTOP> or similar.
+
+C<drift> schedules each iteration at the fixed interval from the time that the
+previous iteration's event handler returns. This allows it to slowly drift over
+time and become desynchronised with other events of the same interval or
+multiples/fractions of it.
+
=back
Once constructed, the timer object will need to be added to the C<Loop> before
@@ -97,6 +116,14 @@
=cut
+sub _init
+{
+ my $self = shift;
+ $self->SUPER::_init( @_ );
+
+ $self->{reschedule} = "hard";
+}
+
sub configure
{
my $self = shift;
@@ -128,6 +155,14 @@
$self->{first_interval} = $first_interval;
}
+ if( exists $params{reschedule} ) {
+ my $resched = delete $params{reschedule} || "hard";
+ grep { $_ eq $resched } qw( hard skip drift ) or
+ croak "Expected 'reschedule' to be one of hard, skip, drift";
+
+ $self->{reschedule} = $resched;
+ }
+
unless( $self->can_event( 'on_tick' ) ) {
croak 'Expected either a on_tick callback or an ->on_tick method';
}
@@ -150,12 +185,24 @@
# become start-pending. We'll calculate it properly when it gets added to
# the Loop
if( my $loop = $self->loop ) {
+ my $now = $loop->time;
+ my $resched = $self->{reschedule};
+
if( !defined $self->{next_time} ) {
- $self->{next_time} = $loop->time + $self->_next_interval;
+ $self->{next_time} = $now + $self->_next_interval;
}
- else {
+ elsif( $resched eq "hard" ) {
$self->{next_time} += $self->_next_interval;
}
+ elsif( $resched eq "skip" ) {
+ # How many ticks are needed?
+ my $ticks = POSIX::ceil( $now - $self->{next_time} );
+ # $self->{last_ticks} = $ticks;
+ $self->{next_time} += $self->_next_interval * $ticks;
+ }
+ elsif( $resched eq "drift" ) {
+ $self->{next_time} = $now + $self->_next_interval;
+ }
}
$self->SUPER::start;
@@ -179,9 +226,10 @@
undef $self->{first_interval};
undef $self->{id};
+
+ $self->invoke_event( on_tick => );
+
$self->start;
-
- $self->invoke_event( on_tick => );
} );
}
=== modified file 't/22timer-periodic.t'
--- t/22timer-periodic.t 2012-03-12 18:33:45 +0000
+++ t/22timer-periodic.t 2012-05-18 18:58:19 +0000
@@ -4,7 +4,7 @@
use IO::Async::Test;
-use Test::More tests => 37;
+use Test::More tests => 47;
use Test::Fatal;
use Test::Refcount;
@@ -122,6 +122,52 @@
is_oneref( $timer, 'Timer has refcount 1 finally' );
}
+# reschedule => "skip"
+{
+ my $tick = 0;
+
+ my $timer = IO::Async::Timer::Periodic->new(
+ interval => 1 * AUT,
+ reschedule => "skip",
+
+ on_tick => sub { $tick++ },
+ );
+
+ $loop->add( $timer );
+ $timer->start;
+
+ time_about( sub { wait_for { $tick == 1 } }, 1, 'skip Timer works' );
+
+ ok( $timer->is_running, 'skip Timer is still running' );
+
+ time_about( sub { wait_for { $tick == 2 } }, 1, 'skip Timer ticks a second time' );
+
+ $loop->remove( $timer );
+}
+
+# reschedule => "drift"
+{
+ my $tick = 0;
+
+ my $timer = IO::Async::Timer::Periodic->new(
+ interval => 1 * AUT,
+ reschedule => "drift",
+
+ on_tick => sub { $tick++ },
+ );
+
+ $loop->add( $timer );
+ $timer->start;
+
+ time_about( sub { wait_for { $tick == 1 } }, 1, 'drift Timer works' );
+
+ ok( $timer->is_running, 'drift Timer is still running' );
+
+ time_about( sub { wait_for { $tick == 2 } }, 1, 'drift Timer ticks a second time' );
+
+ $loop->remove( $timer );
+}
+
## Subclass
my $sub_tick = 0;