From bc8489367da27f2c74a1bf1af28db4d0a34b74a9 Mon Sep 17 00:00:00 2001 From: xlx1212 Date: Fri, 7 Aug 2026 00:22:49 +0800 Subject: [PATCH] fix(desktop): pause IPC polls when window is hidden to reduce background CPU Gate DispatchJobObserver (1800ms), CodeEditor/MarkdownEditor (1000ms file-sync), and BackgroundCommandOutputPanel (1000ms) polls on document.visibilityState so they stop issuing Tauri IPC round-trips when the app is backgrounded. Fixes #1079 --- .../dispatch/DispatchJobObserver.test.ts | 28 +++++++++++++++++-- .../features/dispatch/DispatchJobObserver.ts | 14 +++++++++- .../BackgroundCommandOutputPanel.tsx | 1 + .../tools/editor/components/CodeEditor.tsx | 1 + .../editor/components/MarkdownEditor.tsx | 1 + 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/web-ui/src/features/dispatch/DispatchJobObserver.test.ts b/src/web-ui/src/features/dispatch/DispatchJobObserver.test.ts index 21d065dc50..68be93bd8c 100644 --- a/src/web-ui/src/features/dispatch/DispatchJobObserver.test.ts +++ b/src/web-ui/src/features/dispatch/DispatchJobObserver.test.ts @@ -845,7 +845,7 @@ describe('DispatchJobObserver', () => { cleanup(); }); - it('continues polling while hidden so background notifications can observe changes', async () => { + it('pauses polling while hidden to reduce background CPU usage', async () => { registerRunningJob(); mocks.status.mockResolvedValue(status({ state: 'running' })); Object.defineProperty(document, 'visibilityState', { @@ -855,7 +855,7 @@ describe('DispatchJobObserver', () => { const cleanup = installDispatchJobObserver(createContext()); await vi.advanceTimersByTimeAsync(0); - expect(mocks.status).toHaveBeenCalledTimes(1); + expect(mocks.status).not.toHaveBeenCalled(); cleanup(); Object.defineProperty(document, 'visibilityState', { @@ -864,6 +864,30 @@ describe('DispatchJobObserver', () => { }); }); + it('resumes polling when the window becomes visible again', async () => { + registerRunningJob(); + mocks.status.mockResolvedValue(status({ state: 'running' })); + Object.defineProperty(document, 'visibilityState', { + configurable: true, + value: 'hidden', + }); + const cleanup = installDispatchJobObserver(createContext()); + + await vi.advanceTimersByTimeAsync(0); + expect(mocks.status).not.toHaveBeenCalled(); + + Object.defineProperty(document, 'visibilityState', { + configurable: true, + value: 'visible', + }); + document.dispatchEvent(new Event('visibilitychange')); + + await vi.advanceTimersByTimeAsync(0); + expect(mocks.status).toHaveBeenCalled(); + + cleanup(); + }); + it('does not present target cancellation as a successful completion', async () => { registerRunningJob(); const context = createContext(); diff --git a/src/web-ui/src/features/dispatch/DispatchJobObserver.ts b/src/web-ui/src/features/dispatch/DispatchJobObserver.ts index f86fa3660d..8d7736db5e 100644 --- a/src/web-ui/src/features/dispatch/DispatchJobObserver.ts +++ b/src/web-ui/src/features/dispatch/DispatchJobObserver.ts @@ -998,6 +998,7 @@ export function installDispatchJobObserver(context: FlowChatContext): () => void async function run(requestedJobId?: string): Promise { if (!ownsLease() || isPeerDeviceModeActive()) return; + if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return; if (inFlight) { queuedJobId = requestedJobId; return; @@ -1095,8 +1096,19 @@ export function installDispatchJobObserver(context: FlowChatContext): () => void void run(); }, DISPATCH_JOB_POLL_INTERVAL_MS); handleVisibilityChanged = () => { - if (typeof document === 'undefined' || document.visibilityState === 'visible') { + if (typeof document === 'undefined') return; + if (document.visibilityState === 'visible') { + if (interval === null) { + interval = setInterval(() => { + void run(); + }, DISPATCH_JOB_POLL_INTERVAL_MS); + } schedule(); + } else { + if (interval !== null) { + clearInterval(interval); + interval = null; + } } }; if (typeof document !== 'undefined') { diff --git a/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandOutputPanel.tsx b/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandOutputPanel.tsx index 189705e19d..df99c068eb 100644 --- a/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandOutputPanel.tsx +++ b/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandOutputPanel.tsx @@ -178,6 +178,7 @@ export const BackgroundCommandOutputPanel: React.FC = ({ } const tick = () => { + if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return; void checkFileModification(); }; const pollOffsetMs = getPollOffsetMs(filePath); diff --git a/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx b/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx index af36da6b9f..c9ecf0a692 100644 --- a/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx +++ b/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx @@ -414,6 +414,7 @@ const MarkdownEditor: React.FC = ({ return; } const tick = () => { + if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return; void checkMarkdownDisk(); }; const pollOffsetMs = getPollOffsetMs(filePath);