Skip to main content

subms_hdr_histogram/
lib.rs

1//! Log-linear bucket histogram with significant-digit precision.
2//!
3//! Each value is mapped to a bucket index built from two pieces:
4//!
5//! - **Major bucket**: `floor(log2(value)) - log2(sub_count) + 1`, clamped at 0.
6//!   Each major covers a doubling range (1, 2, 4, 8, ...).
7//! - **Sub-bucket**: linear position within the major range.
8//!
9//! Together they give constant relative error within the significant-digit
10//! precision: a value's bucket is never wider than `1 / sub_count` of the value
11//! itself. `sub_count` is `2 * 10^d` rounded up to a power of two, so `d = 3`
12//! gives `2^11 = 2048` sub-buckets and a worst-case quantisation error of
13//! 1/2048 (0.049%), inside the half-unit-in-the-third-digit that three
14//! significant digits demands.
15//!
16//! The counter array starts at `sub_count` entries and grows lazily to cover
17//! the largest value recorded: at `d = 3` a range topping out at 10^6 lands at
18//! index 20290 (~20k counters, 163 KB), and one topping out at 10^9 at index
19//! 40678 (~41k counters, 326 KB).
20//!
21//! ```
22//! use subms_hdr_histogram::HdrHistogram;
23//! let mut h = HdrHistogram::new(3);
24//! for v in [10u64, 20, 30, 40, 50] { h.record(v); }
25//! assert_eq!(h.count(), 5);
26//! let p50 = h.value_at_percentile(0.5);
27//! assert!((20..=30).contains(&p50), "p50={p50}");
28//! assert_eq!(h.max(), 50);
29//! ```
30//!
31//! Full writeup, design notes and measured benchmarks:
32//! <https://www.submillisecond.com/cookbook/recipes/subms-hdr-histogram>
33
34/// Histogram with `significant_digits` of precision in `[1, 5]`.
35pub struct HdrHistogram {
36    /// Number of sub-buckets within each major bucket. Power of two.
37    sub_count: u32,
38    /// Bit-width of sub_count (so `value >> shift` ignores the sub portion).
39    sub_count_bits: u32,
40    /// Flat counter array; length grows as bigger values are recorded.
41    counters: Vec<u64>,
42    /// Total count across counters.
43    total: u64,
44    /// Highest non-zero counter index seen, for fast iteration.
45    high_index: usize,
46}
47
48impl HdrHistogram {
49    /// `significant_digits` in `[1, 5]`; clamped if out of range.
50    pub fn new(significant_digits: u32) -> Self {
51        let sig = significant_digits.clamp(1, 5);
52        // sub_count = 2 * 10^sig rounded up to next power of two.
53        let target = 2u32 * 10u32.pow(sig);
54        let sub_count_bits = (32 - target.leading_zeros()).max(1);
55        let sub_count = 1u32 << sub_count_bits;
56        Self {
57            sub_count,
58            sub_count_bits,
59            counters: vec![0u64; sub_count as usize],
60            total: 0,
61            high_index: 0,
62        }
63    }
64
65    pub fn count(&self) -> u64 {
66        self.total
67    }
68
69    /// Highest value recorded (approximated to the bucket's lower bound).
70    pub fn max(&self) -> u64 {
71        if self.total == 0 {
72            return 0;
73        }
74        value_from_index(self.high_index, self.sub_count_bits)
75    }
76
77    pub fn record(&mut self, value: u64) {
78        let idx = index_of(value, self.sub_count_bits) as usize;
79        if idx >= self.counters.len() {
80            self.counters.resize(idx + 1, 0);
81        }
82        self.counters[idx] += 1;
83        self.total += 1;
84        if idx > self.high_index {
85            self.high_index = idx;
86        }
87    }
88
89    /// Record `value`, then correct for coordinated omission. Under a fixed-rate
90    /// load generator, one slow operation blocks every request that should have
91    /// been issued while it stalled; those requests are never sampled, so the
92    /// tail reads far better than the system delivered. When `value` exceeds
93    /// `expected_interval`, this backfills the samples the generator would have
94    /// taken during the stall - synthetic values at `value - expected_interval`,
95    /// `value - 2*expected_interval`, ... down to `expected_interval` - so the
96    /// percentiles reflect the latency those blocked requests would have seen.
97    ///
98    /// This is Gil Tene's `recordValueWithExpectedInterval`. `expected_interval
99    /// == 0` (or a `value` no larger than it) disables the correction, leaving
100    /// this equivalent to [`Self::record`].
101    pub fn record_with_expected_interval(&mut self, value: u64, expected_interval: u64) {
102        self.record(value);
103        // Guard the u64 subtraction below: also the correct no-op when the op
104        // ran at or under the expected cadence (nothing was omitted).
105        if expected_interval == 0 || value <= expected_interval {
106            return;
107        }
108        let mut missing = value - expected_interval;
109        while missing >= expected_interval {
110            self.record(missing);
111            missing -= expected_interval;
112        }
113    }
114
115    /// Value at the given quantile (`0.0..=1.0`). 0 if empty.
116    pub fn value_at_percentile(&self, q: f64) -> u64 {
117        if self.total == 0 {
118            return 0;
119        }
120        let target = ((q.clamp(0.0, 1.0) * self.total as f64) as u64).max(1);
121        let mut cum = 0u64;
122        // Bound the sweep to the populated range (parity with the Java port); the
123        // dead tail past high_index holds only zeros and never shifts the result.
124        for (i, &c) in self.counters.iter().take(self.high_index + 1).enumerate() {
125            cum += c;
126            if cum >= target {
127                return value_from_index(i, self.sub_count_bits);
128            }
129        }
130        value_from_index(self.high_index, self.sub_count_bits)
131    }
132
133    pub fn sub_count(&self) -> u32 {
134        self.sub_count
135    }
136
137    /// Lowest value recorded, as its bucket's lower bound. 0 if empty.
138    /// Read-side sweep, same cost class as [`Self::value_at_percentile`].
139    pub fn min(&self) -> u64 {
140        if self.total == 0 {
141            return 0;
142        }
143        for (i, &c) in self.counters.iter().take(self.high_index + 1).enumerate() {
144            if c > 0 {
145                return value_from_index(i, self.sub_count_bits);
146            }
147        }
148        0
149    }
150
151    /// Arithmetic mean over the recorded bucket lower bounds. 0.0 if empty.
152    /// Quantised the same way the percentiles are, so it sits within the
153    /// significant-digit error band rather than being exact.
154    pub fn mean(&self) -> f64 {
155        if self.total == 0 {
156            return 0.0;
157        }
158        let mut sum = 0f64;
159        for (i, &c) in self.counters.iter().take(self.high_index + 1).enumerate() {
160            if c > 0 {
161                sum += c as f64 * value_from_index(i, self.sub_count_bits) as f64;
162            }
163        }
164        sum / self.total as f64
165    }
166
167    /// Recordings that landed in `value`'s bucket. Constant time - the same
168    /// index computation `record` does.
169    pub fn count_at_value(&self, value: u64) -> u64 {
170        let idx = index_of(value, self.sub_count_bits) as usize;
171        self.counters.get(idx).copied().unwrap_or(0)
172    }
173
174    /// Fraction of recordings at or below `value`'s bucket, in `0.0..=1.0`.
175    /// The inverse of [`Self::value_at_percentile`]: that maps a rank to a
176    /// value, this maps a value to its rank.
177    pub fn percentile_at_or_below_value(&self, value: u64) -> f64 {
178        if self.total == 0 {
179            return 0.0;
180        }
181        let idx = index_of(value, self.sub_count_bits) as usize;
182        let end = (idx + 1).min(self.high_index + 1).min(self.counters.len());
183        let cum: u64 = self.counters[..end].iter().sum();
184        cum as f64 / self.total as f64
185    }
186
187    /// Counter-array footprint in bytes. Grows with the largest value
188    /// recorded, not with how many values were recorded.
189    pub fn footprint_bytes(&self) -> usize {
190        self.counters.len() * size_of::<u64>()
191    }
192
193    /// Drop every recorded value. Keeps the array allocated, so a histogram
194    /// recycled across reporting intervals never re-enters the allocator.
195    pub fn reset(&mut self) {
196        self.counters.fill(0);
197        self.total = 0;
198        self.high_index = 0;
199    }
200
201    // ----- crate-private accessors for feature modules -----
202
203    #[cfg(feature = "iterators")]
204    #[inline]
205    pub(crate) fn sub_count_bits(&self) -> u32 {
206        self.sub_count_bits
207    }
208
209    #[cfg(feature = "iterators")]
210    #[inline]
211    pub(crate) fn counters(&self) -> &[u64] {
212        &self.counters
213    }
214
215    #[cfg(feature = "iterators")]
216    #[inline]
217    pub(crate) fn high_index(&self) -> usize {
218        self.high_index
219    }
220
221    /// Add another histogram's counters into this one. Used by the
222    /// `merge` feature module. Errors if the two histograms have
223    /// different `sub_count_bits` (different significant-digit shapes).
224    #[cfg(feature = "merge")]
225    pub(crate) fn add_counts_from(&mut self, other: &HdrHistogram) -> Result<(), &'static str> {
226        if self.sub_count_bits != other.sub_count_bits {
227            return Err("significant-digit mismatch");
228        }
229        if other.high_index >= self.counters.len() {
230            self.counters.resize(other.high_index + 1, 0);
231        }
232        for (i, &c) in other.counters.iter().enumerate() {
233            if c == 0 {
234                continue;
235            }
236            self.counters[i] += c;
237            if i > self.high_index {
238                self.high_index = i;
239            }
240        }
241        self.total += other.total;
242        Ok(())
243    }
244}
245
246/// Bucket index. Values `< sub_count` go in the linear part of the first
247/// major. Larger values use a major bucket equal to `bits(value) - bits(sub_count-1)`.
248pub(crate) fn index_of(value: u64, sub_count_bits: u32) -> u32 {
249    let sub_mask = (1u64 << sub_count_bits) - 1;
250    if value <= sub_mask {
251        return value as u32;
252    }
253    let bits = 64 - value.leading_zeros();
254    let major = bits - sub_count_bits;
255    // sub portion: top sub_count_bits bits of value after the leading 1.
256    let sub = ((value >> (major - 1)) & sub_mask) as u32;
257    (major << sub_count_bits) | sub
258}
259
260pub(crate) fn value_from_index(idx: usize, sub_count_bits: u32) -> u64 {
261    let sub_count = 1u64 << sub_count_bits;
262    let sub_mask = sub_count - 1;
263    let idx = idx as u64;
264    if idx < sub_count {
265        return idx;
266    }
267    let major = idx >> sub_count_bits;
268    let sub = idx & sub_mask;
269    (sub | sub_count) << (major - 1)
270}
271
272#[cfg(test)]
273#[path = "hdr_tests.rs"]
274mod hdr_tests;
275
276#[cfg(test)]
277#[path = "sample_app_tests.rs"]
278mod sample_app_tests;
279
280#[cfg(feature = "harness")]
281pub mod growth;
282
283#[cfg(feature = "harness")]
284pub mod recipe;
285
286// Opt-in feature modules. Base histogram is zero-dep + std-only; each
287// opt-in adds a focused capability under its own Cargo feature.
288#[cfg(any(
289    feature = "dual-recorder",
290    feature = "concurrent-writes",
291    feature = "merge",
292    feature = "decay",
293    feature = "value-tagging",
294    feature = "iterators",
295))]
296pub mod features;
297
298#[cfg(feature = "concurrent-writes")]
299pub use features::concurrent_writes::ConcurrentHdrHistogram;
300#[cfg(feature = "decay")]
301pub use features::decay::{Clock, DecayingHdrHistogram, ManualClock};
302#[cfg(feature = "dual-recorder")]
303pub use features::dual_recorder::DualRecorder;
304#[cfg(feature = "iterators")]
305pub use features::iterators::{HdrLinearIter, HdrLogarithmicIter, HdrPercentileIter, IterEntry};
306#[cfg(feature = "merge")]
307pub use features::merge::merge;
308#[cfg(feature = "value-tagging")]
309pub use features::value_tagging::TaggedHdrHistogram;