perf(bench): introduce standardized 25-metric CAD and UI benchmark suite

- Add comprehensive performance benchmark harness in benches/performance_benchmarks.rs
  configured with harness = false in Cargo.toml (runs via cargo bench in --release,
  isolated from cargo test).
- Benchmark coverage across 25 distinct CAD & UI operations:
  * Entity Ingestion, GPU Wire Partitioning, Analytical & Wide/Tapered Arc Tessellation
  * 100x Zoom Navigation, Spatial InteractionIndex, Multi-mode Hit-Testing, OSnap Tracking
  * 2D Parametric Dogleg Constraint Solving, DXF Import/Export Throughput
  * UI Ribbon View Construction, Viewport Grid Projection, Grid Cache Evaluation
  * Themed SVG Icon Lookup Caching, Cold Extents AABB Calculation
  * Batch Entity Transformation, Draworder Depth Map Full/Patch, Delta-Undo Recording
- Expose internal calculation hooks with #[doc(hidden)] pub to prevent API leakage.
- Remove 644 lines of legacy, scattered ad-hoc #[test] #[ignore] benchmarks across src/.
- Add developer and regression documentation in benches/README.md.
This commit is contained in:
Karim Jerbi committed 2026-09-12 13:46:01 +01:00
1 parent 754cc89fab
commit 58c9fcb748
9 files changed
+1786 -651

No files matched your search

+7 -258
View File
@@ -2661,7 +2661,8 @@ impl Scene {
/// is `None` for a freshly added entity. No-op when not recording — the
/// callers guard with [`Scene::is_recording_undo`] so the clone is skipped
/// entirely on the common (no-recording) path.
pub(crate) fn record_undo_before(&mut self, handle: Handle, before: Option<Arc<EntityType>>) {
#[doc(hidden)]
pub fn record_undo_before(&mut self, handle: Handle, before: Option<Arc<EntityType>>) {
if let Some(rec) = self.undo_recording.as_mut() {
if !rec.before.contains_key(&handle) {
rec.order.push(handle);
@@ -5377,7 +5378,8 @@ impl Scene {
/// frustum cull, no zoom LOD, and no re-tessellation/re-upload on camera
/// moves. The per-tile camera args are unused (kept for call-site symmetry
/// with the paper-space wire sources).
pub(super) fn model_tile_wires_arc(
#[doc(hidden)]
pub fn model_tile_wires_arc(
&self,
_tile_idx: usize,
_cam: &Camera,
@@ -6226,7 +6228,8 @@ impl Scene {
self.draw_depth_generation.get()
}
pub(super) fn draw_depth_map(&self) -> Arc<HashMap<u64, [f32; 2]>> {
#[doc(hidden)]
pub fn draw_depth_map(&self) -> Arc<HashMap<u64, [f32; 2]>> {
{
let cache = self.draw_depth_cache.borrow();
if let Some(cache) = cache.as_ref() {
@@ -12535,260 +12538,6 @@ mod layout_cache_tests {
2,
"Mixed polyline should have its 2 bulge arcs routed to analytical CircleGpu"
);
assert_eq!(
partitioned.regular.len(),
1,
"Mixed polyline straight lines should form 1 regular wire"
);
}
#[test]
#[ignore = "benchmark"]
fn bench_analytical_rendering() {
use std::time::Instant;
use acadrust::entities::{Arc as AcadArc, Circle, Ellipse, LwPolyline, LwVertex};
use acadrust::types::{Vector2, Vector3};
let n_circles = 5000;
let n_arcs = 5000;
let n_ellipses = 2000;
let n_polylines = 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 * std::f64::consts::PI;
arc.end_angle = arc.start_angle + 0.75 * std::f64::consts::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 = std::f64::consts::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 t_tess_start = Instant::now();
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
let tess_duration = t_tess_start.elapsed();
let total_wires = wires.len();
let mut chord_points = 0usize;
let mut analytical_tangents = 0usize;
for w in wires.iter() {
chord_points += w.points.len();
analytical_tangents += w.tangent_geoms.len();
}
let depths = rustc_hash::FxHashMap::default();
let t_part_start = Instant::now();
let iters = 100;
let mut part = None;
for _ in 0..iters {
part = Some(crate::scene::pipeline::wire_arena::partition_wires(&wires, &depths));
}
let part_duration = t_part_start.elapsed() / (iters as u32);
let p = part.unwrap();
// Zoom simulation
let zoom_levels = [0.1f32, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 50.0, 100.0, 1000.0];
let t_zoom_start = 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);
}
let zoom_100_duration = t_zoom_start.elapsed();
let per_zoom_ms = zoom_100_duration.as_secs_f64() * 1000.0 / 100.0;
let line_vram_mb = (chord_points * 36) as f64 / (1024.0 * 1024.0);
let inst_vram_mb = (p.circle_instances.len() * 128 + p.ellipse_instances.len() * 144) as f64 / (1024.0 * 1024.0);
println!("\n=== BENCHMARK REPORT: CURRENT HEAD (WITH GPU ANALYTICAL & OPTIMIZATIONS) ===");
println!("Curved Entities Tested: {total_entities}");
println!("Initial Wire Build Time: {:.2?}", tess_duration);
println!("Total Wires: {total_wires}");
println!("Tessellated Chord Vertices: {chord_points}");
println!("Analytical Tangent Geometries: {analytical_tangents}");
println!("Partitioning Time (per frame): {:.3?}", part_duration);
println!("Regular Line Wires: {}", p.regular.len());
println!("GPU Circle/Arc Instances: {}", p.circle_instances.len());
println!("GPU Ellipse Instances: {}", p.ellipse_instances.len());
println!("Line Arena VRAM (chord lines): {:.2} MB", line_vram_mb);
println!("GPU Analytical VRAM: {:.2} MB", inst_vram_mb);
println!("VRAM Reduction Ratio: {:.1}x", if inst_vram_mb > 0.0 { line_vram_mb / inst_vram_mb } else { 0.0 });
println!("100 Camera Zoom Navigations: {:.2?}", zoom_100_duration);
println!("Zoom Frame Overhead: {:.3} ms ({:.0} FPS)", per_zoom_ms, 1000.0 / per_zoom_ms.max(0.001));
println!("============================================================================\n");
}
#[test]
#[ignore = "benchmark"]
fn bench_wide_and_tapered_arc_rendering() {
use std::time::Instant;
use acadrust::entities::LwPolyline;
use acadrust::types::Vector2;
let n_wide_arcs = 2500;
let n_tapered_arcs = 2500;
let n_donuts = 2500;
let total_entities = n_wide_arcs + n_tapered_arcs + n_donuts;
let mut scene = Scene::new();
// 1. Wide polyline arcs (constant width 6.0)
for i in 0..n_wide_arcs {
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![
acadrust::entities::LwVertex {
location: Vector2::new(x, y),
bulge: 0.5,
start_width: 0.0,
end_width: 0.0,
vertex_id: 0,
},
acadrust::entities::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_tapered_arcs {
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![
acadrust::entities::LwVertex {
location: Vector2::new(x, y),
bulge: 0.7,
start_width: 2.0,
end_width: 12.0,
vertex_id: 0,
},
acadrust::entities::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 semicircular arc segments, constant width 16.0)
for i in 0..n_donuts {
let cx = (i % 50) as f64 * 80.0;
let cy = (i / 50) as f64 * 80.0 + 9000.0;
let donut = crate::modules::draw::draw::donut::make_donut(cx, cy, 0.0, 10.0, 26.0);
scene.add_entity(donut);
}
let cam = Camera::default();
let t_tess_start = Instant::now();
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
let tess_duration = t_tess_start.elapsed();
let total_wires = wires.len();
let mut chord_points = 0usize;
let mut analytical_tangents = 0usize;
for w in wires.iter() {
chord_points += w.points.len();
analytical_tangents += w.tangent_geoms.len();
}
let depths = rustc_hash::FxHashMap::default();
let t_part_start = Instant::now();
let iters = 100;
let mut part = None;
for _ in 0..iters {
part = Some(crate::scene::pipeline::wire_arena::partition_wires(&wires, &depths));
}
let part_duration = t_part_start.elapsed() / (iters as u32);
let p = part.unwrap();
// Zoom simulation
let zoom_levels = [0.1f32, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 50.0, 100.0, 1000.0];
let t_zoom_start = 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);
}
let zoom_100_duration = t_zoom_start.elapsed();
let per_zoom_ms = zoom_100_duration.as_secs_f64() * 1000.0 / 100.0;
let line_vram_mb = (chord_points * 36) as f64 / (1024.0 * 1024.0);
let inst_vram_mb = (p.circle_instances.len() * 128) as f64 / (1024.0 * 1024.0);
println!("\n=== BENCHMARK REPORT: THICK & TAPERED ANALYTICAL ARCS ===");
println!("Thick & Tapered Entities: {total_entities}");
println!("Initial Wire Build Time: {:.2?}", tess_duration);
println!("Total Wires: {total_wires}");
println!("Tessellated Chord Vertices: {chord_points}");
println!("Analytical Tangent Geometries: {analytical_tangents}");
println!("Partitioning Time (per frame): {:.3?}", part_duration);
println!("Regular Line Wires: {}", p.regular.len());
println!("GPU Circle/Arc Instances: {}", p.circle_instances.len());
println!("Line Arena VRAM (chord lines): {:.2} MB", line_vram_mb);
println!("GPU Analytical VRAM: {:.2} MB", inst_vram_mb);
println!("VRAM Reduction Ratio: {:.1}x", if inst_vram_mb > 0.0 { line_vram_mb / inst_vram_mb } else { 0.0 });
println!("100 Camera Zoom Navigations: {:.2?}", zoom_100_duration);
println!("Zoom Frame Overhead: {:.3} ms ({:.0} FPS)", per_zoom_ms, 1000.0 / per_zoom_ms.max(0.001));
println!("========================================================\n");
}
}
-82
View File
@@ -78,85 +78,3 @@ impl SelectionState {
}
}
#[cfg(test)]
mod bench_selection_clone_tests {
use super::SelectionState;
use iced::Point;
use std::hint::black_box;
use std::sync::Arc;
use std::time::Instant;
fn make_state(poly_len: usize) -> SelectionState {
let mut s = SelectionState {
vp_size: (1920.0, 1080.0),
poly_points: vec![Point::new(10.0, 10.0); poly_len],
..Default::default()
};
s.box_anchor = Some(Point::new(0.0, 0.0));
s.box_current = Some(Point::new(100.0, 100.0));
s
}
#[test]
#[ignore]
fn bench_selection_state_clone() {
let state = make_state(64);
for _ in 0..20 {
black_box(state.clone());
}
let n = 5000u32;
let start = Instant::now();
for _ in 0..n {
black_box(state.clone());
}
let elapsed = start.elapsed();
let per = elapsed / n;
println!(
"SelectionState::clone (deep, 64 pts): {:?} per clone (n={}, total {:?})",
per, n, elapsed
);
assert!(per.as_secs_f64() > 0.0);
}
#[test]
#[ignore]
fn bench_selection_arc_clone() {
let state = Arc::new(make_state(64));
for _ in 0..20 {
black_box(Arc::clone(&state));
}
let n = 5000u32;
let start = Instant::now();
for _ in 0..n {
black_box(Arc::clone(&state));
}
let elapsed = start.elapsed();
let per = elapsed / n;
println!(
"Arc<SelectionState>::clone (Arc bump, 64 pts): {:?} per clone (n={}, total {:?})",
per, n, elapsed
);
assert!(per.as_secs_f64() > 0.0);
}
#[test]
#[ignore]
fn bench_selection_state_clone_empty() {
let state = make_state(0);
for _ in 0..20 {
black_box(state.clone());
}
let n = 5000u32;
let start = Instant::now();
for _ in 0..n {
black_box(state.clone());
}
let elapsed = start.elapsed();
let per = elapsed / n;
println!(
"SelectionState::clone (deep, 0 pts): {:?} per clone (n={}, total {:?})",
per, n, elapsed
);
assert!(per.as_secs_f64() > 0.0);
}
}