Files
orca/src/main/opencode-usage/store.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

169 lines
5.0 KiB
TypeScript

import { codexOpenCodeTokenSessions } from '../usage/agent-token-usage'
import { app } from 'electron'
import { join } from 'node:path'
import type {
OpenCodeUsageBreakdownKind,
OpenCodeUsageBreakdownRow,
OpenCodeUsageDailyPoint,
OpenCodeUsageRange,
OpenCodeUsageScope,
OpenCodeUsageSessionRow,
OpenCodeUsageSnapshot,
OpenCodeUsageSummary
} from '../../shared/opencode-usage-types'
import type { Store } from '../persistence'
import type {
OpenCodeUsageDailyAggregate,
OpenCodeUsagePersistedState,
OpenCodeUsageSession
} from './types'
import { openCodeUsageProvider } from './opencode-usage-provider'
import { getDefaultState, normalizePersistedState } from './persisted-state-normalization'
import {
filterDailyAggregatesByScopeAndRange,
filterSessionsByScopeAndRange
} from './scope-range-filter'
import {
buildOpenCodeUsageBreakdownRows,
buildOpenCodeUsageDailyPoints,
buildOpenCodeUsageRecentSessions,
buildOpenCodeUsageSummary
} from './snapshot-rollups'
import { UsageProviderStoreLifecycle } from '../usage/usage-provider-store-lifecycle'
import { splitUsageCacheFileViaWorker } from '../usage/usage-scan-worker-spawn'
let _openCodeUsageFile: string | null = null
export function initOpenCodeUsagePath(): void {
_openCodeUsageFile = join(app.getPath('userData'), 'orca-opencode-usage.json')
}
function getOpenCodeUsageFile(): string {
if (!_openCodeUsageFile) {
_openCodeUsageFile = join(app.getPath('userData'), 'orca-opencode-usage.json')
}
return _openCodeUsageFile
}
export class OpenCodeUsageStore extends UsageProviderStoreLifecycle<
'processedDatabases',
OpenCodeUsagePersistedState,
'hasAnyOpenCodeData'
> {
constructor(store: Pick<Store, 'getRepos' | 'getAllWorktreeMeta'>) {
super(store, {
tokenUsage: {
provider: 'opencode',
selectSessions: (state) => codexOpenCodeTokenSessions(state.sessions)
},
logTag: '[opencode-usage]',
resolveCacheFile: getOpenCodeUsageFile,
createDefaultState: getDefaultState,
normalizeState: normalizePersistedState,
sourceKey: 'processedDatabases',
dataPresenceKey: 'hasAnyOpenCodeData',
scan: openCodeUsageProvider.scan,
splitCacheFile: splitUsageCacheFileViaWorker
})
}
getSnapshot(
scope: OpenCodeUsageScope,
range: OpenCodeUsageRange,
recentSessionLimit = 10
): OpenCodeUsageSnapshot {
return {
scanState: this.getScanState(),
summary: this.buildSummary(scope, range),
daily: this.buildDaily(scope, range),
modelBreakdown: this.buildBreakdown(scope, range, 'model'),
projectBreakdown: this.buildBreakdown(scope, range, 'project'),
recentSessions: this.buildRecentSessions(scope, range, recentSessionLimit)
}
}
async getSummary(
scope: OpenCodeUsageScope,
range: OpenCodeUsageRange
): Promise<OpenCodeUsageSummary> {
await this.refresh(false)
return this.buildSummary(scope, range)
}
private buildSummary(scope: OpenCodeUsageScope, range: OpenCodeUsageRange): OpenCodeUsageSummary {
return buildOpenCodeUsageSummary(
scope,
range,
this.getFilteredDaily(scope, range),
this.getFilteredSessions(scope, range)
)
}
async getDaily(
scope: OpenCodeUsageScope,
range: OpenCodeUsageRange
): Promise<OpenCodeUsageDailyPoint[]> {
await this.refresh(false)
return this.buildDaily(scope, range)
}
private buildDaily(
scope: OpenCodeUsageScope,
range: OpenCodeUsageRange
): OpenCodeUsageDailyPoint[] {
return buildOpenCodeUsageDailyPoints(this.getFilteredDaily(scope, range))
}
async getBreakdown(
scope: OpenCodeUsageScope,
range: OpenCodeUsageRange,
kind: OpenCodeUsageBreakdownKind
): Promise<OpenCodeUsageBreakdownRow[]> {
await this.refresh(false)
return this.buildBreakdown(scope, range, kind)
}
private buildBreakdown(
scope: OpenCodeUsageScope,
range: OpenCodeUsageRange,
kind: OpenCodeUsageBreakdownKind
): OpenCodeUsageBreakdownRow[] {
return buildOpenCodeUsageBreakdownRows(
kind,
this.getFilteredDaily(scope, range),
this.getFilteredSessions(scope, range)
)
}
async getRecentSessions(
scope: OpenCodeUsageScope,
range: OpenCodeUsageRange,
limit = 10
): Promise<OpenCodeUsageSessionRow[]> {
await this.refresh(false)
return this.buildRecentSessions(scope, range, limit)
}
private buildRecentSessions(
scope: OpenCodeUsageScope,
range: OpenCodeUsageRange,
limit = 10
): OpenCodeUsageSessionRow[] {
return buildOpenCodeUsageRecentSessions(this.getFilteredSessions(scope, range), limit)
}
private getFilteredDaily(
scope: OpenCodeUsageScope,
range: OpenCodeUsageRange
): OpenCodeUsageDailyAggregate[] {
return filterDailyAggregatesByScopeAndRange(this.state.dailyAggregates, scope, range)
}
private getFilteredSessions(
scope: OpenCodeUsageScope,
range: OpenCodeUsageRange
): OpenCodeUsageSession[] {
return filterSessionsByScopeAndRange(this.state.sessions, scope, range)
}
}