File Coverage

blib/lib/Algorithm/EventsPerSecond.pm
Criterion Covered Total %
statement 79 79 100.0
branch 27 28 96.4
condition 5 5 100.0
subroutine 16 16 100.0
pod 7 7 100.0
total 134 135 99.2


line stmt bran cond sub pod time code
1             package Algorithm::EventsPerSecond;
2              
3 13     13   332147 use 5.006;
  13         42  
4 13     13   59 use strict;
  13         17  
  13         251  
5 13     13   38 use warnings;
  13         22  
  13         1393  
6              
7             =head1 NAME
8              
9             Algorithm::EventsPerSecond - A sliding-window events-per-second rate counter with a optional XS backend for additional zoomies.
10              
11             =head1 VERSION
12              
13             Version 0.1.0
14              
15             =cut
16              
17             our $VERSION = '0.1.0';
18              
19             our $BACKEND;
20              
21             BEGIN {
22 13     13   40 $BACKEND = 'PP';
23 13 100       1827 unless ( $ENV{ALGORITHM_EVENTSPERSECOND_PP} ) {
24 11         19 local $@;
25 11 50       17 if ( eval { require Algorithm::EventsPerSecond::XS; 1 } ) {
  11         3999  
  11         1564  
26 11         9547 $BACKEND = 'XS';
27             }
28             }
29             }
30              
31             =head1 SYNOPSIS
32              
33             use Algorithm::EventsPerSecond;
34              
35             my $meter = Algorithm::EventsPerSecond->new( window => 10 ); # 10-second window
36              
37             while (my $event = get_next_event()) {
38             $meter->mark; # record one event
39             # $meter->mark(5); # or record several at once
40              
41             printf "current rate: %.2f events/sec\n", $meter->rate;
42             }
43              
44             print "events seen in window: ", $meter->count, "\n";
45             print "lifetime total: ", $meter->total, "\n";
46              
47              
48             =head1 DESCRIPTION
49              
50             Algorithm::EventsPerSecond keeps per-second counts in a fixed-size ring buffer and
51             reports the average event rate over the most recent N seconds (the
52             "window"). Memory use is constant regardless of event volume, and both
53             C and C are O(1) averaged out over time.
54              
55             =head1 METHODS
56              
57             =head2 new( window => $seconds )
58              
59             Construct a meter. C is the length of the averaging window in
60             seconds and defaults to 60.
61              
62             =cut
63              
64             sub new {
65 57     57 1 23946 my ( $class, %args ) = @_;
66              
67 57   100     166 my $window = $args{window} // 60;
68 57 100 100     482 die "window must be a positive integer" unless $window =~ /^\d+$/ && $window > 0;
69              
70 49         125 my $self = {
71             window => $window,
72             total => 0, # lifetime event count
73             started => time(),
74             };
75              
76 49 100       264 if ( $BACKEND eq 'XS' ) {
77             # packed int64_t ring buffers, scanned in C
78 29         166 $self->{buckets} = "\0" x ( $window * 8 );
79 29         73 $self->{stamps} = "\0" x ( $window * 8 );
80             } else {
81 20         124 $self->{buckets} = [ (0) x $window ]; # counts, indexed by (epoch_sec % window)
82 20         103 $self->{stamps} = [ (0) x $window ]; # epoch second each bucket belongs to
83             }
84              
85 49         142 return bless $self, $class;
86             } ## end sub new
87              
88             # Internal, PP backend: get the bucket for the current second, clearing it if stale.
89             sub _bucket_index {
90 1595     1595   1815 my ( $self, $now_sec ) = @_;
91 1595         1930 my $i = $now_sec % $self->{window};
92 1595 100       2417 if ( $self->{stamps}[$i] != $now_sec ) {
93 1589         1834 $self->{buckets}[$i] = 0;
94 1589         1878 $self->{stamps}[$i] = $now_sec;
95             }
96 1595         1979 return $i;
97             }
98              
99             =head2 mark( [$count] )
100              
101             Record one event, or C<$count> events. C<$count> must be a
102             non-negative integer; zero is a no-op. Anything else dies. Returns the
103             meter object, so calls can be chained.
104              
105             =cut
106              
107             sub _mark_pp {
108 1599     1599   14088 my ( $self, $count ) = @_;
109 1599 100       2061 if ( defined $count ) {
110 1595 100       3428 die "count must be a non-negative integer" unless $count =~ /^\d+$/;
111             } else {
112 4         5 $count = 1;
113             }
114              
115 1595         2166 my $now_sec = int( time() );
116 1595         3435 my $i = $self->_bucket_index($now_sec);
117              
118 1595         1844 $self->{buckets}[$i] += $count;
119 1595         1672 $self->{total} += $count;
120              
121 1595         1864 return $self;
122             } ## end sub _mark_pp
123              
124             sub _mark_xs {
125 1604     1604   13146 my ( $self, $count ) = @_;
126 1604 100       1809 if ( defined $count ) {
127 1598 100       2802 die "count must be a non-negative integer" unless $count =~ /^\d+$/;
128             } else {
129 6         8 $count = 1;
130             }
131              
132             Algorithm::EventsPerSecond::XS::_xs_mark( $self->{buckets}, $self->{stamps},
133 1600         2300 $self->{window}, int( time() ), $count, );
134 1598         3313 $self->{total} += $count;
135              
136 1598         1804 return $self;
137             } ## end sub _mark_xs
138              
139             =head2 count
140              
141             Number of events recorded within the current window.
142              
143             =cut
144              
145             sub _count_pp {
146 2021     2021   158240 my ($self) = @_;
147              
148 2021         2717 my $now_sec = int( time() );
149 2021         4545 my $window = $self->{window};
150 2021         2341 my $oldest = $now_sec - $window + 1;
151              
152 2021         2052 my $sum = 0;
153 2021         2812 for my $i ( 0 .. $window - 1 ) {
154             $sum += $self->{buckets}[$i]
155 282755 100       381734 if $self->{stamps}[$i] >= $oldest;
156             }
157 2021         3239 return $sum;
158             } ## end sub _count_pp
159              
160             sub _count_xs {
161 2024     2024   151834 my ($self) = @_;
162              
163 2024         2494 my $now_sec = int( time() );
164             return Algorithm::EventsPerSecond::XS::_xs_count( $self->{buckets}, $self->{stamps},
165 2024         6907 $self->{window}, $now_sec - $self->{window} + 1,
166             );
167             }
168              
169             if ( $BACKEND eq 'XS' ) {
170             *mark = \&_mark_xs;
171             *count = \&_count_xs;
172             } else {
173             *mark = \&_mark_pp;
174             *count = \&_count_pp;
175             }
176              
177             =head2 rate
178              
179             Average events per second over the window. If the meter has been alive
180             for less time than the window, the elapsed lifetime is used instead, so
181             early readings are not artificially deflated.
182              
183             =cut
184              
185             sub rate {
186 12     12 1 31 my ($self) = @_;
187              
188 12         20 my $elapsed = time() - $self->{started};
189 12 100       43 my $span = $elapsed < $self->{window} ? $elapsed : $self->{window};
190 12 100       39 return 0 if $span <= 0;
191              
192 6         32 return $self->count / $span;
193             }
194              
195             =head2 total
196              
197             Lifetime count of all events ever recorded, regardless of window.
198              
199             =cut
200              
201 4027     4027 1 9029 sub total { $_[0]->{total} }
202              
203             =head2 window
204              
205             The configured window length in seconds.
206              
207             =cut
208              
209 4     4 1 736 sub window { $_[0]->{window} }
210              
211             =head2 reset
212              
213             Clear all counts and restart the clock. Returns the meter object.
214              
215             =cut
216              
217             sub reset {
218 2     2 1 11 my ($self) = @_;
219 2 100       8 if ( $BACKEND eq 'XS' ) {
220 1         3 $self->{buckets} = "\0" x ( $self->{window} * 8 );
221 1         3 $self->{stamps} = "\0" x ( $self->{window} * 8 );
222             } else {
223 1         3 @{ $self->{buckets} } = (0) x $self->{window};
  1         3  
224 1         2 @{ $self->{stamps} } = (0) x $self->{window};
  1         2  
225             }
226 2         3 $self->{total} = 0;
227 2         5 $self->{started} = time();
228 2         8 return $self;
229             } ## end sub reset
230              
231             =head2 backend
232              
233             Returns C<'XS'> when the accelerated backend is in use, C<'PP'> for the
234             pure Perl fallback. May be called as a class or instance method.
235              
236             =cut
237              
238 6     6 1 410076 sub backend { $BACKEND }
239              
240             =head2 simd
241              
242             Returns which SIMD flavor the XS backend was compiled with: C<'AVX2'>,
243             C<'SSE4.2'>, or C<'scalar'> (plain C, left to the compiler's
244             auto-vectorizer). Returns undef when the pure Perl backend is in use.
245              
246             =cut
247              
248             sub simd {
249 4 100   4 1 23 return undef unless $BACKEND eq 'XS';
250 3         17 return Algorithm::EventsPerSecond::XS::_xs_simd();
251             }
252              
253             =head1 ACCELERATION
254              
255             If a working C compiler is available at install time, an accelerated XS
256             backend, L, is built and loaded
257             automatically. It keeps the ring buffer in packed C buffers and
258             scans the window in C, using SIMD (AVX2 or SSE4.2) when the compiler
259             targets a CPU that has it. If the backend cannot be loaded for any
260             reason (not built, no compiler at install time), a pure Perl
261             implementation with identical behavior is used instead.
262              
263             The following control this.
264              
265             =head2 IF_OPT
266              
267             Environment variable read by C: the C<-O> optimization
268             value used when compiling the XS backend during install. C
269             (or C) compiles with C<-O2>. The default is C<-O3>.
270              
271             =head2 IF_ARCH
272              
273             Environment variable read by C: optionally sets the
274             architecture used for the build during the install. C
275             (or C) compiles with C<-march=native>, enabling
276             the SIMD paths the build host supports. When unset the compiler's
277             baseline architecture is used.
278              
279             =head2 PUREPERL_ONLY
280              
281             C skips building the XS backend.
282              
283             =head2 ALGORITHM_EVENTSPERSECOND_PP
284              
285             Environment variable read at runtime: when true, skip the XS backend
286             entirely and use pure Perl.
287              
288             Which backend is in use can be checked via L and L.
289              
290             =head1 AUTHOR
291              
292             Zane C. Bowers-Hadley, C<< >>
293              
294             =head1 BUGS
295              
296             Please report any bugs or feature requests to C, or through
297             the web interface at L. I will be notified, and then you'll
298             automatically be notified of progress on your bug as I make changes.
299              
300              
301              
302              
303             =head1 SUPPORT
304              
305             You can find documentation for this module with the perldoc command.
306              
307             perldoc Algorithm::EventsPerSecond
308              
309              
310             You can also look for information at:
311              
312             =over 4
313              
314             =item * RT: CPAN's request tracker (report bugs here)
315              
316             L
317              
318             =item * CPAN Ratings
319              
320             L
321              
322             =item * Search CPAN
323              
324             L
325              
326             =back
327              
328              
329             =head1 ACKNOWLEDGEMENTS
330              
331              
332             =head1 LICENSE AND COPYRIGHT
333              
334             This software is Copyright (c) 2026 by Zane C. Bowers-Hadley.
335              
336             This is free software, licensed under:
337              
338             The GNU Lesser General Public License, Version 2.1, February 1999
339              
340              
341             =cut
342              
343             1; # End of Algorithm::EventsPerSecond