File Coverage

blib/lib/IO/AsyncX/SharedTimer.pm
Criterion Covered Total %
statement 39 39 100.0
branch 3 6 50.0
condition 7 11 63.6
subroutine 13 13 100.0
pod 3 4 75.0
total 65 73 89.0


line stmt bran cond sub pod time code
1             package IO::AsyncX::SharedTimer;
2             # ABSTRACT: Low-accuracy shared timers for IO::Async
3 2     2   36161 use strict;
  2         4  
  2         87  
4 2     2   10 use warnings;
  2         3  
  2         61  
5              
6 2     2   919 use parent qw(IO::Async::Notifier);
  2         554  
  2         13  
7              
8             our $VERSION = '0.001';
9              
10             =head1 NAME
11              
12             IO::AsyncX::SharedTimer - provides L timers which sacrifice accuracy for performance
13              
14             =head1 VERSION
15              
16             version 0.001
17              
18             =head1 SYNOPSIS
19              
20             # Needs to be added to a loop before you can
21             # call any other methods
22             my $loop = IO::Async::Loop->new;
23             $loop->add(
24             my $timer = IO::AsyncX::SharedTimer->new(
25             # Combine timers into 50ms buckets, and
26             # use cached value for ->now with 50ms expiry
27             resolution => 0.050,
28             )
29             );
30              
31             # Report current time, accurate to ~50ms (defined
32             # by the resolution parameter, as above)
33             use feature qw(say);
34             say "Time is roughly " . $timer->now;
35              
36             # Set a timeout for ~30s on an I/O operation
37             Future->wait_any(
38             $client->read_until(qr/\n/),
39             $timer->timeout_future(after => 30)
40             )->get;
41              
42             =head1 DESCRIPTION
43              
44             This module provides various time-related utility methods for use
45             with larger L applications.
46              
47             In situations where you have many related timers - connection expiry,
48             for example - there may be some overhead in having each of these in the
49             timer priority queue as a separate event. Sometimes the exact trigger
50             time is not so important, which is where this class comes in. You get
51             to specify an accuracy, and all timers which would occur within the
52             same window of time will be grouped together as a single timer event.
53              
54             This may reduce calls to poll/epoll and timer management overhead, but
55             please benchmark the difference before assuming that this module will
56             be worth using - for some applications the overhead this introduces will
57             outweigh any potential benefits.
58              
59             One benchmark gave the following results for 1ms resolution across 100
60             timers set to rand(1) seconds:
61              
62             Rate normal shared
63             normal 32.9/s -- -50%
64             shared 65.7/s 100% --
65              
66             See the examples/ directory for code.
67              
68             =cut
69              
70 2     2   28022 use Time::HiRes ();
  2         3  
  2         26  
71 2     2   879 use curry::weak;
  2         1399  
  2         280  
72              
73             =head1 METHODS
74              
75             =cut
76              
77             =head2 configure
78              
79             Change the current resolution.
80              
81             Takes one named parameter:
82              
83             =over 4
84              
85             =item * resolution - the resolution for timers and L, in seconds
86              
87             =back
88              
89             =cut
90              
91             sub configure {
92 2     2 1 7310 my ($self, %args) = @_;
93 2 50       9 if(exists $args{resolution}) {
94 2         12 $self->{resolution} = delete $args{resolution};
95             }
96 2         14 $self->SUPER::configure(%args);
97             }
98              
99             =head2 resolution
100              
101             Returns the current resolution.
102              
103             =cut
104              
105 31     31 1 1007 sub resolution { shift->{resolution} }
106              
107             =head2 now
108              
109             Returns an approximation of the current time.
110              
111             On first call, it will return (and cache) the value provided by
112             the L C
113              
114             Subsequent calls will return this same value. The cached value expires
115             after L seconds - note that the expiry happens via the event
116             loop so if your code does not cede control back to the main event loop
117             in a timely fashion, the cached value will not expire. Put another way:
118             the value will be cached for at least L seconds.
119              
120             There's a good chance that the method call overhead will incur a heavier
121             performance impact than just calling L C
122             As always, profile and benchmark first.
123              
124             Example usage:
125              
126             my $start = $timer->now;
127             $loop->run;
128             my $elapsed = 1000.0 * ($timer->now - $start);
129             say "Operation took about ${elapsed}ms to complete.";
130              
131             =cut
132              
133             sub now {
134 22     22 1 1002727 my ($self) = @_;
135 22   50     92 $self->{after} ||= ($self->loop or die "Must add this timer to a loop first")->delay_future(
      66        
136             after => $self->resolution
137             )->on_ready($self->curry::weak::expire);
138 22   66     6662 $self->{now} //= Time::HiRes::time;
139             }
140              
141             =head2 delay_future
142              
143             Returns a L which will resolve after approximately L seconds.
144              
145             See the C documentation in L for more details.
146              
147             =cut
148              
149             =head2 timeout_future
150              
151             Returns a L which will fail after approximately L seconds.
152              
153             See the C documentation in L for more details.
154              
155             =cut
156              
157             BEGIN {
158 2     2   7 for my $m (qw(delay_future timeout_future)) {
159 2     2   10 no strict 'refs';
  2         3  
  2         312  
160 4         152 *{__PACKAGE__ . '::' . $m} = sub {
161 11     11   3435 my ($self, %args) = @_;
162 11 50       44 my $at = exists $args{at} ? delete $args{at} : $self->now + delete $args{after}
    50          
163             or die "Invalid or unspecified time";
164             # Resolution may change over time. We don't want to stomp on old values when this happens.
165             # We also want to support both timeout+delay with the same values.
166 11         29 my $bucket = join '-', $m, $self->resolution, int(($at - $^T) / $self->resolution);
167 11         31 my $f = $self->loop->new_future;
168             ($self->{bucket}{$bucket} ||= $self->loop->$m(at => $at)->on_ready(sub {
169 5     5   1502299 delete $self->{bucket}{$bucket}
170 11   66     247 }))->on_ready($f);
171 11         579 $f
172 4         15 };
173             }
174             }
175              
176             sub expire {
177 5     5 0 199613 my ($self) = @_;
178 5         12 delete @{$self}{qw(now after)};
  5         29  
179             }
180              
181             1;
182              
183             __END__