From 133bfddba9dc994b1b5fadfe581c8ce0ebcd3e2d Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Tue, 22 Sep 2026 22:24:37 +0100 Subject: [PATCH] chore(perf): profile frame pipeline CPU side Adds ui_selection_overlay bench (benches/performance_benchmarks.rs + README metric 31): builds a representative selection overlay element (snap, crosshair, UCS tripod, 128 grips quick / 4096 capped full) and times widget-tree construction. Measured 0.4us typical, 2.4us at the cap (threshold 50us). Adds ribbon/statusbar section marks to the chrome bucket in view_main (src/app/view/mod.rs) so live PERF=1 view-detail traces attribute those two costs separately. Gated on the existing perf switch; no-op otherwise. Adds pub re-exports of CursorType/IsoPlane (src/app/mod.rs) for the bench fixture (same pattern as the existing grip re-exports). Ranked result over all ui_* benches: ribbon view construction ~556us (largest; follow-up already tracked), everything else under 50us or synthetic-fixture shapes. No new follow-up filed. GPU and canvas-draw costs are unmeasurable in headless runs (view() never executes there). --- benches/README.md | 3 +- benches/performance_benchmarks.rs | 121 +++++++++++++++++++++++++++++- src/app/mod.rs | 4 + src/app/view/mod.rs | 5 ++ 4 files changed, 128 insertions(+), 5 deletions(-) diff --git a/benches/README.md b/benches/README.md index 5e48f4325..92483d0e4 100644 --- a/benches/README.md +++ b/benches/README.md @@ -19,7 +19,7 @@ cargo bench --bench performance_benchmarks -- --quick --- -## Benchmark Suite Catalog (30 Metrics) +## Benchmark Suite Catalog (31 Metrics) ### 1. Scene & Entity Management - `scene_entity_ingestion_10k`: Ingestion throughput for 10,000 mixed 2D entities (Lines, Circles, Arcs, Polylines). @@ -56,6 +56,7 @@ cargo bench --bench performance_benchmarks -- --quick - `ui_grip_budget`: Budgeted per-frame selection-grip projection (dense-polyline grips capped to `MAX_SELECTED_GRIPS`). - `ui_grip_budget_build`: Selection-change `apply_grip_budget` cost over the full grip set (sort + FxHashSet + filter). - `ui_constraint_glyphs`: Constraint-glyph cache hit (`cached_glyph_placements` key build + `Arc` clone over dozens of glyphs, no recompute). +- `ui_selection_overlay`: Selection overlay Element construction (widget tree with capped grip count, no canvas draw). ### 6. State Management & Draworder - `selection_state_deep_clone`: Deep cloning of `SelectionState` (with 64 polygon points). diff --git a/benches/performance_benchmarks.rs b/benches/performance_benchmarks.rs index 4ab634200..8f37b0b2e 100644 --- a/benches/performance_benchmarks.rs +++ b/benches/performance_benchmarks.rs @@ -62,7 +62,9 @@ use OpenCADStudio::scene::{ChangeKind, Scene}; use OpenCADStudio::snap::Snapper; use OpenCADStudio::ui::icons::{self, CHECK}; use OpenCADStudio::ui::overlay::{ - grid_segments, should_reuse, GridCanvasState, GridKey, GridParams, GridStyle, + 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}; @@ -1362,9 +1364,9 @@ fn bench_ui_icon_caching(runner: &mut BenchmarkRunner) { } } -// ── 12b. Plot Style Layer-Usage Table Rebuild ─────────────────────────────── -// Covers Mission #19 `build_layer_usage`: layer names bucketed by ACI into a -// 256-bucket table, rebuilt per Plot Style modal view. +// ── 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") { @@ -1712,6 +1714,116 @@ fn bench_ui_constraint_glyphs(runner: &mut BenchmarkRunner) { ); } +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, + }, + SelectionVisualOptions::default(), + empty_glyphs, + empty_selected, + None, + None, + ) +} + // ── 13. Wide & Tapered Arc + Donut Tessellation ───────────────────────────── fn bench_wide_and_tapered_arc_tessellation(runner: &mut BenchmarkRunner) { @@ -2216,6 +2328,7 @@ fn main() { 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); diff --git a/src/app/mod.rs b/src/app/mod.rs index 367fab660..4b3dcb04e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -50,6 +50,10 @@ pub use style_ops::StyleKind; /// private / `pub(crate)`; only these two names are reachable externally. pub use properties::apply_grip_budget; pub use settings::MAX_SELECTED_GRIPS; +/// Re-exported for the `cargo bench` harness (`ui_selection_overlay` +/// constructs `CrosshairOptions`, which names these types). Modules stay +/// as they are; only these two names are reachable externally. +pub use settings::{CursorType, IsoPlane}; use document::DocumentTab; diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 962515415..a614e72c8 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -2121,6 +2121,10 @@ bg={bg_ms:.1}ms n={view_count}" self.show_block_palette, )); } + // Split the `chrome` bucket so live `view-detail` traces show + // ribbon vs status-bar construction separately (both gated on + // PERF; zero cost otherwise). + mark("ribbon"); if self.show_file_tabs { col = col.push(doc_tab_bar( &self.tabs, @@ -2262,6 +2266,7 @@ bg={bg_ms:.1}ms n={view_count}" }) .width(Fill) .height(Fill); + mark("statusbar"); // History labels are built only when their dropdown is actually open; // otherwise an empty slice short-circuits the overlay gate