Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

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()
}