perf improvements + /login fix
This commit is contained in:
163
scripts/perf.ts
Normal file
163
scripts/perf.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user