cc-haha/scripts/perf/local-index-benchmark.ts
程序员阿江(Relakkes) 53a15098c0 perf(desktop): add SQLite-backed local data index #1016
Project sessions, activity, scheduled runs, traces, and searchable content into rebuildable managed databases while preserving canonical-file fallbacks. Add diagnostics, recovery, parity coverage, and performance acceptance checks.
2026-07-16 04:16:02 +08:00

1903 lines
61 KiB
TypeScript

import { createHash } from 'node:crypto'
import {
appendFile,
mkdir,
mkdtemp,
readFile,
rm,
stat,
utimes,
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, join, relative, sep } from 'node:path'
import { monitorEventLoopDelay } from 'node:perf_hooks'
import { createLocalIndexCorpus } from './local-index-corpus.js'
import type { LocalIndexCoordinator } from '../../src/server/services/localIndex/coordinator.js'
import type {
LocalIndexBinding,
LocalIndexDatabase,
LocalIndexReadOperation,
LocalIndexWriteOperation,
} from '../../src/server/services/localIndex/database.js'
import type {
SessionListShadowComparison,
} from '../../src/server/services/sessionService.js'
import type {
LocalIndexGateway,
SessionIndex,
} from '../../src/server/services/localIndex/sessionIndex.js'
import type {
SessionProjector,
SessionSourceCandidate,
} from '../../src/server/services/localIndex/sessionProjector.js'
import type { LocalIndexStatus } from '../../src/server/services/localIndex/types.js'
type BenchmarkMode = 'file' | 'sqlite' | 'shadow'
type BenchmarkScenario = 'baseline' | 'append'
export type BenchmarkOptions = {
sessions: number
entriesPerSession: number
largeTranscriptBytes: number
runs: number
warmupRuns: number
seed: number
mode: BenchmarkMode
scenario: BenchmarkScenario
timeoutMs: number
sidebarLimit: number
keep: boolean
}
type ManifestSummary = {
sourcePath: string
sessionId: string
title: string
createdAt: string
modifiedAt: string
messageCount: number
}
type CorpusManifest = {
expected: {
normalizedSessionOrder: string[]
summaries: ManifestSummary[]
}
sources: Array<{
path: string
bytes: number
sha256: string
}>
largeTranscript: {
sourcePath: string
requestedBytes: number
actualBytes: number
} | null
}
type SessionListItem = {
id: string
title: string
createdAt: string
modifiedAt: string
messageCount: number
projectPath: string
}
type SessionListResult = {
sessions: SessionListItem[]
total: number
}
export type BenchmarkSampleContext = {
phase: 'warmup' | 'measured'
index: number
rootDir: string
}
export type BenchmarkDependencies = {
executeSessionList?: (
execute: () => Promise<SessionListResult>,
context: BenchmarkSampleContext,
) => Promise<SessionListResult>
onProductGatesStart?: (context: { rootDir: string }) => void
}
type LoopbackSessionResponse = SessionListResult & {
index: LocalIndexStatus
}
type LoopbackServer = {
port: number
stop(closeActiveConnections?: boolean): void
}
export class ProductGateTimeoutError extends Error {
constructor(readonly timeoutMs: number) {
super(`sqlite product gates exceeded ${timeoutMs}ms`)
this.name = 'ProductGateTimeoutError'
}
}
export class BenchmarkSampleTimeoutError extends Error {
readonly timeoutCount = 1
constructor(
readonly phase: BenchmarkSampleContext['phase'],
readonly index: number,
readonly timeoutMs: number,
) {
super(`${phase} sample ${index + 1} exceeded ${timeoutMs}ms; timeoutCount=1`)
this.name = 'BenchmarkSampleTimeoutError'
}
}
export async function runWithSampleDeadline<T>(options: {
execute: () => Promise<T>
timeoutMs: number
phase: BenchmarkSampleContext['phase']
index: number
setTimer?: typeof setTimeout
clearTimer?: typeof clearTimeout
}): Promise<T> {
const setTimer = options.setTimer ?? setTimeout
const clearTimer = options.clearTimer ?? clearTimeout
return new Promise<T>((resolve, reject) => {
let settled = false
const operation = Promise.resolve().then(options.execute)
const timer = setTimer(() => {
if (settled) return
settled = true
clearTimer(timer)
reject(new BenchmarkSampleTimeoutError(
options.phase,
options.index,
options.timeoutMs,
))
}, options.timeoutMs)
operation.then(
value => {
if (settled) return
settled = true
clearTimer(timer)
resolve(value)
},
error => {
if (settled) return
settled = true
clearTimer(timer)
reject(error)
},
)
})
}
export async function captureStartupResponses<TFirst, TReady>(options: {
firstUsefulRequest: Promise<TFirst>
readyRequest: Promise<TReady>
elapsedSinceServerStartMs: () => number
onFirstUsefulSettled?: (durationMs: number) => void
}): Promise<{
firstUseful: TFirst
ready: TReady
firstUsefulDurationMs: number
}> {
const guardedReady = options.readyRequest.then(
value => ({ ok: true as const, value }),
error => ({ ok: false as const, error }),
)
const firstUseful = await options.firstUsefulRequest
const firstUsefulDurationMs = options.elapsedSinceServerStartMs()
options.onFirstUsefulSettled?.(firstUsefulDurationMs)
const readyResult = await guardedReady
if (!readyResult.ok) throw readyResult.error
return { firstUseful, ready: readyResult.value, firstUsefulDurationMs }
}
type RecordedSql = {
kind: 'get' | 'all' | 'run' | 'exec'
sql: string
bindings: LocalIndexBinding[]
}
type RecordingDatabase = {
database: LocalIndexDatabase
beginRecording(): void
endRecording(): RecordedSql[]
}
export type DeterministicAcceptanceOptions = {
sessions: number
entriesPerSession: number
seed: number
readTranscriptBodyOpenCount: () => number
}
export type DeterministicAcceptanceReport = {
schemaVersion: number
fixture: {
sessions: number
pageSize: number
corpusFingerprint: string
}
sourceIntegrity: {
unchangedAfterBackfill: boolean
changedSourceCountAfterAppend: number
unrelatedSourcesUnchangedAfterAppend: boolean
}
scheduling: {
maxBatchSize: number
yieldCount: number
}
queries: {
page1: { offset: number; rowCount: number; statementCount: number }
page100: { offset: number; rowCount: number; statementCount: number }
globalPlan: string[]
projectPlan: string[]
}
indexedSessionList: {
total: number
rowCount: number
transcriptBodyOpens: number
}
componentAppend: {
action: string
changedSourceRows: string[]
changedSessionRows: string[]
messageCountDelta: number
}
shadow: {
comparisonCount: number
mismatchCount: number
differenceCount: number
transcriptBodyOpens: number
}
productAppend: {
measured: false
reason: 'LOCAL_INDEX_RECONCILIATION_NOT_ACCEPTED'
}
}
const DEFAULT_OPTIONS: BenchmarkOptions = {
sessions: 500,
entriesPerSession: 8,
largeTranscriptBytes: 0,
runs: 3,
warmupRuns: 1,
seed: 20260714,
mode: 'file',
scenario: 'baseline',
timeoutMs: 120_000,
sidebarLimit: 400,
keep: false,
}
function integerArgument(
name: string,
rawValue: string | undefined,
minimum: number,
): number {
if (rawValue === undefined) {
throw new Error(`${name} requires a value`)
}
const value = Number(rawValue)
if (!Number.isSafeInteger(value) || value < minimum) {
throw new Error(`${name} must be a safe integer >= ${minimum}`)
}
return value
}
export function parseBenchmarkArgs(args: string[]): BenchmarkOptions {
const options = { ...DEFAULT_OPTIONS }
for (let index = 0; index < args.length; index += 1) {
const argument = args[index]!
if (argument === '--') continue
if (argument === '--keep') {
options.keep = true
continue
}
const value = args[index + 1]
if (argument === '--sessions') {
options.sessions = integerArgument(argument, value, 1)
} else if (argument === '--entries-per-session') {
options.entriesPerSession = integerArgument(argument, value, 6)
} else if (argument === '--large-transcript-bytes') {
options.largeTranscriptBytes = integerArgument(argument, value, 0)
} else if (argument === '--large-transcript-mib') {
const mebibytes = integerArgument(argument, value, 1)
const bytes = mebibytes * 1024 * 1024
if (!Number.isSafeInteger(bytes)) {
throw new Error('--large-transcript-mib is too large')
}
options.largeTranscriptBytes = bytes
} else if (argument === '--runs') {
options.runs = integerArgument(argument, value, 1)
} else if (argument === '--warmup-runs') {
options.warmupRuns = integerArgument(argument, value, 0)
} else if (argument === '--seed') {
options.seed = integerArgument(argument, value, 0)
} else if (argument === '--timeout-ms') {
options.timeoutMs = integerArgument(argument, value, 1)
} else if (argument === '--sidebar-limit') {
options.sidebarLimit = integerArgument(argument, value, 1)
} else if (argument === '--mode') {
if (value !== 'file' && value !== 'sqlite' && value !== 'shadow') {
throw new Error('--mode must be file, sqlite, or shadow')
}
options.mode = value
} else if (argument === '--scenario') {
if (value !== 'baseline' && value !== 'append') {
throw new Error('--scenario must be baseline or append')
}
options.scenario = value
} else {
throw new Error(`unknown argument: ${argument}`)
}
index += 1
}
return options
}
function percentile(samples: number[], quantile: number): number {
const sorted = [...samples].sort((a, b) => a - b)
const index = Math.max(0, Math.ceil(sorted.length * quantile) - 1)
return sorted[index]!
}
function roundedMilliseconds(value: number): number {
return Math.round(value * 1000) / 1000
}
function corpusFingerprint(manifest: CorpusManifest): string {
const hash = createHash('sha256')
for (const source of manifest.sources) {
hash.update(`${source.path}\0${source.bytes}\0${source.sha256}\n`)
}
return hash.digest('hex')
}
function normalizedSessionKey(session: SessionListItem): string {
return `${session.id}@projects/${session.projectPath}/${session.id}.jsonl`
}
function validateResult(
result: SessionListResult,
manifest: CorpusManifest,
limit = Number.MAX_SAFE_INTEGER,
): {
expectedSessions: number
actualSessions: number
orderMatches: boolean
summariesMatch: boolean
} {
const actualOrder = result.sessions.map(normalizedSessionKey)
const expectedOrder = manifest.expected.normalizedSessionOrder.slice(0, limit)
const expectedBySource = new Map(
manifest.expected.summaries.map(summary => [summary.sourcePath, summary]),
)
const summariesMatch = result.sessions.every(session => {
const source = `projects/${session.projectPath}/${session.id}.jsonl`
const expected = expectedBySource.get(source)
return expected !== undefined &&
session.id === expected.sessionId &&
session.title === expected.title &&
session.createdAt === expected.createdAt &&
session.modifiedAt === expected.modifiedAt &&
session.messageCount === expected.messageCount
})
return {
expectedSessions: manifest.expected.summaries.length,
actualSessions: result.total,
orderMatches:
JSON.stringify(actualOrder) ===
JSON.stringify(expectedOrder),
summariesMatch:
summariesMatch && result.sessions.length === expectedOrder.length,
}
}
function restoreEnvironment(
name:
| 'HOME'
| 'CLAUDE_CONFIG_DIR'
| 'CC_HAHA_LOCAL_INDEX'
| 'CC_HAHA_LOCAL_ACCESS_TOKEN',
value: string | undefined,
): void {
if (value === undefined) {
delete process.env[name]
} else {
process.env[name] = value
}
}
function createRecordingDatabase(target: LocalIndexDatabase): RecordingDatabase {
let recording = false
let records: RecordedSql[] = []
const record = (
kind: RecordedSql['kind'],
sql: string,
bindings: LocalIndexBinding[] = [],
): void => {
if (recording) records.push({ kind, sql, bindings: [...bindings] })
}
const readOperation = (
operation: LocalIndexReadOperation,
): LocalIndexReadOperation => ({
get<T>(sql: string, ...bindings: LocalIndexBinding[]): T | null {
record('get', sql, bindings)
return operation.get<T>(sql, ...bindings)
},
all<T>(sql: string, ...bindings: LocalIndexBinding[]): T[] {
record('all', sql, bindings)
return operation.all<T>(sql, ...bindings)
},
})
const writeOperation = (
operation: LocalIndexWriteOperation,
): LocalIndexWriteOperation => ({
...readOperation(operation),
run(sql: string, ...bindings: LocalIndexBinding[]) {
record('run', sql, bindings)
return operation.run(sql, ...bindings)
},
exec(sql: string): void {
record('exec', sql)
operation.exec(sql)
},
})
return {
database: {
read: callback => target.read(operation => callback(readOperation(operation))),
write: callback => target.write(operation => callback(writeOperation(operation))),
transaction: callback => target.transaction(
operation => callback(writeOperation(operation)),
),
close: () => target.close(),
},
beginRecording(): void {
records = []
recording = true
},
endRecording(): RecordedSql[] {
recording = false
return records.map(entry => ({
...entry,
bindings: [...entry.bindings],
}))
},
}
}
async function hashManifestSources(
configDir: string,
sources: CorpusManifest['sources'],
): Promise<Map<string, { bytes: number; sha256: string }>> {
const hashes = new Map<string, { bytes: number; sha256: string }>()
for (const source of sources) {
const content = await readFile(join(configDir, source.path))
hashes.set(source.path, {
bytes: content.byteLength,
sha256: createHash('sha256').update(content).digest('hex'),
})
}
return hashes
}
function hashesMatchManifest(
hashes: Map<string, { bytes: number; sha256: string }>,
sources: CorpusManifest['sources'],
): boolean {
return sources.every(source => {
const actual = hashes.get(source.path)
return actual?.bytes === source.bytes && actual.sha256 === source.sha256
})
}
function rowMap(
rows: Array<Record<string, unknown>>,
key: string,
): Map<string, string> {
return new Map(rows.map(row => [String(row[key]), JSON.stringify(row)]))
}
function changedRowKeys(before: Map<string, string>, after: Map<string, string>): string[] {
const keys = new Set([...before.keys(), ...after.keys()])
return [...keys]
.filter(key => before.get(key) !== after.get(key))
.sort()
}
function requireSql(
records: RecordedSql[],
predicate: (record: RecordedSql) => boolean,
label: string,
): RecordedSql {
const record = records.find(predicate)
if (!record) throw new Error(`acceptance did not capture ${label} SQL`)
return record
}
function createOnGateway(coordinator: LocalIndexCoordinator): LocalIndexGateway {
return {
start: () => coordinator.start(),
stop: () => coordinator.stop(),
getMode: () => 'on',
getPublicStatus: () => coordinator.getPublicStatus(),
isSessionScopeReady: () => coordinator.isSessionScopeReady(),
rebuild: () => coordinator.rebuild(),
listSessions: options => coordinator.listSessions(options),
findSessionFiles: sessionId => coordinator.findSessionFiles(sessionId),
}
}
async function waitForIndexReady(
coordinator: LocalIndexCoordinator,
timeoutMs: number,
): Promise<LocalIndexStatus> {
const startedAt = Bun.nanoseconds()
while (true) {
const status = coordinator.getPublicStatus()
if (status.state === 'ready') return status
if (status.state === 'degraded') {
throw new Error(
`local index degraded before ready: ${status.lastErrorCode ?? 'LOCAL_INDEX_UNKNOWN'}`,
)
}
const elapsedMs = (Bun.nanoseconds() - startedAt) / 1_000_000
if (elapsedMs > timeoutMs) {
throw new Error(`local index did not become ready within ${timeoutMs}ms`)
}
await Bun.sleep(5)
}
}
function throwIfAborted(signal: AbortSignal): void {
if (!signal.aborted) return
throw signal.reason instanceof Error
? signal.reason
: new Error('sqlite product gates aborted')
}
async function sleepWithSignal(ms: number, signal: AbortSignal): Promise<void> {
throwIfAborted(signal)
await new Promise<void>((resolveSleep, rejectSleep) => {
const timer = setTimeout(() => {
signal.removeEventListener('abort', onAbort)
resolveSleep()
}, ms)
const onAbort = (): void => {
clearTimeout(timer)
rejectSleep(signal.reason instanceof Error
? signal.reason
: new Error('sqlite product gates aborted'))
}
signal.addEventListener('abort', onAbort, { once: true })
})
}
function eventLoopMilliseconds(nanoseconds: number): number {
return Number.isFinite(nanoseconds)
? roundedMilliseconds(nanoseconds / 1_000_000)
: 0
}
export async function runSqliteProductBenchmark(
options: BenchmarkOptions,
context: {
rootDir: string
configDir: string
manifestPath: string
manifest: CorpusManifest
localAccessToken: string
},
dependencies: BenchmarkDependencies = {},
): Promise<Record<string, unknown>> {
const controller = new AbortController()
const deadline = setTimeout(() => {
controller.abort(new ProductGateTimeoutError(options.timeoutMs))
}, options.timeoutMs)
const signal = controller.signal
const eventLoop = monitorEventLoopDelay({ resolution: 10 })
const rssBaselineBytes = process.memoryUsage().rss
let peakRssBytes = rssBaselineBytes
let rssSampleCount = 1
const sampleRss = (): void => {
peakRssBytes = Math.max(peakRssBytes, process.memoryUsage().rss)
rssSampleCount += 1
}
const rssSampler = setInterval(sampleRss, 5)
rssSampler.unref?.()
eventLoop.enable()
let server: LoopbackServer | undefined
let stopServerRuntime: (() => Promise<void>) | undefined
let coordinator: LocalIndexCoordinator | undefined
let operationError: unknown
let monitorStopped = false
const originalConsoleLog = console.log
const originalConsoleInfo = console.info
const originalConsoleWarn = console.warn
console.log = () => {}
console.info = () => {}
console.warn = () => {}
const stopMonitors = (): void => {
if (monitorStopped) return
monitorStopped = true
clearInterval(rssSampler)
sampleRss()
eventLoop.disable()
}
try {
dependencies.onProductGatesStart?.({ rootDir: context.rootDir })
const serverModule = await import('../../src/server/index.js')
const coordinatorModule = await import(
'../../src/server/services/localIndex/coordinator.js'
)
coordinator = coordinatorModule.localIndexCoordinator
stopServerRuntime = () => serverModule.stopServerRuntimeForShutdown({
waitForCli: true,
})
const serverStartedAt = Bun.nanoseconds()
server = serverModule.startServer(0, '127.0.0.1')
const baseUrl = `http://127.0.0.1:${server.port}`
const requestSessions = async (): Promise<{
body: LoopbackSessionResponse
durationMs: number
status: number
}> => {
throwIfAborted(signal)
const startedAt = Bun.nanoseconds()
let response: Response
try {
response = await fetch(
`${baseUrl}/api/sessions?limit=${options.sidebarLimit}&offset=0`,
{
signal,
headers: {
Authorization: `Bearer ${context.localAccessToken}`,
},
},
)
} catch (error) {
throwIfAborted(signal)
throw error
}
if (!response.ok) {
throw new Error(`sessions HTTP returned ${response.status}`)
}
const body = await response.json() as LoopbackSessionResponse
if (
!Array.isArray(body.sessions) ||
!Number.isSafeInteger(body.total) ||
!body.index ||
typeof body.index.state !== 'string'
) {
throw new Error('sessions HTTP returned an invalid product response')
}
sampleRss()
return {
body,
durationMs: (Bun.nanoseconds() - startedAt) / 1_000_000,
status: response.status,
}
}
const immediateRequestOffsetMs =
(Bun.nanoseconds() - serverStartedAt) / 1_000_000
const expectedReadyRows = Math.min(
options.sidebarLimit,
context.manifest.expected.summaries.length,
)
const firstUsefulRequest = requestSessions()
const foregroundDurations: number[] = []
let foregroundSuccessfulResponses = 0
let foregroundBuildingResponses = 0
const readyRequest = (async () => {
while (true) {
const result = await requestSessions()
foregroundDurations.push(result.durationMs)
foregroundSuccessfulResponses += 1
if (result.body.index.state === 'degraded') {
throw new Error(
`local index degraded during foreground polling: ${result.body.index.lastErrorCode ?? 'LOCAL_INDEX_UNKNOWN'}`,
)
}
if (result.body.index.state === 'building') {
foregroundBuildingResponses += 1
}
if (
result.body.index.state === 'ready' &&
result.body.total === context.manifest.expected.summaries.length &&
result.body.sessions.length === expectedReadyRows
) {
return result
}
await sleepWithSignal(5, signal)
}
})()
const { firstUseful, ready, firstUsefulDurationMs } =
await captureStartupResponses({
firstUsefulRequest,
readyRequest,
elapsedSinceServerStartMs: () =>
(Bun.nanoseconds() - serverStartedAt) / 1_000_000,
})
if (firstUseful.body.sessions.length === 0) {
throw new Error('immediate sessions HTTP did not return useful content')
}
if (
ready.body.total !== context.manifest.expected.summaries.length ||
ready.body.sessions.length !== expectedReadyRows
) {
throw new Error(`ready sessions HTTP was incomplete: ${JSON.stringify({
expectedTotal: context.manifest.expected.summaries.length,
actualTotal: ready.body.total,
expectedRows: expectedReadyRows,
actualRows: ready.body.sessions.length,
})}`)
}
const readyDurationMs =
(Bun.nanoseconds() - serverStartedAt) / 1_000_000
const authProbeUrl =
`${baseUrl}/api/sessions?limit=${Math.min(1, options.sidebarLimit)}&offset=0`
const [missingTokenResponse, wrongTokenResponse] = await Promise.all([
fetch(authProbeUrl, { signal }),
fetch(authProbeUrl, {
signal,
headers: { Authorization: 'Bearer wrong-local-access-token' },
}),
])
const loopbackAuth = {
measured: true,
missingTokenStatus: missingTokenResponse.status,
wrongTokenStatus: wrongTokenResponse.status,
correctTokenStatus: firstUseful.status,
}
if (
loopbackAuth.missingTokenStatus !== 403 ||
loopbackAuth.wrongTokenStatus !== 403 ||
loopbackAuth.correctTokenStatus !== 200
) {
throw new Error(`loopback local-access auth proof failed: ${JSON.stringify(loopbackAuth)}`)
}
const execute = async (): Promise<SessionListResult> => {
const result = await requestSessions()
return {
sessions: result.body.sessions,
total: result.body.total,
}
}
const executeDeadlineSample = (
phase: BenchmarkSampleContext['phase'],
index: number,
): Promise<SessionListResult> => runWithSampleDeadline({
execute: () => dependencies.executeSessionList
? dependencies.executeSessionList(execute, {
phase,
index,
rootDir: context.rootDir,
})
: execute(),
timeoutMs: options.timeoutMs,
phase,
index,
})
for (let index = 0; index < options.warmupRuns; index += 1) {
await executeDeadlineSample('warmup', index)
}
const samples: number[] = []
const cpuStart = process.cpuUsage()
const rssStart = process.memoryUsage().rss
let lastResult: SessionListResult | undefined
for (let index = 0; index < options.runs; index += 1) {
const startedAt = Bun.nanoseconds()
lastResult = await executeDeadlineSample('measured', index)
samples.push((Bun.nanoseconds() - startedAt) / 1_000_000)
}
const cpu = process.cpuUsage(cpuStart)
const fileValidation = validateResult(
lastResult!,
context.manifest,
options.sidebarLimit,
)
if (
fileValidation.actualSessions !== fileValidation.expectedSessions ||
!fileValidation.orderMatches ||
!fileValidation.summariesMatch
) {
throw new Error(`sessions HTTP did not match the corpus manifest: ${JSON.stringify(fileValidation)}`)
}
const appendTarget = ready.body.sessions[0]
if (!appendTarget) throw new Error('product append has no target session')
const appendPath = join(
context.configDir,
'projects',
appendTarget.projectPath,
`${appendTarget.id}.jsonl`,
)
await stat(appendPath)
const appendStartedAt = Bun.nanoseconds()
await appendFile(appendPath, `${JSON.stringify({
parentUuid: null,
isSidechain: false,
type: 'assistant',
message: {
id: 'msg_product_watcher_append',
type: 'message',
role: 'assistant',
model: 'claude-synthetic-benchmark',
content: [{ type: 'text', text: 'Product watcher append' }],
usage: {
input_tokens: 1,
output_tokens: 1,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
uuid: 'product-watcher-append-entry',
timestamp: '2099-12-31T23:59:59.000Z',
})}\n`)
const { SessionService } = await import(
'../../src/server/services/sessionService.js'
)
let appendedMessageCount = appendTarget.messageCount
let appendPollCount = 0
while (true) {
throwIfAborted(signal)
appendPollCount += 1
const status = coordinator.getPublicStatus()
if (status.state === 'degraded') {
throw new Error(
`local index degraded during product append: ${status.lastErrorCode ?? 'LOCAL_INDEX_UNKNOWN'}`,
)
}
const result = await new SessionService(
createOnGateway(coordinator),
).listSessions({ limit: options.sidebarLimit, offset: 0 })
const updated = result.sessions.find(session =>
session.id === appendTarget.id &&
session.projectPath === appendTarget.projectPath,
)
appendedMessageCount = updated?.messageCount ?? appendTarget.messageCount
if (
appendedMessageCount === appendTarget.messageCount + 1 &&
status.state === 'ready'
) {
break
}
await sleepWithSignal(5, signal)
}
const appendDurationMs =
(Bun.nanoseconds() - appendStartedAt) / 1_000_000
stopMonitors()
const median = percentile(samples, 0.5)
const p95 = percentile(samples, 0.95)
const max = Math.max(...samples)
const eventLoopSampleCount = Number(eventLoop.count)
return {
schemaVersion: 1,
mode: options.mode,
scenario: options.scenario,
fixture: {
rootDir: context.rootDir,
configDir: context.configDir,
manifestPath: context.manifestPath,
seed: options.seed,
sessions: options.sessions,
entriesPerSession: options.entriesPerSession,
largeTranscriptBytes: options.largeTranscriptBytes,
sidebarLimit: options.sidebarLimit,
corpusFingerprint: corpusFingerprint(context.manifest),
kept: options.keep,
},
measurement: {
operation: 'sessions-http',
warmupRuns: options.warmupRuns,
runs: options.runs,
durationMs: {
median: roundedMilliseconds(median),
p95: roundedMilliseconds(p95),
max: roundedMilliseconds(max),
},
timeoutMs: options.timeoutMs,
timeoutCount: 0,
cpuMs: {
user: roundedMilliseconds(cpu.user / 1000),
system: roundedMilliseconds(cpu.system / 1000),
total: roundedMilliseconds((cpu.user + cpu.system) / 1000),
},
rssDeltaBytes: process.memoryUsage().rss - rssStart,
io: {
instrumented: false,
filesOpened: null,
bytesRead: null,
},
productAppend: {
measured: true,
watcherObserved: true,
messageCountDelta: appendedMessageCount - appendTarget.messageCount,
durationMs: roundedMilliseconds(appendDurationMs),
pollCount: appendPollCount,
finalState: coordinator.getPublicStatus().state,
},
referenceHardware: {
measured: false,
reason: 'REFERENCE_HARDWARE_NOT_RUN',
},
sidebarApi: {
measured: true,
transport: 'loopback-http',
limit: options.sidebarLimit,
rowCount: lastResult!.sessions.length,
total: lastResult!.total,
},
firstUsefulContent: {
measured: true,
requestedImmediately: true,
requestOffsetMs: roundedMilliseconds(immediateRequestOffsetMs),
rowCount: firstUseful.body.sessions.length,
total: firstUseful.body.total,
durationMs: roundedMilliseconds(firstUsefulDurationMs),
indexState: firstUseful.body.index.state,
},
foregroundDuringBackfill: {
measured: true,
requestCount: foregroundDurations.length,
successfulResponseCount: foregroundSuccessfulResponses,
buildingResponseCount: foregroundBuildingResponses,
durationMs: {
p95: roundedMilliseconds(percentile(foregroundDurations, 0.95)),
max: roundedMilliseconds(Math.max(...foregroundDurations)),
},
},
eventLoopDelay: {
measured: true,
sampleCount: eventLoopSampleCount,
meanMs: eventLoopMilliseconds(eventLoop.mean),
p95Ms: eventLoopMilliseconds(eventLoop.percentile(95)),
p99Ms: eventLoopMilliseconds(eventLoop.percentile(99)),
maxMs: eventLoopMilliseconds(eventLoop.max),
},
peakRss: {
measured: true,
sampleCount: rssSampleCount,
baselineBytes: rssBaselineBytes,
peakBytes: peakRssBytes,
peakDeltaBytes: peakRssBytes - rssBaselineBytes,
},
loopbackAuth,
},
index: {
readyDurationMs: roundedMilliseconds(readyDurationMs),
status: coordinator.getPublicStatus(),
scheduling: coordinator.getSchedulingMetrics(),
},
validation: fileValidation,
}
} catch (error) {
operationError = signal.aborted && signal.reason instanceof Error
? signal.reason
: error
throw operationError
} finally {
clearTimeout(deadline)
stopMonitors()
let cleanupError: unknown
try {
server?.stop(true)
} catch (error) {
cleanupError = error
}
try {
await stopServerRuntime?.()
} catch (error) {
cleanupError ??= error
}
console.log = originalConsoleLog
console.info = originalConsoleInfo
console.warn = originalConsoleWarn
if (cleanupError && operationError === undefined) throw cleanupError
}
}
const APPEND_BYTES = 4 * 1024
const APPEND_IO_LIMIT_BYTES = 1024 * 1024
function createAppendPayload(): Buffer {
const prefix = '{"type":"progress","marker":"synthetic-append","data":"'
const suffix = '"}\n'
const dataBytes = APPEND_BYTES - Buffer.byteLength(prefix) - Buffer.byteLength(suffix)
if (dataBytes < 0) throw new Error('append payload envelope exceeds target size')
const payload = Buffer.from(`${prefix}${'x'.repeat(dataBytes)}${suffix}`)
if (payload.length !== APPEND_BYTES) {
throw new Error(`append payload must be exactly ${APPEND_BYTES} bytes`)
}
return payload
}
async function runAppendScenario(options: BenchmarkOptions, context: {
rootDir: string
manifest: CorpusManifest
manifestPath: string
configDir: string
}) {
const largeTranscript = context.manifest.largeTranscript
if (!largeTranscript) {
throw new Error('append scenario requires --large-transcript-mib or --large-transcript-bytes')
}
const sourcePath = join(context.configDir, largeTranscript.sourcePath)
const { SessionService } = await import('../../src/server/services/sessionService.js')
const baseline = await new SessionService().listSessions({
limit: Number.MAX_SAFE_INTEGER,
})
const baselineValidation = validateResult(baseline, context.manifest)
if (
baselineValidation.actualSessions !== baselineValidation.expectedSessions ||
!baselineValidation.orderMatches ||
!baselineValidation.summariesMatch
) {
throw new Error(`append baseline did not match the corpus manifest: ${JSON.stringify(baselineValidation)}`)
}
const {
captureSourceFingerprint,
detectSourceChange,
verifySourceFingerprint,
} = await import('../../src/server/services/localIndex/sourceFingerprint.js')
const { readCompleteJsonlRange } = await import(
'../../src/server/services/localIndex/fileReader.js'
)
const beforeAppend = await stat(sourcePath)
const previous = await captureSourceFingerprint({
path: sourcePath,
indexedBytes: beforeAppend.size,
parserVersion: 1,
})
const payload = createAppendPayload()
await appendFile(sourcePath, payload)
const execute = async () => {
const io = { filesOpened: 0, bytesRead: 0, statCalls: 0 }
const change = await detectSourceChange({
path: sourcePath,
previous,
parserVersion: 1,
metrics: io,
})
if (change.kind !== 'append') {
throw new Error(`append scenario expected append, received ${JSON.stringify(change)}`)
}
const range = await readCompleteJsonlRange({
path: sourcePath,
start: change.readFrom,
metrics: io,
})
const current = await captureSourceFingerprint({
path: sourcePath,
indexedBytes: range.nextOffset,
parserVersion: 1,
metrics: io,
})
const verified = await verifySourceFingerprint({
path: sourcePath,
expected: current,
metrics: io,
})
return { change, range, verified, io }
}
for (let index = 0; index < options.warmupRuns; index += 1) {
await runWithSampleDeadline({
execute,
timeoutMs: options.timeoutMs,
phase: 'warmup',
index,
})
}
const samples: number[] = []
const ioSamples: Array<{ filesOpened: number; bytesRead: number; statCalls: number }> = []
const cpuStart = process.cpuUsage()
const rssStart = process.memoryUsage().rss
let lastResult: Awaited<ReturnType<typeof execute>> | undefined
for (let index = 0; index < options.runs; index += 1) {
const startedAt = Bun.nanoseconds()
lastResult = await runWithSampleDeadline({
execute,
timeoutMs: options.timeoutMs,
phase: 'measured',
index,
})
const durationMs = (Bun.nanoseconds() - startedAt) / 1_000_000
samples.push(durationMs)
ioSamples.push(lastResult.io)
}
const maxBytesReadPerRun = Math.max(...ioSamples.map(io => io.bytesRead))
if (maxBytesReadPerRun > APPEND_IO_LIMIT_BYTES) {
throw new Error(
`append fingerprint+range-read exceeded ${APPEND_IO_LIMIT_BYTES} bytes: ${maxBytesReadPerRun}`,
)
}
const cpu = process.cpuUsage(cpuStart)
const median = percentile(samples, 0.5)
const p95 = percentile(samples, 0.95)
const max = Math.max(...samples)
const totals = ioSamples.reduce(
(total, io) => ({
filesOpened: total.filesOpened + io.filesOpened,
bytesRead: total.bytesRead + io.bytesRead,
statCalls: total.statCalls + io.statCalls,
}),
{ filesOpened: 0, bytesRead: 0, statCalls: 0 },
)
const last = lastResult!
return {
schemaVersion: 1,
mode: options.mode,
scenario: options.scenario,
operation: 'fingerprint+range-read',
fixture: {
rootDir: context.rootDir,
configDir: context.configDir,
manifestPath: context.manifestPath,
seed: options.seed,
sessions: options.sessions,
entriesPerSession: options.entriesPerSession,
largeTranscriptBytes: options.largeTranscriptBytes,
actualLargeTranscriptBytes: largeTranscript.actualBytes,
appendBytes: payload.length,
corpusFingerprint: corpusFingerprint(context.manifest),
kept: options.keep,
},
measurement: {
warmupRuns: options.warmupRuns,
runs: options.runs,
durationMs: {
median: roundedMilliseconds(median),
p95: roundedMilliseconds(p95),
max: roundedMilliseconds(max),
},
timeoutMs: options.timeoutMs,
timeoutCount: 0,
cpuMs: {
user: roundedMilliseconds(cpu.user / 1000),
system: roundedMilliseconds(cpu.system / 1000),
total: roundedMilliseconds((cpu.user + cpu.system) / 1000),
},
rssDeltaBytes: process.memoryUsage().rss - rssStart,
io: {
instrumented: true,
...totals,
maxBytesReadPerRun,
limitBytes: APPEND_IO_LIMIT_BYTES,
withinLimit: maxBytesReadPerRun <= APPEND_IO_LIMIT_BYTES,
},
},
validation: {
changeKind: last.change.kind,
nextOffsetAdvancedBy: last.range.nextOffset - previous.indexedBytes,
pendingTailBytes: last.range.pendingTailBytes,
snapshotVerified: last.verified.kind === 'unchanged',
},
}
}
export async function runDeterministicAcceptance(
options: DeterministicAcceptanceOptions,
): Promise<DeterministicAcceptanceReport> {
const rootDir = await mkdtemp(join(tmpdir(), 'cc-haha-local-index-acceptance-'))
const homeDir = join(rootDir, 'home')
const configDir = join(homeDir, '.claude')
const databasePath = join(configDir, 'cc-haha', 'db', 'index-v1.sqlite')
const originalHome = process.env.HOME
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
const originalLocalIndexMode = process.env.CC_HAHA_LOCAL_INDEX
let coordinator: LocalIndexCoordinator | undefined
try {
await mkdir(homeDir, { recursive: true })
process.env.HOME = homeDir
process.env.CLAUDE_CONFIG_DIR = configDir
process.env.CC_HAHA_LOCAL_INDEX = 'shadow'
const corpus = await createLocalIndexCorpus({
rootDir,
sessions: options.sessions,
entriesPerSession: options.entriesPerSession,
seed: options.seed,
})
const manifest = JSON.parse(
await readFile(corpus.manifestPath, 'utf8'),
) as CorpusManifest
const targetPath = corpus.transcriptPaths[0]!
// The corpus normally aligns physical mtime with semantic modifiedAt. Move
// one physical mtime forward before indexing so the projector can prove a
// same-process append from reducer state instead of conservatively taking
// its file-mtime-fallback rebuild branch. Source bytes remain untouched.
const appendFixtureMtime = new Date('2080-01-01T00:00:00.000Z')
await utimes(targetPath, appendFixtureMtime, appendFixtureMtime)
const expectedFingerprint = corpusFingerprint(manifest)
const projectionIo = { filesOpened: 0, bytesRead: 0, statCalls: 0 }
let recordingDatabase: RecordingDatabase | undefined
let activeIndex: SessionIndex | undefined
let activeProjector: SessionProjector | undefined
const {
createLocalIndexCoordinator,
} = await import('../../src/server/services/localIndex/coordinator.js')
const {
openLocalIndexDatabase,
} = await import('../../src/server/services/localIndex/database.js')
const {
createSessionIndex,
} = await import('../../src/server/services/localIndex/sessionIndex.js')
const {
createSessionProjector,
} = await import('../../src/server/services/localIndex/sessionProjector.js')
coordinator = createLocalIndexCoordinator({
resolveMode: () => ({ mode: 'shadow', warningCode: null }),
resolveScope: () => corpus.configDir,
resolveDatabasePath: () => databasePath,
openDatabase: path => {
recordingDatabase = createRecordingDatabase(
openLocalIndexDatabase({ path }),
)
return recordingDatabase.database
},
createIndex: database => {
activeIndex = createSessionIndex(database)
return activeIndex
},
createProjector: projectorOptions => {
activeProjector = createSessionProjector({
...projectorOptions,
metrics: projectionIo,
})
return activeProjector
},
// Task 8 owns watcher/recovery behavior. Keep this Task 9 proof scoped to
// one deterministic startup generation so incidental filesystem events
// cannot turn the scheduling invariant into a timing-dependent count.
createWatcher: () => ({
async start() {},
async stop() {},
queueTranscriptPath() {},
queueFullSweep() {},
getMetrics: () => ({
queuedPaths: 0,
maxBatchSize: 0,
yielded: 0,
fullSweeps: 0,
watchFailures: 0,
}),
}),
yieldToForeground: () => new Promise<void>(resolve => setTimeout(resolve, 0)),
})
await coordinator.start()
const readyStatus = await waitForIndexReady(coordinator, 120_000)
if (!recordingDatabase || !activeIndex || !activeProjector) {
throw new Error('acceptance coordinator did not initialize its production dependencies')
}
if (
readyStatus.indexed !== options.sessions ||
readyStatus.discovered !== options.sessions ||
readyStatus.degradedSources !== 0
) {
throw new Error(`acceptance backfill was incomplete: ${JSON.stringify(readyStatus)}`)
}
const afterBackfillHashes = await hashManifestSources(
corpus.configDir,
manifest.sources,
)
const unchangedAfterBackfill = hashesMatchManifest(
afterBackfillHashes,
manifest.sources,
)
const pageSize = 100
const page100Offset = 9_900
recordingDatabase.beginRecording()
const page1 = activeIndex.listSessions({ limit: pageSize, offset: 0 })
const page1Sql = recordingDatabase.endRecording()
recordingDatabase.beginRecording()
const page100 = activeIndex.listSessions({
limit: pageSize,
offset: page100Offset,
})
const page100Sql = recordingDatabase.endRecording()
const globalSelect = requireSql(
page1Sql,
record => record.kind === 'all' &&
/FROM\s+sessions/i.test(record.sql) &&
/ORDER\s+BY/i.test(record.sql),
'global session list',
)
const globalPlan = recordingDatabase.database.read(operation =>
operation.all<{ detail: string }>(
`EXPLAIN QUERY PLAN ${globalSelect.sql}`,
...globalSelect.bindings,
).map(row => row.detail),
)
const projectPath = page1.sessions[0]?.projectPath
if (!projectPath) throw new Error('acceptance page 1 unexpectedly returned no sessions')
recordingDatabase.beginRecording()
activeIndex.listSessions({ project: projectPath, limit: pageSize, offset: 0 })
const projectSql = recordingDatabase.endRecording()
const projectSelect = requireSql(
projectSql,
record => record.kind === 'all' &&
/FROM\s+sessions/i.test(record.sql) &&
/WHERE\s+project_path\s*=\s*\?/i.test(record.sql),
'project session list',
)
const projectPlan = recordingDatabase.database.read(operation =>
operation.all<{ detail: string }>(
`EXPLAIN QUERY PLAN ${projectSelect.sql}`,
...projectSelect.bindings,
).map(row => row.detail),
)
const hasBareSessionScan = (details: string[]): boolean => details.some(
detail => /^SCAN sessions$/i.test(detail.trim()),
)
const normalizedIndexedKey = (row: { id: string; transcriptPath: string }): string =>
`${row.id}@${relative(corpus.configDir, row.transcriptPath).split(sep).join('/')}`
const page1OrderMatches = page1.sessions.map(normalizedIndexedKey)
.every((key, index) => key === manifest.expected.normalizedSessionOrder[index])
const page100OrderMatches = page100.sessions.map(normalizedIndexedKey)
.every((key, index) =>
key === manifest.expected.normalizedSessionOrder[page100Offset + index],
)
if (
!globalPlan.some(detail => detail.includes('sessions_modified_idx')) ||
!projectPlan.some(detail => detail.includes('sessions_project_modified_idx')) ||
hasBareSessionScan(globalPlan) ||
hasBareSessionScan(projectPlan) ||
[...globalPlan, ...projectPlan].some(
detail => /source_files|USE TEMP B-TREE/i.test(detail),
) ||
!page1OrderMatches ||
!page100OrderMatches
) {
throw new Error(`acceptance indexed pages were invalid: ${JSON.stringify({
globalPlan,
projectPlan,
page1OrderMatches,
page100OrderMatches,
})}`)
}
const { SessionService } = await import(
'../../src/server/services/sessionService.js'
)
const transcriptOpensBefore = options.readTranscriptBodyOpenCount()
const indexedResult = await new SessionService(
createOnGateway(coordinator),
).listSessions({ limit: 400, offset: 0 })
const transcriptBodyOpens =
options.readTranscriptBodyOpenCount() - transcriptOpensBefore
const sourceRowsBefore = recordingDatabase.database.read(operation =>
operation.all<Record<string, unknown>>(
'SELECT * FROM source_files ORDER BY path ASC',
),
)
const sessionRowsBefore = recordingDatabase.database.read(operation =>
operation.all<Record<string, unknown>>(
'SELECT * FROM sessions ORDER BY transcript_path ASC',
),
)
const targetRelativePath = relative(corpus.configDir, targetPath)
.split(sep)
.join('/')
const targetSessionBefore = sessionRowsBefore.find(
row => row.transcript_path === targetPath,
)
const previousMessageCount = Number(targetSessionBefore?.message_count)
const appendTimestamp = '2099-01-01T00:00:00.000Z'
await appendFile(targetPath, `${JSON.stringify({
parentUuid: null,
isSidechain: false,
type: 'assistant',
message: {
id: 'msg_acceptance_append',
type: 'message',
role: 'assistant',
model: 'claude-synthetic-benchmark',
content: [{ type: 'text', text: 'Deterministic acceptance append' }],
usage: {
input_tokens: 1,
output_tokens: 1,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
uuid: 'acceptance-append-entry',
timestamp: appendTimestamp,
})}\n`)
const targetStat = await stat(targetPath)
const candidate: SessionSourceCandidate = {
path: targetPath,
sessionId: basename(targetPath, '.jsonl'),
projectPath: basename(dirname(targetPath)),
fallbackCreatedAt: targetStat.birthtime.toISOString(),
fallbackModifiedAt: targetStat.mtime.toISOString(),
fallbackWorkDir: null,
modifiedAtMs: targetStat.mtimeMs,
}
const appendResult = await activeProjector.projectSource(candidate, {
state: 'ready',
discovered: options.sessions,
indexed: options.sessions,
degraded: 0,
lastErrorCode: null,
})
const sourceRowsAfter = recordingDatabase.database.read(operation =>
operation.all<Record<string, unknown>>(
'SELECT * FROM source_files ORDER BY path ASC',
),
)
const sessionRowsAfter = recordingDatabase.database.read(operation =>
operation.all<Record<string, unknown>>(
'SELECT * FROM sessions ORDER BY transcript_path ASC',
),
)
const changedSourceRows = changedRowKeys(
rowMap(sourceRowsBefore, 'path'),
rowMap(sourceRowsAfter, 'path'),
)
const changedSessionRows = changedRowKeys(
rowMap(sessionRowsBefore, 'transcript_path'),
rowMap(sessionRowsAfter, 'transcript_path'),
)
const targetSessionAfter = sessionRowsAfter.find(
row => row.transcript_path === targetPath,
)
const messageCountDelta =
Number(targetSessionAfter?.message_count) - previousMessageCount
const afterAppendHashes = await hashManifestSources(
corpus.configDir,
manifest.sources,
)
const changedCorpusSources = manifest.sources.filter(source => {
const current = afterAppendHashes.get(source.path)
return current?.bytes !== source.bytes || current.sha256 !== source.sha256
})
const unrelatedSourcesUnchangedAfterAppend = changedCorpusSources.every(
source => source.path === targetRelativePath,
)
if (
changedSourceRows.length !== 1 ||
changedSourceRows[0] !== targetPath ||
changedSessionRows.length !== 1 ||
changedSessionRows[0] !== targetPath
) {
throw new Error(`acceptance append changed unrelated projection rows: ${JSON.stringify({
targetPath,
changedSourceRows,
changedSessionRows,
})}`)
}
const shadowComparisons: SessionListShadowComparison[] = []
const shadowTranscriptOpensBefore = options.readTranscriptBodyOpenCount()
await new SessionService(coordinator, {
shadowComparisonMinIntervalMs: 0,
recordShadowComparison: comparison => shadowComparisons.push(comparison),
}).listSessions({ limit: Number.MAX_SAFE_INTEGER, offset: 0 })
const shadowTranscriptBodyOpens =
options.readTranscriptBodyOpenCount() - shadowTranscriptOpensBefore
const mismatchCount = shadowComparisons.filter(
comparison => !comparison.matched,
).length
const differenceCount = shadowComparisons.reduce(
(total, comparison) => total + comparison.differenceCount,
0,
)
if (transcriptBodyOpens !== 0 || shadowTranscriptBodyOpens <= 0) {
throw new Error(`acceptance transcript observer was invalid: ${JSON.stringify({
indexedTranscriptBodyOpens: transcriptBodyOpens,
shadowTranscriptBodyOpens,
})}`)
}
return {
schemaVersion: 1,
fixture: {
sessions: options.sessions,
pageSize,
corpusFingerprint: expectedFingerprint,
},
sourceIntegrity: {
unchangedAfterBackfill,
changedSourceCountAfterAppend: changedCorpusSources.length,
unrelatedSourcesUnchangedAfterAppend,
},
scheduling: coordinator.getSchedulingMetrics(),
queries: {
page1: {
offset: 0,
rowCount: page1.sessions.length,
statementCount: page1Sql.length,
},
page100: {
offset: page100Offset,
rowCount: page100.sessions.length,
statementCount: page100Sql.length,
},
globalPlan,
projectPlan,
},
indexedSessionList: {
total: indexedResult.total,
rowCount: indexedResult.sessions.length,
transcriptBodyOpens,
},
componentAppend: {
action: appendResult.kind === 'indexed' ? appendResult.action : appendResult.kind,
changedSourceRows,
changedSessionRows,
messageCountDelta,
},
shadow: {
comparisonCount: shadowComparisons.length,
mismatchCount,
differenceCount,
transcriptBodyOpens: shadowTranscriptBodyOpens,
},
productAppend: {
measured: false,
reason: 'LOCAL_INDEX_RECONCILIATION_NOT_ACCEPTED',
},
}
} finally {
let cleanupError: unknown
try {
await coordinator?.stop()
} catch (error) {
cleanupError = error
}
try {
await Promise.all([
rm(databasePath, { force: true }),
rm(`${databasePath}-wal`, { force: true }),
rm(`${databasePath}-shm`, { force: true }),
])
await rm(rootDir, { recursive: true, force: true })
} catch (error) {
cleanupError ??= error
}
restoreEnvironment('HOME', originalHome)
restoreEnvironment('CLAUDE_CONFIG_DIR', originalConfigDir)
restoreEnvironment('CC_HAHA_LOCAL_INDEX', originalLocalIndexMode)
if (cleanupError) throw cleanupError
}
}
export async function runBenchmark(
options: BenchmarkOptions,
dependencies: BenchmarkDependencies = {},
) {
if (options.mode === 'shadow' && options.scenario !== 'baseline') {
throw new Error('shadow benchmark only supports the baseline scenario')
}
if (options.mode === 'sqlite' && options.scenario !== 'baseline') {
throw new Error(
'sqlite product append benchmark requires reconciliation and is not available',
)
}
const rootDir = await mkdtemp(join(tmpdir(), 'cc-haha-local-index-benchmark-'))
const homeDir = join(rootDir, 'home')
const configDir = join(homeDir, '.claude')
const databasePath = join(configDir, 'cc-haha', 'db', 'index-v1.sqlite')
const originalHome = process.env.HOME
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
const originalLocalIndexMode = process.env.CC_HAHA_LOCAL_INDEX
const originalLocalAccessToken = process.env.CC_HAHA_LOCAL_ACCESS_TOKEN
let coordinator: LocalIndexCoordinator | undefined
let report: Record<string, unknown> | undefined
try {
await mkdir(homeDir, { recursive: true })
process.env.HOME = homeDir
process.env.CLAUDE_CONFIG_DIR = configDir
process.env.CC_HAHA_LOCAL_INDEX = options.mode === 'file'
? 'off'
: options.mode === 'shadow'
? 'shadow'
: 'on'
const localAccessToken = createHash('sha256')
.update(`${rootDir}\0${options.seed}\0local-access`)
.digest('base64url')
process.env.CC_HAHA_LOCAL_ACCESS_TOKEN = localAccessToken
const corpus = await createLocalIndexCorpus({
rootDir,
sessions: options.sessions,
entriesPerSession: options.entriesPerSession,
largeTranscriptBytes: options.largeTranscriptBytes,
seed: options.seed,
})
const manifest = JSON.parse(
await readFile(corpus.manifestPath, 'utf8'),
) as CorpusManifest
if (options.scenario === 'append') {
report = await runAppendScenario(options, {
rootDir,
manifest,
manifestPath: corpus.manifestPath,
configDir: corpus.configDir,
})
return report
}
if (options.mode === 'sqlite') {
report = await runSqliteProductBenchmark(options, {
rootDir,
configDir: corpus.configDir,
manifestPath: corpus.manifestPath,
manifest,
localAccessToken,
}, dependencies)
return report
}
let readyStatus: LocalIndexStatus | undefined
let readyDurationMs: number | undefined
if (options.mode !== 'file') {
const coordinatorModule = await import(
'../../src/server/services/localIndex/coordinator.js'
)
coordinator = coordinatorModule.localIndexCoordinator
const readyStartedAt = Bun.nanoseconds()
await coordinator.start()
readyStatus = await waitForIndexReady(coordinator, options.timeoutMs)
readyDurationMs = (Bun.nanoseconds() - readyStartedAt) / 1_000_000
const expectedSources = manifest.expected.summaries.length
if (
readyStatus.discovered !== expectedSources ||
readyStatus.indexed !== expectedSources ||
readyStatus.degradedSources !== 0
) {
throw new Error(
`${options.mode} backfill did not index the complete corpus: ${JSON.stringify({
expectedSources,
discovered: readyStatus.discovered,
indexed: readyStatus.indexed,
degradedSources: readyStatus.degradedSources,
})}`,
)
}
}
const { SessionService } = await import('../../src/server/services/sessionService.js')
const shadowComparisons: SessionListShadowComparison[] = []
const execute = async (): Promise<SessionListResult> => {
const service = options.mode !== 'file'
? new SessionService(coordinator!, {
shadowComparisonMinIntervalMs: 0,
recordShadowComparison: comparison => {
shadowComparisons.push(comparison)
},
})
: new SessionService()
return service.listSessions({ limit: options.sidebarLimit })
}
const executeDeadlineSample = (
phase: BenchmarkSampleContext['phase'],
index: number,
): Promise<SessionListResult> => runWithSampleDeadline({
execute: () => dependencies.executeSessionList
? dependencies.executeSessionList(execute, { phase, index, rootDir })
: execute(),
timeoutMs: options.timeoutMs,
phase,
index,
})
for (let index = 0; index < options.warmupRuns; index += 1) {
await executeDeadlineSample('warmup', index)
}
const samples: number[] = []
const cpuStart = process.cpuUsage()
const rssStart = process.memoryUsage().rss
let lastResult: SessionListResult | undefined
for (let index = 0; index < options.runs; index += 1) {
const startedAt = Bun.nanoseconds()
lastResult = await executeDeadlineSample('measured', index)
const durationMs = (Bun.nanoseconds() - startedAt) / 1_000_000
samples.push(durationMs)
}
const cpu = process.cpuUsage(cpuStart)
const rssDeltaBytes = process.memoryUsage().rss - rssStart
const fileValidation = validateResult(
lastResult!,
manifest,
options.sidebarLimit,
)
if (
fileValidation.actualSessions !== fileValidation.expectedSessions ||
!fileValidation.orderMatches ||
!fileValidation.summariesMatch
) {
throw new Error(`file baseline did not match the corpus manifest: ${JSON.stringify(fileValidation)}`)
}
const comparisonCount = shadowComparisons.length
const mismatchCount = shadowComparisons.filter(comparison => !comparison.matched).length
const differenceCount = shadowComparisons.reduce(
(total, comparison) => total + comparison.differenceCount,
0,
)
if (options.mode === 'shadow') {
const expectedComparisons = options.warmupRuns + options.runs
if (comparisonCount !== expectedComparisons) {
throw new Error(
`shadow benchmark expected ${expectedComparisons} comparisons, received ${comparisonCount}`,
)
}
if (mismatchCount > 0 || differenceCount > 0) {
throw new Error(
`shadow benchmark detected normalized mismatches: ${JSON.stringify({
comparisonCount,
mismatchCount,
differenceCount,
})}`,
)
}
}
const median = percentile(samples, 0.5)
const p95 = percentile(samples, 0.95)
const max = Math.max(...samples)
report = {
schemaVersion: 1,
mode: options.mode,
scenario: options.scenario,
fixture: {
rootDir,
configDir: corpus.configDir,
manifestPath: corpus.manifestPath,
seed: options.seed,
sessions: options.sessions,
entriesPerSession: options.entriesPerSession,
largeTranscriptBytes: options.largeTranscriptBytes,
sidebarLimit: options.sidebarLimit,
corpusFingerprint: corpusFingerprint(manifest),
kept: options.keep,
},
measurement: {
operation: 'session-service-list',
warmupRuns: options.warmupRuns,
runs: options.runs,
durationMs: {
median: roundedMilliseconds(median),
p95: roundedMilliseconds(p95),
max: roundedMilliseconds(max),
},
timeoutMs: options.timeoutMs,
timeoutCount: 0,
cpuMs: {
user: roundedMilliseconds(cpu.user / 1000),
system: roundedMilliseconds(cpu.system / 1000),
total: roundedMilliseconds((cpu.user + cpu.system) / 1000),
},
rssDeltaBytes,
io: {
instrumented: false,
filesOpened: null,
bytesRead: null,
},
productAppend: {
measured: false,
reason: 'LOCAL_INDEX_RECONCILIATION_NOT_ACCEPTED',
},
referenceHardware: {
measured: false,
reason: 'REFERENCE_HARDWARE_NOT_RUN',
},
sidebarApi: {
measured: false,
reason: 'HTTP_API_HARNESS_NOT_RUN',
},
firstUsefulContent: {
measured: false,
reason: 'STARTUP_INTERACTION_HARNESS_NOT_RUN',
},
foregroundDuringBackfill: {
measured: false,
reason: 'CONCURRENT_BACKFILL_HARNESS_NOT_RUN',
},
eventLoopDelay: {
measured: false,
reason: 'EVENT_LOOP_MONITOR_NOT_RUN',
},
peakRss: {
measured: false,
reason: 'PEAK_RSS_SAMPLER_NOT_RUN',
},
},
...(options.mode !== 'file'
? {
index: {
readyDurationMs: roundedMilliseconds(readyDurationMs!),
status: readyStatus,
scheduling: coordinator!.getSchedulingMetrics(),
},
}
: {}),
validation: {
...fileValidation,
...(options.mode === 'shadow'
? { comparisonCount, mismatchCount, differenceCount }
: {}),
},
}
} finally {
let cleanupError: unknown
try {
await coordinator?.stop()
} catch (error) {
cleanupError = error
}
if (!options.keep) {
try {
await Promise.all([
rm(databasePath, { force: true }),
rm(`${databasePath}-wal`, { force: true }),
rm(`${databasePath}-shm`, { force: true }),
])
await rm(rootDir, { recursive: true, force: true })
} catch (error) {
cleanupError ??= error
}
}
restoreEnvironment('HOME', originalHome)
restoreEnvironment('CLAUDE_CONFIG_DIR', originalConfigDir)
restoreEnvironment('CC_HAHA_LOCAL_INDEX', originalLocalIndexMode)
restoreEnvironment('CC_HAHA_LOCAL_ACCESS_TOKEN', originalLocalAccessToken)
if (cleanupError) throw cleanupError
}
return report!
}
async function main(): Promise<void> {
const options = parseBenchmarkArgs(process.argv.slice(2))
const report = await runBenchmark(options)
process.stdout.write(`${JSON.stringify(report)}\n`)
}
if (import.meta.main) {
main().catch(error => {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
process.exitCode = 1
})
}