From ac9dc0bc0d82fc84a09a2ab907a092cb79a54b97 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 22 Oct 2026 13:20:23 -0700 Subject: [PATCH] perf(ai-vault): bound scope cache write batch enumeration (#31950) * perf(ai-vault): bound scope cache write batch enumeration * test(ai-vault): type scope cache stamp fixture --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- .../session-parse-cache-persistence.test.ts | 41 ++++++++++++++++++- .../session-parse-cache-persistence.ts | 26 ++++++------ 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/src/main/ai-vault/session-parse-cache-persistence.test.ts b/src/main/ai-vault/session-parse-cache-persistence.test.ts index 529bd6a9b26..d79ebef7200 100644 --- a/src/main/ai-vault/session-parse-cache-persistence.test.ts +++ b/src/main/ai-vault/session-parse-cache-persistence.test.ts @@ -25,7 +25,12 @@ import { type SessionParseStats } from './session-scanner-parse-cache' import type * as ParseCacheModule from './session-scanner-parse-cache' -import { getSessionParseCacheEntry, storeSessionParseCacheEntry } from './session-parse-cache-store' +import { + getSessionParseCacheEntry, + storeSessionParseCacheEntry, + storeSessionScopeCwd, + type SessionScopeCwdStamp +} from './session-parse-cache-store' import type { SessionSidecarObservation } from './session-sidecar-stat' import { isolatedScanRoots } from './session-scanner-test-fixtures' import { parseClaudeSessionFile } from './session-scanner-primary-parsers' @@ -163,6 +168,40 @@ describe('session parse cache persistence', () => { expect(snapshot).not.toBe(configured) }) + it('persists scope rows across chunks and keeps replacements queued during a yield', async () => { + const root = await makeTempDir() + const cacheFile = join(root, 'session-parse-cache.json') + initSessionParseCachePersistence({ filePath: cacheFile, appVersion: APP_VERSION }) + const paths = Array.from({ length: 1_001 }, (_, index) => join(root, `session-${index}`)) + const stamp: SessionScopeCwdStamp = { + mtimeMs: 1, + sizeBytes: 10, + platform: 'linux', + cwd: '/original' + } + for (const path of paths) { + storeSessionScopeCwd(path, stamp) + } + const flushing = flushSessionParseCachePersist() + const replacement = { ...stamp, mtimeMs: 2, cwd: '/updated' } + storeSessionScopeCwd(paths[0]!, replacement) + storeSessionScopeCwd(paths[750]!, replacement) + await flushing + resetSessionParseCachePersistenceForTests() + resetSessionParseCacheForTests() + + const database = new SessionParseCacheDatabase(join(root, 'session-parse-cache.sqlite')) + try { + const stored = database.readScopeCwds(paths).found + expect(stored.size).toBe(paths.length) + for (const [index, path] of paths.entries()) { + expect(stored.get(path)?.read).toEqual(index === 0 || index === 750 ? replacement : stamp) + } + } finally { + database.close() + } + }) + it('round-trips: a persisted entry is a reused hit after a restart, without reading the transcript', async () => { const root = await makeTempDir() const cacheFile = join(root, 'vault-state', 'session-parse-cache.json') diff --git a/src/main/ai-vault/session-parse-cache-persistence.ts b/src/main/ai-vault/session-parse-cache-persistence.ts index 54ee84c3526..6022fcc5888 100644 --- a/src/main/ai-vault/session-parse-cache-persistence.ts +++ b/src/main/ai-vault/session-parse-cache-persistence.ts @@ -88,24 +88,14 @@ export function resetSessionParseCachePersistenceForTests(): void { export async function flushSessionParseCachePersist(): Promise { clearWriteTimer() while (pendingWrites.size > 0) { - const chunk: [string, PersistedSessionParseCacheEntry][] = [] - for (const row of pendingWrites) { - chunk.push(row) - pendingWrites.delete(row[0]) - if (chunk.length === WRITE_CHUNK_ROWS) { - break - } - } + const chunk = takeWriteChunk(pendingWrites) openedDatabase()?.write(chunk, Date.now()) if (pendingWrites.size > 0) { await yieldToEventLoop() } } while (pendingScopeCwds.size > 0) { - const chunk = [...pendingScopeCwds].slice(0, WRITE_CHUNK_ROWS) - for (const [path] of chunk) { - pendingScopeCwds.delete(path) - } + const chunk = takeWriteChunk(pendingScopeCwds) openedDatabase()?.writeScopeCwds(chunk, Date.now()) if (pendingScopeCwds.size > 0) { await yieldToEventLoop() @@ -113,6 +103,18 @@ export async function flushSessionParseCachePersist(): Promise { } } +function takeWriteChunk(pending: Map): [string, T][] { + const chunk: [string, T][] = [] + for (const row of pending) { + chunk.push(row) + pending.delete(row[0]) + if (chunk.length === WRITE_CHUNK_ROWS) { + break + } + } + return chunk +} + // Opened on first need, so Electron main (which never looks rows up) never opens it. function openedDatabase(): SessionParseCacheDatabase | null { if (database !== undefined || !owner || options === null) {