Files
orca/src/main/codex-usage/store-persistence.test.ts
T
09aff1b94d perf(usage): keep per-source usage caches off the main thread (#26980)
* perf(usage): keep per-source usage caches off the main thread

The usage stores kept every provider's per-source scan cache
(processedFiles / processedDatabases) inside the one JSON file main
loads at startup. On a large Claude history that file is 56 MB, 43 MB
of it per-source records only a scan reads, so main paid a sync
read+parse at launch, a structured clone of the whole cache to and
from the scan worker on every scan, and a pretty-printed rewrite
after every scan.

The usage scan worker now owns those records in a sidecar
(`<cache>-sources.json`): it reads it before a scan, writes it after
(compact, durable rename), and returns only sessions and daily
aggregates. Main keeps and writes only that report, compact. A cache
still carrying inline records is split on the worker the first time
it is loaded (above 8 MB); smaller ones parse on main and rescan cold.
Sync IPC getters and automation lookups wait for the load.

Synthetic 56 MB cache, same machine, interleaved runs:
- first launch: 108 ms sync parse -> 18 ms main busy, 4 ms longest block
- later launches: ~69 ms -> ~7 ms
- forced scan, longest main-thread block: 89 ms -> 11 ms
  (real built app, headless: 84-94 ms -> 11-13 ms)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(usage): preserve cache attribution and legacy warm scans

* fix(usage): reclaim cache temps after worker retirement

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
2026-10-09 17:19:29 -07:00

136 lines
4.4 KiB
TypeScript

import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import type { CodexUsagePersistedState } from './types'
import { createStoreWithState, setupCodexUsageStoreEnv } from './store-test-harness'
const { getPathMock } = vi.hoisted(() => ({
getPathMock: vi.fn(() => '/tmp/orca-test-userdata')
}))
vi.mock('electron', () => ({
app: {
getPath: getPathMock
}
}))
vi.mock('../usage/usage-scan-worker-spawn', () => ({
scanCodexUsageFilesViaWorker: vi.fn(),
splitUsageCacheFileViaWorker: vi.fn()
}))
import { CodexUsageStore, normalizePersistedState } from './store'
import { CODEX_USAGE_SCHEMA_VERSION } from './codex-usage-provider'
import { scanCodexUsageFilesViaWorker } from '../usage/usage-scan-worker-spawn'
describe('CodexUsageStore', () => {
const storeEnv = setupCodexUsageStoreEnv(getPathMock)
it('adapts Codex scans to compact cache persistence', async () => {
const store = createStoreWithState({
schemaVersion: 5,
scanState: {
enabled: true,
lastScanStartedAt: null,
lastScanCompletedAt: null,
lastScanError: null
}
})
await store.refresh(true)
const persistedJson = readFileSync(
join(storeEnv.tempUserData, 'orca-codex-usage.json'),
'utf-8'
)
expect(scanCodexUsageFilesViaWorker).toHaveBeenCalledWith([], {
path: join(storeEnv.tempUserData, 'orca-codex-usage-sources.json'),
schemaVersion: CODEX_USAGE_SCHEMA_VERSION,
worktreeFingerprint: '[]',
reuse: false
})
expect(persistedJson).toBe(JSON.stringify(JSON.parse(persistedJson)))
})
it('drops persisted caches from older schemas that lack scoped model breakdown data', () => {
const normalized = normalizePersistedState({
schemaVersion: 1,
processedFiles: [],
sessions: [
{
sessionId: 'legacy',
firstTimestamp: '2026-04-09T10:00:00.000Z',
lastTimestamp: '2026-04-09T10:10:00.000Z',
primaryModel: 'gpt-5',
hasMixedModels: false,
primaryProjectLabel: 'Repo',
hasMixedLocations: false,
primaryWorktreeId: 'repo-1::/workspace/repo',
primaryRepoId: 'repo-1',
eventCount: 1,
totalInputTokens: 1,
totalCachedInputTokens: 0,
totalOutputTokens: 1,
totalReasoningOutputTokens: 0,
totalTokens: 2,
hasInferredPricing: false,
locationBreakdown: [],
modelBreakdown: []
}
],
dailyAggregates: [],
scanState: {
enabled: true,
lastScanStartedAt: 1,
lastScanCompletedAt: 2,
lastScanError: null
}
} as unknown as CodexUsagePersistedState)
expect(normalized).toEqual({
schemaVersion: 6,
worktreeFingerprint: null,
processedFiles: [],
sessions: [],
dailyAggregates: [],
scanState: {
enabled: true,
lastScanStartedAt: null,
lastScanCompletedAt: null,
lastScanError: null
}
})
})
it('rescans from scratch when the cache predates per-request long-context counts', async () => {
const scanMock = vi.mocked(scanCodexUsageFilesViaWorker)
const sourceCachePath = join(storeEnv.tempUserData, 'orca-codex-usage-sources.json')
const cacheFile = join(storeEnv.tempUserData, 'orca-codex-usage.json')
const seeded = new CodexUsageStore({ getRepos: () => [], getAllWorktreeMeta: () => ({}) })
await seeded.setEnabled(true)
await seeded.refresh(true)
await seeded.flush()
const current = readFileSync(cacheFile, 'utf-8')
scanMock.mockClear()
await new CodexUsageStore({ getRepos: () => [], getAllWorktreeMeta: () => ({}) }).refresh(true)
// Control: a current-schema cache lets the worker reuse its processed files.
expect(scanMock).toHaveBeenLastCalledWith([], {
path: sourceCachePath,
schemaVersion: CODEX_USAGE_SCHEMA_VERSION,
worktreeFingerprint: '[]',
reuse: true
})
writeFileSync(cacheFile, JSON.stringify({ ...JSON.parse(current), schemaVersion: 5 }))
scanMock.mockClear()
await new CodexUsageStore({ getRepos: () => [], getAllWorktreeMeta: () => ({}) }).refresh(false)
expect(scanMock).toHaveBeenLastCalledWith([], {
path: sourceCachePath,
schemaVersion: CODEX_USAGE_SCHEMA_VERSION,
worktreeFingerprint: '[]',
reuse: false
})
})
})