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;