From c4ac62a7eedbd24a5fa8d483b6602c24639f5363 Mon Sep 17 00:00:00 2001 From: kshitij Date: Mon, 3 Aug 2026 18:24:26 +0530 Subject: [PATCH] fix(desktop): cancel the pending commit-cost measurement rAF Follow-up to #77652: each runFlush registered a fresh requestAnimationFrame and never cancelled it. Chromium parks rAF callbacks for hidden renderers, so a long hidden stream at the 33ms floor accumulates thousands of parked closures that all fire in the first frame on refocus (all but one no-oping through the stale-frame guard). Track the pending handle, cancel it before requesting a new one (only the newest flush's measurement matters), and cancel on unmount. --- .../session/hooks/use-message-stream/index.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/app/session/hooks/use-message-stream/index.ts b/apps/desktop/src/app/session/hooks/use-message-stream/index.ts index 4eea88b2afe4a..cbfb266e65076 100644 --- a/apps/desktop/src/app/session/hooks/use-message-stream/index.ts +++ b/apps/desktop/src/app/session/hooks/use-message-stream/index.ts @@ -188,6 +188,9 @@ export function useMessageStream({ // What the previous flush cost on the main thread — drives the adaptive // flush floor in scheduleDeltaFlush so multi-stream load yields to input. const lastFlushCostRef = useRef(0) + // The pending commit-cost measurement rAF, so a newer flush (or unmount) + // can cancel it instead of letting parked callbacks pile up while hidden. + const measureRafRef = useRef(null) const nativeSubagentSessionsRef = useRef>(new Set()) // Turns that auto-compacted: skip post-turn hydrate so live scrollback survives. const compactedTurnRef = useRef>(new Set()) @@ -284,7 +287,16 @@ export function useMessageStream({ // stays as the fallback. const writeCost = performance.now() - startedAt lastFlushCostRef.current = writeCost - window.requestAnimationFrame(frameStart => { + // At most one measurement rAF may be pending: only the newest flush's + // measurement matters (the guard below discards stale frames), and a + // hidden renderer parks rAF callbacks — without cancellation a long + // hidden stream at the floor would accumulate thousands of parked + // closures that all fire in the first frame on refocus. + if (measureRafRef.current !== null) { + window.cancelAnimationFrame(measureRafRef.current) + } + measureRafRef.current = window.requestAnimationFrame(frameStart => { + measureRafRef.current = null // A newer flush already started; its own measurement wins. if (lastFlushAtRef.current !== startedAt) { return @@ -334,6 +346,12 @@ export function useMessageStream({ } flushHandleRef.current = null + + if (measureRafRef.current !== null && typeof window !== 'undefined') { + window.cancelAnimationFrame(measureRafRef.current) + } + + measureRafRef.current = null flushQueuedDeltas() }, [flushQueuedDeltas]