mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
* 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>
136 lines
4.4 KiB
TypeScript
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
|
|
})
|
|
})
|
|
})
|