Skip to main content

HdrHistogram

Struct HdrHistogram 

Source
pub struct HdrHistogram { /* private fields */ }
Expand description

Histogram with significant_digits of precision in [1, 5].

Implementations§

Source§

impl HdrHistogram

Source

pub fn iter_linear(&self) -> HdrLinearIter<'_>

Source

pub fn iter_logarithmic(&self) -> HdrLogarithmicIter<'_>

Examples found in repository?
examples/sample_app.rs (line 277)
271fn iterators_export_bands() {
272    println!("\n== iterators: export the distribution as bands ==");
273    let mut h = HdrHistogram::new(3);
274    for v in 1..=1_000u64 {
275        h.record(v);
276    }
277    let bands = h.iter_logarithmic().count();
278    let quartiles: Vec<u64> = h.iter_percentiles(25.0).map(|e| e.value_lo).collect();
279    println!("  {bands} log2 bands; quartile lower bounds = {quartiles:?}");
280    assert!(bands > 0, "the populated range spans at least one band");
281    assert!(
282        !quartiles.is_empty(),
283        "the percentile walk yields quartile buckets"
284    );
285}
More examples
Hide additional examples
examples/perf_features.rs (line 332)
119fn main() -> io::Result<()> {
120    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
121        .join("..")
122        .join(".subms")
123        .join("features")
124        .join("rust.json");
125    let existing = std::fs::read_to_string(&path).unwrap_or_default();
126    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
127    // Stamp the box these numbers came from. The bench runs wherever it is
128    // invoked, so an unstamped manifest is indistinguishable from a fleet
129    // capture; the renderer will not publish one it cannot attribute.
130    let (source, instance) = SubMsP99Source::from_env();
131    manifest.set_p99_source(source, instance.as_deref());
132
133    // The baseline: base `record`, the per-op write path. Every feature is
134    // classified against the cost of the write it is decorating.
135    let mut base = HdrHistogram::new(CANON_D);
136    let base_p50 = keyed(|i| base.record(value_at(i)), true);
137    eprintln!("base record p50: {base_p50}ns");
138
139    // ---------- concurrent-writes: atomic buckets, &self record ----------
140    #[cfg(feature = "concurrent-writes")]
141    {
142        use subms_hdr_histogram::ConcurrentHdrHistogram;
143        let sw = sweep("concurrent-writes/record", |d| {
144            let c = ConcurrentHdrHistogram::new(d);
145            keyed(|i| c.record(value_at(i)), true)
146        });
147        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
148
149        let c = ConcurrentHdrHistogram::new(CANON_D);
150        let mut p99 = BTreeMap::new();
151        p99.insert(
152            "record".to_string(),
153            keyed(|i| c.record(value_at(i)), false),
154        );
155        p99.insert(
156            "percentile".to_string(),
157            keyed(|_| _ = c.value_at_percentile(99.0), false),
158        );
159        manifest.set_feature("concurrent-writes", cat, &p99, &reason);
160    }
161
162    // ---------- dual-recorder: hot/stable pair, swap and drain ----------
163    #[cfg(feature = "dual-recorder")]
164    {
165        use subms_hdr_histogram::DualRecorder;
166        // Swept on the interval read, not on `record`. The record is the same
167        // atomic write the concurrent feature already covers; the swap-and-drain
168        // is what a dual recorder is FOR, and it is O(buckets).
169        let sw = sweep("dual-recorder/interval", |d| {
170            let r = DualRecorder::new(d);
171            for i in 0..sub_count(d) {
172                r.record(value_at(i));
173            }
174            bulk(|| (), |()| _ = r.get_interval_histogram(), true)
175        });
176        // PINNED structural. The drain is O(buckets) from the source in BOTH
177        // ports, but it is DESTRUCTIVE, so repeating it measures an already-empty
178        // histogram rather than the drain: after the first rep each side has
179        // nothing left to copy. Refilling between reps would put the refill
180        // inside the timed region, which is the bug the ART port shipped. The
181        // two ports disagree precisely here for that reason - Rust copies the
182        // counter array unconditionally (468us at 262144 buckets) while Java
183        // collapses a high-water index and reads 100ns - and neither number is
184        // the operation a caller performs. Recording either as hot-path would
185        // say a whole-array drain is safe per-op.
186        let (cat, reason) = classify_feature(
187            &sw,
188            Some(base_p50),
189            Some(subms::SubMsFeatureCategory::Structural),
190        );
191
192        let r = DualRecorder::new(CANON_D);
193        for i in 0..OPS {
194            r.record(value_at(i));
195        }
196        let mut p99 = BTreeMap::new();
197        p99.insert(
198            "record".to_string(),
199            keyed(|i| r.record(value_at(i)), false),
200        );
201        p99.insert(
202            "interval_read".to_string(),
203            bulk(|| (), |()| _ = r.get_interval_histogram(), false),
204        );
205        manifest.set_feature("dual-recorder", cat, &p99, &reason);
206    }
207
208    // ---------- merge: element-wise add over the bucket arrays ----------
209    #[cfg(feature = "merge")]
210    {
211        use subms_hdr_histogram::merge;
212        // Both histograms are built by `setup`, outside the timed region.
213        // Repeating the merge grows the destination's counts but does identical
214        // work each rep, so the figure is the merge and nothing else.
215        let sw = sweep("merge/merge", |d| {
216            bulk(
217                || (filled(d), filled(d)),
218                |(dst, src)| merge(dst, src).expect("same precision"),
219                true,
220            )
221        });
222        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
223
224        let mut p99 = BTreeMap::new();
225        p99.insert(
226            "merge".to_string(),
227            bulk(
228                || (filled(CANON_D), filled(CANON_D)),
229                |(dst, src)| merge(dst, src).expect("same precision"),
230                false,
231            ),
232        );
233        manifest.set_feature("merge", cat, &p99, &reason);
234    }
235
236    // ---------- decay: exponentially-weighted counts ----------
237    #[cfg(feature = "decay")]
238    {
239        use subms_hdr_histogram::{Clock, DecayingHdrHistogram};
240        // The clock ADVANCES on every read. A frozen clock lets `decay_to_now`
241        // early-return and the feature measures as a plain record, which is the
242        // opposite of the truth: with time moving, every record first brings the
243        // whole counter array up to date, so the write is O(buckets). Freezing
244        // the clock here would have published that as hot-path.
245        struct TickingClock(std::cell::Cell<u64>);
246        impl Clock for TickingClock {
247            fn now_ns(&self) -> u64 {
248                self.0.set(self.0.get() + 1_000_000);
249                self.0.get()
250            }
251        }
252        let halflife = 1_000_000_000;
253        let sw = sweep("decay/record", |d| {
254            let mut x =
255                DecayingHdrHistogram::new(d, halflife, TickingClock(std::cell::Cell::new(0)));
256            keyed(|i| x.record(value_at(i)), true)
257        });
258        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
259
260        let mut x =
261            DecayingHdrHistogram::new(CANON_D, halflife, TickingClock(std::cell::Cell::new(0)));
262        let mut p99 = BTreeMap::new();
263        p99.insert(
264            "record".to_string(),
265            keyed(|i| x.record(value_at(i)), false),
266        );
267        p99.insert(
268            "percentile".to_string(),
269            keyed(|_| _ = x.value_at_percentile(99.0), false),
270        );
271        manifest.set_feature("decay", cat, &p99, &reason);
272    }
273
274    // ---------- value-tagging: a parallel histogram per tag ----------
275    #[cfg(feature = "value-tagging")]
276    {
277        use subms_hdr_histogram::TaggedHdrHistogram;
278        let sw = sweep("value-tagging/record", |d| {
279            let mut t = TaggedHdrHistogram::new(d);
280            keyed(|i| t.record(value_at(i), (i % 4) as u8), true)
281        });
282        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
283
284        let mut t = TaggedHdrHistogram::new(CANON_D);
285        let mut p99 = BTreeMap::new();
286        p99.insert(
287            "record".to_string(),
288            keyed(|i| t.record(value_at(i), (i % 4) as u8), false),
289        );
290        p99.insert(
291            "percentile_for_tag".to_string(),
292            keyed(|_| _ = t.value_at_percentile_for_tag(99.0, 0), false),
293        );
294        manifest.set_feature("value-tagging", cat, &p99, &reason);
295    }
296
297    // ---------- iterators: linear / logarithmic / percentile walks ----------
298    #[cfg(feature = "iterators")]
299    {
300        // A percentile walk accumulates counts across the bucket array, so it is
301        // O(buckets), and the sweep is monotonic and strongly rising. It is not
302        // 64x: the walk emits a BOUNDED number of entries (1% steps, ~100 of
303        // them) however large the array is, so the per-bucket work amortises
304        // better at the top and it measures ~57x over a 128x span - under the
305        // classifier's 0.5 guard.
306        //
307        // `iter_linear` was tried as the swept op instead, on the reasoning that
308        // it visits every bucket. It does not: it steps by VALUE unit, so over a
309        // 10^7 value range it emits millions of entries and the walk never
310        // finished. Wrong op, not a slower one.
311        //
312        // PINNED structural rather than published as hot-path, which would tell
313        // a reader a full percentile walk is safe per-operation. It is not.
314        let sw = sweep("iterators/percentiles", |d| {
315            let h = filled(d);
316            bulk(|| (), |()| _ = h.iter_percentiles(1.0).count(), true)
317        });
318        let (cat, reason) = classify_feature(
319            &sw,
320            Some(base_p50),
321            Some(subms::SubMsFeatureCategory::Structural),
322        );
323
324        let h = filled(CANON_D);
325        let mut p99 = BTreeMap::new();
326        p99.insert(
327            "iter_percentiles".to_string(),
328            bulk(|| (), |()| _ = h.iter_percentiles(1.0).count(), false),
329        );
330        p99.insert(
331            "iter_logarithmic".to_string(),
332            bulk(|| (), |()| _ = h.iter_logarithmic().count(), false),
333        );
334        manifest.set_feature("iterators", cat, &p99, &reason);
335    }
336
337    std::fs::create_dir_all(path.parent().unwrap())?;
338    std::fs::write(&path, manifest.to_json())?;
339    io::stdout().write_all(manifest.to_json().as_bytes())?;
340    Ok(())
341}
Source

pub fn iter_percentiles(&self, step_percent: f64) -> HdrPercentileIter<'_>

Examples found in repository?
examples/sample_app.rs (line 278)
271fn iterators_export_bands() {
272    println!("\n== iterators: export the distribution as bands ==");
273    let mut h = HdrHistogram::new(3);
274    for v in 1..=1_000u64 {
275        h.record(v);
276    }
277    let bands = h.iter_logarithmic().count();
278    let quartiles: Vec<u64> = h.iter_percentiles(25.0).map(|e| e.value_lo).collect();
279    println!("  {bands} log2 bands; quartile lower bounds = {quartiles:?}");
280    assert!(bands > 0, "the populated range spans at least one band");
281    assert!(
282        !quartiles.is_empty(),
283        "the percentile walk yields quartile buckets"
284    );
285}
More examples
Hide additional examples
examples/perf_features.rs (line 316)
119fn main() -> io::Result<()> {
120    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
121        .join("..")
122        .join(".subms")
123        .join("features")
124        .join("rust.json");
125    let existing = std::fs::read_to_string(&path).unwrap_or_default();
126    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
127    // Stamp the box these numbers came from. The bench runs wherever it is
128    // invoked, so an unstamped manifest is indistinguishable from a fleet
129    // capture; the renderer will not publish one it cannot attribute.
130    let (source, instance) = SubMsP99Source::from_env();
131    manifest.set_p99_source(source, instance.as_deref());
132
133    // The baseline: base `record`, the per-op write path. Every feature is
134    // classified against the cost of the write it is decorating.
135    let mut base = HdrHistogram::new(CANON_D);
136    let base_p50 = keyed(|i| base.record(value_at(i)), true);
137    eprintln!("base record p50: {base_p50}ns");
138
139    // ---------- concurrent-writes: atomic buckets, &self record ----------
140    #[cfg(feature = "concurrent-writes")]
141    {
142        use subms_hdr_histogram::ConcurrentHdrHistogram;
143        let sw = sweep("concurrent-writes/record", |d| {
144            let c = ConcurrentHdrHistogram::new(d);
145            keyed(|i| c.record(value_at(i)), true)
146        });
147        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
148
149        let c = ConcurrentHdrHistogram::new(CANON_D);
150        let mut p99 = BTreeMap::new();
151        p99.insert(
152            "record".to_string(),
153            keyed(|i| c.record(value_at(i)), false),
154        );
155        p99.insert(
156            "percentile".to_string(),
157            keyed(|_| _ = c.value_at_percentile(99.0), false),
158        );
159        manifest.set_feature("concurrent-writes", cat, &p99, &reason);
160    }
161
162    // ---------- dual-recorder: hot/stable pair, swap and drain ----------
163    #[cfg(feature = "dual-recorder")]
164    {
165        use subms_hdr_histogram::DualRecorder;
166        // Swept on the interval read, not on `record`. The record is the same
167        // atomic write the concurrent feature already covers; the swap-and-drain
168        // is what a dual recorder is FOR, and it is O(buckets).
169        let sw = sweep("dual-recorder/interval", |d| {
170            let r = DualRecorder::new(d);
171            for i in 0..sub_count(d) {
172                r.record(value_at(i));
173            }
174            bulk(|| (), |()| _ = r.get_interval_histogram(), true)
175        });
176        // PINNED structural. The drain is O(buckets) from the source in BOTH
177        // ports, but it is DESTRUCTIVE, so repeating it measures an already-empty
178        // histogram rather than the drain: after the first rep each side has
179        // nothing left to copy. Refilling between reps would put the refill
180        // inside the timed region, which is the bug the ART port shipped. The
181        // two ports disagree precisely here for that reason - Rust copies the
182        // counter array unconditionally (468us at 262144 buckets) while Java
183        // collapses a high-water index and reads 100ns - and neither number is
184        // the operation a caller performs. Recording either as hot-path would
185        // say a whole-array drain is safe per-op.
186        let (cat, reason) = classify_feature(
187            &sw,
188            Some(base_p50),
189            Some(subms::SubMsFeatureCategory::Structural),
190        );
191
192        let r = DualRecorder::new(CANON_D);
193        for i in 0..OPS {
194            r.record(value_at(i));
195        }
196        let mut p99 = BTreeMap::new();
197        p99.insert(
198            "record".to_string(),
199            keyed(|i| r.record(value_at(i)), false),
200        );
201        p99.insert(
202            "interval_read".to_string(),
203            bulk(|| (), |()| _ = r.get_interval_histogram(), false),
204        );
205        manifest.set_feature("dual-recorder", cat, &p99, &reason);
206    }
207
208    // ---------- merge: element-wise add over the bucket arrays ----------
209    #[cfg(feature = "merge")]
210    {
211        use subms_hdr_histogram::merge;
212        // Both histograms are built by `setup`, outside the timed region.
213        // Repeating the merge grows the destination's counts but does identical
214        // work each rep, so the figure is the merge and nothing else.
215        let sw = sweep("merge/merge", |d| {
216            bulk(
217                || (filled(d), filled(d)),
218                |(dst, src)| merge(dst, src).expect("same precision"),
219                true,
220            )
221        });
222        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
223
224        let mut p99 = BTreeMap::new();
225        p99.insert(
226            "merge".to_string(),
227            bulk(
228                || (filled(CANON_D), filled(CANON_D)),
229                |(dst, src)| merge(dst, src).expect("same precision"),
230                false,
231            ),
232        );
233        manifest.set_feature("merge", cat, &p99, &reason);
234    }
235
236    // ---------- decay: exponentially-weighted counts ----------
237    #[cfg(feature = "decay")]
238    {
239        use subms_hdr_histogram::{Clock, DecayingHdrHistogram};
240        // The clock ADVANCES on every read. A frozen clock lets `decay_to_now`
241        // early-return and the feature measures as a plain record, which is the
242        // opposite of the truth: with time moving, every record first brings the
243        // whole counter array up to date, so the write is O(buckets). Freezing
244        // the clock here would have published that as hot-path.
245        struct TickingClock(std::cell::Cell<u64>);
246        impl Clock for TickingClock {
247            fn now_ns(&self) -> u64 {
248                self.0.set(self.0.get() + 1_000_000);
249                self.0.get()
250            }
251        }
252        let halflife = 1_000_000_000;
253        let sw = sweep("decay/record", |d| {
254            let mut x =
255                DecayingHdrHistogram::new(d, halflife, TickingClock(std::cell::Cell::new(0)));
256            keyed(|i| x.record(value_at(i)), true)
257        });
258        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
259
260        let mut x =
261            DecayingHdrHistogram::new(CANON_D, halflife, TickingClock(std::cell::Cell::new(0)));
262        let mut p99 = BTreeMap::new();
263        p99.insert(
264            "record".to_string(),
265            keyed(|i| x.record(value_at(i)), false),
266        );
267        p99.insert(
268            "percentile".to_string(),
269            keyed(|_| _ = x.value_at_percentile(99.0), false),
270        );
271        manifest.set_feature("decay", cat, &p99, &reason);
272    }
273
274    // ---------- value-tagging: a parallel histogram per tag ----------
275    #[cfg(feature = "value-tagging")]
276    {
277        use subms_hdr_histogram::TaggedHdrHistogram;
278        let sw = sweep("value-tagging/record", |d| {
279            let mut t = TaggedHdrHistogram::new(d);
280            keyed(|i| t.record(value_at(i), (i % 4) as u8), true)
281        });
282        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
283
284        let mut t = TaggedHdrHistogram::new(CANON_D);
285        let mut p99 = BTreeMap::new();
286        p99.insert(
287            "record".to_string(),
288            keyed(|i| t.record(value_at(i), (i % 4) as u8), false),
289        );
290        p99.insert(
291            "percentile_for_tag".to_string(),
292            keyed(|_| _ = t.value_at_percentile_for_tag(99.0, 0), false),
293        );
294        manifest.set_feature("value-tagging", cat, &p99, &reason);
295    }
296
297    // ---------- iterators: linear / logarithmic / percentile walks ----------
298    #[cfg(feature = "iterators")]
299    {
300        // A percentile walk accumulates counts across the bucket array, so it is
301        // O(buckets), and the sweep is monotonic and strongly rising. It is not
302        // 64x: the walk emits a BOUNDED number of entries (1% steps, ~100 of
303        // them) however large the array is, so the per-bucket work amortises
304        // better at the top and it measures ~57x over a 128x span - under the
305        // classifier's 0.5 guard.
306        //
307        // `iter_linear` was tried as the swept op instead, on the reasoning that
308        // it visits every bucket. It does not: it steps by VALUE unit, so over a
309        // 10^7 value range it emits millions of entries and the walk never
310        // finished. Wrong op, not a slower one.
311        //
312        // PINNED structural rather than published as hot-path, which would tell
313        // a reader a full percentile walk is safe per-operation. It is not.
314        let sw = sweep("iterators/percentiles", |d| {
315            let h = filled(d);
316            bulk(|| (), |()| _ = h.iter_percentiles(1.0).count(), true)
317        });
318        let (cat, reason) = classify_feature(
319            &sw,
320            Some(base_p50),
321            Some(subms::SubMsFeatureCategory::Structural),
322        );
323
324        let h = filled(CANON_D);
325        let mut p99 = BTreeMap::new();
326        p99.insert(
327            "iter_percentiles".to_string(),
328            bulk(|| (), |()| _ = h.iter_percentiles(1.0).count(), false),
329        );
330        p99.insert(
331            "iter_logarithmic".to_string(),
332            bulk(|| (), |()| _ = h.iter_logarithmic().count(), false),
333        );
334        manifest.set_feature("iterators", cat, &p99, &reason);
335    }
336
337    std::fs::create_dir_all(path.parent().unwrap())?;
338    std::fs::write(&path, manifest.to_json())?;
339    io::stdout().write_all(manifest.to_json().as_bytes())?;
340    Ok(())
341}
Source§

impl HdrHistogram

Source

pub fn new(significant_digits: u32) -> Self

significant_digits in [1, 5]; clamped if out of range.

Examples found in repository?
examples/perf_features.rs (line 57)
56fn sub_count(d: u32) -> usize {
57    HdrHistogram::new(d).sub_count() as usize
58}
59
60fn stat(h: &SubMsPerfHarness, median: bool) -> u64 {
61    summarize(h)
62        .stages
63        .iter()
64        .find(|s| s.name == "op")
65        .map_or(0, |s| if median { s.p50_ns } else { s.p99_ns })
66}
67
68fn keyed(mut op: impl FnMut(usize), median: bool) -> u64 {
69    let mut h = SubMsPerfHarness::new("hdr-feature", "rust");
70    let st = h.stage("op", OPS);
71    for i in 0..OPS {
72        st.time(|| op(i));
73    }
74    stat(&h, median)
75}
76
77/// A whole-array op. `setup` runs OUTSIDE the timed region and the first
78/// `BULK_WARM` reps are discarded: measured cold, a bulk op lands its
79/// first-touch cost on whichever sweep point runs first, which reads as a curve
80/// that FALLS with size - the opposite of the structural signal.
81fn bulk<T>(mut setup: impl FnMut() -> T, mut op: impl FnMut(&mut T), median: bool) -> u64 {
82    let mut input = setup();
83    let start = std::time::Instant::now();
84    for _ in 0..BULK_WARM_MAX_REPS {
85        op(&mut input);
86        if start.elapsed().as_nanos() as u64 >= BULK_WARM_NANOS {
87            break;
88        }
89    }
90    let mut h = SubMsPerfHarness::new("hdr-feature", "rust");
91    let st = h.stage("op", BULK_REPS);
92    for _ in 0..BULK_REPS {
93        st.time(|| op(&mut input));
94    }
95    stat(&h, median)
96}
97
98/// Sweeps and PRINTS the curve, indexed by SUB-BUCKET COUNT rather than by
99/// digits - the classifier reads the size column as a magnitude.
100fn sweep(label: &str, mut at: impl FnMut(u32) -> u64) -> Vec<(usize, u64)> {
101    let rows: Vec<(usize, u64)> = DIGITS.iter().map(|&d| (sub_count(d), at(d))).collect();
102    eprintln!("sweep {label}: {rows:?}");
103    rows
104}
105
106/// Filled to OCCUPANCY, not to a fixed record count. `merge` and the iterators
107/// visit NON-EMPTY buckets, so a fixed 20k values leaves 20k of them occupied
108/// whether the array holds 2048 buckets or 262144 - the op stops scaling and
109/// both features read flat. Recording `sub_count` values keeps the occupied
110/// fraction roughly constant, which is what makes the sweep a size sweep.
111fn filled(d: u32) -> HdrHistogram {
112    let mut h = HdrHistogram::new(d);
113    for i in 0..sub_count(d) {
114        h.record(value_at(i));
115    }
116    h
117}
118
119fn main() -> io::Result<()> {
120    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
121        .join("..")
122        .join(".subms")
123        .join("features")
124        .join("rust.json");
125    let existing = std::fs::read_to_string(&path).unwrap_or_default();
126    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
127    // Stamp the box these numbers came from. The bench runs wherever it is
128    // invoked, so an unstamped manifest is indistinguishable from a fleet
129    // capture; the renderer will not publish one it cannot attribute.
130    let (source, instance) = SubMsP99Source::from_env();
131    manifest.set_p99_source(source, instance.as_deref());
132
133    // The baseline: base `record`, the per-op write path. Every feature is
134    // classified against the cost of the write it is decorating.
135    let mut base = HdrHistogram::new(CANON_D);
136    let base_p50 = keyed(|i| base.record(value_at(i)), true);
137    eprintln!("base record p50: {base_p50}ns");
138
139    // ---------- concurrent-writes: atomic buckets, &self record ----------
140    #[cfg(feature = "concurrent-writes")]
141    {
142        use subms_hdr_histogram::ConcurrentHdrHistogram;
143        let sw = sweep("concurrent-writes/record", |d| {
144            let c = ConcurrentHdrHistogram::new(d);
145            keyed(|i| c.record(value_at(i)), true)
146        });
147        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
148
149        let c = ConcurrentHdrHistogram::new(CANON_D);
150        let mut p99 = BTreeMap::new();
151        p99.insert(
152            "record".to_string(),
153            keyed(|i| c.record(value_at(i)), false),
154        );
155        p99.insert(
156            "percentile".to_string(),
157            keyed(|_| _ = c.value_at_percentile(99.0), false),
158        );
159        manifest.set_feature("concurrent-writes", cat, &p99, &reason);
160    }
161
162    // ---------- dual-recorder: hot/stable pair, swap and drain ----------
163    #[cfg(feature = "dual-recorder")]
164    {
165        use subms_hdr_histogram::DualRecorder;
166        // Swept on the interval read, not on `record`. The record is the same
167        // atomic write the concurrent feature already covers; the swap-and-drain
168        // is what a dual recorder is FOR, and it is O(buckets).
169        let sw = sweep("dual-recorder/interval", |d| {
170            let r = DualRecorder::new(d);
171            for i in 0..sub_count(d) {
172                r.record(value_at(i));
173            }
174            bulk(|| (), |()| _ = r.get_interval_histogram(), true)
175        });
176        // PINNED structural. The drain is O(buckets) from the source in BOTH
177        // ports, but it is DESTRUCTIVE, so repeating it measures an already-empty
178        // histogram rather than the drain: after the first rep each side has
179        // nothing left to copy. Refilling between reps would put the refill
180        // inside the timed region, which is the bug the ART port shipped. The
181        // two ports disagree precisely here for that reason - Rust copies the
182        // counter array unconditionally (468us at 262144 buckets) while Java
183        // collapses a high-water index and reads 100ns - and neither number is
184        // the operation a caller performs. Recording either as hot-path would
185        // say a whole-array drain is safe per-op.
186        let (cat, reason) = classify_feature(
187            &sw,
188            Some(base_p50),
189            Some(subms::SubMsFeatureCategory::Structural),
190        );
191
192        let r = DualRecorder::new(CANON_D);
193        for i in 0..OPS {
194            r.record(value_at(i));
195        }
196        let mut p99 = BTreeMap::new();
197        p99.insert(
198            "record".to_string(),
199            keyed(|i| r.record(value_at(i)), false),
200        );
201        p99.insert(
202            "interval_read".to_string(),
203            bulk(|| (), |()| _ = r.get_interval_histogram(), false),
204        );
205        manifest.set_feature("dual-recorder", cat, &p99, &reason);
206    }
207
208    // ---------- merge: element-wise add over the bucket arrays ----------
209    #[cfg(feature = "merge")]
210    {
211        use subms_hdr_histogram::merge;
212        // Both histograms are built by `setup`, outside the timed region.
213        // Repeating the merge grows the destination's counts but does identical
214        // work each rep, so the figure is the merge and nothing else.
215        let sw = sweep("merge/merge", |d| {
216            bulk(
217                || (filled(d), filled(d)),
218                |(dst, src)| merge(dst, src).expect("same precision"),
219                true,
220            )
221        });
222        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
223
224        let mut p99 = BTreeMap::new();
225        p99.insert(
226            "merge".to_string(),
227            bulk(
228                || (filled(CANON_D), filled(CANON_D)),
229                |(dst, src)| merge(dst, src).expect("same precision"),
230                false,
231            ),
232        );
233        manifest.set_feature("merge", cat, &p99, &reason);
234    }
235
236    // ---------- decay: exponentially-weighted counts ----------
237    #[cfg(feature = "decay")]
238    {
239        use subms_hdr_histogram::{Clock, DecayingHdrHistogram};
240        // The clock ADVANCES on every read. A frozen clock lets `decay_to_now`
241        // early-return and the feature measures as a plain record, which is the
242        // opposite of the truth: with time moving, every record first brings the
243        // whole counter array up to date, so the write is O(buckets). Freezing
244        // the clock here would have published that as hot-path.
245        struct TickingClock(std::cell::Cell<u64>);
246        impl Clock for TickingClock {
247            fn now_ns(&self) -> u64 {
248                self.0.set(self.0.get() + 1_000_000);
249                self.0.get()
250            }
251        }
252        let halflife = 1_000_000_000;
253        let sw = sweep("decay/record", |d| {
254            let mut x =
255                DecayingHdrHistogram::new(d, halflife, TickingClock(std::cell::Cell::new(0)));
256            keyed(|i| x.record(value_at(i)), true)
257        });
258        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
259
260        let mut x =
261            DecayingHdrHistogram::new(CANON_D, halflife, TickingClock(std::cell::Cell::new(0)));
262        let mut p99 = BTreeMap::new();
263        p99.insert(
264            "record".to_string(),
265            keyed(|i| x.record(value_at(i)), false),
266        );
267        p99.insert(
268            "percentile".to_string(),
269            keyed(|_| _ = x.value_at_percentile(99.0), false),
270        );
271        manifest.set_feature("decay", cat, &p99, &reason);
272    }
273
274    // ---------- value-tagging: a parallel histogram per tag ----------
275    #[cfg(feature = "value-tagging")]
276    {
277        use subms_hdr_histogram::TaggedHdrHistogram;
278        let sw = sweep("value-tagging/record", |d| {
279            let mut t = TaggedHdrHistogram::new(d);
280            keyed(|i| t.record(value_at(i), (i % 4) as u8), true)
281        });
282        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
283
284        let mut t = TaggedHdrHistogram::new(CANON_D);
285        let mut p99 = BTreeMap::new();
286        p99.insert(
287            "record".to_string(),
288            keyed(|i| t.record(value_at(i), (i % 4) as u8), false),
289        );
290        p99.insert(
291            "percentile_for_tag".to_string(),
292            keyed(|_| _ = t.value_at_percentile_for_tag(99.0, 0), false),
293        );
294        manifest.set_feature("value-tagging", cat, &p99, &reason);
295    }
296
297    // ---------- iterators: linear / logarithmic / percentile walks ----------
298    #[cfg(feature = "iterators")]
299    {
300        // A percentile walk accumulates counts across the bucket array, so it is
301        // O(buckets), and the sweep is monotonic and strongly rising. It is not
302        // 64x: the walk emits a BOUNDED number of entries (1% steps, ~100 of
303        // them) however large the array is, so the per-bucket work amortises
304        // better at the top and it measures ~57x over a 128x span - under the
305        // classifier's 0.5 guard.
306        //
307        // `iter_linear` was tried as the swept op instead, on the reasoning that
308        // it visits every bucket. It does not: it steps by VALUE unit, so over a
309        // 10^7 value range it emits millions of entries and the walk never
310        // finished. Wrong op, not a slower one.
311        //
312        // PINNED structural rather than published as hot-path, which would tell
313        // a reader a full percentile walk is safe per-operation. It is not.
314        let sw = sweep("iterators/percentiles", |d| {
315            let h = filled(d);
316            bulk(|| (), |()| _ = h.iter_percentiles(1.0).count(), true)
317        });
318        let (cat, reason) = classify_feature(
319            &sw,
320            Some(base_p50),
321            Some(subms::SubMsFeatureCategory::Structural),
322        );
323
324        let h = filled(CANON_D);
325        let mut p99 = BTreeMap::new();
326        p99.insert(
327            "iter_percentiles".to_string(),
328            bulk(|| (), |()| _ = h.iter_percentiles(1.0).count(), false),
329        );
330        p99.insert(
331            "iter_logarithmic".to_string(),
332            bulk(|| (), |()| _ = h.iter_logarithmic().count(), false),
333        );
334        manifest.set_feature("iterators", cat, &p99, &reason);
335    }
336
337    std::fs::create_dir_all(path.parent().unwrap())?;
338    std::fs::write(&path, manifest.to_json())?;
339    io::stdout().write_all(manifest.to_json().as_bytes())?;
340    Ok(())
341}
More examples
Hide additional examples
examples/sample_app.rs (line 48)
46fn base_tick_to_trade() {
47    println!("== base: tick-to-trade latency capture ==");
48    let mut h = HdrHistogram::new(3);
49
50    // A right-skewed latency stream: most ops sit in a tight band, a small
51    // fraction spike into the tail. Deterministic xorshift so the numbers are
52    // reproducible.
53    let mut rng = 0x2545_F491_4F6C_DD1Du64;
54    let mut next = || {
55        rng ^= rng << 13;
56        rng ^= rng >> 7;
57        rng ^= rng << 17;
58        rng
59    };
60    let n = 2_000u64;
61    for i in 0..n {
62        let base = 700 + next() % 300; // 700..1000 ns steady state
63        if i % 50 == 0 {
64            h.record(4_000 + next() % 4_000); // ~2% tail spike, 4us..8us
65        } else {
66            h.record(base);
67        }
68    }
69
70    let p50 = h.value_at_percentile(0.50);
71    let p99 = h.value_at_percentile(0.99);
72    let p999 = h.value_at_percentile(0.999);
73    println!(
74        "  n={n} p50={p50}ns p99={p99}ns p999={p999}ns max={}ns",
75        h.max()
76    );
77    assert_eq!(h.count(), n, "every sample recorded");
78    assert!(
79        p50 <= 1_100,
80        "median sits in the steady-state band: p50={p50}"
81    );
82    assert!(
83        p99 >= 2_000,
84        "the 2% tail lifts p99 well past the median: p99={p99}"
85    );
86    assert!(p999 >= p99 && h.max() >= p999, "percentiles are monotone");
87
88    // The reporting surface a dashboard actually wants alongside the
89    // percentiles: the floor, the mean, the fraction inside the SLO, and what
90    // the whole thing costs in memory.
91    let within_slo = h.percentile_at_or_below_value(2_000);
92    println!(
93        "  min={}ns mean={:.0}ns within-2us={:.1}% footprint={}KB",
94        h.min(),
95        h.mean(),
96        within_slo * 100.0,
97        h.footprint_bytes() / 1024
98    );
99    assert!(within_slo > 0.9, "most ops sit inside the 2us band");
100
101    // Coordinated omission: a fixed-rate loop issues one op every 10 ns, then
102    // stalls for 1000 ns. The naive histogram sees one slow sample; the
103    // corrected one backfills the 99 requests the stall blocked.
104    let mut naive = HdrHistogram::new(3);
105    let mut corrected = HdrHistogram::new(3);
106    for _ in 0..1_000 {
107        naive.record(10);
108        corrected.record_with_expected_interval(10, 10);
109    }
110    naive.record(1_000);
111    corrected.record_with_expected_interval(1_000, 10);
112    let naive_p99 = naive.value_at_percentile(0.99);
113    let corrected_p99 = corrected.value_at_percentile(0.99);
114    println!("  coordinated omission: naive p99={naive_p99}ns, corrected p99={corrected_p99}ns");
115    assert!(
116        naive_p99 <= 20,
117        "uncorrected tail hides the stall: {naive_p99}"
118    );
119    assert!(
120        corrected_p99 >= 500,
121        "correction lifts the tail: {corrected_p99}"
122    );
123}
124
125/// `concurrent-writes` feature: several market-data feed handlers record into
126/// one histogram from different threads with no external lock - the only
127/// contention is the per-bucket atomic increment.
128#[cfg(feature = "concurrent-writes")]
129fn concurrent_feed_handlers() {
130    use std::sync::Arc;
131    use std::thread;
132    use subms_hdr_histogram::ConcurrentHdrHistogram;
133    println!("\n== concurrent-writes: many feed handlers, one histogram ==");
134    let h = Arc::new(ConcurrentHdrHistogram::new(3));
135    let threads = 4;
136    let per_thread = 50_000u64;
137    let mut handles = vec![];
138    for _ in 0..threads {
139        let h = h.clone();
140        handles.push(thread::spawn(move || {
141            for i in 0..per_thread {
142                h.record((i % 1_000) + 500);
143            }
144        }));
145    }
146    for j in handles {
147        j.join().unwrap();
148    }
149    println!(
150        "  {} records lock-free, p99={}ns",
151        h.count(),
152        h.value_at_percentile(0.99)
153    );
154    assert_eq!(
155        h.count(),
156        threads as u64 * per_thread,
157        "no writes lost under contention"
158    );
159}
160
161/// `dual-recorder` feature: producers record continuously; a reporter thread
162/// grabs an interval snapshot on a timer by rotating the active side and
163/// draining the inactive one, never blocking the producers.
164#[cfg(feature = "dual-recorder")]
165fn dual_recorder_interval_report() {
166    use subms_hdr_histogram::DualRecorder;
167    println!("\n== dual-recorder: lock-free interval percentile report ==");
168    let rec = DualRecorder::new(3);
169    for v in 1..=500u64 {
170        rec.record(v);
171    }
172    let interval = rec.get_interval_histogram();
173    println!(
174        "  interval count={}, p99={}",
175        interval.count(),
176        interval.value_at_percentile(0.99)
177    );
178    let next = rec.get_interval_histogram();
179    assert_eq!(
180        interval.count(),
181        500,
182        "first interval captured every record"
183    );
184    assert_eq!(
185        next.count(),
186        0,
187        "the next interval starts empty after the rotate"
188    );
189}
190
191/// `merge` feature: two shards each keep their own histogram; a periodic
192/// roll-up sums one into the other for a fleet-wide percentile view. The merge
193/// is exact - identical to recording every value into a single histogram.
194#[cfg(feature = "merge")]
195fn merge_shard_rollup() {
196    use subms_hdr_histogram::merge;
197    println!("\n== merge: roll per-shard histograms into a fleet view ==");
198    let mut shard_a = HdrHistogram::new(3);
199    let mut shard_b = HdrHistogram::new(3);
200    for v in 1..=500u64 {
201        shard_a.record(v);
202    }
203    for v in 501..=1_000u64 {
204        shard_b.record(v);
205    }
206    merge(&mut shard_a, &shard_b).expect("identical shape merges");
207    println!(
208        "  fleet count={}, p50={}, p99={}",
209        shard_a.count(),
210        shard_a.value_at_percentile(0.5),
211        shard_a.value_at_percentile(0.99)
212    );
213    assert_eq!(shard_a.count(), 1_000, "both shards folded in");
214    assert!(
215        shard_a.value_at_percentile(0.99) >= 900,
216        "the high tail came from shard b"
217    );
218}
219
220/// `decay` feature: an exponentially-decaying histogram so the current p99
221/// reflects recent activity. An old burst of slow ops fades over a few
222/// half-lives, so a later burst of fast ops dominates the read.
223#[cfg(feature = "decay")]
224fn decay_recency_weighted() {
225    use subms_hdr_histogram::{DecayingHdrHistogram, ManualClock};
226    println!("\n== decay: recency-weighted p50 forgets an old spike ==");
227    let clock = ManualClock::new();
228    let halflife = 1_000_000_000u64; // 1 second
229    let mut h = DecayingHdrHistogram::new(3, halflife, &clock);
230    for _ in 0..1_000 {
231        h.record(5_000); // an old burst of slow ops
232    }
233    clock.advance_ns(halflife * 4); // four half-lives pass
234    for _ in 0..1_000 {
235        h.record(800); // a recent burst of fast ops
236    }
237    let p50 = h.value_at_percentile(0.5);
238    println!("  decayed count~{:.0}, p50={p50}ns", h.count());
239    assert!(
240        p50 < 2_000,
241        "recent fast ops dominate the decayed distribution: p50={p50}"
242    );
243}
244
245/// `value-tagging` feature: one histogram, a 1-byte tag per recording, so
246/// per-venue tails can be read separately at query time without standing up N
247/// histograms.
248#[cfg(feature = "value-tagging")]
249fn value_tagging_by_venue() {
250    use subms_hdr_histogram::TaggedHdrHistogram;
251    println!("\n== value-tagging: slice latency by venue ==");
252    const COLO: u8 = 0;
253    const REMOTE: u8 = 1;
254    let mut h = TaggedHdrHistogram::new(3);
255    for v in 500..=1_000u64 {
256        h.record(v, COLO); // a fast co-located venue
257    }
258    for v in 5_000..=6_000u64 {
259        h.record(v, REMOTE); // a slow remote venue
260    }
261    let p99_colo = h.value_at_percentile_for_tag(0.99, COLO);
262    let p99_remote = h.value_at_percentile_for_tag(0.99, REMOTE);
263    println!("  colo p99={p99_colo}ns, remote p99={p99_remote}ns");
264    assert!(p99_colo < p99_remote, "each venue's tail reads on its own");
265}
266
267/// `iterators` feature: walk the whole distribution rather than pull single
268/// percentiles - here, the powers-of-two bands and quartile lower bounds a
269/// chart or downstream sink would render.
270#[cfg(feature = "iterators")]
271fn iterators_export_bands() {
272    println!("\n== iterators: export the distribution as bands ==");
273    let mut h = HdrHistogram::new(3);
274    for v in 1..=1_000u64 {
275        h.record(v);
276    }
277    let bands = h.iter_logarithmic().count();
278    let quartiles: Vec<u64> = h.iter_percentiles(25.0).map(|e| e.value_lo).collect();
279    println!("  {bands} log2 bands; quartile lower bounds = {quartiles:?}");
280    assert!(bands > 0, "the populated range spans at least one band");
281    assert!(
282        !quartiles.is_empty(),
283        "the percentile walk yields quartile buckets"
284    );
285}
Source

pub fn count(&self) -> u64

Examples found in repository?
examples/sample_app.rs (line 77)
46fn base_tick_to_trade() {
47    println!("== base: tick-to-trade latency capture ==");
48    let mut h = HdrHistogram::new(3);
49
50    // A right-skewed latency stream: most ops sit in a tight band, a small
51    // fraction spike into the tail. Deterministic xorshift so the numbers are
52    // reproducible.
53    let mut rng = 0x2545_F491_4F6C_DD1Du64;
54    let mut next = || {
55        rng ^= rng << 13;
56        rng ^= rng >> 7;
57        rng ^= rng << 17;
58        rng
59    };
60    let n = 2_000u64;
61    for i in 0..n {
62        let base = 700 + next() % 300; // 700..1000 ns steady state
63        if i % 50 == 0 {
64            h.record(4_000 + next() % 4_000); // ~2% tail spike, 4us..8us
65        } else {
66            h.record(base);
67        }
68    }
69
70    let p50 = h.value_at_percentile(0.50);
71    let p99 = h.value_at_percentile(0.99);
72    let p999 = h.value_at_percentile(0.999);
73    println!(
74        "  n={n} p50={p50}ns p99={p99}ns p999={p999}ns max={}ns",
75        h.max()
76    );
77    assert_eq!(h.count(), n, "every sample recorded");
78    assert!(
79        p50 <= 1_100,
80        "median sits in the steady-state band: p50={p50}"
81    );
82    assert!(
83        p99 >= 2_000,
84        "the 2% tail lifts p99 well past the median: p99={p99}"
85    );
86    assert!(p999 >= p99 && h.max() >= p999, "percentiles are monotone");
87
88    // The reporting surface a dashboard actually wants alongside the
89    // percentiles: the floor, the mean, the fraction inside the SLO, and what
90    // the whole thing costs in memory.
91    let within_slo = h.percentile_at_or_below_value(2_000);
92    println!(
93        "  min={}ns mean={:.0}ns within-2us={:.1}% footprint={}KB",
94        h.min(),
95        h.mean(),
96        within_slo * 100.0,
97        h.footprint_bytes() / 1024
98    );
99    assert!(within_slo > 0.9, "most ops sit inside the 2us band");
100
101    // Coordinated omission: a fixed-rate loop issues one op every 10 ns, then
102    // stalls for 1000 ns. The naive histogram sees one slow sample; the
103    // corrected one backfills the 99 requests the stall blocked.
104    let mut naive = HdrHistogram::new(3);
105    let mut corrected = HdrHistogram::new(3);
106    for _ in 0..1_000 {
107        naive.record(10);
108        corrected.record_with_expected_interval(10, 10);
109    }
110    naive.record(1_000);
111    corrected.record_with_expected_interval(1_000, 10);
112    let naive_p99 = naive.value_at_percentile(0.99);
113    let corrected_p99 = corrected.value_at_percentile(0.99);
114    println!("  coordinated omission: naive p99={naive_p99}ns, corrected p99={corrected_p99}ns");
115    assert!(
116        naive_p99 <= 20,
117        "uncorrected tail hides the stall: {naive_p99}"
118    );
119    assert!(
120        corrected_p99 >= 500,
121        "correction lifts the tail: {corrected_p99}"
122    );
123}
124
125/// `concurrent-writes` feature: several market-data feed handlers record into
126/// one histogram from different threads with no external lock - the only
127/// contention is the per-bucket atomic increment.
128#[cfg(feature = "concurrent-writes")]
129fn concurrent_feed_handlers() {
130    use std::sync::Arc;
131    use std::thread;
132    use subms_hdr_histogram::ConcurrentHdrHistogram;
133    println!("\n== concurrent-writes: many feed handlers, one histogram ==");
134    let h = Arc::new(ConcurrentHdrHistogram::new(3));
135    let threads = 4;
136    let per_thread = 50_000u64;
137    let mut handles = vec![];
138    for _ in 0..threads {
139        let h = h.clone();
140        handles.push(thread::spawn(move || {
141            for i in 0..per_thread {
142                h.record((i % 1_000) + 500);
143            }
144        }));
145    }
146    for j in handles {
147        j.join().unwrap();
148    }
149    println!(
150        "  {} records lock-free, p99={}ns",
151        h.count(),
152        h.value_at_percentile(0.99)
153    );
154    assert_eq!(
155        h.count(),
156        threads as u64 * per_thread,
157        "no writes lost under contention"
158    );
159}
160
161/// `dual-recorder` feature: producers record continuously; a reporter thread
162/// grabs an interval snapshot on a timer by rotating the active side and
163/// draining the inactive one, never blocking the producers.
164#[cfg(feature = "dual-recorder")]
165fn dual_recorder_interval_report() {
166    use subms_hdr_histogram::DualRecorder;
167    println!("\n== dual-recorder: lock-free interval percentile report ==");
168    let rec = DualRecorder::new(3);
169    for v in 1..=500u64 {
170        rec.record(v);
171    }
172    let interval = rec.get_interval_histogram();
173    println!(
174        "  interval count={}, p99={}",
175        interval.count(),
176        interval.value_at_percentile(0.99)
177    );
178    let next = rec.get_interval_histogram();
179    assert_eq!(
180        interval.count(),
181        500,
182        "first interval captured every record"
183    );
184    assert_eq!(
185        next.count(),
186        0,
187        "the next interval starts empty after the rotate"
188    );
189}
190
191/// `merge` feature: two shards each keep their own histogram; a periodic
192/// roll-up sums one into the other for a fleet-wide percentile view. The merge
193/// is exact - identical to recording every value into a single histogram.
194#[cfg(feature = "merge")]
195fn merge_shard_rollup() {
196    use subms_hdr_histogram::merge;
197    println!("\n== merge: roll per-shard histograms into a fleet view ==");
198    let mut shard_a = HdrHistogram::new(3);
199    let mut shard_b = HdrHistogram::new(3);
200    for v in 1..=500u64 {
201        shard_a.record(v);
202    }
203    for v in 501..=1_000u64 {
204        shard_b.record(v);
205    }
206    merge(&mut shard_a, &shard_b).expect("identical shape merges");
207    println!(
208        "  fleet count={}, p50={}, p99={}",
209        shard_a.count(),
210        shard_a.value_at_percentile(0.5),
211        shard_a.value_at_percentile(0.99)
212    );
213    assert_eq!(shard_a.count(), 1_000, "both shards folded in");
214    assert!(
215        shard_a.value_at_percentile(0.99) >= 900,
216        "the high tail came from shard b"
217    );
218}
Source

pub fn max(&self) -> u64

Highest value recorded (approximated to the bucket’s lower bound).

Examples found in repository?
examples/sample_app.rs (line 75)
46fn base_tick_to_trade() {
47    println!("== base: tick-to-trade latency capture ==");
48    let mut h = HdrHistogram::new(3);
49
50    // A right-skewed latency stream: most ops sit in a tight band, a small
51    // fraction spike into the tail. Deterministic xorshift so the numbers are
52    // reproducible.
53    let mut rng = 0x2545_F491_4F6C_DD1Du64;
54    let mut next = || {
55        rng ^= rng << 13;
56        rng ^= rng >> 7;
57        rng ^= rng << 17;
58        rng
59    };
60    let n = 2_000u64;
61    for i in 0..n {
62        let base = 700 + next() % 300; // 700..1000 ns steady state
63        if i % 50 == 0 {
64            h.record(4_000 + next() % 4_000); // ~2% tail spike, 4us..8us
65        } else {
66            h.record(base);
67        }
68    }
69
70    let p50 = h.value_at_percentile(0.50);
71    let p99 = h.value_at_percentile(0.99);
72    let p999 = h.value_at_percentile(0.999);
73    println!(
74        "  n={n} p50={p50}ns p99={p99}ns p999={p999}ns max={}ns",
75        h.max()
76    );
77    assert_eq!(h.count(), n, "every sample recorded");
78    assert!(
79        p50 <= 1_100,
80        "median sits in the steady-state band: p50={p50}"
81    );
82    assert!(
83        p99 >= 2_000,
84        "the 2% tail lifts p99 well past the median: p99={p99}"
85    );
86    assert!(p999 >= p99 && h.max() >= p999, "percentiles are monotone");
87
88    // The reporting surface a dashboard actually wants alongside the
89    // percentiles: the floor, the mean, the fraction inside the SLO, and what
90    // the whole thing costs in memory.
91    let within_slo = h.percentile_at_or_below_value(2_000);
92    println!(
93        "  min={}ns mean={:.0}ns within-2us={:.1}% footprint={}KB",
94        h.min(),
95        h.mean(),
96        within_slo * 100.0,
97        h.footprint_bytes() / 1024
98    );
99    assert!(within_slo > 0.9, "most ops sit inside the 2us band");
100
101    // Coordinated omission: a fixed-rate loop issues one op every 10 ns, then
102    // stalls for 1000 ns. The naive histogram sees one slow sample; the
103    // corrected one backfills the 99 requests the stall blocked.
104    let mut naive = HdrHistogram::new(3);
105    let mut corrected = HdrHistogram::new(3);
106    for _ in 0..1_000 {
107        naive.record(10);
108        corrected.record_with_expected_interval(10, 10);
109    }
110    naive.record(1_000);
111    corrected.record_with_expected_interval(1_000, 10);
112    let naive_p99 = naive.value_at_percentile(0.99);
113    let corrected_p99 = corrected.value_at_percentile(0.99);
114    println!("  coordinated omission: naive p99={naive_p99}ns, corrected p99={corrected_p99}ns");
115    assert!(
116        naive_p99 <= 20,
117        "uncorrected tail hides the stall: {naive_p99}"
118    );
119    assert!(
120        corrected_p99 >= 500,
121        "correction lifts the tail: {corrected_p99}"
122    );
123}
Source

pub fn record(&mut self, value: u64)

Examples found in repository?
examples/perf_features.rs (line 114)
111fn filled(d: u32) -> HdrHistogram {
112    let mut h = HdrHistogram::new(d);
113    for i in 0..sub_count(d) {
114        h.record(value_at(i));
115    }
116    h
117}
118
119fn main() -> io::Result<()> {
120    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
121        .join("..")
122        .join(".subms")
123        .join("features")
124        .join("rust.json");
125    let existing = std::fs::read_to_string(&path).unwrap_or_default();
126    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
127    // Stamp the box these numbers came from. The bench runs wherever it is
128    // invoked, so an unstamped manifest is indistinguishable from a fleet
129    // capture; the renderer will not publish one it cannot attribute.
130    let (source, instance) = SubMsP99Source::from_env();
131    manifest.set_p99_source(source, instance.as_deref());
132
133    // The baseline: base `record`, the per-op write path. Every feature is
134    // classified against the cost of the write it is decorating.
135    let mut base = HdrHistogram::new(CANON_D);
136    let base_p50 = keyed(|i| base.record(value_at(i)), true);
137    eprintln!("base record p50: {base_p50}ns");
138
139    // ---------- concurrent-writes: atomic buckets, &self record ----------
140    #[cfg(feature = "concurrent-writes")]
141    {
142        use subms_hdr_histogram::ConcurrentHdrHistogram;
143        let sw = sweep("concurrent-writes/record", |d| {
144            let c = ConcurrentHdrHistogram::new(d);
145            keyed(|i| c.record(value_at(i)), true)
146        });
147        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
148
149        let c = ConcurrentHdrHistogram::new(CANON_D);
150        let mut p99 = BTreeMap::new();
151        p99.insert(
152            "record".to_string(),
153            keyed(|i| c.record(value_at(i)), false),
154        );
155        p99.insert(
156            "percentile".to_string(),
157            keyed(|_| _ = c.value_at_percentile(99.0), false),
158        );
159        manifest.set_feature("concurrent-writes", cat, &p99, &reason);
160    }
161
162    // ---------- dual-recorder: hot/stable pair, swap and drain ----------
163    #[cfg(feature = "dual-recorder")]
164    {
165        use subms_hdr_histogram::DualRecorder;
166        // Swept on the interval read, not on `record`. The record is the same
167        // atomic write the concurrent feature already covers; the swap-and-drain
168        // is what a dual recorder is FOR, and it is O(buckets).
169        let sw = sweep("dual-recorder/interval", |d| {
170            let r = DualRecorder::new(d);
171            for i in 0..sub_count(d) {
172                r.record(value_at(i));
173            }
174            bulk(|| (), |()| _ = r.get_interval_histogram(), true)
175        });
176        // PINNED structural. The drain is O(buckets) from the source in BOTH
177        // ports, but it is DESTRUCTIVE, so repeating it measures an already-empty
178        // histogram rather than the drain: after the first rep each side has
179        // nothing left to copy. Refilling between reps would put the refill
180        // inside the timed region, which is the bug the ART port shipped. The
181        // two ports disagree precisely here for that reason - Rust copies the
182        // counter array unconditionally (468us at 262144 buckets) while Java
183        // collapses a high-water index and reads 100ns - and neither number is
184        // the operation a caller performs. Recording either as hot-path would
185        // say a whole-array drain is safe per-op.
186        let (cat, reason) = classify_feature(
187            &sw,
188            Some(base_p50),
189            Some(subms::SubMsFeatureCategory::Structural),
190        );
191
192        let r = DualRecorder::new(CANON_D);
193        for i in 0..OPS {
194            r.record(value_at(i));
195        }
196        let mut p99 = BTreeMap::new();
197        p99.insert(
198            "record".to_string(),
199            keyed(|i| r.record(value_at(i)), false),
200        );
201        p99.insert(
202            "interval_read".to_string(),
203            bulk(|| (), |()| _ = r.get_interval_histogram(), false),
204        );
205        manifest.set_feature("dual-recorder", cat, &p99, &reason);
206    }
207
208    // ---------- merge: element-wise add over the bucket arrays ----------
209    #[cfg(feature = "merge")]
210    {
211        use subms_hdr_histogram::merge;
212        // Both histograms are built by `setup`, outside the timed region.
213        // Repeating the merge grows the destination's counts but does identical
214        // work each rep, so the figure is the merge and nothing else.
215        let sw = sweep("merge/merge", |d| {
216            bulk(
217                || (filled(d), filled(d)),
218                |(dst, src)| merge(dst, src).expect("same precision"),
219                true,
220            )
221        });
222        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
223
224        let mut p99 = BTreeMap::new();
225        p99.insert(
226            "merge".to_string(),
227            bulk(
228                || (filled(CANON_D), filled(CANON_D)),
229                |(dst, src)| merge(dst, src).expect("same precision"),
230                false,
231            ),
232        );
233        manifest.set_feature("merge", cat, &p99, &reason);
234    }
235
236    // ---------- decay: exponentially-weighted counts ----------
237    #[cfg(feature = "decay")]
238    {
239        use subms_hdr_histogram::{Clock, DecayingHdrHistogram};
240        // The clock ADVANCES on every read. A frozen clock lets `decay_to_now`
241        // early-return and the feature measures as a plain record, which is the
242        // opposite of the truth: with time moving, every record first brings the
243        // whole counter array up to date, so the write is O(buckets). Freezing
244        // the clock here would have published that as hot-path.
245        struct TickingClock(std::cell::Cell<u64>);
246        impl Clock for TickingClock {
247            fn now_ns(&self) -> u64 {
248                self.0.set(self.0.get() + 1_000_000);
249                self.0.get()
250            }
251        }
252        let halflife = 1_000_000_000;
253        let sw = sweep("decay/record", |d| {
254            let mut x =
255                DecayingHdrHistogram::new(d, halflife, TickingClock(std::cell::Cell::new(0)));
256            keyed(|i| x.record(value_at(i)), true)
257        });
258        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
259
260        let mut x =
261            DecayingHdrHistogram::new(CANON_D, halflife, TickingClock(std::cell::Cell::new(0)));
262        let mut p99 = BTreeMap::new();
263        p99.insert(
264            "record".to_string(),
265            keyed(|i| x.record(value_at(i)), false),
266        );
267        p99.insert(
268            "percentile".to_string(),
269            keyed(|_| _ = x.value_at_percentile(99.0), false),
270        );
271        manifest.set_feature("decay", cat, &p99, &reason);
272    }
273
274    // ---------- value-tagging: a parallel histogram per tag ----------
275    #[cfg(feature = "value-tagging")]
276    {
277        use subms_hdr_histogram::TaggedHdrHistogram;
278        let sw = sweep("value-tagging/record", |d| {
279            let mut t = TaggedHdrHistogram::new(d);
280            keyed(|i| t.record(value_at(i), (i % 4) as u8), true)
281        });
282        let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
283
284        let mut t = TaggedHdrHistogram::new(CANON_D);
285        let mut p99 = BTreeMap::new();
286        p99.insert(
287            "record".to_string(),
288            keyed(|i| t.record(value_at(i), (i % 4) as u8), false),
289        );
290        p99.insert(
291            "percentile_for_tag".to_string(),
292            keyed(|_| _ = t.value_at_percentile_for_tag(99.0, 0), false),
293        );
294        manifest.set_feature("value-tagging", cat, &p99, &reason);
295    }
296
297    // ---------- iterators: linear / logarithmic / percentile walks ----------
298    #[cfg(feature = "iterators")]
299    {
300        // A percentile walk accumulates counts across the bucket array, so it is
301        // O(buckets), and the sweep is monotonic and strongly rising. It is not
302        // 64x: the walk emits a BOUNDED number of entries (1% steps, ~100 of
303        // them) however large the array is, so the per-bucket work amortises
304        // better at the top and it measures ~57x over a 128x span - under the
305        // classifier's 0.5 guard.
306        //
307        // `iter_linear` was tried as the swept op instead, on the reasoning that
308        // it visits every bucket. It does not: it steps by VALUE unit, so over a
309        // 10^7 value range it emits millions of entries and the walk never
310        // finished. Wrong op, not a slower one.
311        //
312        // PINNED structural rather than published as hot-path, which would tell
313        // a reader a full percentile walk is safe per-operation. It is not.
314        let sw = sweep("iterators/percentiles", |d| {
315            let h = filled(d);
316            bulk(|| (), |()| _ = h.iter_percentiles(1.0).count(), true)
317        });
318        let (cat, reason) = classify_feature(
319            &sw,
320            Some(base_p50),
321            Some(subms::SubMsFeatureCategory::Structural),
322        );
323
324        let h = filled(CANON_D);
325        let mut p99 = BTreeMap::new();
326        p99.insert(
327            "iter_percentiles".to_string(),
328            bulk(|| (), |()| _ = h.iter_percentiles(1.0).count(), false),
329        );
330        p99.insert(
331            "iter_logarithmic".to_string(),
332            bulk(|| (), |()| _ = h.iter_logarithmic().count(), false),
333        );
334        manifest.set_feature("iterators", cat, &p99, &reason);
335    }
336
337    std::fs::create_dir_all(path.parent().unwrap())?;
338    std::fs::write(&path, manifest.to_json())?;
339    io::stdout().write_all(manifest.to_json().as_bytes())?;
340    Ok(())
341}
More examples
Hide additional examples
examples/sample_app.rs (line 64)
46fn base_tick_to_trade() {
47    println!("== base: tick-to-trade latency capture ==");
48    let mut h = HdrHistogram::new(3);
49
50    // A right-skewed latency stream: most ops sit in a tight band, a small
51    // fraction spike into the tail. Deterministic xorshift so the numbers are
52    // reproducible.
53    let mut rng = 0x2545_F491_4F6C_DD1Du64;
54    let mut next = || {
55        rng ^= rng << 13;
56        rng ^= rng >> 7;
57        rng ^= rng << 17;
58        rng
59    };
60    let n = 2_000u64;
61    for i in 0..n {
62        let base = 700 + next() % 300; // 700..1000 ns steady state
63        if i % 50 == 0 {
64            h.record(4_000 + next() % 4_000); // ~2% tail spike, 4us..8us
65        } else {
66            h.record(base);
67        }
68    }
69
70    let p50 = h.value_at_percentile(0.50);
71    let p99 = h.value_at_percentile(0.99);
72    let p999 = h.value_at_percentile(0.999);
73    println!(
74        "  n={n} p50={p50}ns p99={p99}ns p999={p999}ns max={}ns",
75        h.max()
76    );
77    assert_eq!(h.count(), n, "every sample recorded");
78    assert!(
79        p50 <= 1_100,
80        "median sits in the steady-state band: p50={p50}"
81    );
82    assert!(
83        p99 >= 2_000,
84        "the 2% tail lifts p99 well past the median: p99={p99}"
85    );
86    assert!(p999 >= p99 && h.max() >= p999, "percentiles are monotone");
87
88    // The reporting surface a dashboard actually wants alongside the
89    // percentiles: the floor, the mean, the fraction inside the SLO, and what
90    // the whole thing costs in memory.
91    let within_slo = h.percentile_at_or_below_value(2_000);
92    println!(
93        "  min={}ns mean={:.0}ns within-2us={:.1}% footprint={}KB",
94        h.min(),
95        h.mean(),
96        within_slo * 100.0,
97        h.footprint_bytes() / 1024
98    );
99    assert!(within_slo > 0.9, "most ops sit inside the 2us band");
100
101    // Coordinated omission: a fixed-rate loop issues one op every 10 ns, then
102    // stalls for 1000 ns. The naive histogram sees one slow sample; the
103    // corrected one backfills the 99 requests the stall blocked.
104    let mut naive = HdrHistogram::new(3);
105    let mut corrected = HdrHistogram::new(3);
106    for _ in 0..1_000 {
107        naive.record(10);
108        corrected.record_with_expected_interval(10, 10);
109    }
110    naive.record(1_000);
111    corrected.record_with_expected_interval(1_000, 10);
112    let naive_p99 = naive.value_at_percentile(0.99);
113    let corrected_p99 = corrected.value_at_percentile(0.99);
114    println!("  coordinated omission: naive p99={naive_p99}ns, corrected p99={corrected_p99}ns");
115    assert!(
116        naive_p99 <= 20,
117        "uncorrected tail hides the stall: {naive_p99}"
118    );
119    assert!(
120        corrected_p99 >= 500,
121        "correction lifts the tail: {corrected_p99}"
122    );
123}
124
125/// `concurrent-writes` feature: several market-data feed handlers record into
126/// one histogram from different threads with no external lock - the only
127/// contention is the per-bucket atomic increment.
128#[cfg(feature = "concurrent-writes")]
129fn concurrent_feed_handlers() {
130    use std::sync::Arc;
131    use std::thread;
132    use subms_hdr_histogram::ConcurrentHdrHistogram;
133    println!("\n== concurrent-writes: many feed handlers, one histogram ==");
134    let h = Arc::new(ConcurrentHdrHistogram::new(3));
135    let threads = 4;
136    let per_thread = 50_000u64;
137    let mut handles = vec![];
138    for _ in 0..threads {
139        let h = h.clone();
140        handles.push(thread::spawn(move || {
141            for i in 0..per_thread {
142                h.record((i % 1_000) + 500);
143            }
144        }));
145    }
146    for j in handles {
147        j.join().unwrap();
148    }
149    println!(
150        "  {} records lock-free, p99={}ns",
151        h.count(),
152        h.value_at_percentile(0.99)
153    );
154    assert_eq!(
155        h.count(),
156        threads as u64 * per_thread,
157        "no writes lost under contention"
158    );
159}
160
161/// `dual-recorder` feature: producers record continuously; a reporter thread
162/// grabs an interval snapshot on a timer by rotating the active side and
163/// draining the inactive one, never blocking the producers.
164#[cfg(feature = "dual-recorder")]
165fn dual_recorder_interval_report() {
166    use subms_hdr_histogram::DualRecorder;
167    println!("\n== dual-recorder: lock-free interval percentile report ==");
168    let rec = DualRecorder::new(3);
169    for v in 1..=500u64 {
170        rec.record(v);
171    }
172    let interval = rec.get_interval_histogram();
173    println!(
174        "  interval count={}, p99={}",
175        interval.count(),
176        interval.value_at_percentile(0.99)
177    );
178    let next = rec.get_interval_histogram();
179    assert_eq!(
180        interval.count(),
181        500,
182        "first interval captured every record"
183    );
184    assert_eq!(
185        next.count(),
186        0,
187        "the next interval starts empty after the rotate"
188    );
189}
190
191/// `merge` feature: two shards each keep their own histogram; a periodic
192/// roll-up sums one into the other for a fleet-wide percentile view. The merge
193/// is exact - identical to recording every value into a single histogram.
194#[cfg(feature = "merge")]
195fn merge_shard_rollup() {
196    use subms_hdr_histogram::merge;
197    println!("\n== merge: roll per-shard histograms into a fleet view ==");
198    let mut shard_a = HdrHistogram::new(3);
199    let mut shard_b = HdrHistogram::new(3);
200    for v in 1..=500u64 {
201        shard_a.record(v);
202    }
203    for v in 501..=1_000u64 {
204        shard_b.record(v);
205    }
206    merge(&mut shard_a, &shard_b).expect("identical shape merges");
207    println!(
208        "  fleet count={}, p50={}, p99={}",
209        shard_a.count(),
210        shard_a.value_at_percentile(0.5),
211        shard_a.value_at_percentile(0.99)
212    );
213    assert_eq!(shard_a.count(), 1_000, "both shards folded in");
214    assert!(
215        shard_a.value_at_percentile(0.99) >= 900,
216        "the high tail came from shard b"
217    );
218}
219
220/// `decay` feature: an exponentially-decaying histogram so the current p99
221/// reflects recent activity. An old burst of slow ops fades over a few
222/// half-lives, so a later burst of fast ops dominates the read.
223#[cfg(feature = "decay")]
224fn decay_recency_weighted() {
225    use subms_hdr_histogram::{DecayingHdrHistogram, ManualClock};
226    println!("\n== decay: recency-weighted p50 forgets an old spike ==");
227    let clock = ManualClock::new();
228    let halflife = 1_000_000_000u64; // 1 second
229    let mut h = DecayingHdrHistogram::new(3, halflife, &clock);
230    for _ in 0..1_000 {
231        h.record(5_000); // an old burst of slow ops
232    }
233    clock.advance_ns(halflife * 4); // four half-lives pass
234    for _ in 0..1_000 {
235        h.record(800); // a recent burst of fast ops
236    }
237    let p50 = h.value_at_percentile(0.5);
238    println!("  decayed count~{:.0}, p50={p50}ns", h.count());
239    assert!(
240        p50 < 2_000,
241        "recent fast ops dominate the decayed distribution: p50={p50}"
242    );
243}
244
245/// `value-tagging` feature: one histogram, a 1-byte tag per recording, so
246/// per-venue tails can be read separately at query time without standing up N
247/// histograms.
248#[cfg(feature = "value-tagging")]
249fn value_tagging_by_venue() {
250    use subms_hdr_histogram::TaggedHdrHistogram;
251    println!("\n== value-tagging: slice latency by venue ==");
252    const COLO: u8 = 0;
253    const REMOTE: u8 = 1;
254    let mut h = TaggedHdrHistogram::new(3);
255    for v in 500..=1_000u64 {
256        h.record(v, COLO); // a fast co-located venue
257    }
258    for v in 5_000..=6_000u64 {
259        h.record(v, REMOTE); // a slow remote venue
260    }
261    let p99_colo = h.value_at_percentile_for_tag(0.99, COLO);
262    let p99_remote = h.value_at_percentile_for_tag(0.99, REMOTE);
263    println!("  colo p99={p99_colo}ns, remote p99={p99_remote}ns");
264    assert!(p99_colo < p99_remote, "each venue's tail reads on its own");
265}
266
267/// `iterators` feature: walk the whole distribution rather than pull single
268/// percentiles - here, the powers-of-two bands and quartile lower bounds a
269/// chart or downstream sink would render.
270#[cfg(feature = "iterators")]
271fn iterators_export_bands() {
272    println!("\n== iterators: export the distribution as bands ==");
273    let mut h = HdrHistogram::new(3);
274    for v in 1..=1_000u64 {
275        h.record(v);
276    }
277    let bands = h.iter_logarithmic().count();
278    let quartiles: Vec<u64> = h.iter_percentiles(25.0).map(|e| e.value_lo).collect();
279    println!("  {bands} log2 bands; quartile lower bounds = {quartiles:?}");
280    assert!(bands > 0, "the populated range spans at least one band");
281    assert!(
282        !quartiles.is_empty(),
283        "the percentile walk yields quartile buckets"
284    );
285}
Source

pub fn record_with_expected_interval( &mut self, value: u64, expected_interval: u64, )

Record value, then correct for coordinated omission. Under a fixed-rate load generator, one slow operation blocks every request that should have been issued while it stalled; those requests are never sampled, so the tail reads far better than the system delivered. When value exceeds expected_interval, this backfills the samples the generator would have taken during the stall - synthetic values at value - expected_interval, value - 2*expected_interval, … down to expected_interval - so the percentiles reflect the latency those blocked requests would have seen.

This is Gil Tene’s recordValueWithExpectedInterval. expected_interval == 0 (or a value no larger than it) disables the correction, leaving this equivalent to Self::record.

Examples found in repository?
examples/sample_app.rs (line 108)
46fn base_tick_to_trade() {
47    println!("== base: tick-to-trade latency capture ==");
48    let mut h = HdrHistogram::new(3);
49
50    // A right-skewed latency stream: most ops sit in a tight band, a small
51    // fraction spike into the tail. Deterministic xorshift so the numbers are
52    // reproducible.
53    let mut rng = 0x2545_F491_4F6C_DD1Du64;
54    let mut next = || {
55        rng ^= rng << 13;
56        rng ^= rng >> 7;
57        rng ^= rng << 17;
58        rng
59    };
60    let n = 2_000u64;
61    for i in 0..n {
62        let base = 700 + next() % 300; // 700..1000 ns steady state
63        if i % 50 == 0 {
64            h.record(4_000 + next() % 4_000); // ~2% tail spike, 4us..8us
65        } else {
66            h.record(base);
67        }
68    }
69
70    let p50 = h.value_at_percentile(0.50);
71    let p99 = h.value_at_percentile(0.99);
72    let p999 = h.value_at_percentile(0.999);
73    println!(
74        "  n={n} p50={p50}ns p99={p99}ns p999={p999}ns max={}ns",
75        h.max()
76    );
77    assert_eq!(h.count(), n, "every sample recorded");
78    assert!(
79        p50 <= 1_100,
80        "median sits in the steady-state band: p50={p50}"
81    );
82    assert!(
83        p99 >= 2_000,
84        "the 2% tail lifts p99 well past the median: p99={p99}"
85    );
86    assert!(p999 >= p99 && h.max() >= p999, "percentiles are monotone");
87
88    // The reporting surface a dashboard actually wants alongside the
89    // percentiles: the floor, the mean, the fraction inside the SLO, and what
90    // the whole thing costs in memory.
91    let within_slo = h.percentile_at_or_below_value(2_000);
92    println!(
93        "  min={}ns mean={:.0}ns within-2us={:.1}% footprint={}KB",
94        h.min(),
95        h.mean(),
96        within_slo * 100.0,
97        h.footprint_bytes() / 1024
98    );
99    assert!(within_slo > 0.9, "most ops sit inside the 2us band");
100
101    // Coordinated omission: a fixed-rate loop issues one op every 10 ns, then
102    // stalls for 1000 ns. The naive histogram sees one slow sample; the
103    // corrected one backfills the 99 requests the stall blocked.
104    let mut naive = HdrHistogram::new(3);
105    let mut corrected = HdrHistogram::new(3);
106    for _ in 0..1_000 {
107        naive.record(10);
108        corrected.record_with_expected_interval(10, 10);
109    }
110    naive.record(1_000);
111    corrected.record_with_expected_interval(1_000, 10);
112    let naive_p99 = naive.value_at_percentile(0.99);
113    let corrected_p99 = corrected.value_at_percentile(0.99);
114    println!("  coordinated omission: naive p99={naive_p99}ns, corrected p99={corrected_p99}ns");
115    assert!(
116        naive_p99 <= 20,
117        "uncorrected tail hides the stall: {naive_p99}"
118    );
119    assert!(
120        corrected_p99 >= 500,
121        "correction lifts the tail: {corrected_p99}"
122    );
123}
Source

pub fn value_at_percentile(&self, q: f64) -> u64

Value at the given quantile (0.0..=1.0). 0 if empty.

Examples found in repository?
examples/sample_app.rs (line 70)
46fn base_tick_to_trade() {
47    println!("== base: tick-to-trade latency capture ==");
48    let mut h = HdrHistogram::new(3);
49
50    // A right-skewed latency stream: most ops sit in a tight band, a small
51    // fraction spike into the tail. Deterministic xorshift so the numbers are
52    // reproducible.
53    let mut rng = 0x2545_F491_4F6C_DD1Du64;
54    let mut next = || {
55        rng ^= rng << 13;
56        rng ^= rng >> 7;
57        rng ^= rng << 17;
58        rng
59    };
60    let n = 2_000u64;
61    for i in 0..n {
62        let base = 700 + next() % 300; // 700..1000 ns steady state
63        if i % 50 == 0 {
64            h.record(4_000 + next() % 4_000); // ~2% tail spike, 4us..8us
65        } else {
66            h.record(base);
67        }
68    }
69
70    let p50 = h.value_at_percentile(0.50);
71    let p99 = h.value_at_percentile(0.99);
72    let p999 = h.value_at_percentile(0.999);
73    println!(
74        "  n={n} p50={p50}ns p99={p99}ns p999={p999}ns max={}ns",
75        h.max()
76    );
77    assert_eq!(h.count(), n, "every sample recorded");
78    assert!(
79        p50 <= 1_100,
80        "median sits in the steady-state band: p50={p50}"
81    );
82    assert!(
83        p99 >= 2_000,
84        "the 2% tail lifts p99 well past the median: p99={p99}"
85    );
86    assert!(p999 >= p99 && h.max() >= p999, "percentiles are monotone");
87
88    // The reporting surface a dashboard actually wants alongside the
89    // percentiles: the floor, the mean, the fraction inside the SLO, and what
90    // the whole thing costs in memory.
91    let within_slo = h.percentile_at_or_below_value(2_000);
92    println!(
93        "  min={}ns mean={:.0}ns within-2us={:.1}% footprint={}KB",
94        h.min(),
95        h.mean(),
96        within_slo * 100.0,
97        h.footprint_bytes() / 1024
98    );
99    assert!(within_slo > 0.9, "most ops sit inside the 2us band");
100
101    // Coordinated omission: a fixed-rate loop issues one op every 10 ns, then
102    // stalls for 1000 ns. The naive histogram sees one slow sample; the
103    // corrected one backfills the 99 requests the stall blocked.
104    let mut naive = HdrHistogram::new(3);
105    let mut corrected = HdrHistogram::new(3);
106    for _ in 0..1_000 {
107        naive.record(10);
108        corrected.record_with_expected_interval(10, 10);
109    }
110    naive.record(1_000);
111    corrected.record_with_expected_interval(1_000, 10);
112    let naive_p99 = naive.value_at_percentile(0.99);
113    let corrected_p99 = corrected.value_at_percentile(0.99);
114    println!("  coordinated omission: naive p99={naive_p99}ns, corrected p99={corrected_p99}ns");
115    assert!(
116        naive_p99 <= 20,
117        "uncorrected tail hides the stall: {naive_p99}"
118    );
119    assert!(
120        corrected_p99 >= 500,
121        "correction lifts the tail: {corrected_p99}"
122    );
123}
124
125/// `concurrent-writes` feature: several market-data feed handlers record into
126/// one histogram from different threads with no external lock - the only
127/// contention is the per-bucket atomic increment.
128#[cfg(feature = "concurrent-writes")]
129fn concurrent_feed_handlers() {
130    use std::sync::Arc;
131    use std::thread;
132    use subms_hdr_histogram::ConcurrentHdrHistogram;
133    println!("\n== concurrent-writes: many feed handlers, one histogram ==");
134    let h = Arc::new(ConcurrentHdrHistogram::new(3));
135    let threads = 4;
136    let per_thread = 50_000u64;
137    let mut handles = vec![];
138    for _ in 0..threads {
139        let h = h.clone();
140        handles.push(thread::spawn(move || {
141            for i in 0..per_thread {
142                h.record((i % 1_000) + 500);
143            }
144        }));
145    }
146    for j in handles {
147        j.join().unwrap();
148    }
149    println!(
150        "  {} records lock-free, p99={}ns",
151        h.count(),
152        h.value_at_percentile(0.99)
153    );
154    assert_eq!(
155        h.count(),
156        threads as u64 * per_thread,
157        "no writes lost under contention"
158    );
159}
160
161/// `dual-recorder` feature: producers record continuously; a reporter thread
162/// grabs an interval snapshot on a timer by rotating the active side and
163/// draining the inactive one, never blocking the producers.
164#[cfg(feature = "dual-recorder")]
165fn dual_recorder_interval_report() {
166    use subms_hdr_histogram::DualRecorder;
167    println!("\n== dual-recorder: lock-free interval percentile report ==");
168    let rec = DualRecorder::new(3);
169    for v in 1..=500u64 {
170        rec.record(v);
171    }
172    let interval = rec.get_interval_histogram();
173    println!(
174        "  interval count={}, p99={}",
175        interval.count(),
176        interval.value_at_percentile(0.99)
177    );
178    let next = rec.get_interval_histogram();
179    assert_eq!(
180        interval.count(),
181        500,
182        "first interval captured every record"
183    );
184    assert_eq!(
185        next.count(),
186        0,
187        "the next interval starts empty after the rotate"
188    );
189}
190
191/// `merge` feature: two shards each keep their own histogram; a periodic
192/// roll-up sums one into the other for a fleet-wide percentile view. The merge
193/// is exact - identical to recording every value into a single histogram.
194#[cfg(feature = "merge")]
195fn merge_shard_rollup() {
196    use subms_hdr_histogram::merge;
197    println!("\n== merge: roll per-shard histograms into a fleet view ==");
198    let mut shard_a = HdrHistogram::new(3);
199    let mut shard_b = HdrHistogram::new(3);
200    for v in 1..=500u64 {
201        shard_a.record(v);
202    }
203    for v in 501..=1_000u64 {
204        shard_b.record(v);
205    }
206    merge(&mut shard_a, &shard_b).expect("identical shape merges");
207    println!(
208        "  fleet count={}, p50={}, p99={}",
209        shard_a.count(),
210        shard_a.value_at_percentile(0.5),
211        shard_a.value_at_percentile(0.99)
212    );
213    assert_eq!(shard_a.count(), 1_000, "both shards folded in");
214    assert!(
215        shard_a.value_at_percentile(0.99) >= 900,
216        "the high tail came from shard b"
217    );
218}
Source

pub fn sub_count(&self) -> u32

Examples found in repository?
examples/perf_features.rs (line 57)
56fn sub_count(d: u32) -> usize {
57    HdrHistogram::new(d).sub_count() as usize
58}
Source

pub fn min(&self) -> u64

Lowest value recorded, as its bucket’s lower bound. 0 if empty. Read-side sweep, same cost class as Self::value_at_percentile.

Examples found in repository?
examples/sample_app.rs (line 94)
46fn base_tick_to_trade() {
47    println!("== base: tick-to-trade latency capture ==");
48    let mut h = HdrHistogram::new(3);
49
50    // A right-skewed latency stream: most ops sit in a tight band, a small
51    // fraction spike into the tail. Deterministic xorshift so the numbers are
52    // reproducible.
53    let mut rng = 0x2545_F491_4F6C_DD1Du64;
54    let mut next = || {
55        rng ^= rng << 13;
56        rng ^= rng >> 7;
57        rng ^= rng << 17;
58        rng
59    };
60    let n = 2_000u64;
61    for i in 0..n {
62        let base = 700 + next() % 300; // 700..1000 ns steady state
63        if i % 50 == 0 {
64            h.record(4_000 + next() % 4_000); // ~2% tail spike, 4us..8us
65        } else {
66            h.record(base);
67        }
68    }
69
70    let p50 = h.value_at_percentile(0.50);
71    let p99 = h.value_at_percentile(0.99);
72    let p999 = h.value_at_percentile(0.999);
73    println!(
74        "  n={n} p50={p50}ns p99={p99}ns p999={p999}ns max={}ns",
75        h.max()
76    );
77    assert_eq!(h.count(), n, "every sample recorded");
78    assert!(
79        p50 <= 1_100,
80        "median sits in the steady-state band: p50={p50}"
81    );
82    assert!(
83        p99 >= 2_000,
84        "the 2% tail lifts p99 well past the median: p99={p99}"
85    );
86    assert!(p999 >= p99 && h.max() >= p999, "percentiles are monotone");
87
88    // The reporting surface a dashboard actually wants alongside the
89    // percentiles: the floor, the mean, the fraction inside the SLO, and what
90    // the whole thing costs in memory.
91    let within_slo = h.percentile_at_or_below_value(2_000);
92    println!(
93        "  min={}ns mean={:.0}ns within-2us={:.1}% footprint={}KB",
94        h.min(),
95        h.mean(),
96        within_slo * 100.0,
97        h.footprint_bytes() / 1024
98    );
99    assert!(within_slo > 0.9, "most ops sit inside the 2us band");
100
101    // Coordinated omission: a fixed-rate loop issues one op every 10 ns, then
102    // stalls for 1000 ns. The naive histogram sees one slow sample; the
103    // corrected one backfills the 99 requests the stall blocked.
104    let mut naive = HdrHistogram::new(3);
105    let mut corrected = HdrHistogram::new(3);
106    for _ in 0..1_000 {
107        naive.record(10);
108        corrected.record_with_expected_interval(10, 10);
109    }
110    naive.record(1_000);
111    corrected.record_with_expected_interval(1_000, 10);
112    let naive_p99 = naive.value_at_percentile(0.99);
113    let corrected_p99 = corrected.value_at_percentile(0.99);
114    println!("  coordinated omission: naive p99={naive_p99}ns, corrected p99={corrected_p99}ns");
115    assert!(
116        naive_p99 <= 20,
117        "uncorrected tail hides the stall: {naive_p99}"
118    );
119    assert!(
120        corrected_p99 >= 500,
121        "correction lifts the tail: {corrected_p99}"
122    );
123}
Source

pub fn mean(&self) -> f64

Arithmetic mean over the recorded bucket lower bounds. 0.0 if empty. Quantised the same way the percentiles are, so it sits within the significant-digit error band rather than being exact.

Examples found in repository?
examples/sample_app.rs (line 95)
46fn base_tick_to_trade() {
47    println!("== base: tick-to-trade latency capture ==");
48    let mut h = HdrHistogram::new(3);
49
50    // A right-skewed latency stream: most ops sit in a tight band, a small
51    // fraction spike into the tail. Deterministic xorshift so the numbers are
52    // reproducible.
53    let mut rng = 0x2545_F491_4F6C_DD1Du64;
54    let mut next = || {
55        rng ^= rng << 13;
56        rng ^= rng >> 7;
57        rng ^= rng << 17;
58        rng
59    };
60    let n = 2_000u64;
61    for i in 0..n {
62        let base = 700 + next() % 300; // 700..1000 ns steady state
63        if i % 50 == 0 {
64            h.record(4_000 + next() % 4_000); // ~2% tail spike, 4us..8us
65        } else {
66            h.record(base);
67        }
68    }
69
70    let p50 = h.value_at_percentile(0.50);
71    let p99 = h.value_at_percentile(0.99);
72    let p999 = h.value_at_percentile(0.999);
73    println!(
74        "  n={n} p50={p50}ns p99={p99}ns p999={p999}ns max={}ns",
75        h.max()
76    );
77    assert_eq!(h.count(), n, "every sample recorded");
78    assert!(
79        p50 <= 1_100,
80        "median sits in the steady-state band: p50={p50}"
81    );
82    assert!(
83        p99 >= 2_000,
84        "the 2% tail lifts p99 well past the median: p99={p99}"
85    );
86    assert!(p999 >= p99 && h.max() >= p999, "percentiles are monotone");
87
88    // The reporting surface a dashboard actually wants alongside the
89    // percentiles: the floor, the mean, the fraction inside the SLO, and what
90    // the whole thing costs in memory.
91    let within_slo = h.percentile_at_or_below_value(2_000);
92    println!(
93        "  min={}ns mean={:.0}ns within-2us={:.1}% footprint={}KB",
94        h.min(),
95        h.mean(),
96        within_slo * 100.0,
97        h.footprint_bytes() / 1024
98    );
99    assert!(within_slo > 0.9, "most ops sit inside the 2us band");
100
101    // Coordinated omission: a fixed-rate loop issues one op every 10 ns, then
102    // stalls for 1000 ns. The naive histogram sees one slow sample; the
103    // corrected one backfills the 99 requests the stall blocked.
104    let mut naive = HdrHistogram::new(3);
105    let mut corrected = HdrHistogram::new(3);
106    for _ in 0..1_000 {
107        naive.record(10);
108        corrected.record_with_expected_interval(10, 10);
109    }
110    naive.record(1_000);
111    corrected.record_with_expected_interval(1_000, 10);
112    let naive_p99 = naive.value_at_percentile(0.99);
113    let corrected_p99 = corrected.value_at_percentile(0.99);
114    println!("  coordinated omission: naive p99={naive_p99}ns, corrected p99={corrected_p99}ns");
115    assert!(
116        naive_p99 <= 20,
117        "uncorrected tail hides the stall: {naive_p99}"
118    );
119    assert!(
120        corrected_p99 >= 500,
121        "correction lifts the tail: {corrected_p99}"
122    );
123}
Source

pub fn count_at_value(&self, value: u64) -> u64

Recordings that landed in value’s bucket. Constant time - the same index computation record does.

Source

pub fn percentile_at_or_below_value(&self, value: u64) -> f64

Fraction of recordings at or below value’s bucket, in 0.0..=1.0. The inverse of Self::value_at_percentile: that maps a rank to a value, this maps a value to its rank.

Examples found in repository?
examples/sample_app.rs (line 91)
46fn base_tick_to_trade() {
47    println!("== base: tick-to-trade latency capture ==");
48    let mut h = HdrHistogram::new(3);
49
50    // A right-skewed latency stream: most ops sit in a tight band, a small
51    // fraction spike into the tail. Deterministic xorshift so the numbers are
52    // reproducible.
53    let mut rng = 0x2545_F491_4F6C_DD1Du64;
54    let mut next = || {
55        rng ^= rng << 13;
56        rng ^= rng >> 7;
57        rng ^= rng << 17;
58        rng
59    };
60    let n = 2_000u64;
61    for i in 0..n {
62        let base = 700 + next() % 300; // 700..1000 ns steady state
63        if i % 50 == 0 {
64            h.record(4_000 + next() % 4_000); // ~2% tail spike, 4us..8us
65        } else {
66            h.record(base);
67        }
68    }
69
70    let p50 = h.value_at_percentile(0.50);
71    let p99 = h.value_at_percentile(0.99);
72    let p999 = h.value_at_percentile(0.999);
73    println!(
74        "  n={n} p50={p50}ns p99={p99}ns p999={p999}ns max={}ns",
75        h.max()
76    );
77    assert_eq!(h.count(), n, "every sample recorded");
78    assert!(
79        p50 <= 1_100,
80        "median sits in the steady-state band: p50={p50}"
81    );
82    assert!(
83        p99 >= 2_000,
84        "the 2% tail lifts p99 well past the median: p99={p99}"
85    );
86    assert!(p999 >= p99 && h.max() >= p999, "percentiles are monotone");
87
88    // The reporting surface a dashboard actually wants alongside the
89    // percentiles: the floor, the mean, the fraction inside the SLO, and what
90    // the whole thing costs in memory.
91    let within_slo = h.percentile_at_or_below_value(2_000);
92    println!(
93        "  min={}ns mean={:.0}ns within-2us={:.1}% footprint={}KB",
94        h.min(),
95        h.mean(),
96        within_slo * 100.0,
97        h.footprint_bytes() / 1024
98    );
99    assert!(within_slo > 0.9, "most ops sit inside the 2us band");
100
101    // Coordinated omission: a fixed-rate loop issues one op every 10 ns, then
102    // stalls for 1000 ns. The naive histogram sees one slow sample; the
103    // corrected one backfills the 99 requests the stall blocked.
104    let mut naive = HdrHistogram::new(3);
105    let mut corrected = HdrHistogram::new(3);
106    for _ in 0..1_000 {
107        naive.record(10);
108        corrected.record_with_expected_interval(10, 10);
109    }
110    naive.record(1_000);
111    corrected.record_with_expected_interval(1_000, 10);
112    let naive_p99 = naive.value_at_percentile(0.99);
113    let corrected_p99 = corrected.value_at_percentile(0.99);
114    println!("  coordinated omission: naive p99={naive_p99}ns, corrected p99={corrected_p99}ns");
115    assert!(
116        naive_p99 <= 20,
117        "uncorrected tail hides the stall: {naive_p99}"
118    );
119    assert!(
120        corrected_p99 >= 500,
121        "correction lifts the tail: {corrected_p99}"
122    );
123}
Source

pub fn footprint_bytes(&self) -> usize

Counter-array footprint in bytes. Grows with the largest value recorded, not with how many values were recorded.

Examples found in repository?
examples/sample_app.rs (line 97)
46fn base_tick_to_trade() {
47    println!("== base: tick-to-trade latency capture ==");
48    let mut h = HdrHistogram::new(3);
49
50    // A right-skewed latency stream: most ops sit in a tight band, a small
51    // fraction spike into the tail. Deterministic xorshift so the numbers are
52    // reproducible.
53    let mut rng = 0x2545_F491_4F6C_DD1Du64;
54    let mut next = || {
55        rng ^= rng << 13;
56        rng ^= rng >> 7;
57        rng ^= rng << 17;
58        rng
59    };
60    let n = 2_000u64;
61    for i in 0..n {
62        let base = 700 + next() % 300; // 700..1000 ns steady state
63        if i % 50 == 0 {
64            h.record(4_000 + next() % 4_000); // ~2% tail spike, 4us..8us
65        } else {
66            h.record(base);
67        }
68    }
69
70    let p50 = h.value_at_percentile(0.50);
71    let p99 = h.value_at_percentile(0.99);
72    let p999 = h.value_at_percentile(0.999);
73    println!(
74        "  n={n} p50={p50}ns p99={p99}ns p999={p999}ns max={}ns",
75        h.max()
76    );
77    assert_eq!(h.count(), n, "every sample recorded");
78    assert!(
79        p50 <= 1_100,
80        "median sits in the steady-state band: p50={p50}"
81    );
82    assert!(
83        p99 >= 2_000,
84        "the 2% tail lifts p99 well past the median: p99={p99}"
85    );
86    assert!(p999 >= p99 && h.max() >= p999, "percentiles are monotone");
87
88    // The reporting surface a dashboard actually wants alongside the
89    // percentiles: the floor, the mean, the fraction inside the SLO, and what
90    // the whole thing costs in memory.
91    let within_slo = h.percentile_at_or_below_value(2_000);
92    println!(
93        "  min={}ns mean={:.0}ns within-2us={:.1}% footprint={}KB",
94        h.min(),
95        h.mean(),
96        within_slo * 100.0,
97        h.footprint_bytes() / 1024
98    );
99    assert!(within_slo > 0.9, "most ops sit inside the 2us band");
100
101    // Coordinated omission: a fixed-rate loop issues one op every 10 ns, then
102    // stalls for 1000 ns. The naive histogram sees one slow sample; the
103    // corrected one backfills the 99 requests the stall blocked.
104    let mut naive = HdrHistogram::new(3);
105    let mut corrected = HdrHistogram::new(3);
106    for _ in 0..1_000 {
107        naive.record(10);
108        corrected.record_with_expected_interval(10, 10);
109    }
110    naive.record(1_000);
111    corrected.record_with_expected_interval(1_000, 10);
112    let naive_p99 = naive.value_at_percentile(0.99);
113    let corrected_p99 = corrected.value_at_percentile(0.99);
114    println!("  coordinated omission: naive p99={naive_p99}ns, corrected p99={corrected_p99}ns");
115    assert!(
116        naive_p99 <= 20,
117        "uncorrected tail hides the stall: {naive_p99}"
118    );
119    assert!(
120        corrected_p99 >= 500,
121        "correction lifts the tail: {corrected_p99}"
122    );
123}
Source

pub fn reset(&mut self)

Drop every recorded value. Keeps the array allocated, so a histogram recycled across reporting intervals never re-enters the allocator.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.