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

nordisk/facett

dev/flow-bench/src/main.rs

//! Development-only timing harness. Import the real demo fixture source and
//! re-export its actual dependency types; no copied synthetic graph algorithm.
extern crate self as facett;
pub use facett_core as core;
pub use facett_core::Facet;
pub use facett_pipeline as pipeline;
#[allow(dead_code)]
#[path = "../../../facett-demo/src/synthetic_sdp.rs"]
mod synthetic_sdp;
use facett_core::Elm;
use serde_json::{Value, json};
use std::{path::PathBuf, time::Instant};
const CASES: &[&str] = &[
    "synthetic-sdp100",
    "pipeline10000",
    "plandag1000",
    "brokkrview500",
    "layout100k",
    "layout1m",
];
const SIZE: [f32; 2] = [1200.0, 800.0];

fn percentile(mut values: Vec<f64>, p: f64) -> Option<f64> {
    if values.is_empty() {
        return None;
    }
    values.sort_by(f64::total_cmp);
    Some(values[((values.len() - 1) as f64 * p).round() as usize])
}
enum Host {
    Synthetic(synthetic_sdp::SyntheticSdpView),
    Pipeline(pipeline::PipelineView),
    Plan(facett_plandag::PlanDagView),
    Brokkr(facett_brokkrview::BrokkrView),
}
impl Host {
    fn make(case: &str) -> Self {
        match case {
            "synthetic-sdp100" => Self::Synthetic(
                synthetic_sdp::SyntheticSdpView::new("synthetic-sdp").with_clock(0.0),
            ),
            "pipeline10000" => {
                let dag = synthetic_sdp::SyntheticDag::new(10_000, 7);
                let statuses = dag.statuses_at(3.0);
                let nodes = dag
                    .names
                    .iter()
                    .zip(statuses)
                    .map(|(name, status)| {
                        pipeline::PipelineNode::new(name, status).with_throughput_rows_s(100.0)
                    })
                    .collect();
                Self::Pipeline(
                    pipeline::PipelineView::from_nodes("10k pipeline", nodes, dag.edges)
                        .with_clock(0.0),
                )
            }
            "plandag1000" => {
                use facett_plandag::{PlanDagView, PlanEdge, PlanModel, PlanNode, PlanVersion};
                let graph = synthetic_sdp::SyntheticDag::new(1000, 7);
                let nodes = graph
                    .names
                    .iter()
                    .enumerate()
                    .map(|(i, n)| PlanNode::new(format!("p{i}"), n))
                    .collect();
                let edges = graph
                    .edges
                    .iter()
                    .map(|&(reads, source)| PlanEdge {
                        from: source,
                        to: reads,
                    })
                    .collect();
                Self::Plan(PlanDagView::new(
                    "1k operators",
                    PlanModel::new(PlanVersion::new("initial", nodes, edges)),
                ))
            }
            "brokkrview500" => {
                use facett_brokkrview::{
                    BrokkrView, BrokkrViewMsg, DagNode, DagView, TreeView, ViewMode,
                };
                let graph = synthetic_sdp::SyntheticDag::new(500, 7);
                let nodes = graph
                    .names
                    .iter()
                    .enumerate()
                    .map(|(i, n)| DagNode {
                        step: n.clone(),
                        stage: format!("stage{}", graph.layer_of[i]),
                        status: if i % 7 == 0 { "failed" } else { "passed" }.into(),
                        disposition: None,
                    })
                    .collect();
                let edges = graph
                    .edges
                    .iter()
                    .map(|&(reads, source)| {
                        (graph.names[source].clone(), graph.names[reads].clone())
                    })
                    .collect();
                let mut view = BrokkrView::new(
                    "500 build steps",
                    vec!["benchmark".into()],
                    TreeView {
                        project: "benchmark".into(),
                        pipeline: "benchmark".into(),
                        stages: vec![],
                    },
                    DagView { nodes, edges },
                );
                view.update(BrokkrViewMsg::SetView(ViewMode::Dag));
                Self::Brokkr(view)
            }
            _ => unreachable!(),
        }
    }
    fn ui(&mut self, ui: &mut egui::Ui) {
        facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
        match self {
            Self::Synthetic(v) => v.ui(ui),
            Self::Pipeline(v) => v.ui(ui),
            Self::Plan(v) => v.ui(ui),
            Self::Brokkr(v) => v.ui(ui),
        }
    }
    fn clock(&mut self, t: f64) {
        match self {
            Self::Synthetic(v) => v.set_clock(t),
            Self::Pipeline(v) => v.set_clock(t),
            _ => {}
        }
    }
}
fn cpu_host(case: &str, frames: usize, warmup: usize) -> Value {
    let start = Instant::now();
    let mut host = Host::make(case);
    let build_ms = start.elapsed().as_secs_f64() * 1000.0;
    let ctx = egui::Context::default();
    let mut times = Vec::new();
    let mut shape_counts = Vec::new();
    for frame in 0..frames + warmup {
        host.clock(frame as f64 / 60.0);
        let start = Instant::now();
        let mut output = ctx.run_ui(
            egui::RawInput {
                time: Some(frame as f64 / 60.0),
                screen_rect: Some(egui::Rect::from_min_size(
                    egui::Pos2::ZERO,
                    egui::vec2(SIZE[0], SIZE[1]),
                )),
                ..Default::default()
            },
            |ui| host.ui(ui),
        );
        let ms = start.elapsed().as_secs_f64() * 1000.0;
        if frame >= warmup {
            times.push(ms);
            shape_counts.push(output.shapes.len());
        }
        output.textures_delta.clear();
    }
    json!({"case":case,"mode":"cpu-shape-generation","frames":frames,"warmup_frames":warmup,"fixture_build_ms":build_ms,"egui_ui_ms_p50":percentile(times.clone(),0.5),"egui_ui_ms_p95":percentile(times,0.95),"shape_count_range":[shape_counts.iter().min(),shape_counts.iter().max()],"gpu_pass_ms_p50":null})
}
#[cfg(feature = "gpu")]
fn gpu_host(case: &str, frames: usize, warmup: usize) -> Value {
    use egui_kittest::wgpu::{WgpuTestRenderer, create_render_state};
    use facett_core::render::lane;
    use facett_flow::gpu;
    let _turn = facett_core::render::gputurn::hold();
    let rs = create_render_state(
        facett_core::render::gpu::adapter_wgpu::facett_wgpu_options().wgpu_setup,
        Default::default(),
    );
    assert!(
        gpu::install_renderer(&rs),
        "actual Falsett renderer must install"
    );
    let adapter = rs.adapter.get_info();
    let start = Instant::now();
    let host = Host::make(case);
    let build_ms = start.elapsed().as_secs_f64() * 1000.0;
    let mut h = egui_kittest::Harness::builder()
        .with_size(egui::vec2(SIZE[0], SIZE[1]))
        .renderer(WgpuTestRenderer::from_render_state(rs))
        .build_ui_state(|ui, host: &mut Host| host.ui(ui), host);
    let mut ui_ms = Vec::new();
    let mut render_ms = Vec::new();
    let mut gpu_ms = Vec::new();
    let mut uploaded = 0;
    let mut encoded = 0;
    let mut first_bytes = 0;
    for frame in 0..frames + warmup {
        if frame == warmup {
            first_bytes = lane::clock_stats_of(gpu::lane_name()).upload_bytes_total;
            encoded = lane::frames_of(gpu::lane_name());
        }
        h.state_mut().clock(frame as f64 / 60.0);
        let start = Instant::now();
        h.step();
        let ui_time = start.elapsed().as_secs_f64() * 1000.0;
        let start = Instant::now();
        let image = h.render().expect("GPU frame readback");
        std::hint::black_box(image);
        let render_time = start.elapsed().as_secs_f64() * 1000.0;
        if frame >= warmup {
            ui_ms.push(ui_time);
            render_ms.push(render_time);
            if let Some(us) = lane::clock_stats_of(gpu::lane_name()).last_gpu_pass_us {
                gpu_ms.push(us as f64 / 1000.0);
            }
        }
    }
    let stats = lane::clock_stats_of(gpu::lane_name());
    uploaded += stats.upload_bytes_total - first_bytes;
    encoded = lane::frames_of(gpu::lane_name()) - encoded;
    assert!(
        encoded > 0,
        "Falsett lane must encode frames, not CPU fallback"
    );
    json!({"case":case,"mode":"headless-gpu-readback","adapter":{"name":adapter.name,"backend":format!("{:?}",adapter.backend),"device_type":format!("{:?}",adapter.device_type)},"frames":frames,"warmup_frames":warmup,"fixture_build_ms":build_ms,"egui_step_ms_p50":percentile(ui_ms.clone(),0.5),"egui_step_ms_p95":percentile(ui_ms,0.95),"gpu_render_including_readback_ms_p50":percentile(render_ms,0.5),"gpu_pass_ms_p50":percentile(gpu_ms.clone(),0.5),"gpu_pass_ms_p95":percentile(gpu_ms.clone(),0.95),"gpu_timestamp_observations":gpu_ms.len(),"gpu_timestamps":stats.gpu_timestamps,"encoded_frames":encoded,"sample_window_upload_bytes":uploaded})
}
fn layout(case: &str, rounds: usize) -> Value {
    let n = if case == "layout100k" {
        100_000
    } else {
        1_000_000
    };
    let start = Instant::now();
    // A regular five-layer DAG in dense indices. Do not scale the SDP demo's
    // small-fixture de-duplication scan to a million nodes; this measures core
    // layout on explicitly constructed inputs, not that fixture generator.
    let stride = n / 5;
    let edges: Vec<(u32, u32)> = (0..4 * stride)
        .flat_map(|i| {
            let next = (i / stride + 1) * stride;
            [
                (i as u32, (i + stride) as u32),
                (i as u32, (next + (i % stride + 23) % stride) as u32),
            ]
        })
        .collect();
    let fixture_ms = start.elapsed().as_secs_f64() * 1000.0;
    let mut times = Vec::new();
    for _ in 0..rounds {
        let start = Instant::now();
        let l = facett_core::layered::layered(n, &edges, Default::default());
        let elapsed = start.elapsed().as_secs_f64() * 1000.0;
        assert_eq!(l.col.len(), n);
        assert!(!l.cyclic);
        std::hint::black_box(l);
        times.push(elapsed);
    }
    json!({"case":case,"mode":"cpu-layered","fixture":"regular five-layer dense-index DAG, two outgoing edges per nonsink node","nodes":n,"edges":edges.len(),"rounds":rounds,"fixture_build_ms":fixture_ms,"layout_ms_p50":percentile(times.clone(),0.5),"layout_ms_p95":percentile(times.clone(),0.95),"samples_ms":times})
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<_> = std::env::args().collect();
    let arg = |key: &str| {
        args.iter()
            .position(|s| s == key)
            .and_then(|i| args.get(i + 1))
            .map(String::as_str)
    };
    let case = arg("--case").unwrap_or("all");
    if case != "all" && !CASES.contains(&case) {
        return Err(format!("--case must be all or {}", CASES.join(", ")).into());
    }
    let frames = arg("--frames").unwrap_or("60").parse::<usize>()?;
    let warmup = arg("--warmup").unwrap_or("10").parse::<usize>()?;
    if frames == 0 {
        return Err("--frames must be positive".into());
    }
    let mode = arg("--mode").unwrap_or("cpu");
    if !["cpu", "gpu"].contains(&mode) {
        return Err("--mode must be cpu or gpu".into());
    }
    #[cfg(not(feature = "gpu"))]
    if mode == "gpu" {
        return Err("--mode gpu requires --features gpu".into());
    }
    let selected = if case == "all" {
        CASES.to_vec()
    } else {
        vec![case]
    };
    let mut rows = Vec::new();
    for case in selected {
        eprintln!("measuring {case} ({mode})");
        let row = if case.starts_with("layout") {
            layout(case, frames.min(5))
        } else {
            #[cfg(feature = "gpu")]
            {
                if mode == "gpu" {
                    gpu_host(case, frames, warmup)
                } else {
                    cpu_host(case, frames, warmup)
                }
            }
            #[cfg(not(feature = "gpu"))]
            {
                cpu_host(case, frames, warmup)
            }
        };
        rows.push(row);
    }
    let report = json!({"tool":"facett-flow-bench","build":if cfg!(debug_assertions){"debug"}else{"release"},"os":std::env::consts::OS,"arch":std::env::consts::ARCH,"compiled_flow_features":facett_flow::compiled_features(),"viewport_points":SIZE,"rows":rows,"method":"Development diagnostics; no pass/fail speed threshold. CPU timings include egui frame setup and shape generation. GPU readback wall time is NOT FPS. GPU pass values are actual most-recent resolved lane timestamps and can repeat between observations; null means unavailable. synthetic-sdp100 uses the actual demo driver (reconstructs PipelineView per frame); pipeline10000 retains its view. Layout samples measure core layered only, excluding fixture generation. Compare same toolchain/build/device/viewport with a separately built baseline; local values are not Loki evidence."});
    let text = serde_json::to_string_pretty(&report)?;
    if let Some(path) = arg("--output") {
        let path = PathBuf::from(path);
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::write(path, &text)?;
    }
    println!("{text}");
    Ok(())
}