pub struct ConcurrentHdrHistogram { /* private fields */ }Expand description
Concurrent HDR histogram. All operations are lock-free.
Implementations§
Source§impl ConcurrentHdrHistogram
impl ConcurrentHdrHistogram
Sourcepub fn new(significant_digits: u32) -> Self
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?
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
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 with_majors(significant_digits: u32, majors: u32) -> Self
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).
pub fn sub_count(&self) -> u32
pub fn sub_count_bits(&self) -> u32
Sourcepub fn count(&self) -> u64
pub fn count(&self) -> u64
Examples found in repository?
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}pub fn max(&self) -> u64
Sourcepub fn record(&self, value: u64)
pub fn record(&self, value: u64)
Record a value. Safe from any thread.
Examples found in repository?
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
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 value_at_percentile(&self, q: f64) -> u64
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?
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
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 drain_snapshot(&self) -> Snapshot
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.