From 83f488fd0acdb0530405e8a33c3f8dde581e39e3 Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Mon, 15 Jun 2026 04:31:40 +0800 Subject: [PATCH] perf(canvas): skip idle indicator snapshots --- crates/op-editor-core/src/agent_indicators.rs | 43 ++++++++++++++++++- .../src/widgets/canvas_viewport.rs | 34 +++++++++------ 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/crates/op-editor-core/src/agent_indicators.rs b/crates/op-editor-core/src/agent_indicators.rs index fa7848597..788c2442c 100644 --- a/crates/op-editor-core/src/agent_indicators.rs +++ b/crates/op-editor-core/src/agent_indicators.rs @@ -217,7 +217,7 @@ pub fn end_if_epoch(epoch: u64) { /// uses this to keep requesting redraws so the breathing animates. pub fn is_active() -> bool { let r = REGISTRY.lock().unwrap(); - !r.nodes.is_empty() || !r.frames.is_empty() || !r.reveals.is_empty() + has_active_indicators(&r) } /// A clone of the current indicators for the paint pass to read. @@ -229,12 +229,21 @@ pub fn snapshot() -> AgentIndicators { /// `now_ms` pruned. The paint pass calls this every frame so completed /// reveal animations stop requesting redraws. pub fn snapshot_at(now_ms: u64) -> AgentIndicators { + snapshot_at_if_active(now_ms).unwrap_or_default() +} + +/// A clone of the current indicators after reveal maintenance, or +/// `None` when there is nothing active for paint to consume. +pub fn snapshot_at_if_active(now_ms: u64) -> Option { let mut r = REGISTRY.lock().unwrap(); + if !has_active_indicators(&r) { + return None; + } rebase_external_clock_reveals(&mut r, now_ms); r.reveals .retain(|_, started| now_ms.saturating_sub(*started) <= REVEAL_DURATION_MS); smooth_overdue_reveal_burst(&mut r, now_ms); - r.clone() + has_active_indicators(&r).then(|| r.clone()) } /// Next host-clock millisecond needed for generated-node reveal animation. @@ -333,6 +342,10 @@ fn smooth_overdue_reveal_burst(r: &mut AgentIndicators, now_ms: u64) { } } +fn has_active_indicators(r: &AgentIndicators) -> bool { + !r.nodes.is_empty() || !r.frames.is_empty() || !r.reveals.is_empty() +} + #[cfg(test)] mod tests { use super::*; @@ -435,6 +448,32 @@ mod tests { clear(); } + #[test] + fn snapshot_at_if_active_avoids_idle_snapshot_clone() { + let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + clear(); + assert!( + snapshot_at_if_active(1_000).is_none(), + "idle paint should skip cloning an empty indicator snapshot" + ); + + let epoch = begin(); + add_frame(epoch, "frame", "#4ECDC4", "Mochi"); + assert!( + snapshot_at_if_active(1_000).is_some_and(|snap| snap.frames.contains_key("frame")), + "active frame indicators still produce a paint snapshot" + ); + + clear(); + let epoch = begin(); + add_reveal(epoch, "expired", 1_000); + assert!( + snapshot_at_if_active(3_000).is_none(), + "an expired reveal should prune and leave no idle snapshot" + ); + end_if_epoch(epoch); + } + #[test] fn snapshot_rebases_external_clock_reveals_to_paint_clock() { let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); diff --git a/crates/op-editor-ui/src/widgets/canvas_viewport.rs b/crates/op-editor-ui/src/widgets/canvas_viewport.rs index e59a1f0d1..dfa387f7e 100644 --- a/crates/op-editor-ui/src/widgets/canvas_viewport.rs +++ b/crates/op-editor-ui/src/widgets/canvas_viewport.rs @@ -459,7 +459,7 @@ impl<'a> Widget for CanvasViewport<'a> { // 2. Dotted grid — canvas-local, scales with pan/zoom. let viewport = &self.viewport; super::canvas_viewport_grid::paint_grid(cx, rect, viewport, &self.theme); - let indicators = op_editor_core::agent_indicators::snapshot_at(self.now_ms); + let indicators = op_editor_core::agent_indicators::snapshot_at_if_active(self.now_ms); // 3. Walk the active page; clip enforces widget bounds. if let Some(page) = self.scene.active_page() { @@ -481,7 +481,9 @@ impl<'a> Widget for CanvasViewport<'a> { rect.size.y + CULL_MARGIN * 2.0, ), }; - let reveal_schedule = reveal_schedule_for_paint(&indicators.reveals, self.now_ms); + let reveal_schedule = indicators + .as_ref() + .and_then(|indicators| reveal_schedule_for_paint(&indicators.reveals, self.now_ms)); for child in page.children.iter().rev() { if let Some(reveals) = reveal_schedule { super::canvas_viewport_paint::paint_node_with_reveals( @@ -504,14 +506,16 @@ impl<'a> Widget for CanvasViewport<'a> { ); } } - super::canvas_agent_overlay::paint_agent_frame_indicators_with_snapshot( - cx, - &page.children, - viewport_origin, - viewport.zoom, - self.now_ms, - &indicators, - ); + if let Some(indicators) = indicators.as_ref() { + super::canvas_agent_overlay::paint_agent_frame_indicators_with_snapshot( + cx, + &page.children, + viewport_origin, + viewport.zoom, + self.now_ms, + indicators, + ); + } if let Some(hovered) = self.hovered.as_ref() { if let Some(node) = page.find(hovered) { const HOVER: Color = Color { @@ -595,10 +599,12 @@ impl<'a> Widget for CanvasViewport<'a> { continue; }; if node.hidden - || indicators - .reveals - .get(&node.id) - .is_some_and(|started_at| self.now_ms < *started_at) + || indicators.as_ref().is_some_and(|indicators| { + indicators + .reveals + .get(&node.id) + .is_some_and(|started_at| self.now_ms < *started_at) + }) { continue; }