From 9c8c9a4060f95d949984d389e90ea9552e6f3aea Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 26 Apr 2026 02:17:40 +0300 Subject: [PATCH] perf: cache SortEntitiesTable scan (option E) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the O(objects) linear scan inside wires_for_block() with a sort_cache keyed by geometry_epoch. On a cache miss the index is built once by scanning all document objects; subsequent calls within the same epoch do an O(1) HashMap lookup (block_handle → entity→sort map). Co-Authored-By: Claude Sonnet 4.6 --- PERFORMANCE.md | 15 ++++++++----- src/scene/mod.rs | 57 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 0c94c1486..05d0158e9 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -109,13 +109,16 @@ bounding boxes for all entity types. --- -## Option E — SortEntitiesTable Scan Cache +## Option E — SortEntitiesTable Scan Cache ✅ Implemented -**Status:** Planned +**Status:** Done -Cache the result of the `SortEntitiesTable` lookup that currently performs a linear scan over all -objects on every `wires_for_block()` call. +Added `sort_cache: RefCell>)>>` to `Scene`. -**Impact:** Minor. Removes one O(objects) scan per frame. Low priority compared to A–D. +On the first call to `wires_for_block()` after a geometry change, the cache scans all +`document.objects` once and builds an index: `block_handle → (entity_handle.value() → +sort_handle.value())`. Subsequent calls within the same epoch do an O(1) HashMap lookup +instead of the previous O(objects) `find_map` scan. -**Difficulty:** Easy. +The cache is keyed by `geometry_epoch` and invalidated automatically alongside the wire +and GPU caches whenever geometry changes. diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 4fe652b1d..2685d531c 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -76,6 +76,10 @@ pub struct Scene { /// Keyed by `geometry_epoch`; invalidated automatically when the epoch changes. /// Uses `Arc` so `build_primitive()` avoids a full Vec clone during navigation. wire_cache: RefCell>)>>, + /// Index built from every SortEntitiesTable in the document. + /// Maps block_handle → (entity_handle.value() → sort_handle.value()). + /// Replaces the O(objects) linear scan inside `wires_for_block()` with an O(1) lookup. + sort_cache: RefCell>)>>, /// Active layout name — "Model" or a paper space layout name. pub current_layout: String, /// GPU render data for hatch fills, keyed by the DXF entity Handle. @@ -107,6 +111,7 @@ impl Scene { camera_generation: 0, geometry_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed), wire_cache: RefCell::new(None), + sort_cache: RefCell::new(None), current_layout: "Model".to_string(), hatches: HashMap::new(), meshes: HashMap::new(), @@ -420,14 +425,30 @@ impl Scene { fn wires_for_block(&self, block_handle: Handle) -> Vec { use acadrust::objects::ObjectType; - // Find the SortEntitiesTable for this block (if any). - let sort_table = self.document.objects.values().find_map(|obj| { - if let ObjectType::SortEntitiesTable(t) = obj { - if t.block_owner_handle == block_handle { Some(t) } else { None } - } else { - None + // ── Ensure sort-order index is current ──────────────────────────── + // Replaces the old O(objects) find_map with one rebuild per epoch, + // after which every wires_for_block call is an O(1) HashMap lookup. + { + let needs_rebuild = self.sort_cache.borrow() + .as_ref() + .map(|(e, _)| *e != self.geometry_epoch) + .unwrap_or(true); + + if needs_rebuild { + let mut idx: HashMap> = HashMap::new(); + for obj in self.document.objects.values() { + if let ObjectType::SortEntitiesTable(t) = obj { + if !t.is_empty() { + let map = t.entries() + .map(|e| (e.entity_handle.value(), e.sort_handle.value())) + .collect(); + idx.insert(t.block_owner_handle, map); + } + } + } + *self.sort_cache.borrow_mut() = Some((self.geometry_epoch, idx)); } - }); + } let mut wires: Vec = self.document .entities() @@ -450,16 +471,18 @@ impl Scene { .flat_map(|e| self.tessellate_one(e)) .collect(); - // Apply draw order if a SortEntitiesTable exists and has entries. - if let Some(table) = sort_table { - if !table.is_empty() { - wires.sort_by_key(|w| { - let handle = Self::handle_from_wire_name(&w.name) - .unwrap_or(Handle::NULL); - table.get_sort_handle(handle) - .map(|sh| sh.value()) - .unwrap_or(u64::MAX / 2) // unsorted entities draw in the middle - }); + // Apply draw order via the cached index (O(1) block lookup). + { + let cache = self.sort_cache.borrow(); + if let Some((_, ref idx)) = *cache { + if let Some(sort_map) = idx.get(&block_handle) { + wires.sort_by_key(|w| { + let key = Self::handle_from_wire_name(&w.name) + .map(|h| h.value()) + .unwrap_or(u64::MAX); + sort_map.get(&key).copied().unwrap_or(u64::MAX / 2) + }); + } } } wires