use super::threshold::AdaptiveThreshold;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Usage {
#[default]
Normal,
Over,
Under,
}
pub const DEFAULT_OVERUSE_TIME: Duration = Duration::from_millis(10);
#[derive(Debug, Clone, Copy)]
pub struct OveruseDetector {
threshold: AdaptiveThreshold,
overuse_time: Duration,
outside_since: Option<Instant>,
consecutive: u32,
previous_estimate_ms: f64,
usage: Usage,
}
impl Default for OveruseDetector {
fn default() -> Self {
Self {
threshold: AdaptiveThreshold::new(),
overuse_time: DEFAULT_OVERUSE_TIME,
outside_since: None,
consecutive: 0,
previous_estimate_ms: 0.0,
usage: Usage::Normal,
}
}
}
impl OveruseDetector {
pub fn new() -> Self {
Self::default()
}
pub fn usage(&self) -> Usage {
self.usage
}
pub fn threshold_ms(&self) -> f64 {
self.threshold.value_ms()
}
pub fn update(&mut self, now: Instant, estimate_ms: f64) -> Usage {
let threshold_ms = self.threshold.value_ms();
self.usage = if estimate_ms > threshold_ms {
let since = *self.outside_since.get_or_insert(now);
self.consecutive += 1;
let long_enough = now.saturating_duration_since(since) >= self.overuse_time;
let still_growing = estimate_ms >= self.previous_estimate_ms;
if long_enough && still_growing && self.consecutive > 1 {
Usage::Over
} else {
self.usage
}
} else if estimate_ms < -threshold_ms {
self.outside_since = None;
self.consecutive = 0;
Usage::Under
} else {
self.outside_since = None;
self.consecutive = 0;
Usage::Normal
};
self.previous_estimate_ms = estimate_ms;
self.threshold.update(now, estimate_ms);
self.usage
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_quiet_path_reads_normal() {
let epoch = Instant::now();
let mut detector = OveruseDetector::new();
for step in 0..100u64 {
let usage = detector.update(epoch + Duration::from_millis(step * 20), 1.0);
assert_eq!(Usage::Normal, usage, "at step {step}");
}
}
#[test]
fn one_spike_is_not_overuse() {
let epoch = Instant::now();
let mut detector = OveruseDetector::new();
detector.update(epoch, 0.0);
let usage = detector.update(epoch + Duration::from_millis(20), 40.0);
assert_eq!(
Usage::Normal,
usage,
"a lone reading outside the threshold is noise, not congestion"
);
}
#[test]
fn a_sustained_growing_trend_is_overuse() {
let epoch = Instant::now();
let mut detector = OveruseDetector::new();
let mut usage = Usage::Normal;
for step in 0..10u64 {
usage = detector.update(
epoch + Duration::from_millis(step * 20),
20.0 + step as f64 * 2.0,
);
}
assert_eq!(
Usage::Over,
usage,
"a queue that keeps growing must eventually be declared"
);
}
#[test]
fn recovery_is_not_debounced() {
let epoch = Instant::now();
let mut detector = OveruseDetector::new();
let mut at = epoch;
for step in 0..10u64 {
at = epoch + Duration::from_millis(step * 20);
detector.update(at, 20.0 + step as f64 * 2.0);
}
assert_eq!(Usage::Over, detector.usage());
let usage = detector.update(at + Duration::from_millis(20), 0.0);
assert_eq!(
Usage::Normal,
usage,
"back inside the threshold must be believed at once"
);
}
#[test]
fn a_draining_queue_reads_under() {
let epoch = Instant::now();
let mut detector = OveruseDetector::new();
let usage = detector.update(epoch, -40.0);
assert_eq!(Usage::Under, usage);
}
#[test]
fn a_high_but_flat_trend_does_not_re_declare_overuse() {
let epoch = Instant::now();
let mut detector = OveruseDetector::new();
let mut at = epoch;
for step in 0..10u64 {
at = epoch + Duration::from_millis(step * 20);
detector.update(at, 20.0 + step as f64 * 2.0);
}
assert_eq!(Usage::Over, detector.usage());
for step in 0..200u64 {
at += Duration::from_millis(20);
detector.update(at, 1.0);
let _ = step;
}
assert_eq!(
Usage::Normal,
detector.usage(),
"a settled path must return to normal"
);
}
}