perf_main/perf_main.rs
1//! Self-bench. Measures the harness's own hot paths and emits a
2//! `SubMsBenchSummary` on stdout. Driven by `subms-action-bench` in this
3//! repo's perf workflow - if a future PR slows down the recording overhead
4//! by more than the per-stage threshold, the gate catches it on the PR.
5//!
6//! Run locally:
7//! cargo run --release --example perf_main > perf.json
8//!
9//! Stages:
10//! time_closure - end-to-end cost of `stage.time(|| {})`. The floor for
11//! any user who instruments their hot path with `time()`.
12//! record_ns - just the Vec push (`stage.record(ns)`). Strict lower
13//! bound on per-sample bookkeeping.
14//! summarize - sort + percentile extraction over 50k samples. Cost
15//! of producing one `SubMsBenchSummary`.
16//! summary_to_json - serialisation of one summary into the standard JSON
17//! shape. Cost paid by every CI run that uploads results.
18//! diff_summary - regression-diff math between two summaries. Cost paid
19//! by every PR-time gate.
20
21use std::hint::black_box;
22use std::io::{Cursor, Write};
23
24use subms::{SubMsBenchSummary, SubMsPerfHarness, SubMsTimer, summarize, summary_to_json};
25
26fn main() {
27 let mut h = SubMsPerfHarness::new("subms-self-bench", "rust");
28 h.input("samples_per_stage", "50000");
29 h.add_meta("rust_version", env!("CARGO_PKG_RUST_VERSION"));
30 h.add_meta("crate_version", env!("CARGO_PKG_VERSION"));
31
32 // 1. time_closure: cost of `stage.time(|| {})` with an empty closure.
33 // This is the per-iteration overhead any user pays when they wrap
34 // their hot path with `stage.time(...)`.
35 {
36 let s = h.stage("time_closure", 50_000);
37 for _ in 0..50_000 {
38 s.time(|| black_box(()));
39 }
40 }
41
42 // 2. record_ns: cost of `stage.record(ns)` alone. No timer, just the
43 // Vec push. Strict lower bound on per-sample bookkeeping.
44 {
45 let s = h.stage("record_ns", 50_000);
46 for i in 0..50_000u64 {
47 s.record(black_box(100 + (i & 63)));
48 }
49 }
50
51 // 3. summarize: sort + percentile extraction. Cost of producing one
52 // SubMsBenchSummary. We use SubMsTimer in a parallel harness so the
53 // measurement of the measurement isn't recursive.
54 {
55 let summary_target = build_sample_harness(50_000);
56 let s = h.stage("summarize", 100);
57 for _ in 0..100 {
58 s.time(|| {
59 let summary = summarize(black_box(&summary_target));
60 black_box(summary);
61 });
62 }
63 }
64
65 // 4. summary_to_json: serialise to the canonical JSON shape. Cost paid
66 // by every CI run that uploads results.
67 {
68 let summary = summarize(&build_sample_harness(50_000));
69 let s = h.stage("summary_to_json", 100);
70 for _ in 0..100 {
71 s.time(|| {
72 let mut buf = Cursor::new(Vec::with_capacity(64 * 1024));
73 summary_to_json(black_box(&summary), &mut buf).unwrap();
74 let _ = buf.flush();
75 black_box(buf);
76 });
77 }
78 }
79
80 // 5. diff_summary: regression-diff math between two SubMsBenchSummary
81 // objects. Cost paid by every PR-time gate.
82 {
83 let base: SubMsBenchSummary = summarize(&build_sample_harness(50_000));
84 let cand: SubMsBenchSummary = summarize(&build_sample_harness(50_000));
85 let s = h.stage("diff_summary", 1_000);
86 for _ in 0..1_000 {
87 s.time(|| {
88 let d = subms::diff_summary(black_box(&base), black_box(&cand));
89 black_box(d);
90 });
91 }
92 }
93
94 // Cross-check: SubMsTimer itself rolls a quick checkpoint walk so the
95 // timer's marking overhead is also visible from this perf JSON's meta.
96 let mut t = SubMsTimer::new("self-bench-wall");
97 t.mark("stages-complete");
98 t.stop("emitting-json");
99 h.add_meta("self_bench_wall_ns", &t.elapsed_ns().to_string());
100
101 let summary = summarize(&h);
102 summary_to_json(&summary, &mut std::io::stdout()).unwrap();
103}
104
105/// Build a SubMsPerfHarness pre-populated with a synthetic stage's worth of
106/// samples - used as fodder for the summarize / json / diff stages above.
107fn build_sample_harness(n: usize) -> SubMsPerfHarness {
108 let mut h = SubMsPerfHarness::new("fixture", "rust");
109 let s = h.stage("fixture-stage", n);
110 for i in 0..n as u64 {
111 // Synthetic log-ish distribution: most samples small, a tail
112 // exercises percentile sort + lookup.
113 let v = if i % 1000 == 0 {
114 10_000 + (i & 1023)
115 } else {
116 100 + (i & 63)
117 };
118 s.record(v);
119 }
120 h
121}