Skip to main content

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}