From f1efc73cbe437e8868b271802f85095baf6c0de7 Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:43:46 +0300 Subject: [PATCH] perf: stop rescanning every entity to fill the selection-filter menu Pan and even plain cursor movement over the canvas ran at ~8 fps on a large drawing. Nothing rendered was responsible: the GPU idled at 0-8%, encoding no scene at all changed nothing, and vertices, pixels and draw calls could each be cut without moving the number. `view` was the frame -- 110 ms of it against 3-6 ms on a small drawing -- and inside `view` it was the status bar. The status bar builds the selection-filter menu, which asks for the distinct entity type names in the layout. That call cloned the layout's entire handle list (1 166 357 handles, ~9 MB), then did a map lookup, a String allocation and a BTreeSet insert for each one. About 107 ms of a 110 ms frame, to produce roughly twenty names, for a menu that is only visible when opened. Cache the result against geometry_epoch and the layout block, since it only changes when entities are added or removed, and make the scan itself borrow the handle list rather than clone it and collect &str, allocating only for the names it returns. The immediate-mode UI rebuilds every widget on every mouse move, which is fine as long as nothing in that path is O(document). This was. Co-Authored-By: Claude Opus 5 --- src/app/update/mod.rs | 4 ++-- src/app/view/mod.rs | 5 ++--- src/scene/mod.rs | 6 ++++++ src/scene/selection.rs | 48 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 62ab2c13c..be766631c 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -3687,8 +3687,8 @@ impl OpenCADStudio { let i = self.active_tab; let types = self.tabs[i].scene.entity_type_names_in_layout(); let f = &mut self.tabs[i].scene.selection_filter; - for t in types { - f.insert(t.to_string()); + for t in types.iter() { + f.insert(t.clone()); } Task::none() } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index b293a6ca3..b68282a3a 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1948,9 +1948,8 @@ impl OpenCADStudio { selection_types: tab .scene .entity_type_names_in_layout() - .into_iter() - .map(|name| name.to_string()) - .collect(), + .as_ref() + .clone(), selection_filter: &tab.scene.selection_filter, tooltip_hidden: self.status_menu_tooltip_hidden, }; diff --git a/src/scene/mod.rs b/src/scene/mod.rs index e3bcb3b45..da1ffaf3b 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1715,6 +1715,11 @@ pub struct Scene { /// Reverse map: entity_handle → block_record_handle, built from entity_handles lists. /// Keyed by geometry_epoch. Eliminates the O(B) fallback scan in belongs_to_visible_block. entity_block_map_cache: RefCell)>>, + /// Distinct entity type names present in a layout, keyed by + /// (geometry_epoch, layout block). The status bar asks for this on every + /// frame to populate the selection-filter menu, but the answer only + /// changes when entities are added or removed. + layout_type_names_cache: RefCell>)>>, /// Reverse dependencies from layer/style/block definitions to the top-level /// entities whose resident wire runs actually change. Kept independent from /// `geometry_epoch`: a layer colour toggle can reuse the index, invalidate @@ -1950,6 +1955,7 @@ impl Scene { annotation_affects_wires: std::cell::Cell::new(None), model_extents_cache: RefCell::new(None), entity_block_map_cache: RefCell::new(None), + layout_type_names_cache: RefCell::new(None), dependency_index_cache: RefCell::new(None), associative_hatch_source_cache: RefCell::new(None), block_defn_cache: RefCell::new(HashMap::default()), diff --git a/src/scene/selection.rs b/src/scene/selection.rs index f2cc33654..8f83f4ab8 100644 --- a/src/scene/selection.rs +++ b/src/scene/selection.rs @@ -363,16 +363,50 @@ impl Scene { /// Returns the sorted set of entity-type names present in the active /// layout. Used to populate the Quick Select "Object type" dropdown /// with only the types that actually exist in the drawing. - pub fn entity_type_names_in_layout(&self) -> Vec { + /// Distinct entity type names in the current layout, for the + /// selection-filter menu. + /// + /// The status bar rebuilds on every frame, so this used to run once per + /// mouse move over the canvas: a full clone of the layout's handle list + /// (1.16 M handles, ~9 MB, on the reproducer), then a lookup, a `String` + /// allocation and a `BTreeSet` insert for each one — about 107 ms of a + /// 110 ms frame, to produce roughly twenty names. + /// + /// The answer only changes when entities are added or removed, so it is + /// cached against `geometry_epoch` and the layout block. The scan itself + /// now borrows the handle list instead of cloning it and collects + /// `&str`, allocating only for the names it actually returns. + pub fn entity_type_names_in_layout(&self) -> std::sync::Arc> { use crate::entities::traits::entity_type_name; - let mut names: std::collections::BTreeSet = - std::collections::BTreeSet::new(); - for h in self.current_layout_entity_handles() { - if let Some(e) = self.document.get_entity(h) { - names.insert(entity_type_name(e).to_string()); + let block = self.current_layout_block_handle(); + { + let cache = self.layout_type_names_cache.borrow(); + if let Some((epoch, cached_block, names)) = cache.as_ref() { + if *epoch == self.geometry_epoch && *cached_block == block { + return std::sync::Arc::clone(names); + } } } - names.into_iter().collect() + let mut names: std::collections::BTreeSet<&str> = + std::collections::BTreeSet::new(); + if let Some(record) = self + .document + .block_records + .iter() + .find(|record| record.handle == block) + { + for &handle in &record.entity_handles { + if let Some(entity) = self.document.get_entity(handle) { + names.insert(entity_type_name(entity)); + } + } + } + let names: std::sync::Arc> = std::sync::Arc::new( + names.into_iter().map(str::to_string).collect(), + ); + *self.layout_type_names_cache.borrow_mut() = + Some((self.geometry_epoch, block, std::sync::Arc::clone(&names))); + names } /// True when `handle`'s entity type is allowed by the selection filter.