const BUCKETS: usize = 16;
const BUCKET_MS: u64 = 500;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RateMeter {
total: u64,
samples: u64,
elapsed_ms: u64,
bucket_total: [u64; BUCKETS],
bucket_samples: [u64; BUCKETS],
head: u64,
started_ms: Option<u64>,
}
impl RateMeter {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, amount: u64) {
self.total += amount;
self.samples += 1;
let slot = (self.head % BUCKETS as u64) as usize;
self.bucket_total[slot] += amount;
self.bucket_samples[slot] += 1;
}
pub fn add_empty(&mut self) {
self.add(0);
}
pub fn elapsed(&mut self, elapsed_ms: u64) {
let started = *self.started_ms.get_or_insert(elapsed_ms);
debug_assert!(elapsed_ms >= started, "the clock a meter is given must not go backwards");
let want = elapsed_ms / BUCKET_MS;
if want > self.head {
let advance = (want - self.head).min(BUCKETS as u64);
for step in 1..=advance {
let slot = ((self.head + step) % BUCKETS as u64) as usize;
self.bucket_total[slot] = 0;
self.bucket_samples[slot] = 0;
}
self.head = want;
}
self.elapsed_ms = elapsed_ms;
}
fn window_span_ms(&self) -> u64 {
let oldest_start = (self.head + 1).saturating_sub(BUCKETS as u64) * BUCKET_MS;
self.elapsed_ms.saturating_sub(oldest_start)
}
pub fn total(&self) -> u64 {
self.total
}
pub fn samples(&self) -> u64 {
self.samples
}
pub fn elapsed_ms(&self) -> u64 {
self.elapsed_ms
}
pub fn per_sec(&self) -> f64 {
let span = self.window_span_ms();
if span == 0 {
return 0.0;
}
let recent: u64 = self.bucket_total.iter().sum();
recent as f64 / (span as f64 / 1000.0)
}
pub fn mean(&self) -> f64 {
let samples: u64 = self.bucket_samples.iter().sum();
if samples == 0 {
return 0.0;
}
let recent: u64 = self.bucket_total.iter().sum();
recent as f64 / samples as f64
}
pub fn lifetime_per_sec(&self) -> f64 {
let span = self.elapsed_ms.saturating_sub(self.started_ms.unwrap_or(0));
if span == 0 {
return 0.0;
}
self.total as f64 / (span as f64 / 1000.0)
}
pub fn share_of(&self, whole: &RateMeter) -> f64 {
if whole.total == 0 {
return 0.0;
}
self.total as f64 / whole.total as f64
}
pub fn running_ms(&self) -> u64 {
self.elapsed_ms.saturating_sub(self.started_ms.unwrap_or(0))
}
pub fn reset(&mut self) {
*self = Self::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_rate_is_the_total_over_the_elapsed_time() {
let mut meter = RateMeter::new();
for _ in 0..10 {
meter.add(100);
}
meter.elapsed(2000);
assert_eq!(meter.total(), 1000);
assert_eq!(meter.per_sec(), 500.0);
assert_eq!(meter.mean(), 100.0);
}
#[test]
fn nothing_measured_yet_reads_as_zero_rather_than_dividing_by_zero() {
let meter = RateMeter::new();
assert_eq!(meter.per_sec(), 0.0);
assert_eq!(meter.mean(), 0.0);
assert_eq!(meter.share_of(&RateMeter::new()), 0.0);
let mut sampled = RateMeter::new();
sampled.add(5);
assert_eq!(sampled.per_sec(), 0.0);
let mut timed = RateMeter::new();
timed.elapsed(1000);
assert_eq!(timed.mean(), 0.0);
}
#[test]
fn an_empty_sample_still_counts_against_the_average() {
let mut meter = RateMeter::new();
for _ in 0..5 {
meter.add(10);
meter.add_empty();
}
assert_eq!(meter.samples(), 10);
assert_eq!(meter.mean(), 5.0);
}
#[test]
fn a_share_is_a_fraction_of_another_total() {
let mut whole = RateMeter::new();
whole.add(1000);
let mut part = RateMeter::new();
part.add(250);
assert_eq!(part.share_of(&whole), 0.25);
}
#[test]
fn a_rate_settles_instead_of_creeping_toward_a_risen_steady_state() {
let mut meter = RateMeter::new();
let mut now = 0u64;
for _ in 0..600 {
meter.add(10);
now += 100;
meter.elapsed(now);
}
for _ in 0..2400 {
meter.add(100);
now += 100;
meter.elapsed(now);
}
let settled = meter.per_sec();
assert!((settled - 1000.0).abs() < 50.0, "the window reports what is happening now: {settled:.0}");
for _ in 0..600 {
meter.add(100);
now += 100;
meter.elapsed(now);
}
let later = meter.per_sec();
assert!((later - settled).abs() < 50.0, "and it stays there rather than creeping: {settled:.0} then {later:.0}");
assert!(meter.lifetime_per_sec() < settled * 0.95, "the session mean lags a risen rate, which is why it is not the readout");
}
#[test]
fn a_meter_reset_partway_through_measures_only_what_it_saw() {
let mut meter = RateMeter::new();
let mut now = 0u64;
for _ in 0..6000 {
meter.add(100);
now += 100;
meter.elapsed(now);
}
meter.reset();
for _ in 0..100 {
meter.add(100);
now += 100;
meter.elapsed(now);
}
assert!((meter.lifetime_per_sec() - 1000.0).abs() < 20.0, "after a reset the rate is over what it saw: {:.0}", meter.lifetime_per_sec());
assert!((meter.per_sec() - 1000.0).abs() < 60.0, "and so is the window: {:.0}", meter.per_sec());
assert_eq!(meter.running_ms(), 9_900);
}
#[test]
fn the_readings_a_player_reported_are_reproduced_by_the_defect() {
const TRUE_RATE: f64 = 265.0 * 1024.0; const RESET_AT_MS: u64 = 628_000;
let old_per_sec = |total: f64, absolute_ms: u64| total / (absolute_ms as f64 / 1000.0) / 1024.0;
for (tick, reported) in [(72_394u64, 127.6f64), (82_039, 143.9)] {
let absolute_ms = tick * 1000 / 60;
let since_reset = absolute_ms.saturating_sub(RESET_AT_MS);
let total = TRUE_RATE * (since_reset as f64 / 1000.0);
let predicted = old_per_sec(total, absolute_ms);
assert!(
(predicted - reported).abs() < 2.0,
"the old meter at tick {tick} predicts {predicted:.1} KiB/s against the {reported:.1} actually seen"
);
}
let mut meter = RateMeter::new();
let mut now = RESET_AT_MS;
for _ in 0..600 {
meter.add((TRUE_RATE / 10.0) as u64);
now += 100;
meter.elapsed(now);
}
let windowed = meter.per_sec() / 1024.0;
assert!((windowed - 265.0).abs() < 15.0, "the window reports the rate itself: {windowed:.1} KiB/s");
}
#[test]
fn silence_decays_out_of_the_window() {
let mut meter = RateMeter::new();
let mut now = 0u64;
for _ in 0..100 {
meter.add(50);
now += 100;
meter.elapsed(now);
}
assert!(meter.per_sec() > 0.0);
now += 60_000;
meter.elapsed(now);
assert_eq!(meter.per_sec(), 0.0, "a minute of silence is a rate of zero");
}
#[test]
fn a_reset_meter_describes_only_the_world_after_it() {
let mut meter = RateMeter::new();
meter.add(9999);
meter.elapsed(10_000);
meter.reset();
assert_eq!(meter, RateMeter::new());
meter.add(10);
meter.elapsed(1000);
assert_eq!(meter.per_sec(), 10.0);
}
}