use std::collections::VecDeque;
use std::sync::Mutex;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeakDir {
Max,
Min,
}
#[derive(Debug)]
pub struct PeakTracker {
window: Duration,
dir: PeakDir,
entries: Mutex<VecDeque<(Instant, u64)>>,
}
impl PeakTracker {
pub fn new(window: Duration, dir: PeakDir) -> Self {
assert!(!window.is_zero(), "PeakTracker window must be > 0");
Self {
window,
dir,
entries: Mutex::new(VecDeque::new()),
}
}
pub fn max(window: Duration) -> Self {
Self::new(window, PeakDir::Max)
}
pub fn min(window: Duration) -> Self {
Self::new(window, PeakDir::Min)
}
pub fn window(&self) -> Duration {
self.window
}
pub fn direction(&self) -> PeakDir {
self.dir
}
pub fn record(&self, value: u64, now: Instant) {
let mut g = self.entries.lock().unwrap_or_else(|e| e.into_inner());
self.evict_stale(&mut g, now);
match self.dir {
PeakDir::Max => {
while let Some(&(_, v)) = g.back() {
if v <= value {
g.pop_back();
} else {
break;
}
}
}
PeakDir::Min => {
while let Some(&(_, v)) = g.back() {
if v >= value {
g.pop_back();
} else {
break;
}
}
}
}
g.push_back((now, value));
}
pub fn record_now(&self, value: u64) {
self.record(value, Instant::now());
}
pub fn peek(&self, now: Instant) -> Option<u64> {
let mut g = self.entries.lock().unwrap_or_else(|e| e.into_inner());
self.evict_stale(&mut g, now);
g.front().map(|&(_, v)| v)
}
pub fn peek_now(&self) -> Option<u64> {
self.peek(Instant::now())
}
pub fn len(&self) -> usize {
let g = self.entries.lock().unwrap_or_else(|e| e.into_inner());
g.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
fn evict_stale(&self, q: &mut VecDeque<(Instant, u64)>, now: Instant) {
while let Some(&(t, _)) = q.front() {
if now.duration_since(t) >= self.window {
q.pop_front();
} else {
break;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_on_construction() {
let p = PeakTracker::max(Duration::from_secs(1));
assert!(p.peek_now().is_none());
}
#[test]
fn reports_max_over_window() {
let p = PeakTracker::max(Duration::from_secs(1));
let t0 = Instant::now();
p.record(10, t0);
p.record(30, t0 + Duration::from_millis(100));
p.record(20, t0 + Duration::from_millis(200));
assert_eq!(p.peek(t0 + Duration::from_millis(300)), Some(30));
}
#[test]
fn reports_min_over_window() {
let p = PeakTracker::min(Duration::from_secs(1));
let t0 = Instant::now();
p.record(10, t0);
p.record(5, t0 + Duration::from_millis(100));
p.record(20, t0 + Duration::from_millis(200));
assert_eq!(p.peek(t0 + Duration::from_millis(300)), Some(5));
}
#[test]
fn drops_dominated_entries_from_back() {
let p = PeakTracker::max(Duration::from_secs(10));
let t0 = Instant::now();
p.record(5, t0);
p.record(7, t0 + Duration::from_millis(10));
assert_eq!(p.len(), 1);
assert_eq!(p.peek(t0 + Duration::from_millis(20)), Some(7));
}
#[test]
fn drops_stale_entries_from_front() {
let p = PeakTracker::max(Duration::from_millis(100));
let t0 = Instant::now();
p.record(50, t0);
let later = t0 + Duration::from_millis(200);
assert!(p.peek(later).is_none());
assert_eq!(p.len(), 0);
}
#[test]
fn monotonically_decreasing_keeps_all() {
let p = PeakTracker::max(Duration::from_secs(10));
let t0 = Instant::now();
for i in 0..5 {
p.record(100 - i, t0 + Duration::from_millis(i * 10));
}
assert_eq!(p.len(), 5);
assert_eq!(p.peek(t0 + Duration::from_millis(100)), Some(100));
}
#[test]
fn monotonically_increasing_keeps_one() {
let p = PeakTracker::max(Duration::from_secs(10));
let t0 = Instant::now();
for i in 0..10 {
p.record(i, t0 + Duration::from_millis(i * 10));
}
assert_eq!(p.len(), 1);
assert_eq!(p.peek(t0 + Duration::from_millis(200)), Some(9));
}
#[test]
fn peek_mid_window_drops_stale_without_record() {
let p = PeakTracker::max(Duration::from_millis(50));
let t0 = Instant::now();
p.record(123, t0);
assert!(p.peek(t0 + Duration::from_millis(200)).is_none());
}
#[test]
#[should_panic(expected = "window must be > 0")]
fn zero_window_rejected() {
let _ = PeakTracker::max(Duration::ZERO);
}
}