Skip to main content

ConcurrentHdrHistogram

Struct ConcurrentHdrHistogram 

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

Concurrent HDR histogram. All operations are lock-free.

Implementations§

Source§

impl ConcurrentHdrHistogram

Source

pub fn new(significant_digits: u32) -> Self

New histogram with the given significant-digit precision and default major-bucket capacity (32, tracking values past 4e12 at 3 sig-digits).

Examples found in repository?
examples/sample_app.rs (line 134)
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}
More examples
Hide additional examples
examples/perf_features.rs (line 144)
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 with_majors(significant_digits: u32, majors: u32) -> Self

Explicit major-bucket capacity. Counter array length is sub_count * majors. Records that would land past the last bucket are clamped into the final bucket (no resize possible in a lock-free design).

Source

pub fn sub_count(&self) -> u32

Source

pub fn sub_count_bits(&self) -> u32

Source

pub fn count(&self) -> u64

Examples found in repository?
examples/sample_app.rs (line 151)
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}
Source

pub fn max(&self) -> u64

Source

pub fn record(&self, value: u64)

Record a value. Safe from any thread.

Examples found in repository?
examples/sample_app.rs (line 142)
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}
More examples
Hide additional examples
examples/perf_features.rs (line 145)
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 value_at_percentile(&self, q: f64) -> u64

Snapshot-style percentile read. Walks the array with relaxed loads; not linearisable across all concurrent writers but each individual counter read is intact.

Examples found in repository?
examples/sample_app.rs (line 152)
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}
More examples
Hide additional examples
examples/perf_features.rs (line 157)
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 drain_snapshot(&self) -> Snapshot

Atomically drain every counter and total into a Snapshot, leaving the histogram empty. Used by DualRecorder to harvest the inactive side. Each per-counter swap is independent, so concurrent writers may land their increments in EITHER the drained snapshot OR the now-zeroed live histogram - we never double-count or lose a write.

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.