use std::time::Duration;
#[derive(Debug, Default, Clone)]
pub struct Samples {
samples: Vec<Duration>,
}
impl Samples {
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, d: Duration) {
self.samples.push(d);
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
pub fn summary(&self) -> Option<LatencySummary> {
if self.samples.is_empty() {
return None;
}
let mut sorted: Vec<u128> = self.samples.iter().map(|d| d.as_nanos()).collect();
sorted.sort_unstable();
Some(LatencySummary {
p50_ms: percentile(&sorted, 0.50),
p95_ms: percentile(&sorted, 0.95),
samples: sorted.len(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LatencySummary {
pub p50_ms: f64,
pub p95_ms: f64,
pub samples: usize,
}
fn percentile(sorted_ns: &[u128], q: f64) -> f64 {
debug_assert!(!sorted_ns.is_empty());
let n = sorted_ns.len();
if n == 1 {
return sorted_ns[0] as f64 / 1_000_000.0;
}
let pos = q * (n - 1) as f64;
let lo = pos.floor() as usize;
let hi = pos.ceil() as usize;
let frac = pos - lo as f64;
let lo_ns = sorted_ns[lo] as f64;
let hi_ns = sorted_ns[hi] as f64;
(lo_ns + (hi_ns - lo_ns) * frac) / 1_000_000.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_summary_is_none() {
let s = Samples::new();
assert!(s.summary().is_none());
}
#[test]
fn single_sample_p50_equals_p95() {
let mut s = Samples::new();
s.record(Duration::from_millis(100));
let sum = s.summary().unwrap();
assert!((sum.p50_ms - 100.0).abs() < 1e-6);
assert!((sum.p95_ms - 100.0).abs() < 1e-6);
assert_eq!(sum.samples, 1);
}
#[test]
fn percentile_on_uniform_distribution() {
let mut s = Samples::new();
for ms in 1..=100 {
s.record(Duration::from_millis(ms));
}
let sum = s.summary().unwrap();
assert!((sum.p50_ms - 50.5).abs() < 0.5);
assert!((sum.p95_ms - 95.05).abs() < 0.5);
}
#[test]
fn p95_resists_single_outlier() {
let mut s = Samples::new();
for _ in 0..99 {
s.record(Duration::from_millis(10));
}
s.record(Duration::from_millis(10_000));
let sum = s.summary().unwrap();
assert!(sum.p50_ms < 15.0);
assert!(sum.p95_ms < 100.0);
}
}