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

nordisk/facett

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