use std::time::Duration;
use hdrhistogram::Histogram;
pub struct LatencyHistogram {
inner: Histogram<u64>,
}
impl LatencyHistogram {
pub fn new() -> Self {
let inner = Histogram::<u64>::new_with_bounds(1, 3_600_000_000, 3).unwrap_or_else(|_| {
unreachable!("HDR histogram bounds (1, 3_600_000_000, 3) are always valid")
});
Self { inner }
}
pub fn record(&mut self, d: Duration) {
let us = (d.as_micros() as u64).max(1).min(self.inner.high());
let ok = self.inner.record(us).is_ok();
debug_assert!(
ok,
"HDR histogram record failed for value {us}µs — this should never happen after clamping"
);
}
pub fn quantile_ms(&self, q: f64) -> f64 {
self.inner.value_at_quantile(q) as f64 / 1000.0
}
pub fn min_ms(&self) -> f64 {
self.inner.min() as f64 / 1000.0
}
pub fn max_ms(&self) -> f64 {
self.inner.max() as f64 / 1000.0
}
pub fn mean_ms(&self) -> f64 {
self.inner.mean() / 1000.0
}
pub fn total_count(&self) -> u64 {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn iter_recorded_us(&self) -> impl Iterator<Item = (u64, u64)> + '_ {
self.inner
.iter_recorded()
.map(|v| (v.value_iterated_to(), v.count_at_value()))
}
}
impl Default for LatencyHistogram {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_and_quantile_basic() {
let mut h = LatencyHistogram::new();
h.record(Duration::from_millis(10));
h.record(Duration::from_millis(20));
h.record(Duration::from_millis(30));
let p50 = h.quantile_ms(0.50);
assert!((10.0..=30.0).contains(&p50), "p50={p50} not in [10, 30]");
}
#[test]
fn record_zero_duration_does_not_panic() {
let mut h = LatencyHistogram::new();
h.record(Duration::ZERO);
assert!(!h.is_empty());
}
#[test]
fn is_empty_before_any_record() {
let h = LatencyHistogram::new();
assert!(h.is_empty());
}
#[test]
fn is_not_empty_after_record() {
let mut h = LatencyHistogram::new();
h.record(Duration::from_millis(1));
assert!(!h.is_empty());
}
#[test]
fn min_max_ms_correct() {
let mut h = LatencyHistogram::new();
h.record(Duration::from_millis(10));
h.record(Duration::from_millis(100));
assert!((h.min_ms() - 10.0).abs() < 1.0, "min_ms={}", h.min_ms());
assert!((h.max_ms() - 100.0).abs() < 1.0, "max_ms={}", h.max_ms());
}
#[test]
fn iter_recorded_us_non_empty() {
let mut h = LatencyHistogram::new();
h.record(Duration::from_millis(5));
h.record(Duration::from_millis(50));
let pairs: Vec<_> = h.iter_recorded_us().collect();
assert!(!pairs.is_empty(), "expected at least one recorded bucket");
for (_, count) in &pairs {
assert!(*count > 0);
}
}
}