//! 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 ` or `CAD_BENCH_BASELINE=` //! //! 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::collections::HashMap; use std::env; use std::f64::consts::{PI, TAU}; use std::hint::black_box; use std::io::Cursor; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Instant; use serde::{Deserialize, Serialize}; use acadrust::entities::{Arc as AcadArc, Circle, Ellipse, Line, LwPolyline, LwVertex}; use acadrust::types::{Vector2, Vector3}; use acadrust::{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; // ── Metric Structures ─────────────────────────────────────────────────────── #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BenchmarkMetric { pub name: String, pub description: String, pub unit: String, pub samples: usize, pub min: f64, pub max: f64, pub mean: f64, pub median: f64, pub p95: f64, pub p99: f64, pub std_dev: f64, pub throughput: Option, pub throughput_unit: Option, pub target_threshold: Option, pub passed_target: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BenchmarkSuiteReport { pub timestamp: String, pub os: String, pub arch: String, pub profile: String, pub is_quick_mode: bool, pub metrics: Vec, } pub struct BenchmarkRunner { pub quick_mode: bool, pub filter: Option, pub baseline_path: Option, pub output_path: PathBuf, pub results: Vec, } impl Default for BenchmarkRunner { fn default() -> Self { Self::new() } } impl BenchmarkRunner { pub fn new() -> Self { let args: Vec = env::args().collect(); let quick_mode = args.iter().any(|a| a == "--quick" || a == "-q") || env::var("CAD_BENCH_QUICK").is_ok(); let filter = args .windows(2) .find(|w| w[0] == "--filter") .map(|w| w[1].clone()) .or_else(|| env::var("CAD_BENCH_FILTER").ok()); let baseline_path = args .windows(2) .find(|w| w[0] == "--baseline") .map(|w| PathBuf::from(&w[1])) .or_else(|| env::var("CAD_BENCH_BASELINE").ok().map(PathBuf::from)); let output_path = args .windows(2) .find(|w| w[0] == "--output") .map(|w| PathBuf::from(&w[1])) .or_else(|| env::var("CAD_BENCH_OUTPUT").ok().map(PathBuf::from)) .unwrap_or_else(|| { let target_dir = Path::new("target"); if !target_dir.exists() { let _ = std::fs::create_dir_all(target_dir); } target_dir.join("cad_performance_metrics.json") }); Self { quick_mode, filter, baseline_path, output_path, results: Vec::new(), } } pub fn should_run(&self, name: &str) -> bool { if let Some(ref filter) = self.filter { name.to_lowercase().contains(&filter.to_lowercase()) } else { true } } /// Record a measured benchmark with multiple timing samples. pub fn record( &mut self, name: &str, description: &str, unit: &str, mut samples: Vec, throughput: Option<(f64, &str)>, target_threshold: Option, ) { if samples.is_empty() { return; } samples.sort_by(|a, b| a.total_cmp(b)); let n = samples.len(); let min = samples[0]; let max = samples[n - 1]; let sum: f64 = samples.iter().sum(); let mean = sum / (n as f64); let median = if n % 2 == 0 { (samples[n / 2 - 1] + samples[n / 2]) * 0.5 } else { samples[n / 2] }; let p95_idx = ((n as f64) * 0.95).floor() as usize; let p95 = samples[p95_idx.min(n - 1)]; let p99_idx = ((n as f64) * 0.99).floor() as usize; let p99 = samples[p99_idx.min(n - 1)]; let variance: f64 = samples.iter().map(|&x| (x - mean).powi(2)).sum::() / (n as f64); let std_dev = variance.sqrt(); let passed_target = match target_threshold { Some(thresh) => median <= thresh, None => true, }; let (thru_val, thru_unit) = match throughput { Some((val, u)) => (Some(val), Some(u.to_string())), None => (None, None), }; self.results.push(BenchmarkMetric { name: name.to_string(), description: description.to_string(), unit: unit.to_string(), samples: n, min, max, mean, median, p95, p99, std_dev, throughput: thru_val, throughput_unit: thru_unit, target_threshold, passed_target, }); } /// Print summary table to stdout and persist JSON report. pub fn finish(&self) { let baseline_map: HashMap = if let Some(ref path) = self.baseline_path { if let Ok(content) = std::fs::read_to_string(path) { if let Ok(report) = serde_json::from_str::(&content) { println!( "\nLoaded baseline from '{}' ({} metrics)", path.display(), report.metrics.len() ); report .metrics .into_iter() .map(|m| (m.name.clone(), m)) .collect() } else { eprintln!( "Warning: Failed to parse baseline JSON at '{}'", path.display() ); HashMap::new() } } else { eprintln!( "Warning: Could not read baseline file at '{}'", path.display() ); HashMap::new() } } else { HashMap::new() }; println!("\n========================================================================================================================"); println!(" OPENCADSTUDIO PERFORMANCE BENCHMARK REPORT "); println!("========================================================================================================================"); println!( "{:<38} | {:>8} | {:>8} | {:>8} | {:>8} | {:>16} | {:>10} | {:^6}", "Benchmark Name", "Median", "Mean", "P95", "Min", "Throughput", "Baseline", "Status" ); println!("------------------------------------------------------------------------------------------------------------------------"); for m in &self.results { let throughput_str = match (&m.throughput, &m.throughput_unit) { (Some(val), Some(unit)) => { if *val >= 1_000_000.0 { format!("{:.2}M {}", val / 1_000_000.0, unit) } else if *val >= 1_000.0 { format!("{:.1}k {}", val / 1_000.0, unit) } else { format!("{:.1} {}", val, unit) } } _ => "-".to_string(), }; let baseline_col = if let Some(base) = baseline_map.get(&m.name) { if base.median > 0.0 { let delta_pct = ((m.median - base.median) / base.median) * 100.0; if delta_pct < -1.0 { format!("{:+.1}% (fast)", delta_pct) } else if delta_pct > 1.0 { format!("{:+.1}% (slow)", delta_pct) } else { "~0.0%".to_string() } } else { "-".to_string() } } else { "-".to_string() }; let status = if m.passed_target { "PASS" } else { "FAIL" }; println!( "{:<38} | {:>6.2} {:<1} | {:>6.2} {:<1} | {:>6.2} {:<1} | {:>6.2} {:<1} | {:>16} | {:>10} | {:^6}", m.name, m.median, m.unit, m.mean, m.unit, m.p95, m.unit, m.min, m.unit, throughput_str, baseline_col, status, ); } println!("========================================================================================================================\n"); let report = BenchmarkSuiteReport { timestamp: chrono_now(), os: env::consts::OS.to_string(), arch: env::consts::ARCH.to_string(), profile: if cfg!(debug_assertions) { "debug".to_string() } else { "release".to_string() }, is_quick_mode: self.quick_mode, metrics: self.results.clone(), }; if let Ok(json) = serde_json::to_string_pretty(&report) { if let Some(parent) = self.output_path.parent() { let _ = std::fs::create_dir_all(parent); } if let Err(e) = std::fs::write(&self.output_path, json) { eprintln!( "Error saving metrics to '{}': {}", self.output_path.display(), e ); } else { println!( "Saved complete performance metrics JSON to: {}\n", self.output_path.display() ); } } } } fn chrono_now() -> String { let d = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default(); format!("unix_{}", d.as_secs()) } // ── 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::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 = acadrust::tables::Layer::new(&format!("LAYER_{i:03}")); layer.handle = doc.allocate_handle(); layer.color = acadrust::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 = (0..all_grips.len() as u64) .map(|k| acadrust::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 = (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 = acadrust::Handle::new(0x2000 + i as u64); scene.document.objects.insert( handle, acadrust::objects::ObjectType::Dictionary(acadrust::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, acadrust::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, acadrust::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, acadrust::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(); 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(); }