forked from 1b-agent/opencadstudio-upstream
- Add Section 19 benchmarks covering camera pan viewport uniform updates, spatial hover candidate search, and viewport data assembly scaling - Add navigation telemetry probes in src/perf.rs (active only when PERF=1) to track frame interval, batched input moves, and layout-draw gap
2204 lines
76 KiB
Rust
2204 lines
76 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::{
|
|
SelectionGesture, SelectionState, SelectionView,
|
|
};
|
|
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, NavCursor, 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);
|
|
|
|
let draw_depth = rustc_hash::FxHashMap::default();
|
|
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,
|
|
&draw_depth,
|
|
);
|
|
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 state = SelectionState {
|
|
view: SelectionView {
|
|
vp_size: (1920.0, 1080.0),
|
|
},
|
|
gesture: SelectionGesture {
|
|
box_anchor: Some(Point::new(0.0, 0.0)),
|
|
box_current: Some(Point::new(100.0, 100.0)),
|
|
poly_points: vec![Point::new(10.0, 10.0); 64],
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
};
|
|
|
|
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,
|
|
vp_frozen: None,
|
|
},
|
|
LayerInfo {
|
|
name: "DRAWING".to_string(),
|
|
color: Color::TRANSPARENT,
|
|
visible: true,
|
|
frozen: false,
|
|
locked: false,
|
|
vp_frozen: None,
|
|
},
|
|
LayerInfo {
|
|
name: "DIMENSIONS".to_string(),
|
|
color: Color::TRANSPARENT,
|
|
visible: true,
|
|
frozen: false,
|
|
locked: false,
|
|
vp_frozen: None,
|
|
},
|
|
LayerInfo {
|
|
name: "ANNOTATIONS".to_string(),
|
|
color: Color::TRANSPARENT,
|
|
visible: true,
|
|
frozen: false,
|
|
locked: false,
|
|
vp_frozen: None,
|
|
},
|
|
],
|
|
"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().view.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,
|
|
NavCursor::None,
|
|
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,
|
|
hide_arms: false,
|
|
snap_color: None,
|
|
},
|
|
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)
|
|
);
|
|
}
|
|
|
|
// ── 19. Viewport Navigation & Spatial Candidate Scaling ─────────────────────
|
|
|
|
fn bench_navigation_latency_and_coalescing(runner: &mut BenchmarkRunner) {
|
|
if !runner.should_run("navigation") && !runner.should_run("nav_") {
|
|
return;
|
|
}
|
|
|
|
// Fixture: populated scene with mixed entities
|
|
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 * 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));
|
|
}
|
|
|
|
let bounds = Rectangle {
|
|
x: 0.0,
|
|
y: 0.0,
|
|
width: 1920.0,
|
|
height: 1080.0,
|
|
};
|
|
let mut cam = Camera::default();
|
|
|
|
// Warm up viewports
|
|
let _ = scene.build_viewports(
|
|
bounds,
|
|
codec::entities::ViewportRenderMode::Wireframe2D,
|
|
None,
|
|
false,
|
|
false,
|
|
[1.0, 1.0, 1.0, 1.0],
|
|
);
|
|
|
|
let n_pans = if runner.quick_mode { 2_000 } else { 10_000 };
|
|
let runs = 5;
|
|
|
|
// 1. Pure Camera Pan Calculation + Viewport update (matrix update & generation bump)
|
|
let mut pan_samples = Vec::with_capacity(runs);
|
|
for _ in 0..runs {
|
|
let t0 = Instant::now();
|
|
for step in 0..n_pans {
|
|
let dx = (step % 10) as f32 * 2.0;
|
|
let dy = (step % 5) as f32 * 2.0;
|
|
cam.pan_screen(dx, dy, bounds.height);
|
|
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 per_us = (t0.elapsed().as_micros() as f64) / (n_pans as f64);
|
|
pan_samples.push(per_us);
|
|
}
|
|
let median_pan_us = pan_samples[pan_samples.len() / 2];
|
|
runner.record(
|
|
"nav_camera_pan_viewport_update",
|
|
"Camera pan_screen + build_viewports (pure GPU uniform update)",
|
|
"µs",
|
|
pan_samples,
|
|
Some((1_000_000.0 / median_pan_us, "pans/s")),
|
|
Some(50.0), // Target < 50 µs (< 0.05 ms)
|
|
);
|
|
|
|
// 2. Spatial Hover Candidate Search (R-tree spatial query during idle cursor sweep)
|
|
let wires = scene.entity_wires();
|
|
let (view_rot, eye) = (cam.view_proj_rte(bounds), cam.eye());
|
|
let n_hover = if runner.quick_mode { 500 } else { 2_000 };
|
|
let mut hover_samples = Vec::with_capacity(runs);
|
|
for _ in 0..runs {
|
|
let t0 = Instant::now();
|
|
for i in 0..n_hover {
|
|
let cursor = DVec3::new((i % 200) as f64 * 10.0, ((i / 2) % 200) as f64 * 10.0, 0.0);
|
|
let candidates = scene.interaction_hover_candidates_near(
|
|
Arc::clone(&wires),
|
|
cursor,
|
|
view_rot,
|
|
eye,
|
|
bounds,
|
|
10.0,
|
|
);
|
|
black_box(candidates);
|
|
}
|
|
let per_us = (t0.elapsed().as_micros() as f64) / (n_hover as f64);
|
|
hover_samples.push(per_us);
|
|
}
|
|
let median_hover_us = hover_samples[hover_samples.len() / 2];
|
|
runner.record(
|
|
"nav_spatial_hover_candidate_search",
|
|
"Spatial hover candidate search (R-tree candidate culling for cursor movement)",
|
|
"µs",
|
|
hover_samples,
|
|
Some((1_000_000.0 / median_hover_us, "queries/s")),
|
|
Some(30.0), // Target < 30 µs (< 0.03 ms)
|
|
);
|
|
|
|
// 3. Viewport Assembly Scaling (build_viewports on active camera_generation bump)
|
|
let n_frames = if runner.quick_mode { 200 } else { 1_000 };
|
|
let mut assemble_samples = Vec::with_capacity(runs);
|
|
for _ in 0..runs {
|
|
let t0 = Instant::now();
|
|
for _ in 0..n_frames {
|
|
scene.camera_generation += 1;
|
|
let prim = scene.build_viewports(
|
|
bounds,
|
|
codec::entities::ViewportRenderMode::Wireframe2D,
|
|
None,
|
|
true,
|
|
true,
|
|
[1.0, 1.0, 1.0, 1.0],
|
|
);
|
|
black_box(prim);
|
|
}
|
|
let per_us = (t0.elapsed().as_micros() as f64) / (n_frames as f64);
|
|
assemble_samples.push(per_us);
|
|
}
|
|
let median_assemble_us = assemble_samples[assemble_samples.len() / 2];
|
|
runner.record(
|
|
"nav_viewport_assembly_scaling",
|
|
"ViewportData assembly per camera_generation change (live navigation frame prep)",
|
|
"µs",
|
|
assemble_samples,
|
|
Some((1_000_000.0 / median_assemble_us, "frames/s")),
|
|
Some(500.0), // Target < 500 µs (0.5 ms) for 5k entities
|
|
);
|
|
}
|
|
|
|
// ── 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);
|
|
bench_navigation_latency_and_coalescing(&mut runner);
|
|
|
|
runner.finish();
|
|
}
|
|
|