crates/autonom/src/bench.rs
//! **Bench the scan hot path** (feature `bench`) — parse a sizeable Rust source
//! blob, build the Function surface, and compute the coverage gap, which is the
//! exact inner loop [`crate::report::NeedsTestsReport::from_scan`] runs for every
//! file in a repo. Its throughput (functions classified per second) is the number
//! worth guarding as the feeder scales to constellation-sized trees.
//!
//! Registered with the shared `nornir-bench` harness via [`register_bench!`]; the
//! harness stamps host cores + peak memory telemetry onto the row automatically.
//! OFF by default — a release build carries no `nornir-bench` dependency.
use nornir_bench::api::Bencher;
use nornir_bench::BenchResult;
use crate::feeder::Scanner;
use crate::report::NeedsTestsReport;
use crate::Allowlist;
/// The scan-and-gap bencher: generate a deterministic module of `n_fns`
/// functions (a realistic mix — a test root, a covered chain, and orphans), then
/// scan + gap it `reps` times. The generated source is identical every run (no
/// clock, no rng), so the fold is stable and comparable across hosts.
pub struct ScanFold {
source: String,
n_fns: usize,
reps: usize,
}
impl Default for ScanFold {
fn default() -> Self {
// PREVIEW (`NORNIR_BENCH_PREVIEW=1`): the same syn parse → Function-surface fold,
// emitting the same metric keys, over 1/10 the functions folded 1/6 as often —
// the fast verification pass. A preview run is stamped `<host>-preview` by the
// harness, so its number can never be mistaken for the published one.
if nornir_bench::api::preview_run() {
Self::with_size(60, 5)
} else {
Self::with_size(600, 30)
}
}
}
impl ScanFold {
/// A bencher over a generated module of `n_fns` functions, scanned `reps`
/// times per `run()`.
pub fn with_size(n_fns: usize, reps: usize) -> Self {
Self { source: generate_source(n_fns), n_fns, reps }
}
}
/// Build a deterministic Rust module with `n` functions in a realistic mix: the
/// first ~2/3 form ONE long covered chain (`f{i}` calls `f{i-1}`, `f0` is the
/// base) that a single test at the head reaches — so the reachability BFS
/// traverses a deep chain, the representative work — and the remaining ~1/3 are
/// orphans nobody calls (the non-trivial gap). Exercises the parser, module-walk,
/// call resolution, and the deep BFS.
fn generate_source(n: usize) -> String {
let chain = (n * 2 / 3).max(1);
let mut s = String::with_capacity(n * 48);
// The covered chain f0..f{chain-1}.
s.push_str("pub fn f0() { let _ = 0; }\n");
for i in 1..chain {
s.push_str(&format!("pub fn f{i}() {{ f{}(); }}\n", i - 1));
}
// The orphans (no caller) → the gap.
for i in chain..n {
s.push_str(&format!("pub fn orphan_{i}() {{ let _ = {i}; }}\n"));
}
// One test at the head of the chain → reaches the whole f-chain.
s.push_str(&format!(
"#[cfg(test)] mod tests {{ #[test] fn drives() {{ crate::f{}(); }} }}\n",
chain - 1,
));
s
}
impl Bencher for ScanFold {
fn id(&self) -> &'static str {
"autonom.function_scan"
}
fn run(&self) -> anyhow::Result<BenchResult> {
let t0 = std::time::Instant::now();
let mut classified = 0usize;
let mut last_gap = 0usize;
for _ in 0..self.reps {
let mut sc = Scanner::new();
sc.add_source(&self.source, "bench", false);
let scan = sc.finish();
let report = NeedsTestsReport::from_scan("bench", &scan, &Allowlist::new());
classified += report.total_functions;
last_gap = report.gap_count();
}
let secs = t0.elapsed().as_secs_f64().max(1e-9);
let mut m = serde_json::Map::new();
m.insert("functions".into(), serde_json::json!(self.n_fns as f64));
m.insert("reps".into(), serde_json::json!(self.reps as f64));
m.insert("gap".into(), serde_json::json!(last_gap as f64));
m.insert("fns_per_sec".into(), serde_json::json!(classified as f64 / secs));
Ok(BenchResult::measured(self.id(), m))
}
}
// Register with the shared harness so `nornir bench` / the example entrypoint
// picks up the scan path alongside every other nordisk bencher.
nornir_bench::register_bench!(ScanFold::default());
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scan_fold_bencher_runs_and_reports_throughput() {
let b = ScanFold::with_size(30, 2);
assert_eq!(b.id(), "autonom.function_scan");
let r = b.run().expect("bench runs");
let per_sec = r.metrics.get("fns_per_sec").and_then(|v| v.as_f64()).unwrap();
let gap = r.metrics.get("gap").and_then(|v| v.as_f64()).unwrap();
assert!(per_sec > 0.0, "reports positive throughput");
assert!(gap > 0.0, "the generated module has real untested orphans to find");
}
#[test]
fn generated_source_parses_and_has_a_gap() {
let src = generate_source(30);
let mut sc = Scanner::new();
sc.add_source(&src, "bench", false);
let scan = sc.finish();
assert_eq!(scan.files_skipped, 0, "generated source must parse");
let report = NeedsTestsReport::from_scan("bench", &scan, &Allowlist::new());
assert!(report.gap_count() > 0, "orphans are the gap");
assert!(report.covered > 0, "the f-chain is covered by the head test");
}
}