pub struct HdrHistogram { /* private fields */ }Expand description
Histogram with significant_digits of precision in [1, 5].
Implementations§
Source§impl HdrHistogram
impl HdrHistogram
pub fn iter_linear(&self) -> HdrLinearIter<'_> ⓘ
Sourcepub fn iter_logarithmic(&self) -> HdrLogarithmicIter<'_> ⓘ
pub fn iter_logarithmic(&self) -> HdrLogarithmicIter<'_> ⓘ
Examples found in repository?
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
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}Sourcepub fn iter_percentiles(&self, step_percent: f64) -> HdrPercentileIter<'_> ⓘ
pub fn iter_percentiles(&self, step_percent: f64) -> HdrPercentileIter<'_> ⓘ
Examples found in repository?
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
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
impl HdrHistogram
Sourcepub fn new(significant_digits: u32) -> Self
pub fn new(significant_digits: u32) -> Self
significant_digits in [1, 5]; clamped if out of range.
Examples found in repository?
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
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}Sourcepub fn count(&self) -> u64
pub fn count(&self) -> u64
Examples found in repository?
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}Sourcepub fn max(&self) -> u64
pub fn max(&self) -> u64
Highest value recorded (approximated to the bucket’s lower bound).
Examples found in repository?
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}Sourcepub fn record(&mut self, value: u64)
pub fn record(&mut self, value: u64)
Examples found in repository?
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
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}Sourcepub fn record_with_expected_interval(
&mut self,
value: u64,
expected_interval: u64,
)
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?
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}Sourcepub fn value_at_percentile(&self, q: f64) -> u64
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?
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}Sourcepub fn min(&self) -> u64
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?
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}Sourcepub fn mean(&self) -> f64
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?
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}Sourcepub fn count_at_value(&self, value: u64) -> u64
pub fn count_at_value(&self, value: u64) -> u64
Recordings that landed in value’s bucket. Constant time - the same
index computation record does.
Sourcepub fn percentile_at_or_below_value(&self, value: u64) -> f64
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?
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}Sourcepub fn footprint_bytes(&self) -> usize
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?
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}