Skip to main content

sample_app/
sample_app.rs

1//! Sample app: a tour of `subms-hdr-histogram`, base API first, then each
2//! optional feature. Run the base with `cargo run --example sample_app`; add
3//! `--all-features` (or a subset like `--features merge`) to see the feature
4//! sections light up.
5//!
6//! * base              - tick-to-trade latency capture with p50/p99/p999 reads
7//!   and Gil Tene coordinated-omission correction
8//! * concurrent-writes - many feed-handler threads recording into one histogram
9//! * dual-recorder     - lock-free interval percentile reporting
10//! * merge             - roll per-shard histograms into a fleet-wide view
11//! * decay             - recency-weighted percentiles that forget an old spike
12//! * value-tagging     - slice latency by venue at query time
13//! * iterators         - export the distribution as bands for a chart / sink
14
15use subms_hdr_histogram::HdrHistogram;
16
17fn main() {
18    base_tick_to_trade();
19
20    #[cfg(feature = "concurrent-writes")]
21    concurrent_feed_handlers();
22
23    #[cfg(feature = "dual-recorder")]
24    dual_recorder_interval_report();
25
26    #[cfg(feature = "merge")]
27    merge_shard_rollup();
28
29    #[cfg(feature = "decay")]
30    decay_recency_weighted();
31
32    #[cfg(feature = "value-tagging")]
33    value_tagging_by_venue();
34
35    #[cfg(feature = "iterators")]
36    iterators_export_bands();
37}
38
39/// Base API: a strategy records tick-to-trade latencies (nanoseconds) into a
40/// 3-significant-digit histogram, then reads p50/p99/p999. Recording is one
41/// bucket increment; a percentile is a cumulative sweep over the counter array.
42/// The second half shows why the sample source matters: under a fixed-rate
43/// load, `record_with_expected_interval` backfills the requests a stall
44/// blocked, so the tail reflects what the system delivered rather than the one
45/// event that hurt.
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}