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

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

No files matched your search

+2 -1
View File
@@ -159,7 +159,8 @@ thread_local! {
/// trigger a `RefCell` panic on the second `borrow_mut`, because
/// no `borrow_mut` is held while parsing runs. (The same
/// `thread_local!` + `RefCell` shape is used by `SEMANTIC_CACHE`.)
fn themed_handle(bytes: &'static [u8]) -> svg::Handle {
#[doc(hidden)]
pub fn themed_handle(bytes: &'static [u8]) -> svg::Handle {
let key = (bytes.as_ptr() as usize, bytes.len());
if let Some(handle) = THEMED_CACHE.with(|cache| cache.borrow().get(&key).cloned()) {
return handle;
+14 -172
View File
@@ -380,7 +380,8 @@ pub struct GridParams {
/// uses to size the coloured UCS axes overlay. Returned from `grid_segments` so
/// the renderer-free geometry construction can be unit-tested and benchmarked
/// without an iced `Renderer` (Mission #1, 2026-08-26 bench-first plan).
pub(crate) struct GridGeometry {
#[doc(hidden)]
pub struct GridGeometry {
pub segments: Vec<(Point, Point)>,
pub axis_extent: f32,
}
@@ -390,7 +391,8 @@ impl GridGeometry {
/// early-exit branches of `grid_segments` (zero-sized bounds, no visible
/// samples, non-finite step) so the caller never needs to special-case
/// the `None` path.
fn empty() -> Self {
#[doc(hidden)]
pub fn empty() -> Self {
Self { segments: Vec::new(), axis_extent: 0.0 }
}
}
@@ -405,8 +407,9 @@ impl GridGeometry {
/// `GridParams` set is required for correctness.
///
/// Added 2026-08-26 by Mission #1 (grid overlay cache, Tier 1 #1).
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct GridKey {
pub struct GridKey {
pub grids: Vec<GridParams>,
pub bounds: iced::Rectangle,
pub style: GridStyle,
@@ -414,7 +417,8 @@ pub(crate) struct GridKey {
impl GridKey {
/// Build a key from the per-pane `GridParams`, overlay bounds, and grid style.
pub(crate) fn from_grids(grids: &[GridParams], bounds: iced::Rectangle, style: GridStyle) -> Self {
#[doc(hidden)]
pub fn from_grids(grids: &[GridParams], bounds: iced::Rectangle, style: GridStyle) -> Self {
Self { grids: grids.to_vec(), bounds, style }
}
}
@@ -423,7 +427,8 @@ impl GridKey {
/// `new`. Reference-based to avoid moving the (potentially large) `Vec` of
/// per-pane params; the caller borrows from `RefCell<Option<GridKey>>` on
/// both sides.
pub(crate) fn should_reuse(cached: Option<&GridKey>, new: &GridKey) -> bool {
#[doc(hidden)]
pub fn should_reuse(cached: Option<&GridKey>, new: &GridKey) -> bool {
match cached {
Some(old) => old == new,
None => false,
@@ -440,8 +445,9 @@ pub(crate) fn should_reuse(cached: Option<&GridKey>, new: &GridKey) -> bool {
/// bounds happen to be unchanged (e.g. a pan within the same canvas size).
///
/// Added 2026-08-26 by Mission #1 (grid overlay cache, Tier 1 #1).
#[doc(hidden)]
#[derive(Default)]
pub(crate) struct GridCanvasState {
pub struct GridCanvasState {
pub key: RefCell<Option<GridKey>>,
pub cache: canvas::Cache<iced::Renderer>,
}
@@ -1761,8 +1767,9 @@ fn draw_grid(
/// Extracted from `draw_grid` (2026-08-26, Mission #1 step 1) so the geometry
/// construction can be unit-tested and benchmarked without an iced
/// `Renderer`. Behaviour is identical to the inlined version that preceded it.
#[doc(hidden)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn grid_segments(
pub fn grid_segments(
view_rot: Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
@@ -3306,171 +3313,6 @@ mod clip_tests {
}
}
#[cfg(test)]
mod bench_grid_geometry_tests {
use super::*;
use std::hint::black_box;
use std::time::Instant;
/// Benchmarks the pure grid geometry construction (uncached).
/// Represents a 2-pane tiled Model layout: pane 1 at x=0..1280, pane 2 at
/// x=1280..1920. Slight tilt, typical eye, step 80 (pane 1) / 160 (pane 2).
/// RED: requires `grid_segments(...)` which does not exist yet — compilation
/// must fail with E0425 "cannot find function `grid_segments`". The bench
/// becomes meaningful at Step 1 once the helper is extracted.
#[test]
#[ignore]
fn bench_grid_geometry_uncached() {
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 = iced::Rectangle {
x: 0.0,
y: 0.0,
width: 1280.0,
height: 720.0,
};
let step1 = 80.0_f32;
let grid_origin1 = glam::DVec3::new(0.0, 0.0, 0.0);
let grid_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 = iced::Rectangle {
x: 1280.0,
y: 0.0,
width: 640.0,
height: 720.0,
};
let step2 = 160.0_f32;
let grid_origin2 = glam::DVec3::new(0.0, 0.0, 0.0);
let grid_axes2 = (Vec3::X, Vec3::Y, Vec3::Z);
let limits2: Option<(glam::DVec2, glam::DVec2)> = None;
for _ in 0..20 {
let _ = black_box(grid_segments(
black_box(view_rot1),
black_box(eye1),
black_box(bounds1),
black_box(step1),
black_box(grid_origin1),
black_box(grid_axes1),
black_box(limits1),
));
let _ = black_box(grid_segments(
black_box(view_rot2),
black_box(eye2),
black_box(bounds2),
black_box(step2),
black_box(grid_origin2),
black_box(grid_axes2),
black_box(limits2),
));
}
let n = 200u32;
let start = Instant::now();
for _ in 0..n {
let _ = black_box(grid_segments(
black_box(view_rot1),
black_box(eye1),
black_box(bounds1),
black_box(step1),
black_box(grid_origin1),
black_box(grid_axes1),
black_box(limits1),
));
let _ = black_box(grid_segments(
black_box(view_rot2),
black_box(eye2),
black_box(bounds2),
black_box(step2),
black_box(grid_origin2),
black_box(grid_axes2),
black_box(limits2),
));
}
let elapsed = start.elapsed();
let per_frame = elapsed / n;
println!(
"grid_segments uncached: {:?} per frame (n = {}, total {:?})",
per_frame, n, elapsed
);
assert!(per_frame.as_secs_f64() > 0.0, "per-frame time must be positive");
}
/// A/B partner of `bench_grid_geometry_uncached` (Mission #1, step 6).
/// Times the hit-path decision only: build `GridKey` from the current
/// pane params + canvas bounds, borrow the stored key, call
/// `should_reuse`. Mirrors the body of the hit branch in
/// `GridCanvas::draw`. Excludes the iced `canvas::Cache` internals
/// (Arc-clone + draw_with_bounds fast path) because they live in the
/// fork and are not what we added; measures only the cost we own.
#[test]
#[ignore]
fn bench_grid_geometry_cached() {
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 = iced::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 = iced::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;
let params1 = GridParams {
view_rot: view_rot1, eye: eye1, bounds: bounds1, step: step1,
origin: origin1, axes: axes1, limits: limits1,
};
let params2 = GridParams {
view_rot: view_rot2, eye: eye2, bounds: bounds2, step: step2,
origin: origin2, axes: axes2, limits: limits2,
};
let grids = vec![params1, params2];
// Overall canvas bounds — what `GridCanvas::draw` receives and
// passes to `GridKey::from_grids`. The 1920×720 covers the two
// tiled panes (1280 + 640).
let canvas_bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 1920.0, height: 720.0 };
// Pre-seed a `GridCanvasState` with the same key the bench will
// build each iteration — guaranteed hit path.
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..20 {
let key = GridKey::from_grids(black_box(&grids), black_box(canvas_bounds), GridStyle::default());
let hit = should_reuse(state.key.borrow().as_ref(), &key);
black_box(hit);
}
let n = 200u32;
let start = 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;
}
}
let elapsed = start.elapsed();
let per_frame = elapsed / n;
assert_eq!(hit_count, n, "bench should always hit (sanity)");
println!(
"grid key + should_reuse (hit path): {:?} per frame (n = {}, total {:?})",
per_frame, n, elapsed
);
assert!(per_frame.as_secs_f64() > 0.0, "per-frame time must be positive");
}
}
#[cfg(test)]
mod grid_key_tests {
use super::*;
+1 -72
View File
@@ -1610,76 +1610,5 @@ mod tests {
assert!(ribbon.escape_extension());
assert_eq!(ribbon.open_dropdown, None);
}
/// Reproducible element-construction benchmark for the ribbon view. Run with:
/// `cargo test --lib --release -- --ignored --nocapture bench_ribbon_view_construction`
///
/// Times `Ribbon::view()` element construction — the dominant cost in the
/// cheap (non-render) part of a ribbon frame, and the thing Mission #4
/// reworked. `#[ignore]`d so normal runs stay silent; state is populated so
/// Auto-mode panels actually have four densities to build.
#[test]
#[ignore]
fn bench_ribbon_view_construction() {
use std::time::Instant;
use super::*;
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,
},
],
"0",
);
ribbon.set_available_linetypes(vec![
LinetypeItem {
name: "Continuous".to_string(),
art: String::new(),
},
LinetypeItem {
name: "DASHED".to_string(),
art: String::new(),
},
]);
let n = 200u32;
// Warm-up for allocator/tree-slot settling before timing begins.
let _ = ribbon.view(false, false, 0, 0, false);
let start = Instant::now();
for _ in 0..n {
let _ = ribbon.view(false, false, 0, 0, false);
}
let per_frame = start.elapsed() / n;
println!(
"bench_ribbon_view_construction: {per_frame:?} per `Ribbon::view()` \
element build (n = {n}, release measurement recommended)"
);
}
}