Skip to main content

krabka_bench_driver/
hist.rs

1//! HDR histogram helpers.
2//!
3//! The driver records latencies in microseconds with `(min=1us, max=60s,
4//! sigfig=3)`. That gives enough resolution from sub-ms up to degraded-broker
5//! timeouts, and it keeps memory use low.
6//!
7//! `hdrhistogram` counts raw `u64`s, so microseconds are the histogram's own
8//! unit. [`record`] and [`percentiles`] are the seam that converts to and from
9//! the [`Time`] extents the rest of the driver passes around.
10
11use {
12    crate::scenario::LatencyPercentiles,
13    hdrhistogram::{CreationError, Histogram},
14    krabka_units::prelude::*,
15};
16
17/// One per-task latency histogram. Use this constructor, or
18/// [`Histogram::new_from`] on a histogram it built, because the bounds matter
19/// for accuracy.
20///
21/// # Errors
22///
23/// The [`CreationError`] `hdrhistogram` returns for the driver's bounds.
24pub fn new() -> Result<Histogram<u64>, CreationError> {
25    Histogram::<u64>::new_with_bounds(1, 60_000_000, 3)
26}
27
28/// Records one measured latency. This clamps to the histogram's microsecond
29/// range, so a single outlier cannot break the recorder.
30pub fn record(h: &mut Histogram<u64>, latency: Time) {
31    let micros = u64::try_from(latency.micros_i64()).unwrap_or_default();
32    let _ = h.record(micros.clamp(1, h.high()));
33}
34
35/// Projects a histogram into the public percentile shape.
36#[must_use]
37pub fn percentiles(h: &Histogram<u64>) -> LatencyPercentiles {
38    let at = |quantile: f64| Time::from_micros(saturating_micros(h.value_at_quantile(quantile)));
39    LatencyPercentiles {
40        p50: at(0.50),
41        p95: at(0.95),
42        p99: at(0.99),
43        p999: at(0.999),
44        max: Time::from_micros(saturating_micros(h.max())),
45        mean: micros(1) * h.mean(),
46        count: h.len(),
47    }
48}
49
50/// A histogram bucket value, which is a microsecond count, as the `i64` the
51/// [`Time`] seam takes.
52fn saturating_micros(value: u64) -> i64 { i64::try_from(value).unwrap_or(i64::MAX) }
53
54#[cfg(test)]
55mod tests {
56    use {super::*, assert2::check};
57
58    #[test]
59    fn percentiles_report_recorded_extents() {
60        let mut h = new().unwrap();
61        for v in [millis(1), millis(2), millis(3), millis(4), millis(5)] {
62            record(&mut h, v);
63        }
64        let p = percentiles(&h);
65        check!(p.count == 5);
66        check!(p.p50 > Time::ZERO);
67        check!(p.max >= p.p99);
68        // 3-significant-figure buckets keep a millisecond sample within 0.1%.
69        check!((p.max - millis(5)).secs_f64().abs() < millis(1).secs_f64() * 0.01);
70        check!((p.mean - millis(3)).secs_f64().abs() < millis(1).secs_f64() * 0.01);
71    }
72
73    #[test]
74    fn sub_millisecond_latencies_keep_their_resolution() {
75        let mut h = new().unwrap();
76        record(&mut h, micros(250));
77        let p = percentiles(&h);
78        check!(p.p50 >= micros(240));
79        check!(p.p50 <= micros(260));
80    }
81
82    #[test]
83    fn clamps_above_max_into_range() {
84        let mut h = new().unwrap();
85        record(&mut h, days(1));
86        check!(h.len() == 1);
87    }
88
89    #[test]
90    fn clamps_below_min_into_range() {
91        let mut h = new().unwrap();
92        record(&mut h, Time::ZERO);
93        check!(h.len() == 1);
94    }
95}