From d5859a01a5424747056179c88b15124b2bc02037 Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:12:19 +0300 Subject: [PATCH] perf: separate the two costs inside the visible-entity selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first drawn point costs 1068 ms and `wires-build` owns 710 of it: visible=402.5 build=148.4 [classify=18.4 hits=127.5] sort=41.9 memo_hit=186455 memo_miss=1 candidates=444938 The memo is doing its job. `visible` is not, and two things in that loop could own the 402 ms: resolving each handle through the document's hash index, or the predicate — which reaches `document.layers.get(&c.layer)`, a **string-keyed** lookup, so a string hash per entity, 444 938 times. They call for different fixes, so `visible_probe=` times a resolve-only pass and the remainder is the predicate. The pass repeats the resolve rather than reading the clock inside the loop, where the measurement would cost more than the thing measured, and it runs under PERF only. Co-Authored-By: Claude Opus 5 --- src/scene/mod.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index a7060ac7e..d5130bc76 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -8939,6 +8939,11 @@ impl Scene { // version of this diagnostic report a constant. let visible_path: &str; let mut visible_candidates: usize; + // `visible` costs ~400 ms over 444 937 candidates, and two things in + // the loop could own that: resolving each handle through the document's + // hash index, or the predicate, which looks a layer up **by name** — + // a string hash per entity. Timed separately, under PERF only. + let mut visible_probe_ms = 0.0f64; // Phase 2.1 — quadtree-driven candidate selection. When a view // AABB exists (Model layout with a settled camera), only iterate @@ -9027,6 +9032,20 @@ impl Scene { } } } + if perf { + // A second, resolve-only pass. It repeats work rather than + // instrumenting inside the loop, where a clock read per + // iteration would cost more than what it measures. + let t_probe = iced::time::Instant::now(); + let mut resolved = 0usize; + for &handle in claimed { + if self.document.get_entity(handle).is_some() { + resolved += 1; + } + } + std::hint::black_box(resolved); + visible_probe_ms = t_probe.elapsed().as_secs_f64() * 1000.0; + } out }; @@ -9271,7 +9290,8 @@ impl Scene { "[perf] wires-build total={:.1}ms sort_cache={:.1} visible={:.1} blk_cache={:.1} colors={:.1} \ build={:.1} [classify={:.1} hits={:.1} tess={:.1} materialize={:.1}] sort={:.1}({}) \ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates={} \ -guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}", +guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={} \ +visible_probe={:.1}", crate::perf::elapsed_ms(t_fn), sort_cache_ms, visible_ms, @@ -9298,6 +9318,7 @@ guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}", annotation_scale_handle.map(|h| h.value()).unwrap_or(0), all_visible, crate::scene::text::sdf_atlas::generation(), + visible_probe_ms, ); } wires