codeburn/scripts/perf/gen-fixture.mjs
iamtoruk af9c653d87 refactor(perf): trim the harness to what the receipts need
Drop pin-baselines.mjs: pinning is a once-per-phase paste and the
script hard-coded this phase's iteration-log row, so every later phase
would edit by hand anyway. Replace three hand-rolled argv parsers with
node:util parseArgs. Read machine facts from node:os instead of eight
subprocess probes (hw.model keeps its sysctl call). Track fixture bytes
with a running counter instead of re-walking the tree per file. Delete
the hover proxy that measured an argv the dock never spawns, plus
unused exports (ensureDir, assertAbsolute, GENERATED_FIXTURE_DIR).

Fixture output is byte-identical for the same seed (verified by full
tree diff against the pre-refactor generator) and all harness metrics
reproduce within run-to-run noise.
2026-08-29 15:34:25 -07:00

189 lines
8.5 KiB
JavaScript

#!/usr/bin/env node
// Deterministic 30MB-class synthetic session corpus for the performance harness.
// Sanitized fake paths and lorem content only. Never copies real user sessions.
//
// node scripts/perf/gen-fixture.mjs --home <isolated-home> [--target-mb 30]
//
// Layout matches provider default paths under HOME so the harness only has to
// set HOME + CODEBURN_CACHE_DIR (same isolation as scripts/upgrade-path).
import { mkdirSync, writeFileSync, existsSync, rmSync, readdirSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { parseArgs as parseArgvOptions } from 'node:util'
import { assertIsolatedHome, fixtureBytes } from './lib.mjs'
function parseArgs(argv) {
const { values } = parseArgvOptions({
args: argv,
options: {
home: { type: 'string', default: '' },
'target-mb': { type: 'string', default: '30' },
seed: { type: 'string', default: String(0x9e3779b9) },
force: { type: 'boolean', default: false },
help: { type: 'boolean', short: 'h', default: false },
},
})
const result = { home: values.home, targetMb: Number(values['target-mb']), seed: Number(values.seed), force: values.force, help: values.help }
if (result.help) return result
if (!result.home) throw new Error('--home is required')
if (!Number.isFinite(result.targetMb) || result.targetMb < 1) throw new Error('--target-mb must be >= 1')
return result
}
function mulberry(seed) {
let state = seed | 0
return () => {
state = (state + 0x6d2b79f5) | 0
let t = state
t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
function writeLines(path, lines) {
mkdirSync(join(path, '..'), { recursive: true })
const body = lines.join('\n') + '\n'
writeFileSync(path, body)
return Buffer.byteLength(body)
}
function main() {
let options
try { options = parseArgs(process.argv.slice(2)) }
catch (error) {
console.error(String(error))
console.error('Usage: node scripts/perf/gen-fixture.mjs --home <isolated-home> [--target-mb 30] [--seed N] [--force]')
process.exitCode = 2
return
}
if (options.help) {
console.log('Usage: node scripts/perf/gen-fixture.mjs --home <isolated-home> [--target-mb 30] [--seed N] [--force]')
return
}
const HOME = resolve(options.home)
assertIsolatedHome(HOME)
const marker = join(HOME, '.codeburn-perf-fixture.json')
if (existsSync(HOME) && readdirSync(HOME).length > 0 && !existsSync(marker) && !options.force) {
throw new Error(HOME + ' is not empty and is not a prior perf fixture. Pass --force only after inspecting it.')
}
if (existsSync(marker) && options.force) {
rmSync(HOME, { recursive: true, force: true })
}
mkdirSync(HOME, { recursive: true })
const rnd = mulberry(options.seed)
const pick = arr => arr[Math.floor(rnd() * arr.length)]
const between = (lo, hi) => {
const n = lo + Math.floor(rnd() * (hi - lo))
return n > 0 ? n : lo
}
const DAY_MS = 86_400_000
const SPAN_DAYS = 92
// Relative to UTC midnight so today/7D/30D queries have data, matching
// scripts/upgrade-path/gen-corpus.mjs. The exact day0 is recorded in the
// fixture marker so a baseline row can cite it.
const day0 = Math.floor(Date.now() / DAY_MS) * DAY_MS - (SPAN_DAYS - 1) * DAY_MS
const at = (day, hour, min = 0, sec = 0) => new Date(day0 + day * DAY_MS + hour * 3_600_000 + min * 60_000 + sec * 1000)
const iso = d => d.toISOString()
const PROJECTS = ['/work/api-gateway', '/work/billing', '/work/web-app', '/work/infra']
const MODELS = ['claude-sonnet-4-5', 'claude-opus-4-8', 'claude-haiku-4-5']
const TOOLS = ['Read', 'Edit', 'Bash', 'Glob', 'Grep']
const claudeUser = (sessionId, ts, cwd, text) => JSON.stringify({
type: 'user', sessionId, timestamp: iso(ts), cwd, gitBranch: 'main',
message: { role: 'user', content: text },
})
const claudeAssistant = (sessionId, ts, cwd, msgId, model, usage, content) => JSON.stringify({
type: 'assistant', sessionId, timestamp: iso(ts), cwd, gitBranch: 'main',
message: { id: msgId, type: 'message', role: 'assistant', model, content, usage },
})
const targetBytes = options.targetMb * 1024 * 1024
let files = 0
let sessions = 0
let claudeBytes = 0
const projectsDir = join(HOME, '.claude', 'projects')
let i = 0
while (claudeBytes < targetBytes * 0.88) {
const cwd = PROJECTS[i % PROJECTS.length]
const day = (i * 3) % SPAN_DAYS
const sid = 'perf-' + String(i).padStart(5, '0')
const dirName = cwd.replace(/[/ ]/g, '-')
const turns = 8 + (i % 12)
const lines = []
for (let t = 0; t < turns; t++) {
const ts = at(day, 9 + (t % 8), (t * 7) % 60)
lines.push(claudeUser(sid, ts, cwd, 'synthetic task ' + t + ' for ' + sid + ' on billing totals'))
const content = [
{ type: 'text', text: 'step ' + t + ' ' + 'x'.repeat(40 + (i % 80)) },
{ type: 'tool_use', id: 'tu-' + sid + '-' + t, name: pick(TOOLS), input: { file_path: cwd + '/src/f' + t + '.ts' } },
]
if (t === 2 && i % 17 === 0) content.push({ type: 'text', text: 'pad-' + 'y'.repeat(8 * 1024) })
lines.push(claudeAssistant(sid, at(day, 9 + (t % 8), (t * 7) % 60, 30), cwd, 'msg-' + sid + '-' + t, pick(MODELS), {
input_tokens: between(400, 4000),
output_tokens: between(40, 900),
cache_read_input_tokens: between(0, 20000),
cache_creation_input_tokens: between(0, 3000),
}, content))
}
claudeBytes += writeLines(join(projectsDir, dirName, sid + '.jsonl'), lines)
files++
sessions++
i++
if (i > 4000) break
}
// Codex sessions: mixed event types, cumulative token_count.
const codexRoot = join(HOME, '.codex', 'sessions')
for (let c = 0; c < 40; c++) {
const day = (c * 2) % SPAN_DAYS
const d = new Date(day0 + day * DAY_MS)
const cwd = PROJECTS[c % PROJECTS.length]
const sid = 'codex-perf-' + String(c).padStart(3, '0')
const lines = [JSON.stringify({ type: 'session_meta', timestamp: iso(at(day, 10)), payload: { cwd, originator: 'codex-cli', session_id: sid, model: 'gpt-5.3-codex' } })]
const total = { input_tokens: 0, cached_input_tokens: 0, output_tokens: 0, reasoning_output_tokens: 0, total_tokens: 0 }
for (let t = 0; t < 6; t++) {
const ts = iso(at(day, 10, t * 5))
const last = { input_tokens: between(500, 6000), cached_input_tokens: between(0, 2000), output_tokens: between(50, 800), reasoning_output_tokens: between(0, 300), total_tokens: 0 }
last.total_tokens = last.input_tokens + last.output_tokens
for (const k of Object.keys(total)) total[k] += last[k]
lines.push(JSON.stringify({ type: 'event_msg', timestamp: ts, payload: { type: 'task_started' } }))
lines.push(JSON.stringify({ type: 'response_item', timestamp: ts, payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'task ' + t }] } }))
lines.push(JSON.stringify({ type: 'response_item', timestamp: ts, payload: { type: 'function_call', name: 'shell', call_id: 'c' + t, arguments: JSON.stringify({ command: 'ls' }) } }))
lines.push(JSON.stringify({ type: 'response_item', timestamp: ts, payload: { type: 'function_call_output', call_id: 'c' + t } }))
lines.push(JSON.stringify({ type: 'response_item', timestamp: ts, payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'done' }] } }))
lines.push(JSON.stringify({ type: 'event_msg', timestamp: ts, payload: { type: 'token_count', info: { last_token_usage: last, total_token_usage: { ...total } } } }))
lines.push(JSON.stringify({ type: 'event_msg', timestamp: ts, payload: { type: 'task_complete', duration_ms: 4000 } }))
}
const dir = join(codexRoot, String(d.getUTCFullYear()), String(d.getUTCMonth() + 1).padStart(2, '0'), String(d.getUTCDate()).padStart(2, '0'))
writeLines(join(dir, 'rollout-' + sid + '.jsonl'), lines)
files++
sessions++
}
const bytes = fixtureBytes(HOME)
const manifest = {
kind: 'codeburn-perf-fixture',
version: 1,
generated_at: new Date().toISOString(),
seed: options.seed,
target_mb: options.targetMb,
bytes,
files,
sessions,
span_days: SPAN_DAYS,
day0: new Date(day0).toISOString(),
note: 'Synthetic sanitized sessions only. Do not copy founder transcripts here.',
append_target: join('.claude', 'projects', '-work-api-gateway', 'perf-00000.jsonl'),
}
writeFileSync(marker, JSON.stringify(manifest, null, 2) + '\n')
console.log(JSON.stringify(manifest, null, 2))
}
try { main() }
catch (error) {
console.error(String(error?.stack ?? error))
process.exitCode = 1
}