use serde::{Deserialize, Serialize};
const MAX_SAMPLES: usize = 1024;
const _: () = assert!(
MAX_SAMPLES.is_power_of_two(),
"MAX_SAMPLES must be a power of 2 for bitmask indexing"
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct LatencyStats {
pub count: u64,
pub min_ns: u64,
pub max_ns: u64,
pub avg_ns: u64,
pub p50_ns: u64,
pub p95_ns: u64,
pub p99_ns: u64,
}
#[derive(Debug)]
pub struct LatencyTracker {
samples: [u64; MAX_SAMPLES],
sample_count: usize,
write_index: usize,
total_count: u64,
min_ns: u64,
max_ns: u64,
}
impl LatencyTracker {
pub fn new() -> Self {
Self {
samples: [0; MAX_SAMPLES],
sample_count: 0,
write_index: 0,
total_count: 0,
min_ns: u64::MAX,
max_ns: 0,
}
}
#[inline]
pub fn record(&mut self, latency_ns: u64) {
let idx = self.write_index & (MAX_SAMPLES - 1);
unsafe { *self.samples.get_unchecked_mut(idx) = latency_ns };
self.write_index = idx + 1; if self.sample_count < MAX_SAMPLES {
self.sample_count += 1;
}
self.total_count += 1;
self.min_ns = self.min_ns.min(latency_ns);
self.max_ns = self.max_ns.max(latency_ns);
}
pub fn record_elapsed(&mut self, start_ns: u64, end_ns: u64) -> u64 {
let elapsed = end_ns.saturating_sub(start_ns);
self.record(elapsed);
elapsed
}
pub fn stats(&self) -> Option<LatencyStats> {
if self.sample_count == 0 {
return None;
}
let n = self.sample_count;
let mut sorted = Vec::with_capacity(n);
sorted.extend_from_slice(&self.samples[..n]);
sorted.sort_unstable();
let window_sum: u128 = sorted.iter().map(|&x| x as u128).sum();
let avg = (window_sum / n as u128) as u64;
Some(LatencyStats {
count: self.total_count,
min_ns: self.min_ns,
max_ns: self.max_ns,
avg_ns: avg,
p50_ns: percentile(&sorted, 50),
p95_ns: percentile(&sorted, 95),
p99_ns: percentile(&sorted, 99),
})
}
pub fn reset(&mut self) {
*self = Self::new();
}
}
impl Default for LatencyTracker {
fn default() -> Self {
Self::new()
}
}
fn percentile(sorted: &[u64], p: u32) -> u64 {
debug_assert!(!sorted.is_empty());
debug_assert!(p <= 100);
let n = sorted.len();
let rank = (n as u64 * p as u64).div_ceil(100) as usize;
let idx = rank.saturating_sub(1).min(n - 1);
sorted[idx]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_tracker_returns_none() {
let t = LatencyTracker::new();
assert!(t.stats().is_none());
}
#[test]
fn single_sample() {
let mut t = LatencyTracker::new();
t.record(42_000);
let s = t.stats().unwrap();
assert_eq!(s.count, 1);
assert_eq!(s.min_ns, 42_000);
assert_eq!(s.max_ns, 42_000);
assert_eq!(s.avg_ns, 42_000);
assert_eq!(s.p50_ns, 42_000);
assert_eq!(s.p95_ns, 42_000);
assert_eq!(s.p99_ns, 42_000);
}
#[test]
fn known_percentiles() {
let mut t = LatencyTracker::new();
for i in 1..=100 {
t.record(i);
}
let s = t.stats().unwrap();
assert_eq!(s.count, 100);
assert_eq!(s.min_ns, 1);
assert_eq!(s.max_ns, 100);
assert_eq!(s.p50_ns, 50);
assert_eq!(s.p95_ns, 95);
assert_eq!(s.p99_ns, 99);
}
#[test]
fn circular_buffer_wraps() {
let mut t = LatencyTracker::new();
for i in 0..2048u64 {
t.record(i);
}
let s = t.stats().unwrap();
assert_eq!(s.count, 2048); assert_eq!(s.min_ns, 0); assert_eq!(s.max_ns, 2047);
assert!(s.p50_ns >= 1530 && s.p50_ns <= 1540);
}
#[test]
fn avg_is_window_average() {
let mut t = LatencyTracker::new();
t.record(100);
t.record(200);
t.record(300);
let s = t.stats().unwrap();
assert_eq!(s.avg_ns, 200); }
#[test]
fn record_elapsed() {
let mut t = LatencyTracker::new();
let elapsed = t.record_elapsed(1_000_000, 2_500_000);
assert_eq!(elapsed, 1_500_000);
let s = t.stats().unwrap();
assert_eq!(s.count, 1);
assert_eq!(s.p50_ns, 1_500_000);
}
#[test]
fn record_elapsed_handles_backwards_time() {
let mut t = LatencyTracker::new();
let elapsed = t.record_elapsed(5_000_000, 1_000_000);
assert_eq!(elapsed, 0);
}
#[test]
fn reset_clears_everything() {
let mut t = LatencyTracker::new();
for i in 0..100 {
t.record(i * 1000);
}
assert!(t.stats().is_some());
t.reset();
assert!(t.stats().is_none());
}
#[test]
fn percentile_function_edge_cases() {
assert_eq!(percentile(&[42], 0), 42);
assert_eq!(percentile(&[42], 50), 42);
assert_eq!(percentile(&[42], 100), 42);
assert_eq!(percentile(&[10, 20], 0), 10);
assert_eq!(percentile(&[10, 20], 50), 10);
assert_eq!(percentile(&[10, 20], 51), 20);
assert_eq!(percentile(&[10, 20], 100), 20);
}
#[test]
fn running_min_max_persist_across_window() {
let mut t = LatencyTracker::new();
t.record(1); for _ in 0..(MAX_SAMPLES + 10) {
t.record(1000);
}
let s = t.stats().unwrap();
assert_eq!(s.min_ns, 1);
}
#[test]
fn struct_sizes_and_alignment() {
assert_eq!(std::mem::size_of::<LatencyStats>(), 56);
assert_eq!(std::mem::align_of::<LatencyStats>(), 8);
assert_eq!(std::mem::size_of::<LatencyTracker>(), 8232);
}
}