use std::collections::VecDeque;
#[derive(Clone, Copy, Debug)]
pub struct StalenessSchedule {
pub k1: u64,
pub k2: u64,
pub lambda: f32,
}
impl StalenessSchedule {
#[must_use]
pub fn new(k1: u64, k2: u64, lambda: f32) -> Self {
Self { k1, k2, lambda }
}
#[must_use]
pub fn weight(&self, age: u64) -> f32 {
if age <= self.k1 {
1.0
} else if age <= self.k2 {
(-self.lambda * (age - self.k1) as f32).exp()
} else {
0.0
}
}
}
impl Default for StalenessSchedule {
fn default() -> Self {
Self {
k1: 16,
k2: 64,
lambda: 0.08,
}
}
}
#[derive(Clone, Debug)]
pub struct StalenessWindow {
schedule: StalenessSchedule,
samples: VecDeque<(f32, u64)>,
clock: u64,
cap: usize,
}
impl StalenessWindow {
#[must_use]
pub fn new(schedule: StalenessSchedule, cap: usize) -> Self {
Self {
schedule,
samples: VecDeque::with_capacity(cap),
clock: 0,
cap: cap.max(1),
}
}
pub fn push(&mut self, value: f32) {
self.samples.push_back((value, self.clock));
self.clock += 1;
while self.samples.len() > self.cap {
self.samples.pop_front();
}
while let Some(&(_, t)) = self.samples.front() {
if self.clock.saturating_sub(t) > self.schedule.k2 {
self.samples.pop_front();
} else {
break;
}
}
}
pub fn clear_samples(&mut self) {
self.samples.clear();
}
#[must_use]
pub fn weighted_mean(&self) -> Option<f32> {
let mut num = 0.0f32;
let mut den = 0.0f32;
for &(v, t) in &self.samples {
let w = self.schedule.weight(self.clock.saturating_sub(t));
num += w * v;
den += w;
}
if den > 0.0 {
Some(num / den)
} else {
None
}
}
#[must_use]
pub fn clock(&self) -> u64 {
self.clock
}
#[must_use]
pub fn len(&self) -> usize {
self.samples.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn weight_is_fresh_then_decays_then_drops() {
let s = StalenessSchedule::new(4, 10, 0.5);
assert_eq!(s.weight(0), 1.0);
assert_eq!(s.weight(4), 1.0);
let w5 = s.weight(5);
assert!(w5 < 1.0 && w5 > 0.0); assert!(s.weight(9) < w5); assert_eq!(s.weight(11), 0.0); }
#[test]
fn weighted_mean_favors_recent() {
let mut w = StalenessWindow::new(StalenessSchedule::new(2, 32, 0.3), 64);
for _ in 0..20 {
w.push(1.0);
}
for _ in 0..20 {
w.push(0.0);
}
let m = w.weighted_mean().unwrap();
assert!(
m < 0.25,
"recent-weighted mean {m} should be near the recent 0.0"
);
}
#[test]
fn stale_observations_are_evicted() {
let mut w = StalenessWindow::new(StalenessSchedule::new(2, 8, 0.5), 1000);
for _ in 0..50 {
w.push(1.0);
}
assert!(w.len() <= 9, "expected <=9 live samples, got {}", w.len());
assert!(w.weighted_mean().is_some());
}
#[test]
fn empty_window_has_no_mean() {
let w = StalenessWindow::new(StalenessSchedule::default(), 8);
assert!(w.weighted_mean().is_none());
assert!(w.is_empty());
}
}