167 lines
6.1 KiB
Rust
167 lines
6.1 KiB
Rust
//! Local-only CPU diagnosis. Does not measure native FPS, canvas redraw, or GPU.
|
|
//! Usage: architecture_perf_probe.exe PATH_TO_RELEASE1_MODEL_JSON [SAMPLES=40]
|
|
use oa_core::{persistence, projection, routing, view_options::ViewIndex, *};
|
|
use std::{collections::BTreeMap, hint::black_box, time::Instant};
|
|
|
|
fn measure<T>(label: &str, samples: usize, mut f: impl FnMut(usize) -> T) {
|
|
for i in 0..3 {
|
|
black_box(f(i));
|
|
}
|
|
let mut ms = Vec::with_capacity(samples);
|
|
for i in 0..samples {
|
|
let start = Instant::now();
|
|
let output = black_box(f(i));
|
|
ms.push(start.elapsed().as_secs_f64() * 1000.0);
|
|
drop(output); // Exclude destruction consistently from each timed phase.
|
|
}
|
|
ms.sort_by(f64::total_cmp);
|
|
let percentile = |p: f64| ms[((samples - 1) as f64 * p).ceil() as usize];
|
|
println!(
|
|
"{label}: n={samples} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
|
percentile(0.5),
|
|
percentile(0.95),
|
|
ms[samples - 1]
|
|
);
|
|
}
|
|
|
|
fn probe(p: &Project, d: &Diagram, samples: usize) {
|
|
let index = ViewIndex::new(p);
|
|
let view = projection::project_diagram_with_index(p, d, &index);
|
|
println!(
|
|
"\nview={} projection={:?} nodes={} ports={} edges={}",
|
|
d.name,
|
|
d.projection,
|
|
view.nodes.len(),
|
|
view.ports.len(),
|
|
view.edges.len()
|
|
);
|
|
measure("index_new", samples, |_| ViewIndex::new(black_box(p)));
|
|
measure("projection_reused_index", samples, |_| {
|
|
projection::project_diagram_with_index(black_box(p), black_box(d), black_box(&index))
|
|
});
|
|
measure("projection_fresh_index", samples, |_| {
|
|
projection::project_diagram(black_box(p), black_box(d))
|
|
});
|
|
measure("routing_settled", samples, |_| {
|
|
routing::route_diagram(black_box(p), black_box(d), black_box(&view))
|
|
});
|
|
// Deterministic slight motion of the first root card. Includes the same
|
|
// descendant/route translation as legacy single-card canvas drag. This is
|
|
// presentation-only, not the allocation transaction pipeline.
|
|
let Some(root) = view.nodes.iter().find(|n| n.visual_parent.is_none()) else {
|
|
return;
|
|
};
|
|
println!("moving_root={} element={}", root.id, p.named(root.element));
|
|
let mut moved = projection::descendant_nodes(p, d.id, root.id);
|
|
moved.push(root.id);
|
|
let make_moved = |i: usize| {
|
|
let delta = Position {
|
|
x: (i % 21) as f32 * 3.0,
|
|
y: (i % 11) as f32 * 2.0,
|
|
};
|
|
let mut preview = d.clone();
|
|
let deltas = moved
|
|
.iter()
|
|
.map(|id| (*id, delta))
|
|
.collect::<BTreeMap<_, _>>();
|
|
for route in projection::translated_routes(p, d, &deltas) {
|
|
if let Some(stored) = preview.routes.iter_mut().find(|r| r.edge == route.edge) {
|
|
*stored = route;
|
|
} else {
|
|
preview.routes.push(route);
|
|
}
|
|
}
|
|
for id in &moved {
|
|
if let Some(n) = preview.nodes.get_mut(id) {
|
|
n.position.x += delta.x;
|
|
n.position.y += delta.y;
|
|
}
|
|
}
|
|
preview
|
|
};
|
|
let previews = (0..samples.max(3)).map(make_moved).collect::<Vec<_>>();
|
|
let projected = previews
|
|
.iter()
|
|
.map(|d| projection::project_diagram_with_index(p, d, &index))
|
|
.collect::<Vec<_>>();
|
|
measure("moved_projection", samples, |i| {
|
|
projection::project_diagram_with_index(p, black_box(&previews[i]), &index)
|
|
});
|
|
measure("moved_routing", samples, |i| {
|
|
routing::route_diagram(p, black_box(&previews[i]), black_box(&projected[i]))
|
|
});
|
|
measure("moved_prepare_projection_routing", samples, |i| {
|
|
let preview = make_moved(i);
|
|
let view = projection::project_diagram_with_index(p, &preview, &index);
|
|
routing::route_diagram(p, &preview, &view)
|
|
});
|
|
}
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let args = std::env::args().collect::<Vec<_>>();
|
|
let path = args
|
|
.get(1)
|
|
.ok_or("usage: architecture_perf_probe PATH [SAMPLES]")?;
|
|
let samples = args
|
|
.get(2)
|
|
.map(|s| s.parse::<usize>())
|
|
.transpose()?
|
|
.unwrap_or(40)
|
|
.max(1);
|
|
let p = persistence::decode(&std::fs::read(path)?)?;
|
|
println!(
|
|
"CPU-only probe; samples={samples}; fixture={path}; elements={} relations={}",
|
|
p.elements.len(),
|
|
p.relations.len()
|
|
);
|
|
for d in p.diagrams.values() {
|
|
probe(&p, d, samples);
|
|
}
|
|
// Approximation of the reported eight-card screenshot, not an exact repro:
|
|
// fixture semantics, Overview projection, synthetic positions/card sizes.
|
|
let mut approx = p.clone();
|
|
let id: Id = "f0440000-0000-4000-8000-000000000001".parse()?;
|
|
approx.diagrams.insert(
|
|
id,
|
|
Diagram {
|
|
id,
|
|
alias: "fps44_approx_overview".into(),
|
|
name: "APPROXIMATION eight-card SA Overview".into(),
|
|
description: "Synthetic positions, not user geometry".into(),
|
|
layer: Layer::Sa,
|
|
projection: ProjectionKind::Overview,
|
|
view_options: Default::default(),
|
|
nodes: BTreeMap::new(),
|
|
routes: vec![],
|
|
layout: None,
|
|
},
|
|
);
|
|
for (ordinal, (alias, x, y)) in [
|
|
("sa_assessment_procedure", 30., 240.),
|
|
("sa_triage", 510., 240.),
|
|
("sa_control", 990., 240.),
|
|
("sa_technician", 530., 440.),
|
|
("sa_assess", 1490., 240.),
|
|
("sa_repair", 1030., 440.),
|
|
("sa_confirm", 1530., 440.),
|
|
("sa_reset", 30., 440.),
|
|
]
|
|
.into_iter()
|
|
.enumerate()
|
|
{
|
|
let element = approx
|
|
.elements
|
|
.values()
|
|
.find(|e| e.alias == alias)
|
|
.ok_or_else(|| format!("missing alias {alias}"))?
|
|
.id;
|
|
let generated = approx.place(id, element, Position { x, y });
|
|
let diagram = approx.diagrams.get_mut(&id).unwrap();
|
|
let mut node = diagram.nodes.remove(&generated).unwrap();
|
|
node.id = format!("f0440000-0000-4000-8000-{:012}", ordinal + 2).parse()?;
|
|
diagram.nodes.insert(node.id, node);
|
|
}
|
|
probe(&approx, &approx.diagrams[&id], samples);
|
|
Ok(())
|
|
}
|