#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct IntervalStats {
pub min: u64,
pub max: u64,
pub mean: u64,
pub p50: u64,
pub p99: u64,
pub p999: u64,
}
#[derive(Debug)]
pub struct LatencyRing<const CAP: usize = 4096> {
buf: [u64; CAP],
head: usize,
len: usize,
}
impl<const CAP: usize> LatencyRing<CAP> {
#[must_use]
pub const fn new() -> Self {
const {
assert!(CAP > 0, "LatencyRing capacity must be non-zero");
}
Self {
buf: [0; CAP],
head: 0,
len: 0,
}
}
pub fn record(&mut self, interval_ns: u64) {
#[allow(clippy::indexing_slicing)]
{
self.buf[self.head] = interval_ns;
}
self.head += 1;
if self.head == CAP {
self.head = 0;
}
if self.len < CAP {
self.len += 1;
}
}
#[allow(clippy::indexing_slicing)]
#[must_use]
pub fn stats(&self) -> IntervalStats {
if self.len == 0 {
return IntervalStats::default();
}
let mut start = self.head + CAP - self.len;
if start >= CAP {
start -= CAP;
}
let mut scratch = [0_u64; CAP];
let mut written = 0;
for i in 0..self.len {
let idx = start + i;
let idx = if idx >= CAP { idx - CAP } else { idx };
scratch[written] = self.buf[idx];
written += 1;
}
let live = &mut scratch[..written];
heapsort(live);
let n = self.len;
let mut sum = 0_u128;
let mut min = u64::MAX;
let mut max = 0_u64;
for &v in live.iter() {
sum += u128::from(v);
if v < min {
min = v;
}
if v > max {
max = v;
}
}
IntervalStats {
min,
max,
#[allow(clippy::cast_possible_truncation)] mean: (sum / n as u128) as u64,
p50: nearest_rank(live, n, 50, 100),
p99: nearest_rank(live, n, 99, 100),
p999: nearest_rank(live, n, 999, 1_000),
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.len
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
pub fn reset(&mut self) {
self.head = 0;
self.len = 0;
}
}
impl<const CAP: usize> Default for LatencyRing<CAP> {
fn default() -> Self {
Self::new()
}
}
fn nearest_rank(sorted: &[u64], n: usize, num: usize, den: usize) -> u64 {
let rank = (n.saturating_mul(num).saturating_add(den - 1)) / den;
let idx = rank.saturating_sub(1).min(n - 1);
#[allow(clippy::indexing_slicing)]
sorted[idx]
}
#[allow(clippy::indexing_slicing)]
fn heapsort(v: &mut [u64]) {
let n = v.len();
if n < 2 {
return;
}
let mut start = n / 2;
while start > 0 {
start -= 1;
sift_down(v, start, n);
}
let mut end = n;
while end > 1 {
end -= 1;
v.swap(0, end);
sift_down(v, 0, end);
}
}
#[allow(clippy::indexing_slicing)]
fn sift_down(v: &mut [u64], root: usize, end: usize) {
let mut r = root;
loop {
let left = 2 * r + 1;
if left >= end {
return;
}
let child = if left + 1 < end && v[left + 1] > v[left] {
left + 1
} else {
left
};
if v[child] <= v[r] {
return;
}
v.swap(r, child);
r = child;
}
}
#[cfg(test)]
mod tests {
use super::{IntervalStats, LatencyRing};
#[test]
fn ring_stats_exact_small_cap() {
let mut ring = LatencyRing::<8>::new();
ring.record(500);
ring.record(100);
ring.record(300);
let s = ring.stats();
assert_eq!(
s,
IntervalStats {
min: 100,
max: 500,
mean: 300,
p50: 300,
p99: 500, p999: 500,
}
);
}
#[test]
fn ring_eviction_order_and_reset() {
let mut ring = LatencyRing::<4>::new();
for v in 1..=6_u64 {
ring.record(v);
}
assert_eq!(ring.len(), 4);
let s = ring.stats();
assert_eq!(s.min, 3);
assert_eq!(s.max, 6);
assert_eq!(s.mean, 4);
assert_eq!(s.p50, 4);
ring.reset();
assert!(ring.is_empty());
assert_eq!(ring.stats(), IntervalStats::default());
}
#[test]
fn ring_edges() {
let mut ring = LatencyRing::<2>::new();
assert!(ring.is_empty());
assert_eq!(ring.stats(), IntervalStats::default());
ring.record(u64::MAX);
let s = ring.stats();
assert_eq!(
(s.min, s.max, s.mean, s.p50),
(u64::MAX, u64::MAX, u64::MAX, u64::MAX)
);
}
#[test]
fn ring_mean_floors() {
let mut ring = LatencyRing::<3>::new();
ring.record(1);
ring.record(1);
ring.record(2);
assert_eq!(ring.stats().mean, 1); }
}