From 1f14ba35d17431d7f105b0269eb4c53c6e029516 Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Sat, 5 Sep 2026 11:22:49 +0300 Subject: [PATCH] perf: index the entities the quadtree cannot hold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After querying the quadtree and appending the unbounded entities, the quadtree path has to add the ones the tree cannot index — Insert, Viewport, Block, BlockEnd — and did so by walking every entity in the document. On a 1.17 M-entity drawing that is a full walk per resident rebuild to find a few thousand, which defeats the point of having a spatial index on that path at all. Membership depends only on the entity's type, so it is resolved once per `geometry_epoch` and reused. The list is built by walking `document.entities()`, so it stays in document order and the wires come out in the order the loop produced before — which matters, because that is draw order for a pass that alpha-blends. This is the same treatment the previous commit gave the block-membership path. Which of the two a layout switch actually takes is still unmeasured: the diagnostic added to report it was defaulted rather than assigned, so its `visible_path=quadtree` output was a constant. The next commit fixes that. Both branches carried an avoidable full walk either way. Co-Authored-By: Claude Opus 5 --- src/scene/mod.rs | 50 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 9c38d148c..2a151ba04 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1848,6 +1848,13 @@ pub struct Scene { /// `belongs_to_visible_block` treats as belonging to everything — but only /// when no block record enumerates its contents at all. block_members_cache: RefCell>, + /// Handles of entities the quadtree cannot index — Insert, Viewport, Block, + /// BlockEnd — in document order, keyed by `geometry_epoch`. + /// + /// The quadtree path has to append these regardless of the view, and did so + /// by walking every entity in the document: ~300 ms of every resident + /// rebuild on a 1.17 M-entity drawing, to find a few thousand. + unindexable_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 @@ -2093,6 +2100,7 @@ impl Scene { model_extents_cache: RefCell::new(None), entity_block_map_cache: RefCell::new(None), block_members_cache: RefCell::new(None), + unindexable_cache: RefCell::new(None), layout_type_names_cache: RefCell::new(None), dependency_index_cache: RefCell::new(None), associative_hatch_source_cache: RefCell::new(None), @@ -8902,7 +8910,7 @@ impl Scene { let sort_cache_ms = crate::perf::elapsed_ms(t_fn); let t_visible = perf.then(iced::time::Instant::now); // Which path the selection took, and how many candidates it looked at. - let mut visible_path = "quadtree"; + let mut visible_path = "quadtree+index"; let mut visible_candidates = 0usize; // Phase 2.1 — quadtree-driven candidate selection. When a view @@ -8941,10 +8949,19 @@ impl Scene { } } // Inserts/Viewports/Block/BlockEnd — handled by their own paths - // (block expansion, viewport rendering); always candidates. - for e in self.document.entities() { - if is_unindexable_entity(e) && visibility_ok(e) { - out.push(e); + // (block expansion, viewport rendering); always candidates. Taken + // from the per-epoch list rather than by walking the document, + // which preserves document order because the list is built that + // way. + { + let unindexable = self.unindexable_handles(); + visible_candidates = unindexable.1.len(); + for &h in &unindexable.1 { + if let Some(e) = self.document.get_entity(h) { + if visibility_ok(e) { + out.push(e); + } + } } } out @@ -9239,6 +9256,29 @@ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates /// * an entity neither names nor is named by any block is "unowned", and /// counts towards every block only when no block record enumerates its /// contents at all — the legacy-DXF case that predicate allows. + /// Entities the quadtree cannot index, in document order, resolved once per + /// `geometry_epoch`. Membership depends only on the entity's type, so an + /// edit that does not add or remove one leaves the list unchanged. + fn unindexable_handles(&self) -> std::cell::Ref<'_, (u64, Vec)> { + { + let cache = self.unindexable_cache.borrow(); + if cache.as_ref().is_some_and(|(epoch, _)| *epoch == self.geometry_epoch) { + drop(cache); + return std::cell::Ref::map(self.unindexable_cache.borrow(), |c| { + c.as_ref().unwrap() + }); + } + } + let handles: Vec = self + .document + .entities() + .filter(|entity| is_unindexable_entity(entity)) + .map(|entity| entity.common().handle) + .collect(); + *self.unindexable_cache.borrow_mut() = Some((self.geometry_epoch, handles)); + std::cell::Ref::map(self.unindexable_cache.borrow(), |c| c.as_ref().unwrap()) + } + fn block_members(&self) -> std::cell::Ref<'_, BlockMembers> { { let cache = self.block_members_cache.borrow();