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