From 753d959d39b694d66feec772004ebca0332fd6f7 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 26 Sep 2026 12:56:48 -0700 Subject: [PATCH] perf(renderer): release unrelated state during cache saves (#23021) --- .../store/github/cache-persistence.test.ts | 64 +++++++++++++++++++ .../src/store/github/cache-persistence.ts | 6 +- 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/store/github/cache-persistence.test.ts diff --git a/src/renderer/src/store/github/cache-persistence.test.ts b/src/renderer/src/store/github/cache-persistence.test.ts new file mode 100644 index 00000000000..70f21372dab --- /dev/null +++ b/src/renderer/src/store/github/cache-persistence.test.ts @@ -0,0 +1,64 @@ +import { setImmediate } from 'node:timers/promises' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { createTestStore } from '../slices/store-test-helpers' +import { debouncedSaveCache } from './cache-persistence' + +const setGitHub = vi.fn() + +beforeEach(() => { + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) + setGitHub.mockReset() + vi.stubGlobal('window', { api: { cache: { setGitHub } } }) +}) + +afterEach(() => { + vi.clearAllTimers() + vi.useRealTimers() + vi.unstubAllGlobals() +}) + +it('releases unrelated discarded renderer state while the cache save is pending', async () => { + const store = createTestStore() + const schedule = () => { + const drafts = { closedFile: Buffer.alloc(8 * 1024 * 1024, 65).toString('utf8') } + const state = { ...store.getState(), editorDrafts: drafts } + debouncedSaveCache(state) + return { state: new WeakRef(state), drafts: new WeakRef(drafts) } + } + const discarded = schedule() + if (!global.gc) { + throw new Error('config/vitest.config.ts must pass --expose-gc') + } + for (let turn = 0; turn < 3; turn += 1) { + await setImmediate() + global.gc() + } + + expect(setGitHub).not.toHaveBeenCalled() + expect(discarded.state.deref() === undefined).toBe(true) + expect(discarded.drafts.deref() === undefined).toBe(true) + vi.advanceTimersByTime(1000) + expect(setGitHub).toHaveBeenCalledOnce() +}) + +it('persists the latest requested cache maps once after the trailing second', () => { + const state = createTestStore().getState() + const first = { ...state, prCache: {}, issueCache: {} } + const latest = { ...state, prCache: {}, issueCache: {} } + debouncedSaveCache(first) + vi.advanceTimersByTime(999) + expect(setGitHub).not.toHaveBeenCalled() + + debouncedSaveCache(latest) + vi.advanceTimersByTime(999) + expect(setGitHub).not.toHaveBeenCalled() + vi.advanceTimersByTime(1) + expect(setGitHub).toHaveBeenCalledOnce() + expect(setGitHub.mock.calls[0][0]).toEqual({ + cache: { pr: latest.prCache, issue: latest.issueCache } + }) + expect(setGitHub.mock.calls[0][0].cache.pr).toBe(latest.prCache) + expect(setGitHub.mock.calls[0][0].cache.issue).toBe(latest.issueCache) + vi.advanceTimersByTime(1000) + expect(setGitHub).toHaveBeenCalledOnce() +}) diff --git a/src/renderer/src/store/github/cache-persistence.ts b/src/renderer/src/store/github/cache-persistence.ts index 06337e94a11..10c0c67ef13 100644 --- a/src/renderer/src/store/github/cache-persistence.ts +++ b/src/renderer/src/store/github/cache-persistence.ts @@ -5,12 +5,14 @@ let saveTimer: ReturnType | null = null /** Schedules the renderer's single trailing GitHub cache persistence write. */ export function debouncedSaveCache(state: AppState): void { clearTimeout(saveTimer ?? undefined) + // Keep unrelated renderer state out of the pending timer's closure. + const { prCache, issueCache } = state saveTimer = setTimeout(() => { saveTimer = null window.api.cache.setGitHub({ cache: { - pr: state.prCache, - issue: state.issueCache + pr: prCache, + issue: issueCache } }) }, 1000)