crates/nornir-build-thing/examples/bench_tree.rs
//! **Bench: the settings tree** — measurement, not decoration (edda's bench
//! convention: an example with `Instant`, no harness dependency; numbers go to
//! the bench log by hand).
//!
//! ```text
//! cargo run --release -p nornir-build-thing --example bench_tree [-- <build-variants.toml>]
//! ```
//!
//! Five subjects, the ones the owner asked for (2026-08-26):
//!
//! 1. tree derivation from a declaration (gunnar's, when the checkout is beside
//! edda; else a synthetic installer of gunnar's shape);
//! 2. `get(path)` / `resolve(full)` / `resolve(alias)` on a Kconfig-scale
//! synthetic tree (~20k nodes);
//! 3. `flags_of` + parse round trip over that tree (law C's work);
//! 4. `check_bijective` over a build thing whose installer tree is that large;
//! 5. (wave 3) the edge DAG — `BuildDag::derive` for `--build kernel { … }` over
//! that tree with an in-memory donor, the canonicalisation alone, the grammar alone.
use std::collections::BTreeMap;
use std::time::{Duration, Instant};
use nornir_build_thing::flags::{Flag, flags_of};
use nornir_build_thing::laws::check_bijective;
use nornir_build_thing::options::OptionSpec;
use nornir_build_thing::params::{ParamKind, ParamSpec};
use nornir_build_thing::plugin::{BuildThing, Manifest};
use nornir_build_thing::tree::{SettingsIndex, SettingsTree, ToggleRequest};
use nornir_build_thing::variant::{ArtifactKind, BuildVariantSet};
fn per(d: Duration, n: usize) -> String {
let ns = d.as_nanos() as f64 / n.max(1) as f64;
if ns < 1_000.0 {
format!("{ns:.0} ns")
} else if ns < 1_000_000.0 {
format!("{:.2} µs", ns / 1_000.0)
} else {
format!("{:.2} ms", ns / 1_000_000.0)
}
}
fn line(subject: &str, status: &str, detail: String) {
println!("{subject:<28} {status:<6} {detail}");
}
/// A synthetic installer with `roots` options × `per` conditional params:
/// (roots × (1 + per)) nodes, every param name unique so aliases resolve.
fn synthetic_rows(roots: usize, per: usize) -> (Vec<OptionSpec>, Vec<ParamSpec>) {
let mut options = Vec::with_capacity(roots);
let mut params = Vec::with_capacity(roots * per);
for i in 0..roots {
let mut o = OptionSpec {
name: format!("feat{i}"),
feature: format!("cargo-feat-{i}"),
..Default::default()
};
for j in 0..per {
let name = format!("p{i}_{j}");
params.push(ParamSpec {
name: name.clone(),
kind: ParamKind::Text,
boot_param: format!("korp.{name}"),
..Default::default()
});
if j % 2 == 0 {
o.params_when_on.push(name);
} else {
o.params_when_off.push(name);
}
}
options.push(o);
}
(options, params)
}
struct Large(BuildVariantSet);
impl BuildThing for Large {
fn manifest(&self) -> Manifest {
Manifest { abi: nornir_build_thing::ABI_VERSION, repo: "bench".into(), source_crate: "bench-build-thing".into(), summary: "bench".into() }
}
fn variants(&self) -> BuildVariantSet {
self.0.clone()
}
}
fn main() {
let arg = std::env::args().nth(1);
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let gunnar = arg.map(std::path::PathBuf::from).unwrap_or_else(|| workspace.join("../gunnar/.nornir/build-variants.toml"));
// ── 1. derivation from a declaration ────────────────────────────────────
let (set, label) = match std::fs::read_to_string(&gunnar).ok().and_then(|t| BuildVariantSet::from_toml_str(&t).ok()) {
Some(set) => (set, format!("gunnar ({})", gunnar.display())),
None => {
let (options, params) = synthetic_rows(4, 3);
let mut set = BuildVariantSet::default();
set.installers.push(nornir_build_thing::InstallerBuild { name: "synthetic-installer".into(), options, params, ..Default::default() });
(set, "synthetic (gunnar not beside edda)".to_string())
}
};
let n = 2_000;
let t0 = Instant::now();
let mut nodes = 0;
for _ in 0..n {
nodes = SettingsTree::forest(&set).iter().map(|t| t.len()).sum::<usize>();
}
line("derive forest", "ok", format!("{label}: {} variants, {nodes} nodes — {}/derivation (×{n})", SettingsTree::forest(&set).len(), per(t0.elapsed(), n)));
if let Some(inst) = set.installers.first() {
let t0 = Instant::now();
for _ in 0..n {
std::hint::black_box(SettingsTree::of_installer(inst, None));
}
line("derive installer", "ok", format!("'{}': {} nodes — {}/derivation (×{n})", inst.name, SettingsTree::of_installer(inst, None).len(), per(t0.elapsed(), n)));
}
// ── 2. lookups on a Kconfig-scale tree ──────────────────────────────────
let (options, params) = synthetic_rows(2_000, 9);
let t0 = Instant::now();
let big = SettingsTree::of_rows("big", ArtifactKind::Installer, &options, ¶ms, &BTreeMap::new());
let derive_big = t0.elapsed();
let n_nodes = big.len();
line("derive 20k", "ok", format!("{n_nodes} nodes from {} options × 9 params — {}", options.len(), per(derive_big, 1)));
let n = 20_000;
let paths: Vec<String> = big.walk().iter().map(|n| n.path.clone()).collect();
let t0 = Instant::now();
for i in 0..n {
std::hint::black_box(big.get(&paths[(i * 7919) % paths.len()]));
}
line("get(path)", "ok", format!("{n_nodes} nodes — {}/call (×{n}, linear scan of roots, then children)", per(t0.elapsed(), n)));
let t0 = Instant::now();
for i in 0..n {
std::hint::black_box(big.resolve(&paths[(i * 7919) % paths.len()]).ok());
}
line("resolve(full path)", "ok", format!("{}/call (×{n})", per(t0.elapsed(), n)));
let names: Vec<String> = big.walk().iter().map(|n| n.name().to_string()).collect();
let t0 = Instant::now();
for i in 0..n {
std::hint::black_box(big.resolve(&names[(i * 7919) % names.len()]).ok());
}
line("resolve(alias)", "ok", format!("{}/call (×{n}, one full visit per alias — no index)", per(t0.elapsed(), n)));
let t0 = Instant::now();
let index = SettingsIndex::of(&big);
let built = t0.elapsed();
let t0 = Instant::now();
for i in 0..n {
std::hint::black_box(index.resolve(&names[(i * 7919) % names.len()]).ok());
}
line("index.resolve(alias)", "ok", format!("build {} — {}/call (×{n})", per(built, 1), per(t0.elapsed(), n)));
let t0 = Instant::now();
for i in 0..n {
std::hint::black_box(big.try_toggle(&ToggleRequest::new("big", &paths[(i * 7919) % paths.len()], i % 2 == 0)));
}
line("try_toggle native", "ok", format!("{}/call (×{n}, plain features incl. refusals of settings)", per(t0.elapsed(), n)));
// ── 3. flags over the tree ──────────────────────────────────────────────
let t0 = Instant::now();
let flags = flags_of(&big);
let render = t0.elapsed();
let t0 = Instant::now();
let mut ok = 0usize;
for f in &flags {
if Flag::parse(&f.render()).ok().as_ref() == Some(f) {
ok += 1;
}
}
line("flags render+parse", "ok", format!("{} flags: render {} — parse round trip {}/flag ({ok} ok)", flags.len(), per(render, 1), per(t0.elapsed(), flags.len())));
// ── 4. the laws over a large tree ───────────────────────────────────────
let mut large = BuildVariantSet::default();
large.installers.push(nornir_build_thing::InstallerBuild { name: "big".into(), options, params, ..Default::default() });
let thing = Large(large);
let t0 = Instant::now();
let problems = check_bijective(&thing);
line("check_bijective 20k", if problems.is_empty() { "ok" } else { "RED" }, format!("{n_nodes}-node installer — {} (problems: {})", per(t0.elapsed(), 1), problems.len()));
{
use nornir_build_thing::laws::*;
let set = thing.variants();
let t0 = Instant::now();
let a = law_declaration_struct(&set).len();
let ta = t0.elapsed();
let t0 = Instant::now();
let a2 = law_rows_tree(&set).len();
let ta2 = t0.elapsed();
let t0 = Instant::now();
let b = law_tree_declaration(&thing).len();
let tb = t0.elapsed();
let t0 = Instant::now();
let c = law_path_flag(&thing).len();
let tc = t0.elapsed();
line(" per law 20k", "ok", format!("A toml {} ({a}) · A rows↔tree {} ({a2}) · B tree {} ({b}) · C paths {} ({c})", per(ta, 1), per(ta2, 1), per(tb, 1), per(tc, 1)));
}
let t0 = Instant::now();
let problems = check_bijective(&Large(set.clone()));
line("check_bijective decl", if problems.is_empty() { "ok" } else { "RED" }, format!("{label} — {} (problems: {})", per(t0.elapsed(), 1), problems.len()));
for p in problems.iter().take(3) {
println!(" {p}");
}
// ── 5. the edge DAG over the 20k-node tree (wave 3, gap #4) ─────────────
// The 20k installer gets a kernel edge to an in-memory donor; the DAG for
// `--build kernel { --opt unix=off }` is derived n times. The number is
// the WHOLE derivation (tree derivation + canonicalisation + walk), then
// the grammar alone.
{
use nornir_build_thing::build_dag::{BuildDag, BuildLine, Coordinate, Declarations, InMemoryDeclarations};
use nornir_build_thing::installer::KernelDonor;
let mut consumer = BuildVariantSet::default();
let (options, params) = synthetic_rows(2_000, 9);
consumer.installers.push(nornir_build_thing::InstallerBuild {
name: "big".into(),
options,
params,
kernel_donor: Some(KernelDonor { repo: "donor".into(), kernel: "big-kernel".into() }),
..Default::default()
});
let mut donor = BuildVariantSet::default();
donor.kernels.push(nornir_build_thing::KernelBuild {
name: "big-kernel".into(),
version: "6.12.104".into(),
tarball_sha256: "0".repeat(64),
..Default::default()
});
let decls = InMemoryDeclarations::default().with("big-repo", consumer).with("donor", donor);
let target = Coordinate::new("big-repo", "big");
let bl = BuildLine::parse("--build kernel { --opt unix=off --opt preset=nuked } --opt feat7=off --param p3_1=x").unwrap();
let n = 50;
let t0 = Instant::now();
let mut steps = 0;
for _ in 0..n {
steps = BuildDag::derive(&target, &bl, &decls).expect("derives").steps.len();
}
let whole = t0.elapsed();
let tree = SettingsTree::of_set(decls.declaration("big-repo").unwrap().as_ref(), "big").unwrap();
let t0 = Instant::now();
for _ in 0..n {
std::hint::black_box(bl.canonical_in(&tree).expect("canonical"));
}
let canon = t0.elapsed();
let n_parse = 20_000;
let scoped = bl.render_scoped();
let t0 = Instant::now();
for _ in 0..n_parse {
std::hint::black_box(BuildLine::parse(&scoped).expect("parses"));
}
line("dag derive 20k", "ok", format!("{steps} steps over a {}-node consumer — {}/derivation (×{n}; incl. tree derivation)", tree.len(), per(whole, n)));
line(" canonical_in 20k", "ok", format!("{}/line (×{n}; alias resolve per flag over the tree)", per(canon, n)));
line(" grammar parse", "ok", format!("'{scoped}' — {}/parse (×{n_parse})", per(t0.elapsed(), n_parse)));
}
}