Skip to main content

lmn_core/histogram/
latency.rs

1use std::time::Duration;
2
3use hdrhistogram::Histogram;
4
5// ── LatencyHistogram ──────────────────────────────────────────────────────────
6
7/// HDR histogram for recording request latencies.
8///
9/// Records durations in microseconds. Covers 1µs to 1 hour at 3 significant
10/// digits of precision. Provides exact quantile queries with bounded error.
11pub struct LatencyHistogram {
12    inner: Histogram<u64>,
13}
14
15impl LatencyHistogram {
16    /// Creates a new histogram covering 1µs to 1 hour at 3 significant digits.
17    pub fn new() -> Self {
18        // Bounds are compile-time constants: low=1µs, high=1h, sig_figs=3.
19        // new_with_bounds only fails for invalid bounds (low==0, high<2*low, sig_figs>5),
20        // none of which can occur here.
21        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    /// Records a duration. Values are clamped to [1µs, 1 hour].
28    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    /// Returns the value at quantile `q` (0.0–1.0) in milliseconds.
38    pub fn quantile_ms(&self, q: f64) -> f64 {
39        self.inner.value_at_quantile(q) as f64 / 1000.0
40    }
41
42    /// Returns the minimum recorded value in milliseconds.
43    pub fn min_ms(&self) -> f64 {
44        self.inner.min() as f64 / 1000.0
45    }
46
47    /// Returns the maximum recorded value in milliseconds.
48    pub fn max_ms(&self) -> f64 {
49        self.inner.max() as f64 / 1000.0
50    }
51
52    /// Returns the arithmetic mean of recorded values in milliseconds.
53    pub fn mean_ms(&self) -> f64 {
54        self.inner.mean() / 1000.0
55    }
56
57    /// Returns the total number of recorded values.
58    pub fn total_count(&self) -> u64 {
59        self.inner.len()
60    }
61
62    /// Returns `true` if no values have been recorded.
63    pub fn is_empty(&self) -> bool {
64        self.inner.is_empty()
65    }
66
67    /// Returns `(value_us, count)` pairs for all recorded distinct values.
68    /// Used by the CLI to render the latency bar chart.
69    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// ── Tests ─────────────────────────────────────────────────────────────────────
83
84#[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        // p50 of [10, 20, 30] should be approximately 20ms
95        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        // Duration::ZERO is 0µs — clamped to 1µs internally
103        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        // All counts should be > 0
138        for (_, count) in &pairs {
139            assert!(*count > 0);
140        }
141    }
142}