use std::{collections::VecDeque, num::NonZeroUsize};
use anyhow::{Result, ensure};
use itertools::Itertools;
use nonzero_ext::nonzero;
use tokio::time::Duration;
use crate::report::IterReport;
use super::Counter;
pub struct RotateWindow {
buckets: VecDeque<Counter>,
size: NonZeroUsize,
}
impl RotateWindow {
fn new(size: NonZeroUsize) -> Self {
let mut win = Self { buckets: VecDeque::with_capacity(size.get()), size };
win.rotate(Counter::default());
win
}
fn push(&mut self, item: &IterReport) {
self.buckets.front_mut().unwrap().append(item);
}
fn rotate(&mut self, bucket: Counter) {
if self.buckets.len() == self.size.get() {
self.buckets.pop_back();
}
self.buckets.push_front(bucket);
}
fn len(&self) -> usize {
self.buckets.len()
}
fn front(&self) -> &Counter {
self.buckets.front().unwrap()
}
fn back(&self) -> &Counter {
self.buckets.back().unwrap()
}
fn get(&self, index: usize) -> Option<&Counter> {
self.buckets.get(index)
}
pub fn iter(&self) -> impl Iterator<Item = &Counter> {
self.buckets.iter()
}
}
pub struct RotateWindowGroup {
pub counter: u64,
periods: Vec<usize>,
windows: Vec<RotateWindow>,
}
impl RotateWindowGroup {
pub fn new<I, P>(buckets: NonZeroUsize, periods: I) -> Result<Self>
where
I: IntoIterator<Item = P>,
P: Into<usize>,
{
let periods = periods.into_iter().map(Into::into).collect_vec();
ensure!(!periods.is_empty(), "periods must be non-empty");
ensure!(periods.iter().all(|p| *p > 0), "periods must be > 0");
let windows = periods.iter().map(|_| RotateWindow::new(buckets)).collect();
Ok(Self { counter: 0, periods, windows })
}
pub fn push(&mut self, stats: &IterReport) {
for win in &mut self.windows {
win.push(stats);
}
}
pub fn rotate(&mut self) {
self.counter += 1;
for (period, win) in self.periods.iter().zip(self.windows.iter_mut()) {
if self.counter % (*period as u64) == 0 {
win.rotate(Counter::default());
}
}
}
pub fn window_for_secs(&self, secs: usize) -> Option<&RotateWindow> {
self.periods
.iter()
.position(|p| *p == secs)
.map(|idx| &self.windows[idx])
}
}
pub struct RotateDiffWindow {
interval: Duration,
fps: usize,
window: RotateWindow,
}
impl RotateDiffWindow {
pub fn new(fps: NonZeroUsize) -> Self {
let interval = Duration::from_secs_f64(1.0 / fps.get() as f64);
let mut win = Self {
interval,
fps: fps.get(),
window: RotateWindow::new(fps.saturating_mul(nonzero!(600usize)).saturating_add(1)),
};
win.rotate(Counter::default());
win
}
pub fn rotate(&mut self, counter: Counter) {
self.window.rotate(counter);
}
pub fn counter_for_secs(&self, secs: usize) -> (Counter, Duration) {
let frames_back = self.fps * secs;
let clamped = frames_back.min(self.window.len().saturating_sub(1));
let duration = clamped as u32 * self.interval;
let back = self.window.get(clamped).unwrap_or_else(|| self.window.back());
(self.window.front() - back, duration)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_counter(iters: u64) -> Counter {
Counter {
iters,
items: iters * 10,
bytes: iters * 100,
duration: Duration::from_millis(iters),
}
}
#[test]
fn rotate_diff_window_one_sec() {
let fps = nonzero!(10usize); let mut win = RotateDiffWindow::new(fps);
for i in 1..=10 {
win.rotate(make_counter(i * 100));
}
let (counter, duration) = win.counter_for_secs(1);
assert_eq!(counter.iters, 1000);
assert_eq!(duration, Duration::from_secs(1));
}
#[test]
fn rotate_diff_window_partial_fill() {
let fps = nonzero!(10usize);
let mut win = RotateDiffWindow::new(fps);
for i in 1..=5 {
win.rotate(make_counter(i * 10));
}
let (counter, duration) = win.counter_for_secs(1);
assert_eq!(counter.iters, 50); assert_eq!(duration, Duration::from_millis(600)); }
#[test]
fn rotate_diff_window_multiple_spans() {
let fps = nonzero!(10usize);
let mut win = RotateDiffWindow::new(fps);
for i in 1..=100 {
win.rotate(make_counter(i));
}
let (c1, d1) = win.counter_for_secs(1);
let (c10, d10) = win.counter_for_secs(10);
assert_eq!(c1.iters, 100 - 90);
assert_eq!(d1, Duration::from_secs(1));
assert_eq!(c10.iters, 100);
assert_eq!(d10, Duration::from_secs(10));
}
#[test]
fn rotate_window_group_rotates_by_periods() {
let mut group = RotateWindowGroup::new(nonzero!(2usize), [1usize, 10]).expect("valid periods");
assert_eq!(group.window_for_secs(10).unwrap().len(), 1);
for _ in 0..9 {
group.rotate();
}
assert_eq!(group.window_for_secs(10).unwrap().len(), 1);
group.rotate();
assert_eq!(group.window_for_secs(10).unwrap().len(), 2);
assert_eq!(group.window_for_secs(1).unwrap().len(), 2);
}
#[test]
fn rotate_window_group_rejects_zero_period() {
let err = RotateWindowGroup::new(nonzero!(2usize), [0usize])
.err()
.expect("expected error");
assert_eq!(err.to_string(), "periods must be > 0");
}
#[test]
fn rotate_window_group_rejects_empty_periods() {
let err = RotateWindowGroup::new(nonzero!(2usize), std::iter::empty::<usize>())
.err()
.expect("expected error");
assert_eq!(err.to_string(), "periods must be non-empty");
}
}