dwarves-gui/examples/step-console-bench.rs
//! **The step-console bench** (wave 3) — frame cost of the ⚒ console leaf over a
//! 50-step synthetic DAG, and the build GRAPH's frame at gunnar scale after the
//! `BYGG <kant>` work, headless, on the shipped scene.
//!
//! ```text
//! CARGO_TARGET_DIR=… cargo run -p dwarves-gui --release --example step-console-bench
//! ```
//!
//! Prints one JSON object: `{"suite":"dwarves.step_console","results":[…]}` — the
//! same shape the `hammer-graph-bench` arms report (`frame_ms`, `graph_paint_us`,
//! `iters`), so the graph arm compares 1:1 with `dwarves.hammer_graph_frame_gunnar`.
//! It lives in `dwarves-gui/examples` because the console is dwarves-gui's; folding
//! the two arms into `bench/examples/hammer-graph-bench.rs` as registered `Bencher`s
//! is the bench lane's call.
//!
//! `NORNIR_BENCH_PREVIEW=1` shrinks iteration counts, never the scale.
use std::time::Instant;
use facett_core::Facet;
use nornir_build_thing::variant::ArtifactKind;
use nornir_robotui::egui_kittest::Harness;
use serde_json::{json, Value};
use dwarves_gui::build_forge::DeclaredForge;
use dwarves_gui::build_scene::{Act, Axis, AxisPick, BuildScene, Forge, Menu, Query, Report};
use dwarves_gui::build_steps::{Step, StepPlan, StepRole, StepState};
use dwarves_gui::build_thing_scene::{CONSOLE, GRAPH, MENU};
use dwarves_gui::tabbed::LeafPanel;
fn preview() -> bool {
std::env::var_os("NORNIR_BENCH_PREVIEW").is_some_and(|v| !v.is_empty() && v != "0")
}
fn scaled(heavy: u64, light: u64) -> u64 {
if preview() {
light
} else {
heavy
}
}
struct NoForge;
impl Forge for NoForge {
fn axis(&mut self, _pick: &AxisPick) -> Axis {
Axis::Absent
}
fn resolve(&mut self, _q: &Query) -> Result<Menu, String> {
Err("bench".into())
}
fn act(&mut self, _act: Act) {}
fn poll(&mut self) -> Report {
Report::default()
}
}
struct App(BuildScene);
/// 50 steps: 49 donors cycling through the five states, the consumer last.
fn synthetic_dag(n: usize) -> StepPlan {
let mut steps = Vec::with_capacity(n);
for i in 0..n - 1 {
let coordinate = format!("donor{i}/variant{i}");
let mut s = Step::queued(
&coordinate,
StepRole::Donor,
ArtifactKind::Lib,
vec![format!("--build {coordinate}")],
);
s.state = match i % 5 {
0 => StepState::Queued,
1 => StepState::Running {
image: format!("nordisk-cargo-toolchain@sha256:{i:064x}"),
},
2 => StepState::Cached {
key: format!("{i:064x}"),
},
3 => StepState::Done {
sha: format!("{i:064x}"),
fragment: Some(format!("{:064x}", i * 7)),
},
_ => StepState::Refused {
why: format!("steg {i}: ingen [build.toolchain]"),
},
};
s.elapsed_ms = (i as u64) * 1_000;
steps.push(s);
}
steps.push(Step::queued(
"orten-installer",
StepRole::Consumer,
ArtifactKind::Installer,
vec![],
));
StepPlan {
product: Some("orten".into()),
variant: "orten-installer".into(),
edge_path: "kernel".into(),
coordinate: "donor0/variant0".into(),
cli: "dwarves buildthing orten --variant orten-installer --build kernel".into(),
steps,
settings: Default::default(),
params: Vec::new(),
}
}
fn console_frames(n_steps: usize, iters: u64) -> Value {
let scene = BuildScene::new(Box::new(NoForge)).hosted();
let mut h = Harness::builder()
.with_size(nornir_robotui::egui::Vec2::new(1600.0, 1000.0))
.build_ui_state(
|ui, app: &mut App| {
app.0.ui_leaf(CONSOLE, ui);
},
App(scene),
);
h.run();
let plan = synthetic_dag(n_steps);
h.state_mut()
.0
.stage_steps(&serde_json::to_value(&plan).unwrap())
.expect("a plan");
h.run();
let t = Instant::now();
for _ in 0..iters {
h.run();
}
let frame_ms = t.elapsed().as_secs_f64() * 1000.0 / iters as f64;
let steps = h.state().0.steps_json()["steps"]
.as_array()
.map(Vec::len)
.unwrap_or(0);
json!({
"name": "dwarves.step_console_frame_50",
"steps": steps,
"frame_ms": frame_ms,
"iters": iters,
})
}
const PIN: &str = "949d166a1263bd0cf0c5aab44eeb57869615099e7c48bf88d0e4adb77379b999";
/// gunnar's shape — the same staged recipe `hammer-graph-bench`'s gunnar arm uses.
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"]
[[build.variant]]
name = "orten-container"
package = "orten"
role = "server"
default = false
suites = ["ORIGINAL"]
kind = "container"
"#;
fn staged_gunnar() -> 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");
let _ = PIN;
std::fs::write(repo.join(".nornir/build-variants.toml"), GUNNAR_SHAPE).expect("declaration");
dir
}
fn graph_frames(iters: u64) -> Value {
let dir = staged_gunnar();
let scene = BuildScene::new(Box::new(DeclaredForge::at(dir.path().to_path_buf()))).hosted();
let mut h = Harness::builder()
.with_size(nornir_robotui::egui::Vec2::new(1600.0, 1000.0))
.build_ui_state(
|ui, app: &mut App| {
app.0.ui_leaf(GRAPH, ui);
app.0.ui_leaf(MENU, ui);
},
App(scene),
);
h.state_mut().0.set_artifact("orten-installer");
h.state_mut().0.resolve_artifact();
h.run();
h.run();
h.state_mut().0.select_node("orten-installer");
h.run();
let nodes = h.state().0.graph_report()["nodes"].as_u64().unwrap_or(0);
let verbs = Facet::state_json(&h.state().0)["menu"]["build_verbs"]
.as_array()
.map(Vec::len)
.unwrap_or(0);
let t = Instant::now();
for _ in 0..iters {
h.run();
}
let frame_ms = t.elapsed().as_secs_f64() * 1000.0 / iters as f64;
let graph_us = h.state().0.graph_report()["last_frame_us"]
.as_u64()
.unwrap_or(0);
json!({
"name": "dwarves.hammer_graph_frame_gunnar",
"nodes": nodes,
"build_verbs_on_menu": verbs,
"frame_ms": frame_ms,
"graph_paint_us": graph_us,
"iters": iters,
})
}
fn main() {
let iters = scaled(120, 10);
let results = vec![console_frames(50, iters), graph_frames(iters)];
println!(
"{}",
json!({
"suite": "dwarves.step_console",
"machine": std::env::var("HOSTNAME").unwrap_or_default(),
"cores": std::thread::available_parallelism().map(|n| n.get()).unwrap_or(0),
"results": results,
})
);
}