use std::collections::VecDeque;
const WINDOW: usize = 20;
const OVERUSE_THRESHOLD: f64 = 12.5;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BandwidthState {
Underuse,
Normal,
Overuse,
}
#[derive(Debug, Clone)]
pub struct TrendlineDetector {
deltas: VecDeque<(f64, f64)>,
pub(crate) state: BandwidthState,
}
impl TrendlineDetector {
pub fn new() -> Self {
Self {
deltas: VecDeque::with_capacity(WINDOW + 1),
state: BandwidthState::Normal,
}
}
pub fn update(&mut self, arrival_delta_ms: f64, send_delta_ms: f64) {
if self.deltas.len() >= WINDOW {
self.deltas.pop_front();
}
self.deltas.push_back((arrival_delta_ms, send_delta_ms));
if self.deltas.len() < 3 {
return;
}
let slope = self.trendline_slope();
self.state = if slope > OVERUSE_THRESHOLD {
BandwidthState::Overuse
} else if slope < -OVERUSE_THRESHOLD {
BandwidthState::Underuse
} else {
BandwidthState::Normal
};
}
pub fn overuse(&self) -> bool {
self.state == BandwidthState::Overuse
}
pub fn state(&self) -> BandwidthState {
self.state
}
fn trendline_slope(&self) -> f64 {
let n = self.deltas.len() as f64;
let accumulated: Vec<f64> = self
.deltas
.iter()
.scan(0.0f64, |acc, (a, s)| {
*acc += a - s;
Some(*acc)
})
.collect();
let x_bar = (n - 1.0) / 2.0;
let y_bar: f64 = accumulated.iter().sum::<f64>() / n;
let mut num = 0.0f64;
let mut den = 0.0f64;
for (i, y) in accumulated.iter().enumerate() {
let x = i as f64;
num += (x - x_bar) * (y - y_bar);
den += (x - x_bar).powi(2);
}
if den.abs() < 1e-10 {
0.0
} else {
num / den
}
}
}
impl Default for TrendlineDetector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stable_arrival_times_stay_normal() {
let mut d = TrendlineDetector::new();
for _ in 0..25 {
d.update(20.0, 20.0);
}
assert_eq!(d.state, BandwidthState::Normal);
}
#[test]
fn increasing_delay_triggers_overuse() {
let mut d = TrendlineDetector::new();
for i in 0..25 {
d.update(20.0 + i as f64 * 2.0, 20.0);
}
assert_eq!(d.state, BandwidthState::Overuse);
}
#[test]
fn insufficient_samples_stay_normal() {
let mut d = TrendlineDetector::new();
d.update(100.0, 0.0);
d.update(200.0, 0.0);
assert_eq!(d.state, BandwidthState::Normal, "need at least 3 samples");
}
#[test]
fn decreasing_delay_triggers_underuse() {
let mut d = TrendlineDetector::new();
for i in 1..=25u32 {
d.update(1.0, (i as f64) * 3.0);
}
assert_eq!(d.state, BandwidthState::Underuse);
}
#[test]
fn window_limits_sample_count() {
let mut d = TrendlineDetector::new();
for _ in 0..(WINDOW + 10) {
d.update(20.0, 20.0);
}
assert!(d.deltas.len() <= WINDOW);
}
}