Algorithm-EventsPerSecond

 view release on metacpan or  search on metacpan

lib/Algorithm/EventsPerSecond.pm  view on Meta::CPAN

package Algorithm::EventsPerSecond;

use 5.006;
use strict;
use warnings;

=head1 NAME

Algorithm::EventsPerSecond - A sliding-window events-per-second rate counter with a optional XS backend for additional zoomies.

=head1 VERSION

Version 0.1.0

=cut

our $VERSION = '0.1.0';

our $BACKEND;

BEGIN {
	$BACKEND = 'PP';
	unless ( $ENV{ALGORITHM_EVENTSPERSECOND_PP} ) {
		local $@;
		if ( eval { require Algorithm::EventsPerSecond::XS; 1 } ) {
			$BACKEND = 'XS';
		}
	}
}

=head1 SYNOPSIS

    use Algorithm::EventsPerSecond;

    my $meter = Algorithm::EventsPerSecond->new( window => 10 );  # 10-second window

    while (my $event = get_next_event()) {
        $meter->mark;                 # record one event
        # $meter->mark(5);            # or record several at once

        printf "current rate: %.2f events/sec\n", $meter->rate;
    }

    print "events seen in window: ", $meter->count, "\n";
    print "lifetime total:        ", $meter->total, "\n";


=head1 DESCRIPTION

Algorithm::EventsPerSecond keeps per-second counts in a fixed-size ring buffer and
reports the average event rate over the most recent N seconds (the
"window"). Memory use is constant regardless of event volume, and both
C<mark> and C<rate> are O(1) averaged out over time.

=head1 METHODS

=head2 new( window => $seconds )

Construct a meter. C<window> is the length of the averaging window in
seconds and defaults to 60.

=cut

sub new {
	my ( $class, %args ) = @_;

	my $window = $args{window} // 60;
	die "window must be a positive integer" unless $window =~ /^\d+$/ && $window > 0;

	my $self = {
		window  => $window,
		total   => 0,         # lifetime event count
		started => time(),
	};

	if ( $BACKEND eq 'XS' ) {
		# packed int64_t ring buffers, scanned in C
		$self->{buckets} = "\0" x ( $window * 8 );
		$self->{stamps}  = "\0" x ( $window * 8 );
	} else {
		$self->{buckets} = [ (0) x $window ];    # counts, indexed by (epoch_sec % window)
		$self->{stamps}  = [ (0) x $window ];    # epoch second each bucket belongs to
	}

	return bless $self, $class;
} ## end sub new

# Internal, PP backend: get the bucket for the current second, clearing it if stale.
sub _bucket_index {
	my ( $self, $now_sec ) = @_;
	my $i = $now_sec % $self->{window};
	if ( $self->{stamps}[$i] != $now_sec ) {
		$self->{buckets}[$i] = 0;
		$self->{stamps}[$i]  = $now_sec;
	}
	return $i;
}

=head2 mark( [$count] )

Record one event, or C<$count> events. C<$count> must be a
non-negative integer; zero is a no-op. Anything else dies. Returns the
meter object, so calls can be chained.

=cut

sub _mark_pp {
	my ( $self, $count ) = @_;
	if ( defined $count ) {
		die "count must be a non-negative integer" unless $count =~ /^\d+$/;
	} else {
		$count = 1;
	}

	my $now_sec = int( time() );
	my $i       = $self->_bucket_index($now_sec);

	$self->{buckets}[$i] += $count;
	$self->{total} += $count;

lib/Algorithm/EventsPerSecond.pm  view on Meta::CPAN

sub _count_pp {
	my ($self) = @_;

	my $now_sec = int( time() );
	my $window  = $self->{window};
	my $oldest  = $now_sec - $window + 1;

	my $sum = 0;
	for my $i ( 0 .. $window - 1 ) {
		$sum += $self->{buckets}[$i]
			if $self->{stamps}[$i] >= $oldest;
	}
	return $sum;
} ## end sub _count_pp

sub _count_xs {
	my ($self) = @_;

	my $now_sec = int( time() );
	return Algorithm::EventsPerSecond::XS::_xs_count( $self->{buckets}, $self->{stamps},
		$self->{window}, $now_sec - $self->{window} + 1,
	);
}

if ( $BACKEND eq 'XS' ) {
	*mark  = \&_mark_xs;
	*count = \&_count_xs;
} else {
	*mark  = \&_mark_pp;
	*count = \&_count_pp;
}

=head2 rate

Average events per second over the window. If the meter has been alive
for less time than the window, the elapsed lifetime is used instead, so
early readings are not artificially deflated.

=cut

sub rate {
	my ($self) = @_;

	my $elapsed = time() - $self->{started};
	my $span    = $elapsed < $self->{window} ? $elapsed : $self->{window};
	return 0 if $span <= 0;

	return $self->count / $span;
}

=head2 total

Lifetime count of all events ever recorded, regardless of window.

=cut

sub total { $_[0]->{total} }

=head2 window

The configured window length in seconds.

=cut

sub window { $_[0]->{window} }

=head2 reset

Clear all counts and restart the clock. Returns the meter object.

=cut

sub reset {
	my ($self) = @_;
	if ( $BACKEND eq 'XS' ) {
		$self->{buckets} = "\0" x ( $self->{window} * 8 );
		$self->{stamps}  = "\0" x ( $self->{window} * 8 );
	} else {
		@{ $self->{buckets} } = (0) x $self->{window};
		@{ $self->{stamps} }  = (0) x $self->{window};
	}
	$self->{total}   = 0;
	$self->{started} = time();
	return $self;
} ## end sub reset

=head2 backend

Returns C<'XS'> when the accelerated backend is in use, C<'PP'> for the
pure Perl fallback. May be called as a class or instance method.

=cut

sub backend { $BACKEND }

=head2 simd

Returns which SIMD flavor the XS backend was compiled with: C<'AVX2'>,
C<'SSE4.2'>, or C<'scalar'> (plain C, left to the compiler's
auto-vectorizer). Returns undef when the pure Perl backend is in use.

=cut

sub simd {
	return undef unless $BACKEND eq 'XS';
	return Algorithm::EventsPerSecond::XS::_xs_simd();
}

=head1 ACCELERATION

If a working C compiler is available at install time, an accelerated XS
backend, L<Algorithm::EventsPerSecond::XS>, is built and loaded
automatically. It keeps the ring buffer in packed C<int64_t> buffers and
scans the window in C, using SIMD (AVX2 or SSE4.2) when the compiler
targets a CPU that has it. If the backend cannot be loaded for any
reason (not built, no compiler at install time), a pure Perl
implementation with identical behavior is used instead.

The following control this.

=head2 IF_OPT



( run in 1.933 second using v1.01-cache-2.11-cpan-995e09ba956 )