Files
OneCAD-Draft/benches/performance_benchmarks.rs
T
Karim Jerbi 7978d1eb2e bench: add COMMANDS suite with EXPLODE metrics
Shares the benchmark harness between both suites, extracts the plan/batch/apply helpers out of the EXPLODE command arm, and adds the command_benchmarks target.
2026-09-28 06:58:47 +01:00

2053 lines
71 KiB
Rust

//! OpenCADStudio Performance Benchmarking Suite
//!
//! Measures latency, percentiles, throughput, and memory metrics across core CAD subsystems:
//! 1. Scene Entity Ingestion & Population
//! 2. Analytical & Curved Entity Tessellation (incorporating existing 14,000-entity benchmark)
//! 3. Wide & Tapered Arc Tessellation (incorporating existing 7,500-entity benchmark)
//! 4. Spatial Broadphase Index Construction (InteractionIndex)
//! 5. Interactive Hit-Testing & Picking (click_hit, window box_hit, crossing box_hit, click_hits_all)
//! 6. Real-Time Object Snapping (Endpoint, Midpoint, Center, Intersection, Nearest)
//! 7. 2D Parametric Geometric Constraint Solving (Dogleg solver)
//! 8. DXF File I/O Serialization & Parsing Throughput
//! 9. Selection State Deep vs Arc Cloning
//! 10. UI Ribbon View Widget Tree Construction
//! 11. UI Viewport Grid Geometry Projection & Overlay Cache Key Evaluation
//! 12. UI Themed SVG Icon Lookup Caching vs Uncached Parse
//! 12b. Plot Style Layer-Usage Table Rebuild (256-bucket ACI table)
//! 13. Wide & Tapered Arc + Donut Tessellation (7.5k offset polyline curves)
//! 14. Model Space Extents & Bounding Box Calculation (ZOOM EXTENTS)
//! 15. Batch Entity Transformation & Incremental Dirty-Tracking
//! 16. Draworder Depth Map Full Build & Incremental Patch
//! 17. Delta-Undo Transaction Before-Image Recording
//! 18. Viewport Render Pipeline Construction (10k Objects)
//!
//! Output:
//! - Human-readable ASCII / Markdown summary table on stdout
//! - Machine-readable JSON metrics saved to `target/cad_performance_metrics.json`
//! - Baseline regression comparison via `--baseline <file>` or `CAD_BENCH_BASELINE=<path>`
//!
//! Run with:
//! `cargo bench` (runs in --release mode, does not run on cargo test)
//! `cargo run --release --bench performance_benchmarks`
//! `cargo run --release --bench performance_benchmarks -- --quick` (quick smoke test)
#![allow(clippy::field_reassign_with_default, clippy::manual_is_multiple_of)]
use std::f64::consts::{PI, TAU};
use std::hint::black_box;
use std::io::Cursor;
use std::sync::Arc;
use std::time::Instant;
use codec::entities::{Arc as AcadArc, Circle, Ellipse, Line, LwPolyline, LwVertex};
use codec::types::{Vector2, Vector3};
use codec::{CadDocument, DxfReader, DxfWriter, EntityType};
use glam::{DVec3, Mat4, Vec3};
use iced::{Color, Point, Rectangle};
use OpenCADStudio::scene::parametric_constraints::{
ConstraintKind, ParametricRef, ParametricScope,
};
use OpenCADStudio::scene::pick::hit_test::{box_hit, click_hit};
use OpenCADStudio::scene::pick::interaction_index::InteractionIndex;
use OpenCADStudio::scene::pick::selection_state::SelectionState;
use OpenCADStudio::scene::pipeline::wire_arena::partition_wires;
use OpenCADStudio::scene::view::camera::Camera;
use OpenCADStudio::scene::{ChangeKind, Scene};
use OpenCADStudio::snap::Snapper;
use OpenCADStudio::ui::icons::{self, CHECK};
use OpenCADStudio::ui::overlay::{
grid_segments, selection_overlay, should_reuse, CrosshairOptions, GridCanvasState,
GridKey, GridParams, GridStyle, GripMarker, OstTrackPoint, SelectionVisualOptions,
UcsIconParams,
};
use OpenCADStudio::ui::properties::LinetypeItem;
use OpenCADStudio::ui::ribbon::{LayerInfo, Ribbon};
use OpenCADStudio::ui::style::plotstyle::build_layer_usage;
#[path = "support/harness.rs"]
mod harness;
use harness::BenchmarkRunner;
// ── 1. Scene Entity Ingestion & Batch Creation ───────────────────────────────
fn bench_scene_entity_ingestion(runner: &mut BenchmarkRunner) {
let name = "scene_entity_ingestion_10k";
if !runner.should_run(name) {
return;
}
let n_entities = if runner.quick_mode { 2_000 } else { 10_000 };
let runs = if runner.quick_mode { 3 } else { 10 };
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let mut scene = Scene::new();
let t0 = Instant::now();
let n_lines = (n_entities * 4) / 10;
let n_circles = (n_entities * 3) / 10;
let n_arcs = (n_entities * 2) / 10;
let n_polylines = n_entities - n_lines - n_circles - n_arcs;
for i in 0..n_lines {
let x = (i % 100) as f64 * 20.0;
let y = (i / 100) as f64 * 20.0;
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 15.0, y + 15.0, 0.0);
scene.add_entity(EntityType::Line(line));
}
for i in 0..n_circles {
let x = (i % 100) as f64 * 30.0;
let y = (i / 100) as f64 * 30.0 + 2000.0;
let mut circle = Circle::default();
circle.center = Vector3::new(x, y, 0.0);
circle.radius = 12.0;
scene.add_entity(EntityType::Circle(circle));
}
for i in 0..n_arcs {
let x = (i % 100) as f64 * 30.0;
let y = (i / 100) as f64 * 30.0 + 4000.0;
let mut arc = AcadArc::default();
arc.center = Vector3::new(x, y, 0.0);
arc.radius = 10.0;
arc.start_angle = 0.0;
arc.end_angle = PI * 0.75;
scene.add_entity(EntityType::Arc(arc));
}
for i in 0..n_polylines {
let x = (i % 50) as f64 * 50.0;
let y = (i / 50) as f64 * 50.0 + 6000.0;
let mut pl = LwPolyline::new();
pl.vertices = vec![
LwVertex {
location: Vector2::new(x, y),
bulge: 0.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 0,
},
LwVertex {
location: Vector2::new(x + 20.0, y),
bulge: 0.5,
start_width: 0.0,
end_width: 0.0,
vertex_id: 1,
},
LwVertex {
location: Vector2::new(x + 20.0, y + 20.0),
bulge: 0.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 2,
},
];
scene.add_entity(EntityType::LwPolyline(pl));
}
let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
samples.push(elapsed_ms);
black_box(&scene);
}
let median_sec = samples[samples.len() / 2] / 1000.0;
let throughput = (n_entities as f64) / median_sec;
runner.record(
name,
"Ingesting 10,000 mixed entities (Lines, Circles, Arcs, Polylines) into Scene",
"ms",
samples,
Some((throughput, "entities/s")),
Some(150.0),
);
}
// ── 2. Analytical & Curved Entity Tessellation & Zoom ────────────────────────
// Incorporates the existing `bench_analytical_rendering` (14,000 curved entities)
fn bench_analytical_tessellation_zoom(runner: &mut BenchmarkRunner) {
let name_tess = "analytical_curved_tessellation";
let name_zoom = "analytical_curved_zoom_100x";
if !runner.should_run("analytical") {
return;
}
let (n_circles, n_arcs, n_ellipses, n_polylines) = if runner.quick_mode {
(1000, 1000, 400, 400)
} else {
(5000, 5000, 2000, 2000)
};
let total_entities = n_circles + n_arcs + n_ellipses + n_polylines;
let mut scene = Scene::new();
for i in 0..n_circles {
let x = (i % 100) as f64 * 50.0;
let y = (i / 100) as f64 * 50.0;
let mut circle = Circle::default();
circle.center = Vector3::new(x, y, 0.0);
circle.radius = 10.0 + (i % 20) as f64;
scene.add_entity(EntityType::Circle(circle));
}
for i in 0..n_arcs {
let x = (i % 100) as f64 * 50.0 + 25.0;
let y = (i / 100) as f64 * 50.0;
let mut arc = AcadArc::default();
arc.center = Vector3::new(x, y, 0.0);
arc.radius = 15.0;
arc.start_angle = ((i % 8) as f64) * 0.25 * PI;
arc.end_angle = arc.start_angle + 0.75 * PI;
scene.add_entity(EntityType::Arc(arc));
}
for i in 0..n_ellipses {
let x = (i % 50) as f64 * 100.0;
let y = (i / 50) as f64 * 100.0 + 5000.0;
let mut el = Ellipse::default();
el.center = Vector3::new(x, y, 0.0);
el.major_axis = Vector3::new(20.0, 0.0, 0.0);
el.minor_axis_ratio = 0.5;
el.start_parameter = 0.0;
el.end_parameter = TAU;
scene.add_entity(EntityType::Ellipse(el));
}
for i in 0..n_polylines {
let x = (i % 50) as f64 * 100.0 + 50.0;
let y = (i / 50) as f64 * 100.0 + 5000.0;
let mut pline = LwPolyline::new();
pline.is_closed = true;
pline.vertices = vec![
LwVertex {
location: Vector2::new(x, y),
bulge: 1.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 0,
},
LwVertex {
location: Vector2::new(x + 30.0, y),
bulge: 1.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 1,
},
];
scene.add_entity(EntityType::LwPolyline(pline));
}
let cam = Camera::default();
let tess_runs = if runner.quick_mode { 3 } else { 5 };
let mut tess_samples = Vec::with_capacity(tess_runs);
let mut wires_opt = None;
for _ in 0..tess_runs {
scene.bump_geometry();
let t0 = Instant::now();
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
tess_samples.push(t0.elapsed().as_secs_f64() * 1000.0);
wires_opt = Some(wires);
}
let wires = wires_opt.unwrap();
let median_tess_sec = tess_samples[tess_samples.len() / 2] / 1000.0;
let tess_throughput = (total_entities as f64) / median_tess_sec;
runner.record(
name_tess,
"Initial wire generation for 14,000 curved entities (circles, arcs, ellipses, polylines)",
"ms",
tess_samples,
Some((tess_throughput, "entities/s")),
Some(120.0),
);
let depths = rustc_hash::FxHashMap::default();
let part_runs = if runner.quick_mode { 20 } else { 50 };
let mut part_samples = Vec::with_capacity(part_runs);
for _ in 0..part_runs {
let t0 = Instant::now();
let p = partition_wires(&wires, &depths);
part_samples.push(t0.elapsed().as_secs_f64() * 1000.0);
black_box(p);
}
runner.record(
"wire_partitioning_per_frame",
"Partitioning 14k wires into GPU circles/ellipses and regular lines",
"ms",
part_samples,
None,
Some(2.0),
);
let zoom_levels = [0.1f32, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 50.0, 100.0, 1000.0];
let zoom_runs = if runner.quick_mode { 2 } else { 5 };
let mut zoom_samples = Vec::with_capacity(zoom_runs);
for _ in 0..zoom_runs {
let t0 = Instant::now();
let mut dynamic_cam = Camera::default();
for &zoom in zoom_levels.iter().cycle().take(100) {
dynamic_cam.distance = 1000.0 / zoom;
let _w = scene.model_tile_wires_arc(0, &dynamic_cam, 1.0, 1000.0);
}
zoom_samples.push(t0.elapsed().as_secs_f64() * 1000.0);
}
let median_zoom_ms = zoom_samples[zoom_samples.len() / 2];
let fps = 100.0 / (median_zoom_ms / 1000.0);
runner.record(
name_zoom,
"100 camera zoom navigations over 14,000 curved entities",
"ms",
zoom_samples,
Some((fps, "FPS")),
Some(50.0),
);
}
// ── 3. Spatial Broadphase Index Construction ────────────────────────────────
fn bench_spatial_index_construction(runner: &mut BenchmarkRunner) {
let name = "spatial_interaction_index_build";
if !runner.should_run(name) {
return;
}
let n_lines = if runner.quick_mode { 2_000 } else { 10_000 };
let mut scene = Scene::new();
for i in 0..n_lines {
let x = (i % 100) as f64 * 30.0;
let y = (i / 100) as f64 * 30.0;
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 25.0, y + 25.0, 0.0);
scene.add_entity(EntityType::Line(line));
}
let cam = Camera::default();
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
let runs = if runner.quick_mode { 5 } else { 15 };
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
let index = InteractionIndex::build(&wires);
index.prepare_screen();
samples.push(t0.elapsed().as_secs_f64() * 1000.0);
black_box(index);
}
let median_sec = samples[samples.len() / 2] / 1000.0;
let throughput = (wires.len() as f64) / median_sec;
runner.record(
name,
"Constructing spatial InteractionIndex & prepare_screen for 10k wires",
"ms",
samples,
Some((throughput, "wires/s")),
Some(35.0),
);
}
// ── 4. Interactive Picking & Hit-Testing ─────────────────────────────────────
fn bench_hit_test_picking(runner: &mut BenchmarkRunner) {
if !runner.should_run("hit_test") && !runner.should_run("pick") {
return;
}
let n_wires = if runner.quick_mode { 1_000 } else { 5_000 };
let mut scene = Scene::new();
for i in 0..n_wires {
let x = (i % 100) as f64 * 20.0;
let y = (i / 100) as f64 * 20.0;
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 18.0, y + 18.0, 0.0);
scene.add_entity(EntityType::Line(line));
}
let cam = Camera::default();
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
let index = InteractionIndex::build(&wires);
index.prepare_screen();
let view_rot = Mat4::IDENTITY;
let eye = DVec3::new(1000.0, 1000.0, 2000.0);
let bounds = Rectangle {
x: 0.0,
y: 0.0,
width: 1920.0,
height: 1080.0,
};
let n_queries = if runner.quick_mode { 200 } else { 1_000 };
let mut click_samples = Vec::with_capacity(5);
for _ in 0..5 {
let t0 = Instant::now();
for q in 0..n_queries {
let cx = ((q * 37) % 1920) as f32;
let cy = ((q * 53) % 1080) as f32;
let hit = click_hit(
Point::new(cx, cy),
wires.as_slice(),
view_rot,
eye,
bounds,
false,
8.0,
);
black_box(hit);
}
let per_query_us = (t0.elapsed().as_secs_f64() * 1_000_000.0) / (n_queries as f64);
click_samples.push(per_query_us);
}
let median_click_us = click_samples[click_samples.len() / 2];
runner.record(
"hit_test_click_pick_latency",
"Single-point click_hit query across 5,000 entities in screen space",
"µs",
click_samples,
Some((1_000_000.0 / median_click_us, "queries/s")),
Some(100.0),
);
let mut box_win_samples = Vec::with_capacity(5);
for _ in 0..5 {
let t0 = Instant::now();
for q in 0..n_queries {
let cx = ((q * 41) % 1500) as f32;
let cy = ((q * 47) % 800) as f32;
let p1 = Point::new(cx, cy);
let p2 = Point::new(cx + 150.0, cy + 150.0);
let hits = box_hit(p1, p2, false, wires.as_slice(), view_rot, eye, bounds);
black_box(hits);
}
let per_query_us = (t0.elapsed().as_secs_f64() * 1_000_000.0) / (n_queries as f64);
box_win_samples.push(per_query_us);
}
let median_win_us = box_win_samples[box_win_samples.len() / 2];
runner.record(
"hit_test_box_window_enclosed",
"Window selection box_hit (enclosed) across 5,000 entities",
"µs",
box_win_samples,
Some((1_000_000.0 / median_win_us, "queries/s")),
Some(250.0),
);
let mut box_cross_samples = Vec::with_capacity(5);
for _ in 0..5 {
let t0 = Instant::now();
for q in 0..n_queries {
let cx = ((q * 41) % 1500) as f32;
let cy = ((q * 47) % 800) as f32;
let p1 = Point::new(cx, cy);
let p2 = Point::new(cx + 150.0, cy + 150.0);
let hits = box_hit(p1, p2, true, wires.as_slice(), view_rot, eye, bounds);
black_box(hits);
}
let per_query_us = (t0.elapsed().as_secs_f64() * 1_000_000.0) / (n_queries as f64);
box_cross_samples.push(per_query_us);
}
let median_cross_us = box_cross_samples[box_cross_samples.len() / 2];
runner.record(
"hit_test_box_crossing_boundary",
"Crossing selection box_hit (boundary intersection) across 5,000 entities",
"µs",
box_cross_samples,
Some((1_000_000.0 / median_cross_us, "queries/s")),
Some(500.0),
);
}
// ── 5. Real-Time Object Snapping (OSNAP) ─────────────────────────────────────
fn bench_osnap_evaluation(runner: &mut BenchmarkRunner) {
let name = "osnap_cursor_tracking_latency";
if !runner.should_run("snap") && !runner.should_run(name) {
return;
}
let n_entities = if runner.quick_mode { 1_000 } else { 5_000 };
let mut scene = Scene::new();
for i in 0..n_entities {
let x = (i % 100) as f64 * 30.0;
let y = (i / 100) as f64 * 30.0;
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 25.0, y + 25.0, 0.0);
scene.add_entity(EntityType::Line(line));
}
let cam = Camera::default();
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
let index = InteractionIndex::build(&wires);
index.prepare_screen();
let mut snapper = Snapper::default();
snapper.toggle_global();
snapper.enable_all();
let view_rot = Mat4::IDENTITY;
let eye = DVec3::new(500.0, 500.0, 1000.0);
let bounds = Rectangle {
x: 0.0,
y: 0.0,
width: 1920.0,
height: 1080.0,
};
let grid_origin = Vec3::ZERO;
let grid_axes = (Vec3::X, Vec3::Y, Vec3::Z);
let n_evals = if runner.quick_mode { 300 } else { 1_500 };
let mut samples = Vec::with_capacity(5);
for _ in 0..5 {
let t0 = Instant::now();
for q in 0..n_evals {
let wx = (q as f64 * 7.3) % 2500.0;
let wy = (q as f64 * 11.7) % 2500.0;
let cursor_world = DVec3::new(wx, wy, 0.0);
let cursor_screen = Point::new(((q * 23) % 1920) as f32, ((q * 31) % 1080) as f32);
let hit = snapper.snap(
cursor_world,
cursor_screen,
wires.as_slice(),
view_rot,
eye,
bounds,
grid_origin,
grid_axes,
None,
);
black_box(hit);
}
let per_snap_us = (t0.elapsed().as_secs_f64() * 1_000_000.0) / (n_evals as f64);
samples.push(per_snap_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
name,
"Real-time OSNAP evaluation (Endpoint, Mid, Center, Intersect, Nearest) across 5k entities",
"µs",
samples,
Some((1_000_000.0 / median_us, "snaps/s")),
Some(350.0),
);
}
// ── 6. 2D Parametric Geometric Constraint Solving ───────────────────────────
fn bench_geometric_constraint_solving(runner: &mut BenchmarkRunner) {
let name = "constraint_solver_dogleg_system";
if !runner.should_run("constraint") && !runner.should_run("solve") {
return;
}
let n_chains = if runner.quick_mode { 5 } else { 15 };
let runs = if runner.quick_mode { 5 } else { 15 };
let mut samples = Vec::with_capacity(runs);
for run_idx in 0..runs {
let mut scene = Scene::new();
let mut lines = Vec::new();
for i in 0..n_chains {
let y = i as f64 * 30.0;
let mut l1 = Line::new();
l1.start = Vector3::new(0.0, y, 0.0);
l1.end = Vector3::new(50.0, y, 0.0);
let h1 = scene.add_entity(EntityType::Line(l1));
let mut l2 = Line::new();
l2.start = Vector3::new(50.0, y, 0.0);
l2.end = Vector3::new(50.0, y + 20.0, 0.0);
let h2 = scene.add_entity(EntityType::Line(l2));
let mut l3 = Line::new();
l3.start = Vector3::new(50.0, y + 20.0, 0.0);
l3.end = Vector3::new(0.0, y + 20.0, 0.0);
let h3 = scene.add_entity(EntityType::Line(l3));
lines.push((h1, h2, h3));
}
let cs = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
for &(h1, h2, h3) in &lines {
cs.add(
ConstraintKind::Perpendicular,
vec![ParametricRef::whole(h1), ParametricRef::whole(h2)],
None,
);
cs.add(
ConstraintKind::Perpendicular,
vec![ParametricRef::whole(h2), ParametricRef::whole(h3)],
None,
);
cs.add(
ConstraintKind::Parallel,
vec![ParametricRef::whole(h1), ParametricRef::whole(h3)],
None,
);
}
let target = lines[run_idx % lines.len()].0;
if let Some(EntityType::Line(ref mut l)) = scene.document.get_entity_mut(target) {
l.end.y += 10.0;
}
let t0 = Instant::now();
scene.bump_entities(&[(target, ChangeKind::Modified)]);
samples.push(t0.elapsed().as_secs_f64() * 1000.0);
}
let median_ms = samples[samples.len() / 2];
runner.record(
name,
"Solving coupled nonlinear 2D constraints (Perp, Parallel, Length) via Dogleg solver",
"ms",
samples,
Some((1000.0 / median_ms, "solves/s")),
Some(10.0),
);
}
// ── 7. DXF File I/O Serialization & Parsing Throughput ──────────────────────
fn bench_dxf_io_throughput(runner: &mut BenchmarkRunner) {
if !runner.should_run("dxf") && !runner.should_run("io") {
return;
}
let n_entities = if runner.quick_mode { 1_000 } else { 5_000 };
let mut doc = CadDocument::new();
for i in 0..n_entities {
let x = (i % 100) as f64 * 20.0;
let y = (i / 100) as f64 * 20.0;
if i % 2 == 0 {
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 10.0, y + 10.0, 0.0);
let _ = doc.add_entity(EntityType::Line(line));
} else {
let mut circle = Circle::default();
circle.center = Vector3::new(x, y, 0.0);
circle.radius = 8.0;
let _ = doc.add_entity(EntityType::Circle(circle));
}
}
let runs = if runner.quick_mode { 3 } else { 8 };
let mut write_samples = Vec::with_capacity(runs);
let mut dxf_bytes = Vec::new();
for _ in 0..runs {
let t0 = Instant::now();
let bytes = DxfWriter::new(&doc)
.write_to_vec()
.expect("DXF write succeeds");
write_samples.push(t0.elapsed().as_secs_f64() * 1000.0);
dxf_bytes = bytes;
}
let mb_size = (dxf_bytes.len() as f64) / (1024.0 * 1024.0);
let median_write_sec = write_samples[write_samples.len() / 2] / 1000.0;
let write_throughput_mb = mb_size / median_write_sec;
runner.record(
"dxf_export_write_throughput",
"Serializing 5,000 entities to DXF binary stream",
"ms",
write_samples,
Some((write_throughput_mb, "MB/s")),
Some(100.0),
);
let mut read_samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
let parsed_doc = DxfReader::from_reader(Cursor::new(dxf_bytes.clone()))
.expect("DXF reader created")
.read()
.expect("DXF parse succeeds");
read_samples.push(t0.elapsed().as_secs_f64() * 1000.0);
black_box(parsed_doc);
}
let median_read_sec = read_samples[read_samples.len() / 2] / 1000.0;
let read_throughput_mb = mb_size / median_read_sec;
runner.record(
"dxf_import_parse_throughput",
"Parsing DXF stream of 5,000 entities into CadDocument",
"ms",
read_samples,
Some((read_throughput_mb, "MB/s")),
Some(80.0),
);
}
// ── 8. Selection State Deep vs Arc Cloning ───────────────────────────────────
// Incorporates the existing `bench_selection_state_clone`
fn bench_selection_cloning(runner: &mut BenchmarkRunner) {
if !runner.should_run("selection") && !runner.should_run("clone") {
return;
}
let mut state = SelectionState {
vp_size: (1920.0, 1080.0),
poly_points: vec![Point::new(10.0, 10.0); 64],
..Default::default()
};
state.box_anchor = Some(Point::new(0.0, 0.0));
state.box_current = Some(Point::new(100.0, 100.0));
let n = if runner.quick_mode { 1_000 } else { 5_000 };
let runs = 5;
let mut deep_samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
black_box(state.clone());
}
let per_ns = (t0.elapsed().as_nanos() as f64) / (n as f64);
deep_samples.push(per_ns);
}
let median_deep_ns = deep_samples[deep_samples.len() / 2];
runner.record(
"selection_state_deep_clone",
"SelectionState::clone (deep clone with 64 polygon vertices)",
"ns",
deep_samples,
Some((1_000_000_000.0 / median_deep_ns, "clones/s")),
Some(1000.0),
);
let arc_state = Arc::new(state);
let mut arc_samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
black_box(Arc::clone(&arc_state));
}
let per_ns = (t0.elapsed().as_nanos() as f64) / (n as f64);
arc_samples.push(per_ns);
}
let median_arc_ns = arc_samples[arc_samples.len() / 2];
runner.record(
"selection_state_arc_clone",
"Arc<SelectionState>::clone (atomic reference count increment)",
"ns",
arc_samples,
Some((1_000_000_000.0 / median_arc_ns, "clones/s")),
Some(20.0),
);
}
// ── 10. UI Ribbon View Construction Benchmark ──────────────────────────────
fn bench_ui_ribbon_view_construction(runner: &mut BenchmarkRunner) {
if !runner.should_run("ui_ribbon_view_construction") {
return;
}
let mut ribbon = Ribbon::new();
ribbon.set_styles(
vec![
"Standard".to_string(),
"Title".to_string(),
"Annotative".to_string(),
],
"Standard",
vec!["Standard".to_string()],
"Standard",
vec!["Standard".to_string()],
"Standard",
vec!["Standard".to_string()],
"Standard",
);
ribbon.set_layers(
vec![
LayerInfo {
name: "0".to_string(),
color: Color::TRANSPARENT,
visible: true,
frozen: false,
locked: false,
},
LayerInfo {
name: "DRAWING".to_string(),
color: Color::TRANSPARENT,
visible: true,
frozen: false,
locked: false,
},
LayerInfo {
name: "DIMENSIONS".to_string(),
color: Color::TRANSPARENT,
visible: true,
frozen: false,
locked: false,
},
LayerInfo {
name: "ANNOTATIONS".to_string(),
color: Color::TRANSPARENT,
visible: true,
frozen: false,
locked: false,
},
],
"0",
);
ribbon.set_available_linetypes(vec![
LinetypeItem {
name: "Continuous".to_string(),
art: String::new(),
},
LinetypeItem {
name: "DASHED".to_string(),
art: String::new(),
},
LinetypeItem {
name: "CENTER".to_string(),
art: String::new(),
},
LinetypeItem {
name: "HIDDEN".to_string(),
art: String::new(),
},
]);
// Warm-up for allocator and tree settling
for _ in 0..10 {
let _ = black_box(ribbon.view(false, false, 0, 0, false));
}
let n = if runner.quick_mode { 20 } else { 100 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let elem = ribbon.view(false, false, 0, 0, false);
black_box(elem);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n as f64);
samples.push(per_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
"ui_ribbon_view_construction",
"Ribbon::view widget tree construction per frame (populated tabs, styles, layers)",
"µs",
samples,
Some((1_000_000.0 / median_us, "views/s")),
Some(3000.0), // Target threshold < 3.0 ms
);
}
// ── 11. UI Grid Geometry Generation & Cache Hit Benchmark ──────────────────
fn bench_ui_grid_geometry(runner: &mut BenchmarkRunner) {
let view_rot1 = Mat4::from_rotation_x(0.15) * Mat4::from_rotation_y(0.05);
let eye1 = glam::DVec3::new(4.0, 3.5, 9.0);
let bounds1 = Rectangle {
x: 0.0,
y: 0.0,
width: 1280.0,
height: 720.0,
};
let step1 = 80.0_f32;
let origin1 = glam::DVec3::new(0.0, 0.0, 0.0);
let axes1 = (Vec3::X, Vec3::Y, Vec3::Z);
let limits1: Option<(glam::DVec2, glam::DVec2)> = None;
let view_rot2 = Mat4::from_rotation_x(0.15) * Mat4::from_rotation_y(0.05);
let eye2 = glam::DVec3::new(4.0, 3.5, 9.0);
let bounds2 = Rectangle {
x: 1280.0,
y: 0.0,
width: 640.0,
height: 720.0,
};
let step2 = 160.0_f32;
let origin2 = glam::DVec3::new(0.0, 0.0, 0.0);
let axes2 = (Vec3::X, Vec3::Y, Vec3::Z);
let limits2: Option<(glam::DVec2, glam::DVec2)> = None;
// Both panes use a square grid, so one step per pane plus a shared
// major-line interval covers the new step_x / step_y / major_every args.
let major_every = 5u32;
// ── 1. Uncached grid geometry generation ──
if runner.should_run("ui_grid_geometry_uncached") {
for _ in 0..10 {
let _ = black_box(grid_segments(
view_rot1, eye1, bounds1, step1, step1, major_every, origin1, axes1, limits1,
));
let _ = black_box(grid_segments(
view_rot2, eye2, bounds2, step2, step2, major_every, origin2, axes2, limits2,
));
}
let n = if runner.quick_mode { 20 } else { 100 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let g1 = grid_segments(view_rot1, eye1, bounds1, step1, step1, major_every, origin1, axes1, limits1);
let g2 = grid_segments(view_rot2, eye2, bounds2, step2, step2, major_every, origin2, axes2, limits2);
black_box(g1);
black_box(g2);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n as f64);
samples.push(per_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
"ui_grid_geometry_uncached",
"Viewport 2-pane tiled grid projection & segment generation (uncached)",
"µs",
samples,
Some((1_000_000.0 / median_us, "frames/s")),
Some(800.0), // Target threshold < 800 µs
);
}
// ── 2. Cached grid key hit evaluation ──
if runner.should_run("ui_grid_cache_hit_evaluation") {
let params1 = GridParams {
view_rot: view_rot1,
eye: eye1,
bounds: bounds1,
step_x: step1,
step_y: step1,
major_every,
origin: origin1,
axes: axes1,
limits: limits1,
};
let params2 = GridParams {
view_rot: view_rot2,
eye: eye2,
bounds: bounds2,
step_x: step2,
step_y: step2,
major_every,
origin: origin2,
axes: axes2,
limits: limits2,
};
let grids = vec![params1, params2];
let canvas_bounds = Rectangle {
x: 0.0,
y: 0.0,
width: 1920.0,
height: 720.0,
};
let state = GridCanvasState::default();
let stored_key = GridKey::from_grids(&grids, canvas_bounds, GridStyle::default());
*state.key.borrow_mut() = Some(stored_key);
for _ in 0..50 {
let key = GridKey::from_grids(&grids, canvas_bounds, GridStyle::default());
let hit = should_reuse(state.key.borrow().as_ref(), &key);
black_box(hit);
}
let n = if runner.quick_mode { 1_000 } else { 10_000 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
let mut hit_count = 0u32;
for _ in 0..n {
let key = GridKey::from_grids(
black_box(&grids),
black_box(canvas_bounds),
GridStyle::default(),
);
if should_reuse(state.key.borrow().as_ref(), &key) {
hit_count += 1;
}
}
assert_eq!(hit_count as usize, n);
let per_ns = (t0.elapsed().as_nanos() as f64) / (n as f64);
samples.push(per_ns);
}
let median_ns = samples[samples.len() / 2];
runner.record(
"ui_grid_cache_hit_evaluation",
"Grid overlay cache key construction + reuse hit decision",
"ns",
samples,
Some((1_000_000_000.0 / median_ns, "evals/s")),
Some(500.0), // Target threshold < 500 ns
);
}
}
// ── 12. UI Themed SVG Icon Caching Benchmark ───────────────────────────────
fn bench_ui_icon_caching(runner: &mut BenchmarkRunner) {
let bytes = CHECK;
// Warm up the themed handle cache
let _ = icons::themed_handle(bytes);
if runner.should_run("ui_icon_handle_cached") {
let n = if runner.quick_mode { 5_000 } else { 50_000 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let handle = icons::themed_handle(black_box(bytes));
black_box(handle);
}
let per_ns = (t0.elapsed().as_nanos() as f64) / (n as f64);
samples.push(per_ns);
}
let median_ns = samples[samples.len() / 2];
runner.record(
"ui_icon_handle_cached",
"Themed SVG icon cache hit (FxHashMap lookup + Handle clone)",
"ns",
samples,
Some((1_000_000_000.0 / median_ns, "lookups/s")),
Some(100.0), // Target threshold < 100 ns
);
}
if runner.should_run("ui_icon_handle_uncached") {
let n = if runner.quick_mode { 200 } else { 2_000 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let handle = iced::widget::svg::Handle::from_memory(black_box(bytes));
black_box(handle);
}
let per_ns = (t0.elapsed().as_nanos() as f64) / (n as f64);
samples.push(per_ns);
}
let median_ns = samples[samples.len() / 2];
runner.record(
"ui_icon_handle_uncached",
"Uncached SVG icon memory parse + Handle allocation",
"ns",
samples,
Some((1_000_000_000.0 / median_ns, "parses/s")),
None, // Reference comparison
);
}
}
// ── Plot Style Layer-Usage Table Rebuild ─────────────────────────────────────
// Covers `build_layer_usage`: layer names bucketed by ACI into a 256-bucket
// table, rebuilt per Plot Style modal view.
fn bench_ui_plotstyle_layer_usage(runner: &mut BenchmarkRunner) {
if !runner.should_run("ui_plotstyle_layer_usage") {
return;
}
let mut doc = CadDocument::new();
for i in 0..200 {
let mut layer = codec::tables::Layer::new(&format!("LAYER_{i:03}"));
layer.handle = doc.allocate_handle();
layer.color = codec::types::Color::Index((i % 8 + 1) as u8);
let _ = doc.layers.add(layer);
}
// Warm-up for allocator settling
for _ in 0..10 {
let _ = black_box(build_layer_usage(&doc));
}
let n = if runner.quick_mode { 20 } else { 100 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let usage = build_layer_usage(black_box(&doc));
black_box(usage);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n as f64);
samples.push(per_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
"ui_plotstyle_layer_usage",
"Plot Style 256-bucket ACI layer-usage table rebuild (200 layers, 8 buckets x25)",
"µs",
samples,
Some((1_000_000.0 / median_us, "rebuilds/s")),
Some(50.0), // Target threshold < 50 µs
);
}
// ── 12c. UI Status Bar Derived-Data Cache Hit ─────────────────────────────────
// Covers the per-frame status-bar path (`cached_layout_names` +
// `cached_scale_picker_list`): layout names and the annotation-scale picker
// list are served as shared `Arc` clones instead of full doc scans.
fn bench_ui_statusbar_derived_data(runner: &mut BenchmarkRunner) {
if !runner.should_run("ui_statusbar_derived_data") {
return;
}
// Scene shaped like a working drawing: several thousand entities,
// multiple paper layouts, and a populated annotation-scale list.
let mut scene = Scene::new();
for i in 0..3_000 {
let x = (i % 100) as f64 * 20.0;
let y = (i / 100) as f64 * 20.0;
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 15.0, y + 15.0, 0.0);
scene.add_entity(EntityType::Line(line));
}
for i in 0..8 {
let _ = scene.add_layout(&format!("SB_BENCH_{i}"));
}
for (i, (paper, drawing)) in [(1.0, 50.0), (1.0, 100.0), (0.5, 12.0), (1.0, 48.0)]
.iter()
.enumerate()
{
let _ = scene.add_scale(&format!("SB_SCALE_{i}"), *paper, *drawing);
}
// Warm-up: populate both caches and settle the allocator.
for _ in 0..10 {
let layouts = scene.cached_layout_names();
let scales = scene.cached_scale_picker_list();
black_box(layouts);
black_box(scales);
}
let n = if runner.quick_mode { 20 } else { 100 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let layouts = scene.cached_layout_names();
let scales = scene.cached_scale_picker_list();
black_box(layouts);
black_box(scales);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n as f64);
samples.push(per_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
"ui_statusbar_derived_data",
"Status-bar cached derived data hit (layout names + scale picker Arc clones)",
"µs",
samples,
Some((1_000_000.0 / median_us, "hits/s")),
Some(50.0), // Target threshold < 50 µs
);
}
// ── 12d. UI Grip Vertex Budget (budgeted build + per-frame projection) ──────
// Covers the selection-grip vertex budget (`MAX_SELECTED_GRIPS` in
// app/settings.rs): one dense 100K-vertex polyline emits ~2 grips/vertex
// (vertex + midpoint). The budget keeps entity order with vertex grips first
// and truncates the parallel handle vec in lockstep, so the per-frame
// `grips_to_screen` projection only ever sees the capped set.
//
// NOTE: the budget step below calls the real `apply_grip_budget`
// (app/properties.rs) with the real `MAX_SELECTED_GRIPS` (app/settings.rs),
// re-exported via `OpenCADStudio::app` so this external bench crate can reach
// them (both modules are otherwise private/`pub(crate)`).
// Projection is measured with the real `grips_to_screen` on the capped set,
// which bounds the per-frame cost by construction: uncapped it is O(vertices),
// budgeted it is O(cap).
fn bench_ui_grip_budget(runner: &mut BenchmarkRunner) {
if !runner.should_run("ui_grip_budget") && !runner.should_run("ui_grip_budget_build") {
return;
}
use OpenCADStudio::app::{apply_grip_budget, MAX_SELECTED_GRIPS};
use OpenCADStudio::scene::model::object::{GripDef, GripShape};
use OpenCADStudio::scene::pick::grip::grips_to_screen;
// Fixture: one dense polyline's grips — 2 per vertex (vertex + midpoint),
// i.e. ~200K grips for a 100K-vertex polyline in full mode. Midpoint grips
// mirror the real LWPolyline producer (entities/lwpolyline.rs via
// entities/common.rs `rectangle_grip`): `Rectangle` shape with `dir:
// Some(chord)` so the bench exercises the second `camera.project` in
// `grips_to_screen` (scene/pick/grip.rs), exactly like production.
let n_vertices = if runner.quick_mode { 10_000 } else { 100_000 };
let mut all_grips = Vec::with_capacity(2 * n_vertices);
for v in 0..n_vertices {
let base = (v % 1024) as f64;
all_grips.push(GripDef {
id: 2 * v,
world: glam::DVec3::new(base, base * 0.5, 0.0),
is_midpoint: false,
shape: GripShape::Square,
dir: None,
axis: None,
});
all_grips.push(GripDef {
id: 2 * v + 1,
world: glam::DVec3::new(base + 0.5, base * 0.5 + 0.25, 0.0),
is_midpoint: true,
shape: GripShape::Rectangle,
// In-plane segment direction of the synthetic polyline
// (vertices run along (1, 0.5)), matching the chord `dir` the
// real producer passes to `rectangle_grip`.
dir: Some(glam::DVec3::new(1.0, 0.5, 0.0)),
axis: None,
});
}
let all_handles: Vec<codec::Handle> = (0..all_grips.len() as u64)
.map(|k| codec::Handle::new(k + 1))
.collect();
let n_fixture_grips = all_grips.len();
// Budget once (selection-change path): the per-frame loop below only ever
// sees the capped set, exactly like view/mod.rs after refresh. Cloned so
// the full fixture stays available for the `ui_grip_budget_build` timing
// below, which measures this same selection-change cost per iteration.
let (capped_grips, _) = apply_grip_budget(all_grips.clone(), all_handles.clone());
assert_eq!(capped_grips.len(), MAX_SELECTED_GRIPS.min(2 * n_vertices));
let cam = Camera::default();
let bounds = Rectangle {
x: 0.0,
y: 0.0,
width: 1920.0,
height: 1080.0,
};
// Warm-up for allocator settling.
for _ in 0..10 {
let projected = grips_to_screen(black_box(&capped_grips), &cam, bounds);
black_box(projected);
}
let n = if runner.quick_mode { 20 } else { 100 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let projected = grips_to_screen(black_box(&capped_grips), &cam, bounds);
black_box(projected);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n as f64);
samples.push(per_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
"ui_grip_budget",
&format!(
"Budgeted per-frame grip projection ({}-grip polyline capped to {} grips)",
n_fixture_grips,
capped_grips.len()
),
"µs",
samples,
Some(((capped_grips.len() as f64) / (median_us / 1_000_000.0), "grips/s")),
Some(500.0), // Target threshold < 500 µs (measured ~132 µs quick / ~147 µs full)
);
// Selection-change cost (previously unmeasured): `apply_grip_budget` sorts
// ~200K grips by `is_midpoint` (O(n log n)), builds an FxHashSet of the
// kept indices, and filters both parallel vecs — once per selection change,
// not per frame. Iteration inputs are pre-cloned before the timer so only
// the budget itself is measured, not the clone.
let n_build = if runner.quick_mode { 20 } else { 5 };
let mut build_samples = Vec::with_capacity(runs);
for _ in 0..runs {
let mut build_inputs = Vec::with_capacity(n_build);
for _ in 0..n_build {
build_inputs.push((all_grips.clone(), all_handles.clone()));
}
let t0 = Instant::now();
for (grips_in, handles_in) in build_inputs {
let grips_in = black_box(grips_in);
let handles_in = black_box(handles_in);
let (capped, _) = apply_grip_budget(grips_in, handles_in);
black_box(capped);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n_build as f64);
build_samples.push(per_us);
}
let build_median_us = build_samples[build_samples.len() / 2];
runner.record(
"ui_grip_budget_build",
&format!(
"Selection-change grip budget over {} grips (sort + FxHashSet + filter)",
n_fixture_grips
),
"µs",
build_samples,
Some(((n_fixture_grips as f64) / (build_median_us / 1_000_000.0), "grips/s")),
Some(7_000.0), // Target < 7 ms (selection-change hitch budget; measured ~2.36 ms full / ~0.30 ms quick, ~3x headroom)
);
}
// ── 12e. UI Constraint-Glyph Cache Hit (memoised placements) ────────────────
// Covers the parametric constraint-glyph memo (`Scene::cached_glyph_placements`,
// src/scene/parametric_constraints.rs): dozens of constraints whose per-frame
// view + hit-test + dwell + click share one key, so the hot path is a cache
// hit (key build + Arc clone), not a placements recompute (entity lookups,
// intersections, String labels, projections).
//
// NOTE: the bench calls the real `cached_glyph_placements` with the same key
// inputs the production consumers (app/view, viewport hit paths, overlay) and
// the `cached_glyph_placements_*` unit-test fixture use
// (`ParametricScope::ModelSpace`, full-canvas vp, values on, display 3,
// bar 4095), so the measured hit is exactly what the frame shares.
fn bench_ui_constraint_glyphs(runner: &mut BenchmarkRunner) {
if !runner.should_run("ui_constraint_glyphs") {
return;
}
// Fixture: dozens of horizontal constraints over a grid of short lines
// around the origin — mirrors the `cached_glyph_placements_*` unit-test
// fixture (Scene::new + add_entity + constraint add + note applied).
let n_constraints = if runner.quick_mode { 24 } else { 60 };
let mut scene = Scene::new();
let cols = 8;
for i in 0..n_constraints {
let x = ((i % cols) as f64) * 30.0 - 100.0;
let y = ((i / cols) as f64) * 30.0 - 100.0;
let line = scene.add_entity(EntityType::Line(Line::from_points(
Vector3::new(x, y, 0.0),
Vector3::new(x + 20.0, y, 0.0),
)));
let id = scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(line)],
None,
);
scene.note_parametric_constraint_applied(ParametricScope::ModelSpace, id, 3);
}
scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
let vp = (1920.0_f32, 1080.0_f32);
// Prime the cache so every timed call below is a hit (same key, same Arc).
let primed =
scene.cached_glyph_placements(ParametricScope::ModelSpace, vp, true, 3, 4095);
assert!(
!primed.is_empty(),
"glyph fixture must yield placements"
);
let n_glyphs = primed.len();
// Warm-up for allocator settling.
for _ in 0..10 {
let hit =
scene.cached_glyph_placements(ParametricScope::ModelSpace, vp, true, 3, 4095);
black_box(hit);
}
let n = if runner.quick_mode { 200 } else { 1_000 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let hit = scene.cached_glyph_placements(
black_box(ParametricScope::ModelSpace),
black_box(vp),
black_box(true),
black_box(3),
black_box(4095),
);
black_box(hit);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n as f64);
samples.push(per_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
"ui_constraint_glyphs",
&format!(
"Constraint-glyph cache hit over {} glyphs (key build + Arc clone, no recompute)",
n_glyphs
),
"µs",
samples,
Some(((n_glyphs as f64) / (median_us / 1_000_000.0), "glyphs/s")),
Some(0.5), // Target threshold < 0.5 µs (measured ~0.06 µs quick / ~0.15 µs full, ~3x headroom)
);
}
fn bench_ui_selection_overlay(runner: &mut BenchmarkRunner) {
if !runner.should_run("ui_selection_overlay") {
return;
}
// Fixture: the per-frame selection-overlay shape — an active snap,
// crosshair, one UCS tripod, empty constraint glyphs. Grip count is the
// scaling axis: typical selections carry dozens, the grip budget caps at
// 4096, so full mode measures the cap. Element (widget-tree)
// construction only; canvas draw/tessellation runs in the renderer.
let n_grips = if runner.quick_mode { 128 } else { 4096 };
// Warm-up for allocator settling.
for _ in 0..10 {
let _ = black_box(build_selection_overlay_element(n_grips));
}
let n = if runner.quick_mode { 20 } else { 100 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let elem = build_selection_overlay_element(black_box(n_grips));
black_box(elem);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n as f64);
samples.push(per_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
"ui_selection_overlay",
&format!(
"Selection overlay Element construction with {} grips (widget tree, no draw)",
n_grips
),
"µs",
samples,
Some((1_000_000.0 / median_us, "overlays/s")),
Some(50.0), // Target < 50 µs (2.4 µs at the 4096-grip cap, ~20x headroom)
);
}
fn build_selection_overlay_element(
n_grips: usize,
) -> iced::Element<'static, OpenCADStudio::app::Message> {
use OpenCADStudio::app::{CursorType, IsoPlane};
use OpenCADStudio::scene::model::object::GripShape;
use OpenCADStudio::scene::parametric_constraints::GlyphEntry;
use OpenCADStudio::snap::SnapType;
let selection = Arc::new(std::cell::RefCell::new(SelectionState::default()));
let grips: Vec<GripMarker> = (0..n_grips)
.map(|i| GripMarker {
pos: Point::new(100.0 + i as f32 * 5.0, 200.0),
shape: GripShape::Square,
is_hot: false,
is_hovered: false,
dir: None,
})
.collect();
let ucs_icons = vec![UcsIconParams {
view_proj: Mat4::IDENTITY,
bounds: Rectangle { x: 0.0, y: 0.0, width: 1920.0, height: 1080.0 },
axes: (Vec3::X, Vec3::Y, Vec3::Z),
origin_screen: None,
hover: false,
selected: false,
}];
let empty_glyphs: Arc<[GlyphEntry]> = Arc::from([]);
let empty_selected: Arc<[bool]> = Arc::from([]);
selection_overlay(
selection,
Some((Point::new(400.0, 300.0), SnapType::Endpoint)),
None,
None,
grips,
None,
None,
ucs_icons,
vec![OstTrackPoint { screen: Point::new(500.0, 500.0) }],
vec![(Point::new(0.0, 0.0), Point::new(100.0, 100.0))],
None,
true,
vec![],
None,
None,
false,
false,
false,
[0.1, 0.1, 0.1, 1.0],
CrosshairOptions {
size_percent: 5,
pick_box: 3,
cursor_type: CursorType::Crosshair,
color: None,
isometric: false,
iso_plane: IsoPlane::Top,
snap_angle_deg: 0.0,
point_mode: false,
pick_pending: false,
},
SelectionVisualOptions::default(),
empty_glyphs,
empty_selected,
None,
None,
)
}
// ── 13. Wide & Tapered Arc + Donut Tessellation ─────────────────────────────
fn bench_wide_and_tapered_arc_tessellation(runner: &mut BenchmarkRunner) {
let name = "wide_and_tapered_arc_tessellation";
if !runner.should_run(name) {
return;
}
let (n_wide, n_taper, n_donut) = if runner.quick_mode {
(500, 500, 500)
} else {
(2500, 2500, 2500)
};
let total_entities = n_wide + n_taper + n_donut;
let runs = if runner.quick_mode { 3 } else { 5 };
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let mut scene = Scene::new();
// 1. Wide polyline arcs (constant width 6.0)
for i in 0..n_wide {
let x = (i % 50) as f64 * 80.0;
let y = (i / 50) as f64 * 80.0;
let mut pline = LwPolyline::new();
pline.constant_width = 6.0;
pline.vertices = vec![
LwVertex {
location: Vector2::new(x, y),
bulge: 0.5,
start_width: 0.0,
end_width: 0.0,
vertex_id: 0,
},
LwVertex {
location: Vector2::new(x + 40.0, y),
bulge: 0.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 1,
},
];
scene.add_entity(EntityType::LwPolyline(pline));
}
// 2. Tapered arcs (width: 2.0 -> 12.0)
for i in 0..n_taper {
let x = (i % 50) as f64 * 80.0;
let y = (i / 50) as f64 * 80.0 + 4500.0;
let mut pline = LwPolyline::new();
pline.vertices = vec![
LwVertex {
location: Vector2::new(x, y),
bulge: 0.7,
start_width: 2.0,
end_width: 12.0,
vertex_id: 0,
},
LwVertex {
location: Vector2::new(x + 35.0, y + 10.0),
bulge: 0.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 1,
},
];
scene.add_entity(EntityType::LwPolyline(pline));
}
// 3. Donuts (2 closed arc segments, inner 10.0, outer 26.0)
for i in 0..n_donut {
let cx = (i % 50) as f64 * 80.0;
let cy = (i / 50) as f64 * 80.0 + 9000.0;
let inner_r = 10.0;
let outer_r = 26.0;
let r_avg = (inner_r + outer_r) * 0.5;
let width = outer_r - inner_r;
let mut pline = LwPolyline::new();
pline.is_closed = true;
pline.constant_width = width;
pline.vertices = vec![
LwVertex {
location: Vector2::new(cx - r_avg, cy),
bulge: 1.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 0,
},
LwVertex {
location: Vector2::new(cx + r_avg, cy),
bulge: 1.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 1,
},
];
scene.add_entity(EntityType::LwPolyline(pline));
}
let cam = Camera::default();
let t0 = Instant::now();
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
black_box(wires);
let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
samples.push(elapsed_ms);
}
let median_ms = samples[samples.len() / 2];
let throughput = (total_entities as f64) / (median_ms / 1000.0);
runner.record(
name,
"Tessellating 7.5k wide arcs, tapered arcs & donuts (mesh offset curves)",
"ms",
samples,
Some((throughput, "entities/s")),
Some(100.0),
);
}
// ── 14. Model Space Extents Calculation (ZOOM EXTENTS) ──────────────────────
fn bench_zoom_extents_calculation(runner: &mut BenchmarkRunner) {
let name = "zoom_extents_bounding_box_calc";
if !runner.should_run(name) {
return;
}
let n = if runner.quick_mode { 4_000 } else { 20_000 };
let mut scene = Scene::new();
for i in 0..n {
let x = (i % 200) as f64 * 50.0;
let y = (i / 200) as f64 * 50.0;
match i % 4 {
0 => {
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 40.0, y + 40.0, 0.0);
scene.add_entity(EntityType::Line(line));
}
1 => {
let mut c = Circle::new();
c.center = Vector3::new(x + 20.0, y + 20.0, 0.0);
c.radius = 15.0;
scene.add_entity(EntityType::Circle(c));
}
2 => {
let mut a = AcadArc::new();
a.center = Vector3::new(x + 20.0, y + 20.0, 0.0);
a.radius = 15.0;
a.start_angle = 0.5;
a.end_angle = 3.5;
scene.add_entity(EntityType::Arc(a));
}
_ => {
let mut pl = LwPolyline::new();
pl.vertices = vec![
LwVertex::new(Vector2::new(x, y)),
LwVertex::new(Vector2::new(x + 20.0, y + 10.0)),
LwVertex::new(Vector2::new(x + 40.0, y)),
];
scene.add_entity(EntityType::LwPolyline(pl));
}
}
}
let runs = if runner.quick_mode { 5 } else { 15 };
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
// Invalidate extents cache to measure cold AABB traversal across all entities
scene.invalidate_draw_depth();
scene.bump_entities(&[]);
let t0 = Instant::now();
let ext = scene.model_space_extents();
black_box(ext);
let elapsed_us = t0.elapsed().as_micros() as f64;
samples.push(elapsed_us);
}
let median_us = samples[samples.len() / 2];
let throughput = (n as f64) / (median_us / 1_000_000.0);
runner.record(
name,
"Computing model space bounding box across 20k mixed entities (ZOOM EXTENTS)",
"µs",
samples,
Some((throughput, "entities/s")),
Some(10000.0), // Target threshold < 10.0 ms
);
}
// ── 15. Batch Entity Transformation & Mutation ──────────────────────────────
fn bench_batch_entity_mutation(runner: &mut BenchmarkRunner) {
let name = "batch_entity_transform_mutation";
if !runner.should_run(name) {
return;
}
let total_entities = if runner.quick_mode { 2_000 } else { 10_000 };
let mutate_count = if runner.quick_mode { 200 } else { 1_000 };
let mut scene = Scene::new();
let mut handles = Vec::with_capacity(total_entities);
for i in 0..total_entities {
let x = (i % 100) as f64 * 30.0;
let y = (i / 100) as f64 * 30.0;
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 20.0, y + 20.0, 0.0);
let h = scene.add_entity(EntityType::Line(line));
handles.push(h);
}
let cam = Camera::default();
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
let index = InteractionIndex::build(&wires);
index.prepare_screen();
let runs = if runner.quick_mode { 5 } else { 10 };
let mut samples = Vec::with_capacity(runs);
for run_idx in 0..runs {
let offset = (run_idx as f64 + 1.0) * 2.0;
let mut modified = Vec::with_capacity(mutate_count);
for &h in handles.iter().take(mutate_count) {
if let Some(EntityType::Line(ref mut line)) = scene.document.get_entity_mut(h) {
line.start.x += offset;
line.end.x += offset;
}
modified.push((h, ChangeKind::Modified));
}
let t0 = Instant::now();
scene.bump_entities(&modified);
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
let idx = InteractionIndex::build(&wires);
idx.prepare_screen();
let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
black_box(idx);
samples.push(elapsed_ms);
}
let median_ms = samples[samples.len() / 2];
let throughput = (mutate_count as f64) / (median_ms / 1000.0);
runner.record(
name,
"Incremental dirty propagation & spatial index update for 1,000 modified entities",
"ms",
samples,
Some((throughput, "entities/s")),
Some(15.0), // Target threshold < 15.0 ms
);
}
// ── 16. Draworder Evaluation (Full Build & Incremental Patch) ───────────────
fn bench_draworder_evaluation(runner: &mut BenchmarkRunner) {
let total_entities = if runner.quick_mode { 2_000 } else { 10_000 };
let mut scene = Scene::new();
let mut handles = Vec::with_capacity(total_entities);
for i in 0..total_entities {
let x = (i % 100) as f64 * 30.0;
let y = (i / 100) as f64 * 30.0;
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 20.0, y + 20.0, 0.0);
let h = scene.add_entity(EntityType::Line(line));
handles.push(h);
}
// ── Full build ──
if runner.should_run("draworder_depth_map_full_build") {
let runs = if runner.quick_mode { 5 } else { 10 };
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
scene.invalidate_draw_depth();
let t0 = Instant::now();
let depths = scene.draw_depth_map();
black_box(depths);
let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
samples.push(elapsed_ms);
}
let median_ms = samples[samples.len() / 2];
let throughput = (total_entities as f64) / (median_ms / 1000.0);
runner.record(
"draworder_depth_map_full_build",
"Initial draworder depth map construction across 10,000 entities",
"ms",
samples,
Some((throughput, "entities/s")),
Some(15.0),
);
}
// ── Incremental patch ──
if runner.should_run("draworder_depth_map_incremental_patch") {
let _ = scene.draw_depth_map();
let n_patch = if runner.quick_mode { 100 } else { 1_000 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for j in 0..n_patch {
let h = handles[j % handles.len()];
scene.bump_entities(&[(h, ChangeKind::Modified)]);
let depths = scene.draw_depth_map();
black_box(depths);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n_patch as f64);
samples.push(per_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
"draworder_depth_map_incremental_patch",
"Draworder depth map fast-path retrieval under entity modifications",
"µs",
samples,
Some((1_000_000.0 / median_us, "patches/s")),
Some(50.0), // Target threshold < 50 µs
);
}
}
// ── 17. Delta-Undo Transaction Before-Image Recording ───────────────────────
fn bench_undo_delta_recording(runner: &mut BenchmarkRunner) {
let name = "undo_delta_recording_cycle";
if !runner.should_run(name) {
return;
}
let total = if runner.quick_mode { 1_000 } else { 5_000 };
let mutate_count = if runner.quick_mode { 100 } else { 500 };
let mut scene = Scene::new();
let mut handles = Vec::with_capacity(total);
for i in 0..total {
let x = (i % 100) as f64 * 30.0;
let y = (i / 100) as f64 * 30.0;
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 20.0, y + 20.0, 0.0);
let h = scene.add_entity(EntityType::Line(line));
handles.push(h);
}
let runs = if runner.quick_mode { 5 } else { 15 };
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
scene.begin_undo_recording();
for &h in handles.iter().take(mutate_count) {
let before = scene.document.get_entity(h).map(|e| Arc::new(e.clone()));
scene.record_undo_before(h, before);
}
let rec = scene.take_undo_recording();
black_box(rec);
let elapsed_us = t0.elapsed().as_micros() as f64;
samples.push(elapsed_us);
}
let median_us = samples[samples.len() / 2];
let throughput = (mutate_count as f64) / (median_us / 1_000_000.0);
runner.record(
name,
"Capturing delta-undo transaction before-images for 500 entity mutations",
"µs",
samples,
Some((throughput, "records/s")),
Some(1000.0), // Target threshold < 1000 µs (1 ms)
);
}
fn bench_view_render_viewport_construction(runner: &mut BenchmarkRunner) {
if !runner.should_run("view_render") {
return;
}
let obj_count = if runner.quick_mode { 1_000 } else { 10_000 };
let mut scene = Scene::new();
// Populate scene with non-graphical document objects
for i in 0..obj_count {
let handle = codec::Handle::new(0x2000 + i as u64);
scene.document.objects.insert(
handle,
codec::objects::ObjectType::Dictionary(codec::objects::Dictionary::default()),
);
}
// Add lines to model space
for i in 0..100 {
let x = (i % 10) as f64 * 10.0;
let y = (i / 10) as f64 * 10.0;
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 5.0, y + 5.0, 0.0);
scene.add_entity(EntityType::Line(line));
}
let runs = if runner.quick_mode { 10 } else { 30 };
let bounds = Rectangle {
x: 0.0,
y: 0.0,
width: 1920.0,
height: 1080.0,
};
// 1. Uncached / Before: Linear scans over document objects on every frame
let mut uncached_samples = Vec::with_capacity(runs);
for _ in 0..runs {
scene.invalidate_render_environment_cache();
let t0 = Instant::now();
let primitive = scene.build_viewports(
bounds,
codec::entities::ViewportRenderMode::Wireframe2D,
None,
false,
false,
[1.0, 1.0, 1.0, 1.0],
);
black_box(primitive);
let elapsed_us = t0.elapsed().as_micros() as f64;
uncached_samples.push(elapsed_us);
}
let median_uncached_us = uncached_samples[uncached_samples.len() / 2];
let fps_uncached = 1_000_000.0 / median_uncached_us.max(1.0);
runner.record(
"view_render_viewport_uncached (Before)",
"Uncached: 2 linear scans over document objects on every frame",
"µs",
uncached_samples,
Some((fps_uncached, "FPS")),
None,
);
// 2. Cached / After: Memoized document render environment + O(1) fast paths
let _ = scene.build_viewports(
bounds,
codec::entities::ViewportRenderMode::Wireframe2D,
None,
false,
false,
[1.0, 1.0, 1.0, 1.0],
);
let mut cached_samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
let primitive = scene.build_viewports(
bounds,
codec::entities::ViewportRenderMode::Wireframe2D,
None,
false,
false,
[1.0, 1.0, 1.0, 1.0],
);
black_box(primitive);
let elapsed_us = t0.elapsed().as_micros() as f64;
cached_samples.push(elapsed_us);
}
let median_cached_us = cached_samples[cached_samples.len() / 2];
let fps_cached = 1_000_000.0 / median_cached_us.max(1.0);
runner.record(
"view_render_viewport_cached (After)",
"Memoized: O(1) render environment & Face3D fast-path",
"µs",
cached_samples,
Some((fps_cached, "FPS")),
Some(1000.0), // Target threshold < 1000 µs (1 ms)
);
}
// ── Main Entrypoint ─────────────────────────────────────────────────────────
fn main() {
println!("\nInitializing OpenCADStudio Performance Benchmarks...");
let mut runner = BenchmarkRunner::new(
" OPENCADSTUDIO PERFORMANCE BENCHMARK REPORT ",
"cad_performance_metrics.json",
);
if runner.quick_mode {
println!("Mode: QUICK (reduced iteration/entity counts for fast validation)");
} else {
println!("Mode: FULL (production scale benchmarks for baseline metrics)");
}
bench_scene_entity_ingestion(&mut runner);
bench_analytical_tessellation_zoom(&mut runner);
bench_spatial_index_construction(&mut runner);
bench_hit_test_picking(&mut runner);
bench_osnap_evaluation(&mut runner);
bench_geometric_constraint_solving(&mut runner);
bench_dxf_io_throughput(&mut runner);
bench_selection_cloning(&mut runner);
bench_ui_ribbon_view_construction(&mut runner);
bench_ui_grid_geometry(&mut runner);
bench_ui_icon_caching(&mut runner);
bench_ui_plotstyle_layer_usage(&mut runner);
bench_ui_statusbar_derived_data(&mut runner);
bench_ui_grip_budget(&mut runner);
bench_ui_constraint_glyphs(&mut runner);
bench_ui_selection_overlay(&mut runner);
bench_wide_and_tapered_arc_tessellation(&mut runner);
bench_zoom_extents_calculation(&mut runner);
bench_batch_entity_mutation(&mut runner);
bench_draworder_evaluation(&mut runner);
bench_undo_delta_recording(&mut runner);
bench_view_render_viewport_construction(&mut runner);
runner.finish();
}