mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-28 18:13:16 +00:00
Every dated command re-walks and re-stats every provider tree before it can decide what the cache already covers. That sweep was strictly serial at every level -- one readdir, one stat, one state.json read at a time, and one provider after another -- so on a 21k-file / 9-provider corpus it owned most of a warm run's wall clock with the machine idle waiting on the kernel. Measured per warm `today` pass on that corpus before this change: 859 ms in discoverAllSessions (kimicode 467, codex 143, claude 135, grok 77) and 727 ms in the Claude project walk + fingerprint pass. - fs-utils: mapWithConcurrency + FS_SCAN_CONCURRENCY, one bounded, order- preserving helper for the whole sweep. - providers/index: run provider discovery concurrently, concatenated in registry order. - claude/codex/grok/kimicode: walk each level with the level fanned out, re-concatenated in readdir order before anything reconciles. - parser: the Claude dir walk and both fingerprint passes (scanProjectDirs and parseProviderSources) fan out, then reconcile serially in discovery order, which is what changedFiles ordering and the seenMsgIds pre-seed depend on. - parser: collectJsonlFiles reads entries with their types, so a plain file no longer costs a wasted subagents/ probe. - codex-cache: share one in-flight load between concurrent readers. The memo is only populated after the read + parse resolves, so concurrent discovery had every caller re-reading and re-parsing the same (here 59 MB) file. No reconciliation logic changed: the same fingerprints reach the same cache comparison in the same order. Warm `today` 5.30s -> 2.99s, cold 34.7s -> 31.1s (medians of 5 / 2, isolated HOME + cache). today/report -p month/models/sessions JSON, warm and cold, per-provider and combined, are byte-identical apart from the run's own `generated` timestamp. Closes #1104
111 lines
4.2 KiB
TypeScript
111 lines
4.2 KiB
TypeScript
// The discovery sweep (provider walks + per-file stats) now issues its metadata
|
|
// syscalls concurrently instead of one at a time. Nothing downstream may notice:
|
|
// source order still follows the registry / readdir order, and nothing that was
|
|
// discovered serially may be dropped.
|
|
|
|
import { mkdtemp, mkdir, rm, symlink, writeFile } from 'fs/promises'
|
|
import { tmpdir } from 'os'
|
|
import { basename, join } from 'path'
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
|
|
import { FS_SCAN_CONCURRENCY, mapWithConcurrency } from '../src/fs-utils.js'
|
|
import { collectJsonlFiles } from '../src/parser.js'
|
|
import { discoverAllSessions } from '../src/providers/index.js'
|
|
import type { Provider, SessionSource } from '../src/providers/types.js'
|
|
|
|
let root: string
|
|
beforeEach(async () => { root = await mkdtemp(join(tmpdir(), 'codeburn-disc-')) })
|
|
afterEach(async () => { await rm(root, { recursive: true, force: true }) })
|
|
|
|
const tick = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
|
|
|
|
describe('mapWithConcurrency', () => {
|
|
it('returns results in input order even when they settle out of order', async () => {
|
|
const items = [40, 0, 20, 0, 10]
|
|
const out = await mapWithConcurrency(items, 8, async (ms, i) => {
|
|
await tick(ms)
|
|
return i
|
|
})
|
|
expect(out).toEqual([0, 1, 2, 3, 4])
|
|
})
|
|
|
|
it('never exceeds the requested number of in-flight workers', async () => {
|
|
let inFlight = 0
|
|
let peak = 0
|
|
await mapWithConcurrency(Array.from({ length: 50 }, (_, i) => i), 4, async () => {
|
|
inFlight++
|
|
peak = Math.max(peak, inFlight)
|
|
await tick(1)
|
|
inFlight--
|
|
})
|
|
expect(peak).toBe(4)
|
|
})
|
|
|
|
it('handles an empty list without spawning workers', async () => {
|
|
await expect(mapWithConcurrency([], FS_SCAN_CONCURRENCY, async () => 1)).resolves.toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('collectJsonlFiles', () => {
|
|
// The walk skips the subagents/ probe for entries readdir reports as plain
|
|
// files. A symlink is NOT a plain file, so a symlinked session directory must
|
|
// still be probed — dropping it would lose that session's subagent spend.
|
|
it('still probes subagents under a symlinked session directory', async () => {
|
|
const real = join(root, 'real-session')
|
|
await mkdir(join(real, 'subagents'), { recursive: true })
|
|
await writeFile(join(real, 'subagents', 'agent-linked.jsonl'), '{}\n')
|
|
await symlink(real, join(root, 'linked-session'), 'dir')
|
|
// A plain sibling file is the case the skip exists for: no probe, no effect.
|
|
await writeFile(join(root, 'notes.txt'), 'x')
|
|
|
|
const found = (await collectJsonlFiles(root)).map(f => basename(f))
|
|
expect(found).toContain('agent-linked.jsonl')
|
|
expect(found).not.toContain('notes.txt')
|
|
})
|
|
})
|
|
|
|
function fakeProvider(name: string, delayMs: number, paths: string[]): Provider {
|
|
return {
|
|
name,
|
|
displayName: name,
|
|
modelDisplayName: (m: string) => m,
|
|
toolDisplayName: (t: string) => t,
|
|
async discoverSessions(): Promise<SessionSource[]> {
|
|
await tick(delayMs)
|
|
return paths.map(path => ({ path, project: name, provider: name }))
|
|
},
|
|
createSessionParser() { throw new Error('not used') },
|
|
}
|
|
}
|
|
|
|
describe('discoverAllSessions', () => {
|
|
it('concatenates providers in registry order regardless of which finishes first', async () => {
|
|
const providers = [
|
|
fakeProvider('slow', 30, ['/s1', '/s2']),
|
|
fakeProvider('fast', 0, ['/f1']),
|
|
fakeProvider('mid', 10, ['/m1']),
|
|
]
|
|
const sources = await discoverAllSessions('all', providers)
|
|
expect(sources.map(s => s.path)).toEqual(['/s1', '/s2', '/f1', '/m1'])
|
|
})
|
|
|
|
it('keeps a throwing provider isolated without dropping the others', async () => {
|
|
const boom = fakeProvider('boom', 0, [])
|
|
boom.discoverSessions = async () => { throw new Error('nope') }
|
|
const sources = await discoverAllSessions('all', [
|
|
fakeProvider('a', 5, ['/a1']),
|
|
boom,
|
|
fakeProvider('b', 0, ['/b1']),
|
|
])
|
|
expect(sources.map(s => s.path)).toEqual(['/a1', '/b1'])
|
|
})
|
|
|
|
it('honours a provider filter', async () => {
|
|
const sources = await discoverAllSessions('b', [
|
|
fakeProvider('a', 0, ['/a1']),
|
|
fakeProvider('b', 0, ['/b1']),
|
|
])
|
|
expect(sources.map(s => s.path)).toEqual(['/b1'])
|
|
})
|
|
})
|