net.freeutils.util.Speedometer Maven / Gradle / Ivy
Show all versions of jelementary Show documentation
/*
* Copyright © 2003-2024 Amichai Rothman
*
* This file is part of JElementary - the Java Elementary Utilities package.
*
* JElementary 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, either version 2 of the License, or
* (at your option) any later version.
*
* JElementary is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with JElementary. If not, see .
*
* For additional info see https://www.freeutils.net/source/jelementary/
*/
package net.freeutils.util;
import java.util.concurrent.atomic.*;
/**
* The {@code Speedometer} class provides general purpose event firing speed statistics.
*
* The Speedometer counts the firing rate of events within consecutive time units,
* each of a configurable interval, maintaining a bounded history of such units.
* In addition, it maintains a counter for the current unit (which is still in progress).
*
* Various speed calculations can then be performed on these statistics to
* estimate current or average speed.
*/
public class Speedometer {
private final int interval;
private final int units;
private final AtomicIntegerArray buckets;
private final AtomicInteger current = new AtomicInteger();
private volatile long endTime;
/**
* Constructs a Speedometer.
*
* @param interval the duration of a single time unit in milliseconds
* @param units the number of recent units to maintain
*/
public Speedometer(int interval, int units) {
this.interval = interval;
this.units = units;
this.buckets = new AtomicIntegerArray(units);
long now = System.currentTimeMillis();
endTime = (now / interval) * interval; // as if it just finished, and properly rounded to bucket boundary
}
/**
* Fire an event.
*/
public void fire() {
update();
current.incrementAndGet();
}
/**
* Fire multiple events.
*
* @param delta the number of events to fire
*/
public void fire(int delta) {
update();
current.addAndGet(delta);
}
/**
* Returns the approximate speed in the current time unit.
*
* @return the approximate speed in the current time unit
*/
public double getSpeed() {
return sumLastUnit();
}
/**
* Returns the average speed over the specified number of previous units and the current unit.
*
* @param units the number of previous units to average
* @return the average speed
* @throws IllegalArgumentException if units is out of range
*/
public double getAverageSpeed(int units) {
if (units < 0 || units > this.units)
throw new IllegalArgumentException("units out of range: " + units);
long now = update();
int curr = current.get();
int prev = units == this.units ? sumBuckets() : sumBuckets(getBucket(now) - units, units);
double partialUnit = (now % interval) / (double) interval;
return (prev + curr) / (units + partialUnit);
}
/**
* Returns the average speed over all units.
*
* @return the average speed
*/
public double getAverageSpeed() {
return getAverageSpeed(units);
}
/**
* Updates the current counter and bucket values to account for elapsed time.
*
* @return the current time, as used by this method in its calculations
*/
private long update() {
long now = System.currentTimeMillis();
if (now > endTime) {
synchronized (this) {
if (now > endTime) {
// finalize last used bucket
int val = current.get();
current.addAndGet(-val);
buckets.set(getBucket(endTime), val);
endTime += interval;
// reset intermediate buckets
while (now > endTime) {
buckets.set(getBucket(endTime), 0);
endTime += interval;
}
}
}
}
return now;
}
/**
* Returns the index of the bucket corresponding to the given time.
*
* @param time the time whose corresponding bucket is requested
* @return the index of the bucket corresponding to the given time
*/
private int getBucket(long time) {
return (int)((time / interval) % units);
}
/**
* Returns the sum of the values in the given range of buckets.
*
* @param ind the first bucket index (taken modulo the number of buckets, can be negative)
* @param len the number of buckets to include in the sum
* @return the sum of the values in the given range of buckets
*/
private int sumBuckets(int ind, int len) {
ind = units + (ind % units); // fix for negative values
int sum = 0;
for (int i = ind + len - 1; i >= ind; i--)
sum += buckets.get(i % units);
return sum;
}
/**
* Returns the sum of the values in all buckets.
*
* @return the sum of the values in all buckets
*/
private int sumBuckets() {
int sum = 0;
for (int i = units - 1; i >= 0; i--)
sum += buckets.get(i);
return sum;
}
/**
* Returns the (approximate) sum in the last unit of time, by adding the current bucket
* and an appropriate fraction of the previous bucket.
*
* @return the sum in the last unit of time
*/
private double sumLastUnit() {
// estimate the last unit by current units and corresponding part or the previous bucket
long now = update();
int currEvents = current.get(); // current bucket value
double prevEvents = buckets.get(getBucket(now - interval)); // previous bucket value
double partialUnit = (now % interval) / (double) interval; // fraction of unit in current bucket
return currEvents + (1 - partialUnit) * prevEvents; // current and partial previous buckets
}
}