perf(plotstyle): extract and speed up layer-usage table rebuild

view_window() rebuilt the 256-bucket ACI layer-usage table inline on every
modal view: unconditional 256-Vec allocation plus sort_by_key (lowercase key
recomputed per comparison). Extract pure build_layer_usage(), build it only
when the CTB path needs it (STB path now allocates nothing), and sort with
sort_by_cached_key while skipping trivial buckets.

Tests:
- build_layer_usage_buckets_index_colors: ACI bucketing + case-insensitive order
- build_layer_usage_ignores_non_index_colors: truecolor layers excluded

Bench (cargo bench ui_plotstyle_layer_usage, 200 layers): ~13us median, PASS.
A/B vs pre-change inline code: ~15.7us -> ~12-13us (~22% faster).
This commit is contained in:
Karim Jerbi committed 2026-09-20 11:17:05 +01:00
1 parent 768ce2ca82
commit 70d7e9a31f
3 files changed
+141 -18

No files matched your search

+2 -1
View File
@@ -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).
+50
View File
@@ -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);
+89 -17
View File
@@ -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<Vec<String>> {
let mut layer_usage = vec![Vec::<String>::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::<String>::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::<String>::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"
);
}
}