diff --git a/benches/README.md b/benches/README.md index a9af5c970..a91fa0450 100644 --- a/benches/README.md +++ b/benches/README.md @@ -19,7 +19,7 @@ cargo bench --bench performance_benchmarks -- --quick --- -## Benchmark Suite Catalog (25 Metrics) +## Benchmark Suite Catalog (26 Metrics) ### 1. Scene & Entity Management - `scene_entity_ingestion_10k`: Ingestion throughput for 10,000 mixed 2D entities (Lines, Circles, Arcs, Polylines). @@ -51,6 +51,7 @@ cargo bench --bench performance_benchmarks -- --quick - `ui_grid_cache_hit_evaluation`: Grid overlay cache key construction and reuse decision. - `ui_icon_handle_cached`: Themed SVG icon cache hit (FxHashMap lookup + Handle clone). - `ui_icon_handle_uncached`: Uncached SVG icon memory parse and Handle allocation. +- `ui_plotstyle_layer_usage`: Plot Style 256-bucket ACI layer-usage table rebuild (200 layers). ### 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 f51a08aa6..0374eaf68 100644 --- a/benches/performance_benchmarks.rs +++ b/benches/performance_benchmarks.rs @@ -13,6 +13,7 @@ //! 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 @@ -64,6 +65,7 @@ use OpenCADStudio::ui::overlay::{ }; use OpenCADStudio::ui::properties::LinetypeItem; use OpenCADStudio::ui::ribbon::{LayerInfo, Ribbon}; +use OpenCADStudio::ui::style::plotstyle::build_layer_usage; // ── Metric Structures ─────────────────────────────────────────────────────── @@ -1359,6 +1361,53 @@ 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. + +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 + ); +} + // ── 13. Wide & Tapered Arc + Donut Tessellation ───────────────────────────── fn bench_wide_and_tapered_arc_tessellation(runner: &mut BenchmarkRunner) { @@ -1761,6 +1810,7 @@ fn main() { 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_wide_and_tapered_arc_tessellation(&mut runner); bench_zoom_extents_calculation(&mut runner); bench_batch_entity_mutation(&mut runner); diff --git a/src/ui/style/plotstyle.rs b/src/ui/style/plotstyle.rs index cf2b93a0b..1c943fb3d 100644 --- a/src/ui/style/plotstyle.rs +++ b/src/ui/style/plotstyle.rs @@ -84,6 +84,35 @@ fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style { } } +/// Layer names bucketed by ACI (AutoCAD Color Index), index = ACI. +/// +/// Pure function of `document.layers`: one entry per layer whose color is +/// `Color::Index(aci)` with `1 <= aci < 256`, each bucket sorted +/// case-insensitively. Extracted from `view_window` so the table can be +/// unit-tested and benched without building widgets. `pub` (not +/// `pub(crate)`) so the `cargo bench` harness (`benches/`, an external +/// crate) can measure it as `ui_plotstyle_layer_usage`. +pub fn build_layer_usage(document: &acadrust::CadDocument) -> Vec> { + let mut layer_usage = vec![Vec::::new(); 256]; + + for layer in document.layers.iter() { + if let acadrust::types::Color::Index(aci) = &layer.color { + let index = *aci as usize; + + if index > 0 && index < layer_usage.len() { + layer_usage[index].push(layer.name.clone()); + } + } + } + + for layers in &mut layer_usage { + if layers.len() > 1 { + layers.sort_by_cached_key(|name| name.to_ascii_lowercase()); + } + } + layer_usage +} + pub fn view_window<'a>( document: &'a acadrust::CadDocument, table: Option<&'a crate::io::plot_style::PlotStyleTable>, @@ -95,23 +124,11 @@ pub fn view_window<'a>( ) -> Element<'a, Message> { let show_layer_usage = !table.is_some_and(|table| table.is_stb); -let mut layer_usage = vec![Vec::::new(); 256]; - - if show_layer_usage { - for layer in document.layers.iter() { - if let acadrust::types::Color::Index(aci) = &layer.color { - let index = *aci as usize; - - if index > 0 && index < layer_usage.len() { - layer_usage[index].push(layer.name.clone()); - } - } - } - - for layers in &mut layer_usage { - layers.sort_by_key(|name| name.to_ascii_lowercase()); - } - } + let layer_usage = if show_layer_usage { + build_layer_usage(document) + } else { + Vec::new() + }; let table_name = table .map(|t| t.name.clone()) .unwrap_or_else(|| t!("(no table loaded)").into_owned()); @@ -536,3 +553,58 @@ let mut layer_usage = vec![Vec::::new(); 256]; .height(sizing.height) .into() } + +#[cfg(test)] +mod layer_usage_tests { + use super::*; + + fn indexed_layer( + doc: &mut acadrust::CadDocument, + name: &str, + aci: u8, + ) -> acadrust::tables::Layer { + let mut layer = acadrust::tables::Layer::new(name); + layer.handle = doc.allocate_handle(); + layer.color = acadrust::types::Color::Index(aci); + layer + } + + #[test] + fn build_layer_usage_buckets_index_colors() { + let mut doc = acadrust::CadDocument::new(); + let a = indexed_layer(&mut doc, "WALLS", 1); + let b = indexed_layer(&mut doc, "doors", 1); + let c = indexed_layer(&mut doc, "ROOF", 3); + let _ = doc.layers.add(a); + let _ = doc.layers.add(b); + let _ = doc.layers.add(c); + + let usage = build_layer_usage(&doc); + + assert_eq!(usage.len(), 256); + assert_eq!(usage[1], vec!["doors".to_string(), "WALLS".to_string()]); + assert_eq!(usage[3], vec!["ROOF".to_string()]); + assert!(usage[2].is_empty()); + } + + #[test] + fn build_layer_usage_ignores_non_index_colors() { + let mut doc = acadrust::CadDocument::new(); + let mut truecolor = acadrust::tables::Layer::new("TRUECOLOR"); + truecolor.handle = doc.allocate_handle(); + truecolor.color = acadrust::types::Color::Rgb { + r: 10, + g: 20, + b: 30, + }; + let _ = doc.layers.add(truecolor); + + let usage = build_layer_usage(&doc); + + assert!( + usage.iter().all(|bucket| !bucket.iter().any(|n| n == "TRUECOLOR")), + "truecolor layer must not appear in any ACI bucket" + ); + } + +}