use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use serde::{Deserialize, Serialize};
use kuebiko::{FINE_LATENCY_BUCKETS, Histogram, Percentiles};
const WINDOW: usize = 128;
const EVERY: u64 = 16;
const UNKNOWN: u64 = u64::MAX;
pub(crate) struct LatencyEstimate {
recent: Mutex<Ring>,
histogram: Histogram,
p50: AtomicU64,
p99: AtomicU64,
count: AtomicU64,
sum: AtomicU64,
max: AtomicU64,
}
struct Ring {
samples: [u64; WINDOW],
next: usize,
len: usize,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct LatencySnapshot {
pub count: u64,
#[serde(flatten)]
pub percentiles: Percentiles,
pub mean: Option<f64>,
pub max: Option<f64>,
}
impl Default for LatencyEstimate {
fn default() -> Self {
Self::new()
}
}
impl LatencyEstimate {
pub fn new() -> Self {
Self {
recent: Mutex::new(Ring {
samples: [0; WINDOW],
next: 0,
len: 0,
}),
histogram: Histogram::with_buckets(FINE_LATENCY_BUCKETS),
p50: AtomicU64::new(UNKNOWN),
p99: AtomicU64::new(UNKNOWN),
count: AtomicU64::new(0),
sum: AtomicU64::new(0),
max: AtomicU64::new(0),
}
}
pub fn observe(&self, elapsed: Duration) {
self.histogram.observe(elapsed);
let nanos = u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX);
let count = self.count.fetch_add(1, Ordering::Relaxed) + 1;
self.sum.fetch_add(nanos, Ordering::Relaxed);
self.max.fetch_max(nanos, Ordering::Relaxed);
let mut ring = self.recent.lock().unwrap_or_else(|e| e.into_inner());
let next = ring.next;
ring.samples[next] = nanos;
ring.next = (next + 1) % WINDOW;
ring.len = (ring.len + 1).min(WINDOW);
if count <= 4 || count.is_multiple_of(EVERY) {
let len = ring.len;
let mut samples = ring.samples;
drop(ring);
let samples = &mut samples[..len];
let rank = |q: f64| ((len - 1) as f64 * q).round() as usize;
let (p50, p99) = (rank(0.50), rank(0.99));
let (below, &mut at_p99, _) = samples.select_nth_unstable(p99);
let at_p50 = if p50 == p99 {
at_p99
} else {
*below.select_nth_unstable(p50).1
};
self.p50.store(at_p50, Ordering::Relaxed);
self.p99.store(at_p99, Ordering::Relaxed);
}
}
fn get(&self, cell: &AtomicU64) -> Option<Duration> {
match cell.load(Ordering::Relaxed) {
UNKNOWN => None,
nanos => Some(Duration::from_nanos(nanos)),
}
}
pub fn p50(&self) -> Option<Duration> {
self.get(&self.p50)
}
pub fn p99(&self) -> Option<Duration> {
self.get(&self.p99)
}
pub fn count(&self) -> u64 {
self.count.load(Ordering::Relaxed)
}
pub fn histogram(&self) -> &Histogram {
&self.histogram
}
pub fn snapshot(&self) -> LatencySnapshot {
let count = self.count();
LatencySnapshot {
count,
percentiles: self.histogram.percentiles_ms(),
mean: (count > 0).then(|| self.sum.load(Ordering::Relaxed) as f64 / count as f64 / 1e6),
max: (count > 0).then(|| self.max.load(Ordering::Relaxed) as f64 / 1e6),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_before_the_first_sample_then_tracks_the_window() {
let l = LatencyEstimate::new();
assert_eq!(l.p99(), None);
l.observe(Duration::from_micros(10));
assert_eq!(l.p50(), Some(Duration::from_micros(10)));
assert_eq!(l.p99(), Some(Duration::from_micros(10)));
for _ in 0..200 {
l.observe(Duration::from_micros(5));
}
assert_eq!(l.p99(), Some(Duration::from_micros(5)));
let snapshot = l.snapshot();
assert_eq!(snapshot.count, 201);
assert_eq!(l.histogram().count(), 201);
let p50 = snapshot.percentiles.p50.unwrap();
assert!((0.0025..=0.005).contains(&p50), "{p50}");
let p99 = snapshot.percentiles.p99.unwrap();
assert!((0.0025..=0.005).contains(&p99), "{p99}");
assert_eq!(snapshot.max, Some(0.01));
let mean = snapshot.mean.unwrap();
assert!((0.005..0.0051).contains(&mean), "{mean}");
}
#[test]
fn percentiles_are_stale_between_recomputes() {
let l = LatencyEstimate::new();
for _ in 0..4 {
l.observe(Duration::from_micros(10));
}
assert_eq!(l.p99(), Some(Duration::from_micros(10)));
for _ in 4..EVERY - 1 {
l.observe(Duration::from_millis(1));
}
assert_eq!(l.count(), EVERY - 1);
assert_eq!(l.p50(), Some(Duration::from_micros(10)));
assert_eq!(l.p99(), Some(Duration::from_micros(10)));
l.observe(Duration::from_millis(1));
assert_eq!(l.p50(), Some(Duration::from_millis(1)));
assert_eq!(l.p99(), Some(Duration::from_millis(1)));
assert_eq!(l.snapshot().count, EVERY);
assert_eq!(l.snapshot().max, Some(1.0));
}
#[test]
fn p99_sees_the_slow_tail() {
let l = LatencyEstimate::new();
for i in 0..128 {
l.observe(Duration::from_micros(if i % 50 == 0 { 1000 } else { 10 }));
}
assert_eq!(l.p50(), Some(Duration::from_micros(10)));
assert_eq!(l.p99(), Some(Duration::from_micros(1000)));
}
}