use std::sync::Mutex;
use hdrhistogram::Histogram as HdrHistogram;
use super::live_window::HdrBounds;
#[derive(Debug)]
pub struct HdrSummary {
bounds: HdrBounds,
hist: Mutex<HdrHistogram<u64>>,
}
impl HdrSummary {
pub fn new(bounds: HdrBounds) -> Self {
Self {
hist: Mutex::new(bounds_build(&bounds)),
bounds,
}
}
pub fn latency() -> Self {
Self::new(HdrBounds::LATENCY_DEFAULT)
}
pub fn bounds(&self) -> HdrBounds {
self.bounds
}
pub fn record(&self, value: u64) {
let v = value.clamp(self.bounds.low, self.bounds.high);
let mut g = self.hist.lock().unwrap_or_else(|e| e.into_inner());
let _ = g.record(v);
}
pub fn peek_snapshot(&self) -> HdrSnapshot {
let g = self.hist.lock().unwrap_or_else(|e| e.into_inner());
HdrSnapshot { hist: g.clone() }
}
pub fn len(&self) -> u64 {
self.hist.lock().unwrap_or_else(|e| e.into_inner()).len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone)]
pub struct HdrSnapshot {
hist: HdrHistogram<u64>,
}
impl HdrSnapshot {
pub fn len(&self) -> u64 {
self.hist.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn mean(&self) -> f64 {
self.hist.mean()
}
pub fn min(&self) -> u64 {
self.hist.min()
}
pub fn max(&self) -> u64 {
self.hist.max()
}
pub fn percentile(&self, q: f64) -> u64 {
self.hist.value_at_quantile(q / 100.0)
}
pub fn p50(&self) -> u64 {
self.percentile(50.0)
}
pub fn p90(&self) -> u64 {
self.percentile(90.0)
}
pub fn p99(&self) -> u64 {
self.percentile(99.0)
}
pub fn p999(&self) -> u64 {
self.percentile(99.9)
}
}
fn bounds_build(b: &HdrBounds) -> HdrHistogram<u64> {
HdrHistogram::new_with_bounds(b.low, b.high, b.sig_digits).expect("HDR bounds must be valid")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retains_across_reads() {
let s = HdrSummary::latency();
for v in [1_000_000u64, 2_000_000, 3_000_000] {
s.record(v);
}
let a = s.peek_snapshot();
let b = s.peek_snapshot();
assert_eq!(a.len(), 3);
assert_eq!(b.len(), 3);
}
#[test]
fn percentile_accuracy_within_hdr_resolution() {
let s = HdrSummary::latency();
for _ in 0..1_000 {
s.record(1_000_000);
}
let snap = s.peek_snapshot();
assert_eq!(snap.len(), 1_000);
let p50 = snap.p50();
assert!((999_000..=1_001_000).contains(&p50), "p50 = {p50}");
}
#[test]
fn clamps_out_of_range_values() {
let bounds = HdrBounds {
low: 1_000,
high: 60_000_000_000,
sig_digits: 3,
};
let s = HdrSummary::new(bounds);
s.record(0); s.record(u64::MAX); let snap = s.peek_snapshot();
assert_eq!(snap.len(), 2);
assert!(
snap.min() > 0,
"min = {} should be > 0 after clamp",
snap.min()
);
assert!(
snap.min() < snap.max() / 1_000_000,
"min/max should be far apart: min={}, max={}",
snap.min(),
snap.max()
);
assert!(
snap.max() <= 60_000_000_000 + 60_000_000,
"max = {} should be ≤ 60s + one bucket",
snap.max()
);
}
#[test]
fn snapshot_is_independent_of_further_records() {
let s = HdrSummary::latency();
s.record(1_000_000);
let snap = s.peek_snapshot();
s.record(2_000_000);
assert_eq!(snap.len(), 1);
assert_eq!(s.peek_snapshot().len(), 2);
}
#[test]
fn empty_summary_reports_empty_snapshot() {
let s = HdrSummary::latency();
let snap = s.peek_snapshot();
assert!(snap.is_empty());
assert_eq!(snap.len(), 0);
}
#[test]
fn concurrent_record_is_serialized() {
use std::sync::Arc;
use std::thread;
let s = Arc::new(HdrSummary::latency());
let mut handles = Vec::new();
for _ in 0..8 {
let s = s.clone();
handles.push(thread::spawn(move || {
for _ in 0..1_000 {
s.record(1_000_000);
}
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(s.len(), 8_000);
}
}