Files
OneCAD-Draft/benches/performance_benchmarks.rs
T
huanldandClaude Opus 5.5 279a87418e feat(automation): entity, block, publishing and REST operations from the fork
The operations docs/automation/API-SPEC.md (PR #1383) describes, brought
over from huaninstratech/OpenCADStudio so the specification no longer runs
ahead of the code:

- entity CRUD: entities_create, entities_delete, entities_transform,
  entities_copy_to, and `where` filters on query
- blocks and export: block_define, block_delete, a normalised wblock
- publishing: layout_create, page_setup_set, plot (per_page included)
- metadata: sysvar, xdata_set / xdata_get, file_identity
- collections: group_create, selection_set_save / load
- control plane: batch, events, view_focus, close
- the REST transport: `--http <port>`, the /api/v1 routes and their
  OpenAPI 3 description
- person-in-the-loop picks: user_select and getpoint, hosted by the GUI
  over the same REST routes, with on-screen pending signals

Taken from the fork's commits b7a7ed80, 69396b42, 64f1cf6a, c3aafc8e,
bb5a7bfa, 52317a5c, 50d293ba, b90e194f, 517c6397, 53a5c4c9, a49ac045,
37de5a46, a78b1822, 6d9aed2a, 33f522da, 5b5cafe2, 02a7e78d, 94df40dc,
fd5bc14d, 485fac61 and 2c5fc994.

Left out on purpose: 37de5a46 also removed the donation surface (the
Patreon fetch, the donation prompt, the Start-page supporters rail, the
DONATE command and its locale strings) and replaced the About dialog's call
to action; that is the fork's own product decision and stays out. The
fork's version bump and its unrelated IFC/BIM work are not part of this.

Adapted while porting: the REST server and the GUI bridge are desktop-only
and a template start is refused in the web build, so the wasm target still
compiles; the two plot tests read the PDF through the shared stream helper,
because exported streams are Flate-compressed; the benches set the new
crosshair field; comments and docs describe each operation in its own
terms rather than by another product's API names.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 13:41:07 +03:00

2342 lines
81 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::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<f64>,
pub throughput_unit: Option<String>,
pub target_threshold: Option<f64>,
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<BenchmarkMetric>,
}
pub struct BenchmarkRunner {
pub quick_mode: bool,
pub filter: Option<String>,
pub baseline_path: Option<PathBuf>,
pub output_path: PathBuf,
pub results: Vec<BenchmarkMetric>,
}
impl Default for BenchmarkRunner {
fn default() -> Self {
Self::new()
}
}
impl BenchmarkRunner {
pub fn new() -> Self {
let args: Vec<String> = 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<f64>,
throughput: Option<(f64, &str)>,
target_threshold: Option<f64>,
) {
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::<f64>() / (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<String, BenchmarkMetric> =
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::<BenchmarkSuiteReport>(&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<SelectionState>::clone (atomic reference count increment)",
"ns",
arc_samples,
Some((1_000_000_000.0 / median_arc_ns, "clones/s")),
Some(20.0),
);
}
// ── 10. UI Ribbon View Construction Benchmark ──────────────────────────────
fn bench_ui_ribbon_view_construction(runner: &mut BenchmarkRunner) {
if !runner.should_run("ui_ribbon_view_construction") {
return;
}
let mut ribbon = Ribbon::new();
ribbon.set_styles(
vec![
"Standard".to_string(),
"Title".to_string(),
"Annotative".to_string(),
],
"Standard",
vec!["Standard".to_string()],
"Standard",
vec!["Standard".to_string()],
"Standard",
vec!["Standard".to_string()],
"Standard",
);
ribbon.set_layers(
vec![
LayerInfo {
name: "0".to_string(),
color: Color::TRANSPARENT,
visible: true,
frozen: false,
locked: false,
},
LayerInfo {
name: "DRAWING".to_string(),
color: Color::TRANSPARENT,
visible: true,
frozen: false,
locked: false,
},
LayerInfo {
name: "DIMENSIONS".to_string(),
color: Color::TRANSPARENT,
visible: true,
frozen: false,
locked: false,
},
LayerInfo {
name: "ANNOTATIONS".to_string(),
color: Color::TRANSPARENT,
visible: true,
frozen: false,
locked: false,
},
],
"0",
);
ribbon.set_available_linetypes(vec![
LinetypeItem {
name: "Continuous".to_string(),
art: String::new(),
},
LinetypeItem {
name: "DASHED".to_string(),
art: String::new(),
},
LinetypeItem {
name: "CENTER".to_string(),
art: String::new(),
},
LinetypeItem {
name: "HIDDEN".to_string(),
art: String::new(),
},
]);
// Warm-up for allocator and tree settling
for _ in 0..10 {
let _ = black_box(ribbon.view(false, false, 0, 0, false));
}
let n = if runner.quick_mode { 20 } else { 100 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let elem = ribbon.view(false, false, 0, 0, false);
black_box(elem);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n as f64);
samples.push(per_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
"ui_ribbon_view_construction",
"Ribbon::view widget tree construction per frame (populated tabs, styles, layers)",
"µs",
samples,
Some((1_000_000.0 / median_us, "views/s")),
Some(3000.0), // Target threshold < 3.0 ms
);
}
// ── 11. UI Grid Geometry Generation & Cache Hit Benchmark ──────────────────
fn bench_ui_grid_geometry(runner: &mut BenchmarkRunner) {
let view_rot1 = Mat4::from_rotation_x(0.15) * Mat4::from_rotation_y(0.05);
let eye1 = glam::DVec3::new(4.0, 3.5, 9.0);
let bounds1 = Rectangle {
x: 0.0,
y: 0.0,
width: 1280.0,
height: 720.0,
};
let step1 = 80.0_f32;
let origin1 = glam::DVec3::new(0.0, 0.0, 0.0);
let axes1 = (Vec3::X, Vec3::Y, Vec3::Z);
let limits1: Option<(glam::DVec2, glam::DVec2)> = None;
let view_rot2 = Mat4::from_rotation_x(0.15) * Mat4::from_rotation_y(0.05);
let eye2 = glam::DVec3::new(4.0, 3.5, 9.0);
let bounds2 = Rectangle {
x: 1280.0,
y: 0.0,
width: 640.0,
height: 720.0,
};
let step2 = 160.0_f32;
let origin2 = glam::DVec3::new(0.0, 0.0, 0.0);
let axes2 = (Vec3::X, Vec3::Y, Vec3::Z);
let limits2: Option<(glam::DVec2, glam::DVec2)> = None;
// Both panes use a square grid, so one step per pane plus a shared
// major-line interval covers the new step_x / step_y / major_every args.
let major_every = 5u32;
// ── 1. Uncached grid geometry generation ──
if runner.should_run("ui_grid_geometry_uncached") {
for _ in 0..10 {
let _ = black_box(grid_segments(
view_rot1, eye1, bounds1, step1, step1, major_every, origin1, axes1, limits1,
));
let _ = black_box(grid_segments(
view_rot2, eye2, bounds2, step2, step2, major_every, origin2, axes2, limits2,
));
}
let n = if runner.quick_mode { 20 } else { 100 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let g1 = grid_segments(view_rot1, eye1, bounds1, step1, step1, major_every, origin1, axes1, limits1);
let g2 = grid_segments(view_rot2, eye2, bounds2, step2, step2, major_every, origin2, axes2, limits2);
black_box(g1);
black_box(g2);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n as f64);
samples.push(per_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
"ui_grid_geometry_uncached",
"Viewport 2-pane tiled grid projection & segment generation (uncached)",
"µs",
samples,
Some((1_000_000.0 / median_us, "frames/s")),
Some(800.0), // Target threshold < 800 µs
);
}
// ── 2. Cached grid key hit evaluation ──
if runner.should_run("ui_grid_cache_hit_evaluation") {
let params1 = GridParams {
view_rot: view_rot1,
eye: eye1,
bounds: bounds1,
step_x: step1,
step_y: step1,
major_every,
origin: origin1,
axes: axes1,
limits: limits1,
};
let params2 = GridParams {
view_rot: view_rot2,
eye: eye2,
bounds: bounds2,
step_x: step2,
step_y: step2,
major_every,
origin: origin2,
axes: axes2,
limits: limits2,
};
let grids = vec![params1, params2];
let canvas_bounds = Rectangle {
x: 0.0,
y: 0.0,
width: 1920.0,
height: 720.0,
};
let state = GridCanvasState::default();
let stored_key = GridKey::from_grids(&grids, canvas_bounds, GridStyle::default());
*state.key.borrow_mut() = Some(stored_key);
for _ in 0..50 {
let key = GridKey::from_grids(&grids, canvas_bounds, GridStyle::default());
let hit = should_reuse(state.key.borrow().as_ref(), &key);
black_box(hit);
}
let n = if runner.quick_mode { 1_000 } else { 10_000 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
let mut hit_count = 0u32;
for _ in 0..n {
let key = GridKey::from_grids(
black_box(&grids),
black_box(canvas_bounds),
GridStyle::default(),
);
if should_reuse(state.key.borrow().as_ref(), &key) {
hit_count += 1;
}
}
assert_eq!(hit_count as usize, n);
let per_ns = (t0.elapsed().as_nanos() as f64) / (n as f64);
samples.push(per_ns);
}
let median_ns = samples[samples.len() / 2];
runner.record(
"ui_grid_cache_hit_evaluation",
"Grid overlay cache key construction + reuse hit decision",
"ns",
samples,
Some((1_000_000_000.0 / median_ns, "evals/s")),
Some(500.0), // Target threshold < 500 ns
);
}
}
// ── 12. UI Themed SVG Icon Caching Benchmark ───────────────────────────────
fn bench_ui_icon_caching(runner: &mut BenchmarkRunner) {
let bytes = CHECK;
// Warm up the themed handle cache
let _ = icons::themed_handle(bytes);
if runner.should_run("ui_icon_handle_cached") {
let n = if runner.quick_mode { 5_000 } else { 50_000 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let handle = icons::themed_handle(black_box(bytes));
black_box(handle);
}
let per_ns = (t0.elapsed().as_nanos() as f64) / (n as f64);
samples.push(per_ns);
}
let median_ns = samples[samples.len() / 2];
runner.record(
"ui_icon_handle_cached",
"Themed SVG icon cache hit (FxHashMap lookup + Handle clone)",
"ns",
samples,
Some((1_000_000_000.0 / median_ns, "lookups/s")),
Some(100.0), // Target threshold < 100 ns
);
}
if runner.should_run("ui_icon_handle_uncached") {
let n = if runner.quick_mode { 200 } else { 2_000 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let handle = iced::widget::svg::Handle::from_memory(black_box(bytes));
black_box(handle);
}
let per_ns = (t0.elapsed().as_nanos() as f64) / (n as f64);
samples.push(per_ns);
}
let median_ns = samples[samples.len() / 2];
runner.record(
"ui_icon_handle_uncached",
"Uncached SVG icon memory parse + Handle allocation",
"ns",
samples,
Some((1_000_000_000.0 / median_ns, "parses/s")),
None, // Reference comparison
);
}
}
// ── Plot Style Layer-Usage Table Rebuild ─────────────────────────────────────
// Covers `build_layer_usage`: layer names bucketed by ACI into a 256-bucket
// table, rebuilt per Plot Style modal view.
fn bench_ui_plotstyle_layer_usage(runner: &mut BenchmarkRunner) {
if !runner.should_run("ui_plotstyle_layer_usage") {
return;
}
let mut doc = CadDocument::new();
for i in 0..200 {
let mut layer = 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<acadrust::Handle> = (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<GripMarker> = (0..n_grips)
.map(|i| GripMarker {
pos: Point::new(100.0 + i as f32 * 5.0, 200.0),
shape: GripShape::Square,
is_hot: false,
is_hovered: false,
dir: None,
})
.collect();
let ucs_icons = vec![UcsIconParams {
view_proj: Mat4::IDENTITY,
bounds: Rectangle { x: 0.0, y: 0.0, width: 1920.0, height: 1080.0 },
axes: (Vec3::X, Vec3::Y, Vec3::Z),
origin_screen: None,
hover: false,
selected: false,
}];
let empty_glyphs: Arc<[GlyphEntry]> = Arc::from([]);
let empty_selected: Arc<[bool]> = Arc::from([]);
selection_overlay(
selection,
Some((Point::new(400.0, 300.0), SnapType::Endpoint)),
None,
None,
grips,
None,
None,
ucs_icons,
vec![OstTrackPoint { screen: Point::new(500.0, 500.0) }],
vec![(Point::new(0.0, 0.0), Point::new(100.0, 100.0))],
None,
true,
vec![],
None,
None,
false,
false,
false,
[0.1, 0.1, 0.1, 1.0],
CrosshairOptions {
size_percent: 5,
pick_box: 3,
cursor_type: CursorType::Crosshair,
color: None,
isometric: false,
iso_plane: IsoPlane::Top,
snap_angle_deg: 0.0,
point_mode: false,
pick_pending: false,
},
SelectionVisualOptions::default(),
empty_glyphs,
empty_selected,
None,
None,
)
}
// ── 13. Wide & Tapered Arc + Donut Tessellation ─────────────────────────────
fn bench_wide_and_tapered_arc_tessellation(runner: &mut BenchmarkRunner) {
let name = "wide_and_tapered_arc_tessellation";
if !runner.should_run(name) {
return;
}
let (n_wide, n_taper, n_donut) = if runner.quick_mode {
(500, 500, 500)
} else {
(2500, 2500, 2500)
};
let total_entities = n_wide + n_taper + n_donut;
let runs = if runner.quick_mode { 3 } else { 5 };
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let mut scene = Scene::new();
// 1. Wide polyline arcs (constant width 6.0)
for i in 0..n_wide {
let x = (i % 50) as f64 * 80.0;
let y = (i / 50) as f64 * 80.0;
let mut pline = LwPolyline::new();
pline.constant_width = 6.0;
pline.vertices = vec![
LwVertex {
location: Vector2::new(x, y),
bulge: 0.5,
start_width: 0.0,
end_width: 0.0,
vertex_id: 0,
},
LwVertex {
location: Vector2::new(x + 40.0, y),
bulge: 0.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 1,
},
];
scene.add_entity(EntityType::LwPolyline(pline));
}
// 2. Tapered arcs (width: 2.0 -> 12.0)
for i in 0..n_taper {
let x = (i % 50) as f64 * 80.0;
let y = (i / 50) as f64 * 80.0 + 4500.0;
let mut pline = LwPolyline::new();
pline.vertices = vec![
LwVertex {
location: Vector2::new(x, y),
bulge: 0.7,
start_width: 2.0,
end_width: 12.0,
vertex_id: 0,
},
LwVertex {
location: Vector2::new(x + 35.0, y + 10.0),
bulge: 0.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 1,
},
];
scene.add_entity(EntityType::LwPolyline(pline));
}
// 3. Donuts (2 closed arc segments, inner 10.0, outer 26.0)
for i in 0..n_donut {
let cx = (i % 50) as f64 * 80.0;
let cy = (i / 50) as f64 * 80.0 + 9000.0;
let inner_r = 10.0;
let outer_r = 26.0;
let r_avg = (inner_r + outer_r) * 0.5;
let width = outer_r - inner_r;
let mut pline = LwPolyline::new();
pline.is_closed = true;
pline.constant_width = width;
pline.vertices = vec![
LwVertex {
location: Vector2::new(cx - r_avg, cy),
bulge: 1.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 0,
},
LwVertex {
location: Vector2::new(cx + r_avg, cy),
bulge: 1.0,
start_width: 0.0,
end_width: 0.0,
vertex_id: 1,
},
];
scene.add_entity(EntityType::LwPolyline(pline));
}
let cam = Camera::default();
let t0 = Instant::now();
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
black_box(wires);
let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
samples.push(elapsed_ms);
}
let median_ms = samples[samples.len() / 2];
let throughput = (total_entities as f64) / (median_ms / 1000.0);
runner.record(
name,
"Tessellating 7.5k wide arcs, tapered arcs & donuts (mesh offset curves)",
"ms",
samples,
Some((throughput, "entities/s")),
Some(100.0),
);
}
// ── 14. Model Space Extents Calculation (ZOOM EXTENTS) ──────────────────────
fn bench_zoom_extents_calculation(runner: &mut BenchmarkRunner) {
let name = "zoom_extents_bounding_box_calc";
if !runner.should_run(name) {
return;
}
let n = if runner.quick_mode { 4_000 } else { 20_000 };
let mut scene = Scene::new();
for i in 0..n {
let x = (i % 200) as f64 * 50.0;
let y = (i / 200) as f64 * 50.0;
match i % 4 {
0 => {
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 40.0, y + 40.0, 0.0);
scene.add_entity(EntityType::Line(line));
}
1 => {
let mut c = Circle::new();
c.center = Vector3::new(x + 20.0, y + 20.0, 0.0);
c.radius = 15.0;
scene.add_entity(EntityType::Circle(c));
}
2 => {
let mut a = AcadArc::new();
a.center = Vector3::new(x + 20.0, y + 20.0, 0.0);
a.radius = 15.0;
a.start_angle = 0.5;
a.end_angle = 3.5;
scene.add_entity(EntityType::Arc(a));
}
_ => {
let mut pl = LwPolyline::new();
pl.vertices = vec![
LwVertex::new(Vector2::new(x, y)),
LwVertex::new(Vector2::new(x + 20.0, y + 10.0)),
LwVertex::new(Vector2::new(x + 40.0, y)),
];
scene.add_entity(EntityType::LwPolyline(pl));
}
}
}
let runs = if runner.quick_mode { 5 } else { 15 };
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
// Invalidate extents cache to measure cold AABB traversal across all entities
scene.invalidate_draw_depth();
scene.bump_entities(&[]);
let t0 = Instant::now();
let ext = scene.model_space_extents();
black_box(ext);
let elapsed_us = t0.elapsed().as_micros() as f64;
samples.push(elapsed_us);
}
let median_us = samples[samples.len() / 2];
let throughput = (n as f64) / (median_us / 1_000_000.0);
runner.record(
name,
"Computing model space bounding box across 20k mixed entities (ZOOM EXTENTS)",
"µs",
samples,
Some((throughput, "entities/s")),
Some(10000.0), // Target threshold < 10.0 ms
);
}
// ── 15. Batch Entity Transformation & Mutation ──────────────────────────────
fn bench_batch_entity_mutation(runner: &mut BenchmarkRunner) {
let name = "batch_entity_transform_mutation";
if !runner.should_run(name) {
return;
}
let total_entities = if runner.quick_mode { 2_000 } else { 10_000 };
let mutate_count = if runner.quick_mode { 200 } else { 1_000 };
let mut scene = Scene::new();
let mut handles = Vec::with_capacity(total_entities);
for i in 0..total_entities {
let x = (i % 100) as f64 * 30.0;
let y = (i / 100) as f64 * 30.0;
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 20.0, y + 20.0, 0.0);
let h = scene.add_entity(EntityType::Line(line));
handles.push(h);
}
let cam = Camera::default();
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
let index = InteractionIndex::build(&wires);
index.prepare_screen();
let runs = if runner.quick_mode { 5 } else { 10 };
let mut samples = Vec::with_capacity(runs);
for run_idx in 0..runs {
let offset = (run_idx as f64 + 1.0) * 2.0;
let mut modified = Vec::with_capacity(mutate_count);
for &h in handles.iter().take(mutate_count) {
if let Some(EntityType::Line(ref mut line)) = scene.document.get_entity_mut(h) {
line.start.x += offset;
line.end.x += offset;
}
modified.push((h, ChangeKind::Modified));
}
let t0 = Instant::now();
scene.bump_entities(&modified);
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
let idx = InteractionIndex::build(&wires);
idx.prepare_screen();
let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
black_box(idx);
samples.push(elapsed_ms);
}
let median_ms = samples[samples.len() / 2];
let throughput = (mutate_count as f64) / (median_ms / 1000.0);
runner.record(
name,
"Incremental dirty propagation & spatial index update for 1,000 modified entities",
"ms",
samples,
Some((throughput, "entities/s")),
Some(15.0), // Target threshold < 15.0 ms
);
}
// ── 16. Draworder Evaluation (Full Build & Incremental Patch) ───────────────
fn bench_draworder_evaluation(runner: &mut BenchmarkRunner) {
let total_entities = if runner.quick_mode { 2_000 } else { 10_000 };
let mut scene = Scene::new();
let mut handles = Vec::with_capacity(total_entities);
for i in 0..total_entities {
let x = (i % 100) as f64 * 30.0;
let y = (i / 100) as f64 * 30.0;
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 20.0, y + 20.0, 0.0);
let h = scene.add_entity(EntityType::Line(line));
handles.push(h);
}
// ── Full build ──
if runner.should_run("draworder_depth_map_full_build") {
let runs = if runner.quick_mode { 5 } else { 10 };
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
scene.invalidate_draw_depth();
let t0 = Instant::now();
let depths = scene.draw_depth_map();
black_box(depths);
let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
samples.push(elapsed_ms);
}
let median_ms = samples[samples.len() / 2];
let throughput = (total_entities as f64) / (median_ms / 1000.0);
runner.record(
"draworder_depth_map_full_build",
"Initial draworder depth map construction across 10,000 entities",
"ms",
samples,
Some((throughput, "entities/s")),
Some(15.0),
);
}
// ── Incremental patch ──
if runner.should_run("draworder_depth_map_incremental_patch") {
let _ = scene.draw_depth_map();
let n_patch = if runner.quick_mode { 100 } else { 1_000 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for j in 0..n_patch {
let h = handles[j % handles.len()];
scene.bump_entities(&[(h, ChangeKind::Modified)]);
let depths = scene.draw_depth_map();
black_box(depths);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n_patch as f64);
samples.push(per_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
"draworder_depth_map_incremental_patch",
"Draworder depth map fast-path retrieval under entity modifications",
"µs",
samples,
Some((1_000_000.0 / median_us, "patches/s")),
Some(50.0), // Target threshold < 50 µs
);
}
}
// ── 17. Delta-Undo Transaction Before-Image Recording ───────────────────────
fn bench_undo_delta_recording(runner: &mut BenchmarkRunner) {
let name = "undo_delta_recording_cycle";
if !runner.should_run(name) {
return;
}
let total = if runner.quick_mode { 1_000 } else { 5_000 };
let mutate_count = if runner.quick_mode { 100 } else { 500 };
let mut scene = Scene::new();
let mut handles = Vec::with_capacity(total);
for i in 0..total {
let x = (i % 100) as f64 * 30.0;
let y = (i / 100) as f64 * 30.0;
let mut line = Line::new();
line.start = Vector3::new(x, y, 0.0);
line.end = Vector3::new(x + 20.0, y + 20.0, 0.0);
let h = scene.add_entity(EntityType::Line(line));
handles.push(h);
}
let runs = if runner.quick_mode { 5 } else { 15 };
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
scene.begin_undo_recording();
for &h in handles.iter().take(mutate_count) {
let before = scene.document.get_entity(h).map(|e| Arc::new(e.clone()));
scene.record_undo_before(h, before);
}
let rec = scene.take_undo_recording();
black_box(rec);
let elapsed_us = t0.elapsed().as_micros() as f64;
samples.push(elapsed_us);
}
let median_us = samples[samples.len() / 2];
let throughput = (mutate_count as f64) / (median_us / 1_000_000.0);
runner.record(
name,
"Capturing delta-undo transaction before-images for 500 entity mutations",
"µs",
samples,
Some((throughput, "records/s")),
Some(1000.0), // Target threshold < 1000 µs (1 ms)
);
}
fn bench_view_render_viewport_construction(runner: &mut BenchmarkRunner) {
if !runner.should_run("view_render") {
return;
}
let obj_count = if runner.quick_mode { 1_000 } else { 10_000 };
let mut scene = Scene::new();
// Populate scene with non-graphical document objects
for i in 0..obj_count {
let handle = 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();
}