bench/examples/hammer-graph-bench.rs
//! **The ⚒ build-graph benches** — frame cost of the settings graph, and the wasm
//! load+call road (Rickard, 2026-08-26).
//!
//! Arms, every one a registered [`Bencher`] (`nornir_bench::api`), run through
//! `cargo run --release --example hammer-graph-bench` like `nornir-bench`:
//!
//! | arm | what is timed |
//! |---|---|
//! | `dwarves.hammer_graph_frame_gunnar` | one frame of the shipped ⚒ scene over a staged recipe of gunnar's shape (~10 variant/feature nodes) |
//! | `dwarves.hammer_graph_frame_kconfig` | one frame with the `kärna` subgraph UNFOLDED (~80 symbols today) |
//! | `dwarves.hammer_graph_frame_20k` | one frame of the ONE graph pane over a synthetic 20 000-node spec |
//! | `dwarves.hammer_graph_maximise_restore` | facett-scene maximise + restore of the graph leaf, rendered |
//! | `dwarves.hammer_graph_fold_unfold` | fold + unfold of the `kärna` subgraph, rendered |
//! | `dwarves.wasm_load_call_cold` | `PluginHandle::load_with` (wasmtime compile) + `variants()` + `menu()` per module load |
//! | `dwarves.wasm_load_call_warm` | `variants()` + `menu()` on a loaded module |
//!
//! `options("kernel")` is not on the wasm ABI yet (edda stage 2); the warm arm names
//! the calls it measures so the arm grows rather than lies when it lands.
//!
//! PREVIEW (`NORNIR_BENCH_PREVIEW=1`) shrinks iteration counts, never the scale.
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::time::Instant;
use anyhow::{anyhow, Result};
use facett_core::Facet;
use nornir::bench::api::Bencher;
use nornir::bench::BenchResult;
use nornir_robotui::egui_kittest::Harness;
use dwarves_gui::build_forge::DeclaredForge;
use dwarves_gui::build_scene::BuildScene;
use dwarves_gui::build_thing_scene::{GRAPH, MENU, TAB};
use dwarves_gui::dag_graph::{DagNode, DagPane, DagSpec};
use dwarves_gui::tabbed::{dwarves_scene, BODY};
use dwarves_gui::{tab_keys, tab_labels};
fn preview() -> bool {
nornir::bench::api::preview_run()
}
fn scaled(heavy: u64, light: u64) -> u64 {
if preview() { light } else { heavy }
}
fn metrics(pairs: &[(&str, serde_json::Value)]) -> serde_json::Map<String, serde_json::Value> {
pairs.iter().map(|(k, v)| ((*k).to_string(), v.clone())).collect()
}
const PIN: &str = "949d166a1263bd0cf0c5aab44eeb57869615099e7c48bf88d0e4adb77379b999";
/// gunnar's shape: a server, an installer with a donor kernel edge, two options with
/// governed settings, a container — ~10 nodes cold.
const GUNNAR_SHAPE: &str = r#"
[build]
base_suite = "ORIGINAL"
[[build.variant]]
name = "orten-server"
package = "orten"
bin = "orten"
role = "server"
default = true
suites = ["ORIGINAL"]
kind = "exe"
[[build.installer]]
name = "orten-installer"
variant = "orten-server"
kernel_donor = { repo = "tunnr", kernel = "tunnr-kernel" }
[[build.installer.params]]
name = "site"
kind = "text"
required = true
[[build.installer.params]]
name = "static_ip"
kind = "text"
[[build.installer.params]]
name = "hub_conf"
kind = "text"
[[build.installer.options]]
name = "dhcp"
label = "DHCP"
feature = "dhcp"
params_when_off = ["static_ip"]
[[build.installer.options]]
name = "wireguard"
label = "WireGuard"
feature = "wireguard"
params_when_on = ["hub_conf"]
"#;
/// tunnr's shape: an own kernel — the kconfig subgraph.
fn kernel_shape() -> String {
format!(
r#"
[build]
base_suite = "ORIGINAL"
[[build.variant]]
name = "orten-kernel"
role = "server"
default = false
suites = ["ORIGINAL"]
kind = "kernel"
[[build.kernel]]
name = "orten-kernel"
version = "6.12.1"
preset = "full"
sha256 = "{PIN}"
"#
)
}
fn staged(decl: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
let repo = dir.path().join("orten");
std::fs::create_dir_all(repo.join(".nornir")).expect("mkdir");
std::fs::write(repo.join("Cargo.toml"), "[package]\nname = \"orten\"\nversion = \"1.0.0\"\nedition = \"2021\"\n\n[dependencies]\n").expect("manifest");
std::fs::write(repo.join("Cargo.lock"), "version = 4\n\n[[package]]\nname = \"orten\"\nversion = \"1.0.0\"\n").expect("lock");
std::fs::write(repo.join(".nornir/build-variants.toml"), decl).expect("declaration");
dir
}
struct App(BuildScene);
fn scene_over(root: &std::path::Path) -> Harness<'static, App> {
let scene = BuildScene::new(Box::new(DeclaredForge::at(root.to_path_buf()))).hosted();
Harness::builder()
.with_size(nornir_robotui::egui::Vec2::new(1600.0, 1000.0))
.build_ui_state(
|ui, app: &mut App| {
use dwarves_gui::tabbed::LeafPanel;
app.0.ui_leaf(GRAPH, ui);
app.0.ui_leaf(MENU, ui);
},
App(scene),
)
}
fn resolve(h: &mut Harness<'static, App>, name: &str) {
h.state_mut().0.set_artifact(name);
h.state_mut().0.resolve_artifact();
h.run();
h.run();
}
/// Frames per second over `iters` frames, and the graph pane's own last-frame cost.
fn frames(h: &mut Harness<'static, App>, iters: u64) -> (f64, u64) {
let t = Instant::now();
for _ in 0..iters {
h.run();
}
let s = t.elapsed().as_secs_f64();
let last = h.state().0.graph_report()["last_frame_us"].as_u64().unwrap_or(0);
(s * 1000.0 / iters as f64, last)
}
struct GunnarFrame;
impl Bencher for GunnarFrame {
fn id(&self) -> &'static str {
"dwarves.hammer_graph_frame_gunnar"
}
fn run(&self) -> Result<BenchResult> {
let dir = staged(GUNNAR_SHAPE);
let mut h = scene_over(dir.path());
resolve(&mut h, "orten-installer");
h.state_mut().0.select_node("orten-installer");
h.run();
let nodes = h.state().0.graph_report()["nodes"].as_u64().unwrap_or(0);
let iters = scaled(120, 10);
let (frame_ms, graph_us) = frames(&mut h, iters);
Ok(BenchResult::measured(
self.id(),
metrics(&[
("nodes", nodes.into()),
("frame_ms", frame_ms.into()),
("graph_paint_us", graph_us.into()),
("iters", iters.into()),
]),
))
}
}
struct KconfigFrame;
impl Bencher for KconfigFrame {
fn id(&self) -> &'static str {
"dwarves.hammer_graph_frame_kconfig"
}
fn run(&self) -> Result<BenchResult> {
let decl = kernel_shape();
let dir = staged(&decl);
let mut h = scene_over(dir.path());
resolve(&mut h, "orten-kernel");
// Unfold the kärna subgraph: every symbol on screen.
h.state_mut().0.select_node("orten-kernel/kernel");
h.run();
let report = h.state().0.graph_report();
let nodes = report["nodes"].as_u64().unwrap_or(0);
let visible = Facet::state_json(&h.state().0)["graph"]["visible"].as_u64().unwrap_or(0);
let iters = scaled(120, 10);
let (frame_ms, graph_us) = frames(&mut h, iters);
Ok(BenchResult::measured(
self.id(),
metrics(&[
("nodes", nodes.into()),
("visible", visible.into()),
("frame_ms", frame_ms.into()),
("graph_paint_us", graph_us.into()),
("iters", iters.into()),
]),
))
}
}
struct Synthetic20k;
impl Bencher for Synthetic20k {
fn id(&self) -> &'static str {
"dwarves.hammer_graph_frame_20k"
}
fn run(&self) -> Result<BenchResult> {
let n = 20_000usize;
let mut spec = DagSpec::default();
for i in 0..n {
spec.nodes.push(DagNode {
id: format!("k{i}"),
label: format!("CONFIG_SYM_{i}"),
fill: facett_graphview::Color::rgb(50, 130, 90),
ring: None,
badge: None,
});
if i > 0 {
// A name hierarchy: each symbol requires one earlier symbol.
spec.edges.push((format!("k{}", i / 4), format!("k{i}")));
}
}
let t = Instant::now();
let mut pane = DagPane::new("bench");
pane.set_spec(&spec);
let lower_ms = t.elapsed().as_secs_f64() * 1000.0;
let mut h = Harness::builder()
.with_size(nornir_robotui::egui::Vec2::new(1600.0, 1000.0))
.build_ui_state(|ui, p: &mut DagPane| p.ui(ui), pane);
h.run();
let iters = scaled(30, 3);
let t = Instant::now();
for _ in 0..iters {
h.run();
}
let frame_ms = t.elapsed().as_secs_f64() * 1000.0 / iters as f64;
let last = h.state().last_frame_us().unwrap_or(0);
Ok(BenchResult::measured(
self.id(),
metrics(&[
("nodes", (n as u64).into()),
("lower_ms", lower_ms.into()),
("frame_ms", frame_ms.into()),
("graph_paint_us", (last as u64).into()),
("iters", iters.into()),
]),
))
}
}
struct MaximiseRestore;
impl Bencher for MaximiseRestore {
fn id(&self) -> &'static str {
"dwarves.hammer_graph_maximise_restore"
}
fn run(&self) -> Result<BenchResult> {
use facett_app::scene::{self, NavState, SceneHost};
use std::cell::RefCell;
use std::rc::Rc;
struct Host;
impl SceneHost for Host {
fn slot(&mut self, id: &str, ui: &mut nornir_robotui::egui::Ui) -> serde_json::Value {
ui.label(id);
serde_json::json!({ "slot": id })
}
}
let sc = dwarves_scene(&tab_labels());
let idx = tab_keys().iter().position(|k| k == TAB).ok_or_else(|| anyhow!("⚒ is a shipped tab"))?;
let nav = Rc::new(RefCell::new(NavState::new(&sc)));
nav.borrow_mut().set_active_tab(BODY, idx);
let n = nav.clone();
let mut h = Harness::builder().with_size(nornir_robotui::egui::Vec2::new(1400.0, 900.0)).build_ui(move |ui| {
let _ = scene::render(&sc, &mut n.borrow_mut(), ui, &mut Host);
});
h.run();
let iters = scaled(200, 10);
let t = Instant::now();
for _ in 0..iters {
nav.borrow_mut().set_maximized(GRAPH);
h.run();
nav.borrow_mut().toggle_maximize(GRAPH);
h.run();
}
let cycle_ms = t.elapsed().as_secs_f64() * 1000.0 / iters as f64;
Ok(BenchResult::measured(self.id(), metrics(&[("cycle_ms", cycle_ms.into()), ("iters", iters.into())])))
}
}
struct FoldUnfold;
impl Bencher for FoldUnfold {
fn id(&self) -> &'static str {
"dwarves.hammer_graph_fold_unfold"
}
fn run(&self) -> Result<BenchResult> {
let decl = kernel_shape();
let dir = staged(&decl);
let mut h = scene_over(dir.path());
resolve(&mut h, "orten-kernel");
let iters = scaled(100, 5);
let t = Instant::now();
for _ in 0..iters {
h.state_mut().0.select_node("orten-kernel/kernel");
h.run();
h.state_mut().0.fold_node("orten-kernel/kernel");
h.run();
}
let cycle_ms = t.elapsed().as_secs_f64() * 1000.0 / iters as f64;
Ok(BenchResult::measured(self.id(), metrics(&[("cycle_ms", cycle_ms.into()), ("iters", iters.into())])))
}
}
/// A built build-thing module to load: `$DWARVES_BENCH_WASM`, else the newest module
/// under the wasm cache the code road writes, else holger's buildspec cdylib built
/// beside it. Named in the metrics so a number is never about an unknown file.
fn some_wasm() -> Result<PathBuf> {
if let Some(p) = std::env::var_os("DWARVES_BENCH_WASM") {
return Ok(PathBuf::from(p));
}
let target = std::env::var_os("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|| PathBuf::from("target"));
let mut found: Vec<PathBuf> = Vec::new();
fn walk(d: &std::path::Path, out: &mut Vec<PathBuf>, depth: usize) {
if depth > 5 {
return;
}
if let Ok(rd) = std::fs::read_dir(d) {
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
walk(&p, out, depth + 1);
} else if p.extension().is_some_and(|x| x == "wasm") {
out.push(p);
}
}
}
}
walk(&target.join("dwarves-build-thing-wasm"), &mut found, 0);
found.sort_by_key(|p| std::fs::metadata(p).and_then(|m| m.modified()).ok());
found.pop().ok_or_else(|| anyhow!("no build-thing wasm under {}; set DWARVES_BENCH_WASM", target.display()))
}
struct WasmCold;
impl Bencher for WasmCold {
fn id(&self) -> &'static str {
"dwarves.wasm_load_call_cold"
}
fn run(&self) -> Result<BenchResult> {
use nornir_build_thing::plugin_host::{HostFacts, Limits, PluginHandle};
let path = some_wasm()?;
let bytes = std::fs::read(&path)?;
let iters = scaled(10, 2);
let t = Instant::now();
let mut variants = 0usize;
for _ in 0..iters {
let mut h = PluginHandle::load_with(&bytes, Limits::default(), HostFacts::at_rev("bench", false))?;
variants = h.variants()?.declared_count();
let _ = h.menu()?;
}
let ms = t.elapsed().as_secs_f64() * 1000.0 / iters as f64;
Ok(BenchResult::measured(
self.id(),
metrics(&[
("module", path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default().into()),
("bytes", (bytes.len() as u64).into()),
("variants", (variants as u64).into()),
("load_variants_menu_ms", ms.into()),
("calls", "load+variants()+menu()".into()),
("iters", iters.into()),
]),
))
}
}
struct WasmWarm;
impl Bencher for WasmWarm {
fn id(&self) -> &'static str {
"dwarves.wasm_load_call_warm"
}
fn run(&self) -> Result<BenchResult> {
use nornir_build_thing::plugin_host::{HostFacts, Limits, PluginHandle};
let path = some_wasm()?;
let bytes = std::fs::read(&path)?;
let mut h = PluginHandle::load_with(&bytes, Limits::default(), HostFacts::at_rev("bench", false))?;
let _ = h.variants()?;
let iters = scaled(500, 20);
let t = Instant::now();
for _ in 0..iters {
let _ = h.variants()?;
let _ = h.menu()?;
}
let us = t.elapsed().as_secs_f64() * 1e6 / iters as f64;
Ok(BenchResult::measured(
self.id(),
metrics(&[
("module", path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default().into()),
("variants_menu_us", us.into()),
("calls", "variants()+menu() — options(\"kernel\") joins when edda stage 2 lands".into()),
("iters", iters.into()),
]),
))
}
}
nornir::register_bench!(GunnarFrame);
nornir::register_bench!(KconfigFrame);
nornir::register_bench!(Synthetic20k);
nornir::register_bench!(MaximiseRestore);
nornir::register_bench!(FoldUnfold);
nornir::register_bench!(WasmCold);
nornir::register_bench!(WasmWarm);
fn main() -> Result<()> {
let _ = BTreeMap::<String, String>::new();
nornir::bench::api::run_main_json()
}