use std::time::{Duration, Instant};
use parking_lot::RwLock;
pub struct RateWindow {
inner: RwLock<WindowInner>,
}
struct WindowInner {
samples: Vec<(Instant, u64)>,
window_size: Duration,
}
impl RateWindow {
#[must_use]
pub fn new(window_size: Duration) -> Self {
Self {
inner: RwLock::new(WindowInner {
samples: Vec::with_capacity(64),
window_size,
}),
}
}
#[must_use]
pub fn default_window() -> Self {
Self::new(Duration::from_mins(1))
}
pub fn record(&self, counter_value: u64) {
let now = Instant::now();
let mut inner = self.inner.write();
let cutoff = now.checked_sub(inner.window_size).unwrap_or(now);
inner.samples.retain(|&(t, _)| t >= cutoff);
inner.samples.push((now, counter_value));
}
#[cfg(test)]
fn record_at(&self, at: Instant, counter_value: u64) {
let mut inner = self.inner.write();
let cutoff = at.checked_sub(inner.window_size).unwrap_or(at);
inner.samples.retain(|&(t, _)| t >= cutoff);
inner.samples.push((at, counter_value));
}
#[must_use]
pub fn rate_per_second(&self) -> f64 {
let inner = self.inner.read();
if inner.samples.len() < 2 {
return 0.0;
}
let first = inner.samples.first().unwrap();
let last = inner.samples.last().unwrap();
let duration = last.0.duration_since(first.0).as_secs_f64();
if duration <= 0.0 {
return 0.0;
}
let delta = last.1.saturating_sub(first.1) as f64;
delta / duration
}
#[must_use]
pub fn sample_count(&self) -> usize {
self.inner.read().samples.len()
}
pub fn clear(&self) {
self.inner.write().samples.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_window() {
let w = RateWindow::default_window();
assert!((w.rate_per_second()).abs() < f64::EPSILON);
assert_eq!(w.sample_count(), 0);
}
#[test]
fn test_single_sample() {
let w = RateWindow::default_window();
w.record(100);
assert!((w.rate_per_second()).abs() < f64::EPSILON);
assert_eq!(w.sample_count(), 1);
}
#[test]
fn test_two_samples_rate() {
let w = RateWindow::new(Duration::from_mins(1));
let now = Instant::now();
w.record_at(now, 0);
w.record_at(now + Duration::from_secs(10), 1000);
let rate = w.rate_per_second();
assert!((rate - 100.0).abs() < 0.01, "Expected ~100.0, got {rate}");
}
#[test]
fn test_multiple_samples() {
let w = RateWindow::new(Duration::from_mins(1));
let now = Instant::now();
w.record_at(now, 0);
w.record_at(now + Duration::from_secs(5), 500);
w.record_at(now + Duration::from_secs(10), 1000);
let rate = w.rate_per_second();
assert!((rate - 100.0).abs() < 0.01, "Expected ~100.0, got {rate}");
}
#[test]
fn test_window_pruning() {
let w = RateWindow::new(Duration::from_secs(5));
let now = Instant::now();
w.record_at(now.checked_sub(Duration::from_secs(10)).unwrap(), 0);
w.record_at(now.checked_sub(Duration::from_secs(2)).unwrap(), 800);
w.record_at(now, 1000);
assert_eq!(w.sample_count(), 2);
let rate = w.rate_per_second();
assert!((rate - 100.0).abs() < 0.01, "Expected ~100.0, got {rate}");
}
#[test]
fn test_clear() {
let w = RateWindow::default_window();
w.record(100);
w.record(200);
assert_eq!(w.sample_count(), 2);
w.clear();
assert_eq!(w.sample_count(), 0);
assert!((w.rate_per_second()).abs() < f64::EPSILON);
}
#[test]
fn test_zero_duration() {
let w = RateWindow::new(Duration::from_mins(1));
let now = Instant::now();
w.record_at(now, 0);
w.record_at(now, 1000);
assert!((w.rate_per_second()).abs() < f64::EPSILON);
}
#[test]
fn test_counter_wraparound() {
let w = RateWindow::new(Duration::from_mins(1));
let now = Instant::now();
w.record_at(now, 1000);
w.record_at(now + Duration::from_secs(10), 500);
assert!((w.rate_per_second()).abs() < f64::EPSILON);
}
}