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 <[email protected]>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-09-04 16:20:30 +03:00
1 parent 886d5777d8
commit f1efc73cbe
4 files changed
+51 -12

No files matched your search

+2 -2
View File
@@ -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()
}
+2 -3
View File
@@ -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,
};
+6
View File
@@ -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<Option<(u64, HashMap<Handle, Handle>)>>,
/// 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<Option<(u64, Handle, std::sync::Arc<Vec<String>>)>>,
/// 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()),
+41 -7
View File
@@ -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<String> {
/// 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<Vec<String>> {
use crate::entities::traits::entity_type_name;
let mut names: std::collections::BTreeSet<String> =
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<Vec<String>> = 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.