164 lines
4.4 KiB
TypeScript
164 lines
4.4 KiB
TypeScript
import { spawn } from 'child_process'
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from 'fs'
|
|
import { join, resolve } from 'path'
|
|
|
|
type Mode = 'startup' | 'query'
|
|
|
|
type RunSummary = {
|
|
mode: Mode
|
|
command: string[]
|
|
artifactDir: string
|
|
startupReportPath: string | null
|
|
queryReportPath: string | null
|
|
wallTimeMs: number
|
|
exitCode: number
|
|
startupTotalMs: number | null
|
|
queryTtftMs: number | null
|
|
queryTotalTimeMs: number | null
|
|
queryPreApiOverheadMs: number | null
|
|
}
|
|
|
|
function fail(message: string): never {
|
|
process.stderr.write(`${message}\n`)
|
|
process.exit(1)
|
|
}
|
|
|
|
function parseArgs(argv: string[]): { mode: Mode; cliArgs: string[] } {
|
|
const [mode, ...rest] = argv
|
|
if (mode !== 'startup' && mode !== 'query') {
|
|
fail(
|
|
'Usage: bun run scripts/perf.ts <startup|query> -- <better-clawd args...>',
|
|
)
|
|
}
|
|
|
|
const separatorIdx = rest.indexOf('--')
|
|
const cliArgs = separatorIdx === -1 ? rest : rest.slice(separatorIdx + 1)
|
|
return { mode, cliArgs }
|
|
}
|
|
|
|
function ensureBuilt(): void {
|
|
if (!existsSync(resolve('dist/cli.mjs'))) {
|
|
fail('Missing dist/cli.mjs. Run `bun run build` before running perf scripts.')
|
|
}
|
|
}
|
|
|
|
function getRunId(): string {
|
|
return new Date().toISOString().replace(/[:.]/g, '-')
|
|
}
|
|
|
|
function getLatestFile(dir: string): string | null {
|
|
if (!existsSync(dir)) return null
|
|
const files = readdirSync(dir)
|
|
.map(name => join(dir, name))
|
|
.filter(path => statSync(path).isFile())
|
|
.sort(
|
|
(a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs,
|
|
)
|
|
return files[0] ?? null
|
|
}
|
|
|
|
function extractMs(report: string, regex: RegExp): number | null {
|
|
const match = report.match(regex)
|
|
if (!match?.[1]) return null
|
|
const value = Number.parseFloat(match[1])
|
|
return Number.isFinite(value) ? value : null
|
|
}
|
|
|
|
function readMaybe(path: string | null): string | null {
|
|
if (!path) return null
|
|
return readFileSync(path, 'utf8')
|
|
}
|
|
|
|
async function runCommand(
|
|
mode: Mode,
|
|
cliArgs: string[],
|
|
artifactDir: string,
|
|
): Promise<RunSummary> {
|
|
const configDir = join(artifactDir, 'config')
|
|
const debugDir = join(artifactDir, 'debug')
|
|
mkdirSync(configDir, { recursive: true })
|
|
mkdirSync(debugDir, { recursive: true })
|
|
|
|
const childArgs = ['dist/cli.mjs', ...cliArgs]
|
|
if (
|
|
mode === 'query' &&
|
|
!cliArgs.some(
|
|
arg => arg === '--debug-file' || arg.startsWith('--debug-file='),
|
|
)
|
|
) {
|
|
childArgs.splice(1, 0, '--debug-file', join(debugDir, 'query-debug.txt'))
|
|
}
|
|
|
|
const env = {
|
|
...process.env,
|
|
BETTER_CLAWD_CONFIG_DIR: configDir,
|
|
CLAUDE_CODE_DEBUG_LOGS_DIR: debugDir,
|
|
CLAUDE_CODE_PROFILE_STARTUP: mode === 'startup' ? '1' : '0',
|
|
CLAUDE_CODE_PROFILE_QUERY: mode === 'query' ? '1' : '0',
|
|
}
|
|
|
|
const startedAt = Date.now()
|
|
const exitCode = await new Promise<number>((resolveExit, reject) => {
|
|
const child = spawn('node', childArgs, {
|
|
cwd: resolve('.'),
|
|
env,
|
|
stdio: 'inherit',
|
|
})
|
|
|
|
child.on('error', reject)
|
|
child.on('exit', code => resolveExit(code ?? 1))
|
|
})
|
|
const wallTimeMs = Date.now() - startedAt
|
|
|
|
const startupReportPath = getLatestFile(join(configDir, 'startup-perf'))
|
|
const queryReportPath = getLatestFile(join(configDir, 'query-perf'))
|
|
const startupReport = readMaybe(startupReportPath)
|
|
const queryReport = readMaybe(queryReportPath)
|
|
|
|
return {
|
|
mode,
|
|
command: ['node', ...childArgs],
|
|
artifactDir,
|
|
startupReportPath,
|
|
queryReportPath,
|
|
wallTimeMs,
|
|
exitCode,
|
|
startupTotalMs: startupReport
|
|
? extractMs(startupReport, /Total startup time:\s+([0-9.]+)ms/)
|
|
: null,
|
|
queryTtftMs: queryReport
|
|
? extractMs(queryReport, /Total TTFT:\s+([0-9.]+)ms/)
|
|
: null,
|
|
queryTotalTimeMs: queryReport
|
|
? extractMs(queryReport, /Total time:\s+([0-9.]+)ms/)
|
|
: null,
|
|
queryPreApiOverheadMs: queryReport
|
|
? extractMs(queryReport, /Total pre-API overhead\s+([0-9.]+)ms/)
|
|
: null,
|
|
}
|
|
}
|
|
|
|
const { mode, cliArgs } = parseArgs(process.argv.slice(2))
|
|
ensureBuilt()
|
|
|
|
const artifactDir = resolve('.perf-artifacts', `${mode}-${getRunId()}`)
|
|
mkdirSync(artifactDir, { recursive: true })
|
|
|
|
const summary = await runCommand(mode, cliArgs, artifactDir)
|
|
const summaryPath = join(artifactDir, 'summary.json')
|
|
writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8')
|
|
|
|
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`)
|
|
process.stdout.write(`Summary written to ${summaryPath}\n`)
|
|
|
|
if (summary.exitCode !== 0) {
|
|
process.exit(summary.exitCode)
|
|
}
|