Skip to main content

perf_features/
perf_features.rs

1//! Per-feature bench: sweeps each opt-in feature (`typed`, `growable`,
2//! `stats`, `aligned`) across three allocation counts, lets
3//! `classify_feature` DECIDE the category from the shape of that sweep, and
4//! merge-writes the decision into `../.subms/features/rust.json`.
5//!
6//! An arena's "size" is how many allocations it is carrying, so the sweep
7//! fills to N and times the allocate path there. A per-op cost that holds
8//! steady as N grows is `hot-path`; one that climbs with N is `structural`.
9//!
10//! The sweep classifies on p50. p99 over a few dozen samples is just the worst
11//! one, and a single scheduler slice is large enough to swamp the size signal
12//! the sweep is reading. The p99 still goes into the manifest for the stage
13//! table.
14//!
15//! These p99 figures describe THIS machine. They are published only when the
16//! manifest is stamped `p99_source: fleet`; a local run leaves the category,
17//! which is machine independent, and no published number.
18//!
19//! Run:
20//!   cargo run --release --example perf_features \
21//!       --features "harness typed growable stats aligned"
22
23use std::collections::BTreeMap;
24use std::io::{self, Write};
25use std::path::PathBuf;
26
27use subms::{SubMsFeatureManifest, SubMsP99Source, SubMsPerfHarness, classify_feature, summarize};
28
29/// Allocation counts the sweep walks.
30const SIZES: [usize; 3] = [4_096, 32_768, 262_144];
31
32fn stage_stats(h: &SubMsPerfHarness, name: &str) -> (u64, u64) {
33    summarize(h)
34        .stages
35        .iter()
36        .find(|s| s.name == name)
37        .map_or((0, 0), |s| (s.p50_ns, s.p99_ns))
38}
39
40/// (p50, p99) in ns of `op` run `n` times.
41fn run_p50_p99(n: usize, mut op: impl FnMut(usize)) -> (u64, u64) {
42    let mut h = SubMsPerfHarness::new("arena-feature", "rust");
43    {
44        let st = h.stage("op", n);
45        for i in 0..n {
46            st.time(|| op(i));
47        }
48    }
49    stage_stats(&h, "op")
50}
51
52fn main() -> io::Result<()> {
53    let canon = SIZES[SIZES.len() - 1];
54
55    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
56        .join("..")
57        .join(".subms")
58        .join("features")
59        .join("rust.json");
60    let existing = std::fs::read_to_string(&path).unwrap_or_default();
61    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
62    // Stamp the box these numbers came from. The bench runs wherever it is
63    // invoked, so an unstamped manifest is indistinguishable from a fleet
64    // capture; the renderer will not publish one it cannot attribute.
65    let (source, instance) = SubMsP99Source::from_env();
66    manifest.set_p99_source(source, instance.as_deref());
67
68    // ---------- typed: one Copy type, slot handles, reuse on free ----------
69    #[cfg(feature = "typed")]
70    {
71        use subms_arena_allocator::TypedArena;
72        let sweep: Vec<(usize, u64)> = SIZES
73            .iter()
74            .map(|&n| {
75                let mut arena: TypedArena<u64> = TypedArena::with_capacity(n);
76                let (p50, _) = run_p50_p99(n, |i| {
77                    std::hint::black_box(arena.alloc(i as u64));
78                });
79                (n, p50)
80            })
81            .collect();
82        let (cat, reason) = classify_feature(&sweep, None, None);
83
84        let mut arena: TypedArena<u64> = TypedArena::with_capacity(canon);
85        let (_, alloc99) = run_p50_p99(canon, |i| {
86            std::hint::black_box(arena.alloc(i as u64));
87        });
88        // Every timed op here takes the slot the previous one freed, so the
89        // reuse path is what is measured rather than the append path.
90        let mut churn: TypedArena<u64> = TypedArena::with_capacity(2);
91        let (_, free99) = run_p50_p99(canon, |i| {
92            let slot = churn.alloc(i as u64);
93            churn.free(slot);
94        });
95        let mut p99 = BTreeMap::new();
96        p99.insert("alloc".to_string(), alloc99);
97        p99.insert("free".to_string(), free99);
98        manifest.set_feature("typed", cat, &p99, &reason);
99    }
100
101    // ---------- growable: a new chunk when the active one runs out ----------
102    #[cfg(feature = "growable")]
103    {
104        use subms_arena_allocator::GrowableBump;
105        let sweep: Vec<(usize, u64)> = SIZES
106            .iter()
107            .map(|&n| {
108                let mut a = GrowableBump::new();
109                let (p50, _) = run_p50_p99(n, |i| {
110                    std::hint::black_box(a.alloc_copy(i as u64));
111                });
112                (n, p50)
113            })
114            .collect();
115        let (cat, reason) = classify_feature(&sweep, None, None);
116
117        let mut a = GrowableBump::new();
118        let (_, alloc99) = run_p50_p99(canon, |i| {
119            std::hint::black_box(a.alloc_copy(i as u64));
120        });
121        let mut filled = GrowableBump::new();
122        for i in 0..canon {
123            filled.alloc_copy(i as u64);
124        }
125        let (_, reset99) = run_p50_p99(1, |_| filled.reset());
126        let mut p99 = BTreeMap::new();
127        p99.insert("alloc".to_string(), alloc99);
128        p99.insert("reset".to_string(), reset99);
129        manifest.set_feature("growable", cat, &p99, &reason);
130    }
131
132    // ---------- stats: live counters on the alloc path ----------
133    #[cfg(feature = "stats")]
134    {
135        use subms_arena_allocator::StatsBump;
136        let sweep: Vec<(usize, u64)> = SIZES
137            .iter()
138            .map(|&n| {
139                let mut a = StatsBump::new();
140                let (p50, _) = run_p50_p99(n, |i| {
141                    std::hint::black_box(a.alloc_copy(i as u64));
142                });
143                (n, p50)
144            })
145            .collect();
146        let (cat, reason) = classify_feature(&sweep, None, None);
147
148        let mut a = StatsBump::new();
149        let (_, alloc99) = run_p50_p99(canon, |i| {
150            std::hint::black_box(a.alloc_copy(i as u64));
151        });
152        let (_, snap99) = run_p50_p99(canon, |_| {
153            std::hint::black_box(a.stats());
154        });
155        let mut p99 = BTreeMap::new();
156        p99.insert("alloc".to_string(), alloc99);
157        p99.insert("stats".to_string(), snap99);
158        manifest.set_feature("stats", cat, &p99, &reason);
159    }
160
161    // ---------- aligned: explicit per-allocation alignment ----------
162    #[cfg(feature = "aligned")]
163    {
164        use subms_arena_allocator::AlignedBump;
165        let sweep: Vec<(usize, u64)> = SIZES
166            .iter()
167            .map(|&n| {
168                let mut a = AlignedBump::with_capacity(n * 16 + 4096);
169                let (p50, _) = run_p50_p99(n, |_| {
170                    std::hint::black_box(a.alloc_aligned(8, 8).len());
171                });
172                (n, p50)
173            })
174            .collect();
175        let (cat, reason) = classify_feature(&sweep, None, None);
176
177        let mut a = AlignedBump::with_capacity(canon * 16 + 4096);
178        let (_, alloc99) = run_p50_p99(canon, |_| {
179            std::hint::black_box(a.alloc_aligned(8, 8).len());
180        });
181        let mut p99 = BTreeMap::new();
182        p99.insert("alloc_aligned".to_string(), alloc99);
183        manifest.set_feature("aligned", cat, &p99, &reason);
184    }
185
186    std::fs::create_dir_all(path.parent().unwrap())?;
187    std::fs::write(&path, manifest.to_json())?;
188    io::stdout().write_all(manifest.to_json().as_bytes())?;
189    Ok(())
190}