lmn_core/histogram/
latency.rs1use std::time::Duration;
2
3use hdrhistogram::Histogram;
4
5pub struct LatencyHistogram {
12 inner: Histogram<u64>,
13}
14
15impl LatencyHistogram {
16 pub fn new() -> Self {
18 let inner = Histogram::<u64>::new_with_bounds(1, 3_600_000_000, 3).unwrap_or_else(|_| {
22 unreachable!("HDR histogram bounds (1, 3_600_000_000, 3) are always valid")
23 });
24 Self { inner }
25 }
26
27 pub fn record(&mut self, d: Duration) {
29 let us = (d.as_micros() as u64).max(1).min(self.inner.high());
30 let ok = self.inner.record(us).is_ok();
31 debug_assert!(
32 ok,
33 "HDR histogram record failed for value {us}µs — this should never happen after clamping"
34 );
35 }
36
37 pub fn quantile_ms(&self, q: f64) -> f64 {
39 self.inner.value_at_quantile(q) as f64 / 1000.0
40 }
41
42 pub fn min_ms(&self) -> f64 {
44 self.inner.min() as f64 / 1000.0
45 }
46
47 pub fn max_ms(&self) -> f64 {
49 self.inner.max() as f64 / 1000.0
50 }
51
52 pub fn mean_ms(&self) -> f64 {
54 self.inner.mean() / 1000.0
55 }
56
57 pub fn total_count(&self) -> u64 {
59 self.inner.len()
60 }
61
62 pub fn is_empty(&self) -> bool {
64 self.inner.is_empty()
65 }
66
67 pub fn iter_recorded_us(&self) -> impl Iterator<Item = (u64, u64)> + '_ {
70 self.inner
71 .iter_recorded()
72 .map(|v| (v.value_iterated_to(), v.count_at_value()))
73 }
74}
75
76impl Default for LatencyHistogram {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn record_and_quantile_basic() {
90 let mut h = LatencyHistogram::new();
91 h.record(Duration::from_millis(10));
92 h.record(Duration::from_millis(20));
93 h.record(Duration::from_millis(30));
94 let p50 = h.quantile_ms(0.50);
96 assert!((10.0..=30.0).contains(&p50), "p50={p50} not in [10, 30]");
97 }
98
99 #[test]
100 fn record_zero_duration_does_not_panic() {
101 let mut h = LatencyHistogram::new();
102 h.record(Duration::ZERO);
104 assert!(!h.is_empty());
105 }
106
107 #[test]
108 fn is_empty_before_any_record() {
109 let h = LatencyHistogram::new();
110 assert!(h.is_empty());
111 }
112
113 #[test]
114 fn is_not_empty_after_record() {
115 let mut h = LatencyHistogram::new();
116 h.record(Duration::from_millis(1));
117 assert!(!h.is_empty());
118 }
119
120 #[test]
121 fn min_max_ms_correct() {
122 let mut h = LatencyHistogram::new();
123 h.record(Duration::from_millis(10));
124 h.record(Duration::from_millis(100));
125 assert!((h.min_ms() - 10.0).abs() < 1.0, "min_ms={}", h.min_ms());
126 assert!((h.max_ms() - 100.0).abs() < 1.0, "max_ms={}", h.max_ms());
127 }
128
129 #[test]
130 fn iter_recorded_us_non_empty() {
131 let mut h = LatencyHistogram::new();
132 h.record(Duration::from_millis(5));
133 h.record(Duration::from_millis(50));
134
135 let pairs: Vec<_> = h.iter_recorded_us().collect();
136 assert!(!pairs.is_empty(), "expected at least one recorded bucket");
137 for (_, count) in &pairs {
139 assert!(*count > 0);
140 }
141 }
142}