Skip to main content

perf_features/
perf_features.rs

1//! Feature classification bench. For each opt-in feature the harness benches its
2//! representative op across a size sweep, lets `subms` DECIDE the latency
3//! category (hot-path / structural / auxiliary), and merge-writes the decision +
4//! measured `p99ByStage` into the recipe's per-language manifest
5//! `.subms/features/rust.json` - preserving any other fields already there.
6//!
7//! Run:
8//!   cargo run --release --example perf_features \
9//!       --features "harness counting scalable partitioned serde"
10
11use std::collections::BTreeMap;
12use std::io::{self, Write};
13use std::path::PathBuf;
14
15use subms::{SubMsFeatureManifest, SubMsP99Source, SubMsPerfHarness, classify_feature, summarize};
16#[allow(unused_imports)]
17use subms_bloom_filter::BloomFilter;
18
19// Three sizes so the classifier can read a p99-vs-N slope (flat -> hot-path,
20// growing -> structural). Bloom's probes are O(k) in the hash count, not the set
21// size, so they read flat -> hot-path.
22const SIZES: [usize; 3] = [4_096, 32_768, 262_144];
23
24fn keys() -> Vec<String> {
25    (0..SIZES[SIZES.len() - 1])
26        .map(|i| format!("key-{i}"))
27        .collect()
28}
29
30/// p99 (ns) of `probe` over `size` keys against a filter built by `build`.
31fn probe_p99<T>(
32    size: usize,
33    build: impl Fn() -> T,
34    probe: impl Fn(&T, &str),
35    ks: &[String],
36) -> u64 {
37    let f = build();
38    let mut h = SubMsPerfHarness::new("bloom-feature", "rust");
39    let st = h.stage("probe", size);
40    for k in &ks[..size] {
41        st.time(|| probe(&f, k));
42    }
43    summarize(&h)
44        .stages
45        .iter()
46        .find(|s| s.name == "probe")
47        .map_or(0, |s| s.p99_ns)
48}
49
50/// p99 (ns) of a mutating `op` over `size` keys against a fresh filter.
51fn mutate_p99<T>(
52    size: usize,
53    mut make: impl FnMut() -> T,
54    mut op: impl FnMut(&mut T, &str),
55    ks: &[String],
56) -> u64 {
57    let mut f = make();
58    let mut h = SubMsPerfHarness::new("bloom-feature", "rust");
59    let st = h.stage("op", size);
60    for k in &ks[..size] {
61        st.time(|| op(&mut f, k));
62    }
63    summarize(&h)
64        .stages
65        .iter()
66        .find(|s| s.name == "op")
67        .map_or(0, |s| s.p99_ns)
68}
69
70fn main() -> io::Result<()> {
71    let ks = keys();
72    let canon = SIZES[SIZES.len() - 1];
73
74    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
75        .join("..")
76        .join(".subms")
77        .join("features")
78        .join("rust.json");
79    let existing = std::fs::read_to_string(&path).unwrap_or_default();
80    let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
81    // Stamp the box these numbers came from. The bench runs wherever it is
82    // invoked, so an unstamped manifest is indistinguishable from a fleet
83    // capture; the renderer will not publish one it cannot attribute.
84    let (source, instance) = SubMsP99Source::from_env();
85    manifest.set_p99_source(source, instance.as_deref());
86
87    // ---------- counting: adds remove() over 4-bit counters ----------
88    #[cfg(feature = "counting")]
89    {
90        use subms_bloom_filter::CountingBloomFilter;
91        let fill = |n: usize| {
92            let mut c = CountingBloomFilter::new(n);
93            for k in &ks[..n] {
94                c.add(k);
95            }
96            c
97        };
98        let sweep: Vec<(usize, u64)> = SIZES
99            .iter()
100            .map(|&n| {
101                (
102                    n,
103                    probe_p99(n, || fill(n), |c, k| _ = c.might_contain(k), &ks),
104                )
105            })
106            .collect();
107        let (cat, reason) = classify_feature(&sweep, None, None);
108        let mut p99 = BTreeMap::new();
109        p99.insert("contains".to_string(), sweep.last().unwrap().1);
110        p99.insert(
111            "add".to_string(),
112            mutate_p99(
113                canon,
114                || CountingBloomFilter::new(canon),
115                |c, k| c.add(k),
116                &ks,
117            ),
118        );
119        p99.insert(
120            "remove".to_string(),
121            mutate_p99(canon, || fill(canon), |c, k| c.remove(k), &ks),
122        );
123        manifest.set_feature("counting", cat, &p99, &reason);
124    }
125
126    // ---------- scalable: layered add, hit walks layers ----------
127    #[cfg(feature = "scalable")]
128    {
129        use subms_bloom_filter::ScalableBloomFilter;
130        let fill = |n: usize| {
131            let mut s = ScalableBloomFilter::new(1_000);
132            for k in &ks[..n] {
133                s.add(k);
134            }
135            s
136        };
137        let sweep: Vec<(usize, u64)> = SIZES
138            .iter()
139            .map(|&n| {
140                (
141                    n,
142                    probe_p99(n, || fill(n), |s, k| _ = s.might_contain(k), &ks),
143                )
144            })
145            .collect();
146        let (cat, reason) = classify_feature(&sweep, None, None);
147        let mut p99 = BTreeMap::new();
148        p99.insert("contains".to_string(), sweep.last().unwrap().1);
149        p99.insert(
150            "add".to_string(),
151            mutate_p99(
152                canon,
153                || ScalableBloomFilter::new(1_000),
154                |s, k| s.add(k),
155                &ks,
156            ),
157        );
158        manifest.set_feature("scalable", cat, &p99, &reason);
159    }
160
161    // ---------- partitioned: k independent slices ----------
162    #[cfg(feature = "partitioned")]
163    {
164        use subms_bloom_filter::PartitionedBloomFilter;
165        let fill = |n: usize| {
166            let mut p = PartitionedBloomFilter::new(n);
167            for k in &ks[..n] {
168                p.add(k);
169            }
170            p
171        };
172        let sweep: Vec<(usize, u64)> = SIZES
173            .iter()
174            .map(|&n| {
175                (
176                    n,
177                    probe_p99(n, || fill(n), |p, k| _ = p.might_contain(k), &ks),
178                )
179            })
180            .collect();
181        let (cat, reason) = classify_feature(&sweep, None, None);
182        let mut p99 = BTreeMap::new();
183        p99.insert("contains".to_string(), sweep.last().unwrap().1);
184        p99.insert(
185            "add".to_string(),
186            mutate_p99(
187                canon,
188                || PartitionedBloomFilter::new(canon),
189                |p, k| p.add(k),
190                &ks,
191            ),
192        );
193        manifest.set_feature("partitioned", cat, &p99, &reason);
194    }
195
196    // ---------- serde: derive only, no hot-path workload -> auxiliary ----------
197    #[cfg(feature = "serde")]
198    {
199        let (cat, reason) = classify_feature(&[], None, None);
200        manifest.set_feature("serde", cat, &BTreeMap::new(), &reason);
201    }
202
203    std::fs::create_dir_all(path.parent().unwrap())?;
204    std::fs::write(&path, manifest.to_json())?;
205    io::stdout().write_all(manifest.to_json().as_bytes())?;
206    Ok(())
207}