perf(canvas): skip idle indicator snapshots

This commit is contained in:
Kayshen-X 2026-06-15 04:31:40 +08:00
parent 7a30644bb0
commit 83f488fd0a
2 changed files with 61 additions and 16 deletions

View file

@ -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<AgentIndicators> {
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());

View file

@ -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;
}