codeburn/tests/progressive-cold-start.test.ts
iamtoruk 43bbb99a92 perf: paint the cold dashboard from the recent files, index the rest behind it
A cold start parsed the whole corpus before painting anything (~36s on a 21k
corpus) even though the default view only shows the last 7 days. A session log
is append-only, so its last event timestamp is <= its mtime: a file stamped
before the start of the displayed range provably holds nothing that range can
show. On a cold cache the interactive dashboard now parses only the files above
that floor, paints, and indexes the rest from the mounted UI.

- parser: `withColdFirstPaintFloor` scopes a parse to files that can hold
  in-range data (mtime >= rangeStart - 48h clock-skew margin), deferring only
  files with NO cache entry — nothing that has something to serve is held back,
  and network sources (synthetic now-stamped fingerprint) always load.
- A run that deferred anything is a partial hydration: it does not stamp the
  session cache complete and reports hydrationComplete false, so the daily
  backfill cannot finalize history off it and the next launch resumes cold.
  A floored run that deferred NOTHING keeps the normal stamps.
- The floor participates in the parse memo key, so a first-paint result can
  never be served to (or burst-reused by) an unfloored request.
- dashboard: cold + TTY + standard dated view only. The background fill is an
  ordinary unscoped background reload, so its cache writes and its refreshed
  panels are exactly what a full cold parse produces. While it runs, an
  "indexing history N/M files" banner states that the totals cover only what is
  indexed so far.

One-shot outputs (json/csv/markdown, report, sessions, models, serve/menubar/app
payloads) are non-TTY and never enter this path.

Fixes #1107
2026-08-23 01:37:15 -07:00

175 lines
7.3 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { mkdtemp, mkdir, writeFile, rm, utimes } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
import {
parseAllSessions,
clearSessionCache,
withColdFirstPaintFloor,
shouldDeferToBackgroundFill,
filesParsedFromSourceCount,
isSessionHydrationComplete,
FIRST_PAINT_MTIME_MARGIN_MS,
} from '../src/parser.js'
import { clearLoadCacheMemo, isColdCacheOnDisk } from '../src/session-cache.js'
import { readCacheOnDisk } from './fixtures/session-cache-io.js'
const DAY_MS = 24 * 60 * 60 * 1000
let tmpDir: string
beforeEach(async () => {
clearSessionCache()
clearLoadCacheMemo()
tmpDir = await mkdtemp(join(tmpdir(), 'progressive-cold-'))
process.env['CLAUDE_CONFIG_DIR'] = tmpDir
process.env['CODEBURN_CACHE_DIR'] = join(tmpDir, 'cache')
process.env['CODEBURN_DESKTOP_SESSIONS_DIR'] = join(tmpDir, 'desktop-sessions')
})
afterEach(async () => {
clearSessionCache()
clearLoadCacheMemo()
await rm(tmpDir, { recursive: true, force: true })
})
/** One Claude session file whose only turn is `ageDays` old, with an mtime to
* match — the shape the mtime argument is about. */
async function writeSession(name: string, ageDays: number): Promise<string> {
const dir = join(tmpDir, 'projects', 'proj')
await mkdir(dir, { recursive: true })
const at = new Date(Date.now() - ageDays * DAY_MS)
const path = join(dir, `${name}.jsonl`)
await writeFile(path, JSON.stringify({
type: 'assistant',
sessionId: name,
timestamp: at.toISOString(),
cwd: '/tmp/proj',
message: {
id: `msg-${name}`, type: 'message', role: 'assistant', model: 'claude-sonnet-4-5',
content: [], usage: { input_tokens: 100, output_tokens: 50 },
},
}) + '\n')
await utimes(path, at, at)
return path
}
function lastWeek(): Date {
return new Date(Date.now() - 7 * DAY_MS)
}
async function cachedClaudePaths(): Promise<string[]> {
const raw = await readCacheOnDisk()
return Object.keys(raw.providers['claude']?.files ?? {}).sort()
}
describe('first-paint deferral predicate', () => {
const floor = 1_000_000
it('defers only files strictly below the floor', () => {
expect(shouldDeferToBackgroundFill({ mtimeMs: floor - 1 }, undefined, floor)).toBe(true)
expect(shouldDeferToBackgroundFill({ mtimeMs: floor }, undefined, floor)).toBe(false)
expect(shouldDeferToBackgroundFill({ mtimeMs: floor + 1 }, undefined, floor)).toBe(false)
})
it('keeps a file the clock-skew margin rescues', () => {
// The floor the dashboard passes is rangeStart - MARGIN, so a file stamped
// up to MARGIN before the range start is still parsed for the first paint.
const rangeStart = 10 * DAY_MS
const skewed = rangeStart - FIRST_PAINT_MTIME_MARGIN_MS + 1
expect(shouldDeferToBackgroundFill({ mtimeMs: skewed }, undefined, rangeStart - FIRST_PAINT_MTIME_MARGIN_MS)).toBe(false)
expect(shouldDeferToBackgroundFill({ mtimeMs: skewed - 2 }, undefined, rangeStart - FIRST_PAINT_MTIME_MARGIN_MS)).toBe(true)
})
it('never defers a file that has a cache entry, or any file outside the scope', () => {
const cached = { fingerprint: { dev: 0, ino: 0, mtimeMs: 0, sizeBytes: 0 }, mcpInventory: [], turns: [] }
expect(shouldDeferToBackgroundFill({ mtimeMs: floor - 1 }, cached, floor)).toBe(false)
// floor === null is every warm run and every one-shot command.
expect(shouldDeferToBackgroundFill({ mtimeMs: floor - 1 }, undefined, null)).toBe(false)
})
it('never defers a network source', () => {
// Network providers have no on-disk file; they enter the parse with a
// synthetic fingerprint stamped `Date.now()` (and skip this check entirely),
// so no floor a dated view can produce ever holds them back.
expect(shouldDeferToBackgroundFill({ mtimeMs: Date.now() }, undefined, lastWeek().getTime())).toBe(false)
})
})
describe('progressive cold start', () => {
it('paints from the recent files and leaves the rest to the fill', async () => {
await writeSession('recent', 1)
await writeSession('old', 200)
const { result, deferredFiles } = await withColdFirstPaintFloor(lastWeek(), () => parseAllSessions())
expect(deferredFiles).toBe(1)
expect(result.flatMap(p => p.sessions).map(s => s.sessionId)).toEqual(['recent'])
// A pass that deferred files is a partial hydration: it must not stamp the
// session cache complete (the next launch has to come back cold) and must
// not let the daily backfill finalize history off it.
expect(isSessionHydrationComplete()).toBe(false)
expect(await isColdCacheOnDisk()).toBe(true)
// Durable partial progress: the file it DID parse is already cached.
expect(await cachedClaudePaths()).toEqual([join(tmpDir, 'projects', 'proj', 'recent.jsonl')])
})
it('converges on the full-cold-parse state, without re-parsing what pass 1 did', async () => {
await writeSession('recent', 1)
await writeSession('old', 200)
await withColdFirstPaintFloor(lastWeek(), () => parseAllSessions())
clearSessionCache()
// The background fill is an ordinary unscoped parse in the same process.
const parsedBefore = filesParsedFromSourceCount()
const filled = await parseAllSessions()
// Exactly one file parsed: the deferred one. The pass-1 file is served from
// its cache entry, so it is neither re-read nor counted twice.
expect(filesParsedFromSourceCount() - parsedBefore).toBe(1)
expect(filled.flatMap(p => p.sessions).map(s => s.sessionId).sort()).toEqual(['old', 'recent'])
expect(isSessionHydrationComplete()).toBe(true)
expect(await isColdCacheOnDisk()).toBe(false)
const progressive = await readCacheOnDisk()
// Same corpus, a plain full cold parse, in a cache dir of its own.
process.env['CODEBURN_CACHE_DIR'] = join(tmpDir, 'cache-baseline')
clearSessionCache()
clearLoadCacheMemo()
await parseAllSessions()
const baseline = await readCacheOnDisk()
expect(progressive.providers['claude']?.files).toEqual(baseline.providers['claude']?.files)
expect(progressive.complete).toBe(baseline.complete)
})
it('resumes after a fill that never ran', async () => {
await writeSession('recent', 1)
await writeSession('old', 200)
// Pass 1 only, then the process dies: what survives on disk is the recent
// file's entry and an INCOMPLETE marker.
await withColdFirstPaintFloor(lastWeek(), () => parseAllSessions())
expect(await isColdCacheOnDisk()).toBe(true)
// Next launch. The deferred file has no cache entry, so it is still
// discovered as changed and parsed; nothing is stranded behind a "seen" mark.
clearSessionCache()
clearLoadCacheMemo()
const resumed = await parseAllSessions()
expect(resumed.flatMap(p => p.sessions).map(s => s.sessionId).sort()).toEqual(['old', 'recent'])
expect(await isColdCacheOnDisk()).toBe(false)
})
it('is a no-op when every file is recent enough to paint', async () => {
await writeSession('recent', 1)
const { deferredFiles } = await withColdFirstPaintFloor(lastWeek(), () => parseAllSessions())
// Nothing deferred means this pass saw the whole corpus, so it keeps the
// ordinary completeness stamp instead of owing a fill.
expect(deferredFiles).toBe(0)
expect(isSessionHydrationComplete()).toBe(true)
expect(await isColdCacheOnDisk()).toBe(false)
})
})