facet-wrapped/examples/frame_cost.rs
//! Where the CPU goes for ONE wrapped component on a large graph — natively, headless.
//!
//! ```text
//! cargo run --release -p facet-wrapped --example frame_cost -- graphnav 10000 [frames]
//! ```
//!
//! Builds the component from the browser bench's own fixture (`browser_bench::graph_fixture`)
//! through the registry, exactly as the wasm does, then times: the JSON parse, the
//! component build, the first `ui` frame, steady `ui` frames and one `state_json` +
//! serialisation (what `facet_state()` pays). No GPU here, so GPU lanes are not
//! installed: it answers "what does the CPU do per frame", which is the half a GPU
//! cannot take away.
use std::time::Instant;
use facet_wrapped::browser_bench::graph_fixture;
fn ms(t: Instant) -> f64 {
t.elapsed().as_secs_f64() * 1e3
}
fn main() {
let mut a = std::env::args().skip(1);
let component = a.next().unwrap_or_else(|| "graphnav".into());
let nodes: usize = a.next().map_or(10_000, |s| s.parse().expect("nodes"));
let frames: usize = a.next().map_or(10, |s| s.parse().expect("frames"));
let text = graph_fixture(nodes, 7);
if component == "graphnav-breakdown" {
let v: serde_json::Value = serde_json::from_str(&text).expect("fixture is JSON");
graphnav_breakdown(&v);
return;
}
if component == "graphnav-split" {
let v: serde_json::Value = serde_json::from_str(&text).expect("fixture is JSON");
graphnav_split(&v, nodes, frames);
return;
}
let t = Instant::now();
let v: serde_json::Value = serde_json::from_str(&text).expect("fixture is JSON");
let parse = ms(t);
let reg = facet_wrapped::builtins();
let t = Instant::now();
let mut facet = reg.build_with(&component, &v).expect("builds");
let build = ms(t);
let ctx = egui::Context::default();
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0));
let raw = || egui::RawInput {
screen_rect: Some(rect),
..Default::default()
};
let mut frame_ms = Vec::with_capacity(frames);
for _ in 0..frames {
let t = Instant::now();
ctx.run_ui(raw(), |ui| {
egui::CentralPanel::default().show(ui, |ui| facet.ui(ui));
}).textures_delta.clear();
frame_ms.push(ms(t));
}
let t = Instant::now();
let state = serde_json::to_string(&facet.state_json()).unwrap_or_default();
let state_ms = ms(t);
let mut steady = frame_ms[1.min(frame_ms.len() - 1)..].to_vec();
steady.sort_by(|a, b| a.partial_cmp(b).unwrap());
println!(
"{}",
serde_json::json!({
"component": component,
"nodes": nodes,
"fixture_bytes": text.len(),
"parse_ms": parse,
"build_ms": build,
"first_frame_ms": frame_ms[0],
"steady_frame_ms_p50": steady[steady.len() / 2],
"steady_frame_ms_max": steady.last(),
"state_json_ms": state_ms,
"state_bytes": state.len(),
"load": facet_wrapped::browser_bench::load_average(),
})
);
}
/// graphnav's first frame with the LAYOUT taken out of it: in a browser with WebGPU the
/// layout is the GPU job's (`layout_lane: "gpu-job"`), so what the CPU pays on the
/// first frame is everything else. `positions()` runs (and is timed) first, which
/// settles the layout cache; the first `ui` frame after it is the CPU-only remainder.
fn graphnav_split(v: &serde_json::Value, nodes: usize, frames: usize) {
let t = Instant::now();
let p = facet_wrapped::graph::GraphPayload::parse(v).expect("payload");
let parse = ms(t);
let t = Instant::now();
let mut nav = facet_wrapped::graph::graphnav_view(&p);
let build = ms(t);
let t = Instant::now();
let _ = nav.positions();
let layout = ms(t);
let ctx = egui::Context::default();
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0));
let raw = || egui::RawInput {
screen_rect: Some(rect),
..Default::default()
};
let mut frame_ms = Vec::with_capacity(frames.max(2));
for _ in 0..frames.max(2) {
let t = Instant::now();
ctx.run_ui(raw(), |ui| {
egui::CentralPanel::default().show(ui, |ui| facett_core::Facet::ui(&mut nav, ui));
}).textures_delta.clear();
frame_ms.push(ms(t));
}
let t = Instant::now();
let state = serde_json::to_string(&facett_core::Facet::state_json(&nav)).unwrap_or_default();
let state_ms = ms(t);
// The SECOND read: every analytic is cached now, so this is what each later
// `facet_state()` poll (the loader, a robot, the bench settle loop) pays.
let t = Instant::now();
let state2 = serde_json::to_string(&facett_core::Facet::state_json(&nav)).unwrap_or_default();
let state2_ms = ms(t);
let mut steady = frame_ms[1..].to_vec();
steady.sort_by(|a, b| a.partial_cmp(b).unwrap());
println!(
"{}",
serde_json::json!({
"component": "graphnav-split",
"nodes": nodes,
"parse_ms": parse,
"build_ms": build,
"layout_cpu_ms": layout,
"layout_lane": nav.layout_lane(),
"first_frame_after_layout_ms": frame_ms[0],
"steady_frame_ms_p50": steady[steady.len() / 2],
"steady_frame_ms_max": steady.last(),
"state_json_ms": state_ms,
"state_json_again_ms": state2_ms,
"state_bytes": state.len(),
"state_again_bytes": state2.len(),
"load": facet_wrapped::browser_bench::load_average(),
})
);
}
/// graphnav's `state_json` pieces, timed one by one (the whole is seconds at 10k).
#[allow(dead_code)]
fn graphnav_breakdown(v: &serde_json::Value) {
let p = facet_wrapped::graph::GraphPayload::parse(v).expect("payload");
let nav = facet_wrapped::graph::graphnav_view(&p);
let mut out = serde_json::Map::new();
macro_rules! time {
($name:literal, $e:expr) => {{
let t = Instant::now();
let r = $e;
out.insert($name.into(), serde_json::json!(ms(t)));
r
}};
}
let _ = time!("positions", nav.positions());
let _ = time!("positions_again", nav.positions());
let _ = time!("stats", nav.stats());
let _ = time!("stats_again", nav.stats());
let _ = time!("appearance", nav.appearance());
let _ = time!("styling", nav.styling());
let _ = time!("schema", nav.graph.schema());
let _ = time!("search_matches", nav.search_matches());
let _ = time!("cycles_json", nav.cycles_json());
let _ = time!("node_batch", nav.node_batch());
let _ = time!("edge_batch", nav.edge_batch());
let _ = time!("edge_indices", nav.graph.edge_indices());
println!("{}", serde_json::Value::Object(out));
}