chore: add typecheck profiling (#35622)

This commit is contained in:
Aiden Cline 2026-07-06 17:17:30 -05:00 committed by GitHub
parent bf01264661
commit 984430c97d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 242 additions and 1 deletions

View file

@ -0,0 +1,66 @@
#!/usr/bin/env bun
import path from "path"
const root = path.resolve(import.meta.dir, "..")
const proc = Bun.spawn(
[
"bun",
"turbo",
"typecheck",
"--concurrency=1",
"--force",
"--continue=always",
"--summarize",
"--output-logs=errors-only",
],
{
cwd: root,
stdout: "pipe",
stderr: "pipe",
},
)
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited])
const output = stdout + stderr
if (exitCode !== 0) {
process.stdout.write(stdout)
process.stderr.write(stderr)
process.exit(exitCode)
}
const summary = output.match(/Summary:\s+(.+\.json)/)?.[1]?.trim()
if (!summary) {
process.stdout.write(stdout)
process.stderr.write(stderr)
throw new Error("Turbo did not report a run summary")
}
const report = (await Bun.file(summary).json()) as {
tasks: Array<{
taskId: string
execution: { startTime: number; endTime: number; exitCode: number } | null
}>
}
const tasks = report.tasks
.flatMap((task) =>
task.execution
? [
{
task: task.taskId.replace(/#typecheck$/, ""),
durationMs: task.execution.endTime - task.execution.startTime,
},
]
: [],
)
.sort((a, b) => b.durationMs - a.durationMs)
const total = tasks.reduce((duration, task) => duration + task.durationMs, 0)
const width = Math.max(...tasks.map((task) => task.task.length), "Package".length)
console.log(`Package${" ".repeat(width - "Package".length)} Time Share`)
tasks.forEach((task) => {
const duration = `${(task.durationMs / 1000).toFixed(2)}s`.padStart(7)
const share = `${((task.durationMs / total) * 100).toFixed(1)}%`.padStart(6)
console.log(`${task.task.padEnd(width)} ${duration} ${share}`)
})
console.log(`\nTotal serial task time: ${(total / 1000).toFixed(2)}s`)
console.log(`Turbo summary: ${path.relative(root, summary)}`)

172
script/profile-typecheck.ts Normal file
View file

@ -0,0 +1,172 @@
#!/usr/bin/env bun
import { mkdir } from "fs/promises"
import path from "path"
if (process.platform !== "darwin") throw new Error("System typecheck profiling currently supports macOS only")
const root = path.resolve(import.meta.dir, "..")
const startedAt = new Date()
const args = Bun.argv.slice(2)
const command = [
"bun",
"turbo",
"typecheck",
...(args.some((arg) => arg.startsWith("--concurrency")) ? [] : ["--concurrency=3"]),
...args,
]
const before = systemSnapshot()
const proc = Bun.spawn(command, {
cwd: root,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
})
const samples = [processTreeSnapshot(proc.pid, startedAt)]
const timer = setInterval(() => samples.push(processTreeSnapshot(proc.pid, startedAt)), 200)
const exitCode = await proc.exited
clearInterval(timer)
samples.push(processTreeSnapshot(proc.pid, startedAt))
const finishedAt = new Date()
const after = systemSnapshot()
const active = samples.filter((sample) => sample.processes > 0)
const report = {
command,
cwd: root,
startedAt: startedAt.toISOString(),
finishedAt: finishedAt.toISOString(),
durationSeconds: (finishedAt.getTime() - startedAt.getTime()) / 1000,
exitCode,
summary: {
peakCpuPercent: Math.max(0, ...active.map((sample) => sample.cpuPercent)),
averageCpuPercent: average(active.map((sample) => sample.cpuPercent)),
peakAggregateRssMB: Math.max(0, ...active.map((sample) => sample.aggregateRssMB)),
peakProcesses: Math.max(0, ...active.map((sample) => sample.processes)),
peakTsgoRelatedProcesses: Math.max(0, ...active.map((sample) => sample.tsgoRelatedProcesses)),
swapDeltaMB: after.swapUsedMB - before.swapUsedMB,
compressedMemoryDeltaMB: after.compressedMemoryMB - before.compressedMemoryMB,
pageoutDelta: after.pageouts - before.pageouts,
},
system: { before, after },
samples,
}
const directory = path.join(root, ".typecheck-profiles")
const file = path.join(directory, `${startedAt.toISOString().replaceAll(":", "-")}.json`)
await mkdir(directory, { recursive: true })
await Bun.write(file, JSON.stringify(report, null, 2) + "\n")
console.log(`
Typecheck profile
Duration: ${report.durationSeconds.toFixed(1)}s
Average CPU: ${report.summary.averageCpuPercent.toFixed(0)}%
Peak CPU: ${report.summary.peakCpuPercent.toFixed(0)}%
Aggregate RSS: ${report.summary.peakAggregateRssMB.toFixed(0)} MB
Peak processes: ${report.summary.peakProcesses} (${report.summary.peakTsgoRelatedProcesses} tsgo-related)
Swap delta: ${signed(report.summary.swapDeltaMB)} MB
Compressed: ${signed(report.summary.compressedMemoryDeltaMB)} MB
Pageouts: ${signed(report.summary.pageoutDelta)}
Report: ${path.relative(root, file)}
`)
process.exit(exitCode)
function processTreeSnapshot(rootPID: number, startedAt: Date) {
const processes = processList()
const pids = new Set([rootPID])
const pending = [rootPID]
while (pending.length > 0) {
const parent = pending.shift()
processes
.filter((process) => process.ppid === parent && !pids.has(process.pid))
.forEach((process) => {
pids.add(process.pid)
pending.push(process.pid)
})
}
const tree = processes.filter((process) => pids.has(process.pid))
return {
elapsedSeconds: (Date.now() - startedAt.getTime()) / 1000,
processes: tree.length,
tsgoRelatedProcesses: tree.filter((process) => /\btsgo\b/.test(process.command)).length,
cpuPercent: sum(tree.map((process) => process.cpuPercent)),
aggregateRssMB: sum(tree.map((process) => process.rssKB)) / 1024,
}
}
function systemSnapshot() {
const vm = text(["vm_stat"])
const pageSize = Number(vm.match(/page size of (\d+) bytes/)?.[1] ?? 4096)
const fields = Object.fromEntries(
vm
.split("\n")
.map((line) => line.match(/^([^:]+):\s+(\d+)\.?$/))
.filter((match): match is RegExpMatchArray => match !== null)
.map((match) => [match[1], Number(match[2])]),
)
const swap = text(["sysctl", "-n", "vm.swapusage"])
return {
loadAverage: text(["sysctl", "-n", "vm.loadavg"]).trim(),
thermalState: text(["pmset", "-g", "therm"]).trim(),
swapUsedMB: Number(swap.match(/used = ([\d.]+)M/)?.[1] ?? 0),
freeMemoryMB: ((fields["Pages free"] ?? 0) * pageSize) / 1024 / 1024,
compressedMemoryMB: ((fields["Pages occupied by compressor"] ?? 0) * pageSize) / 1024 / 1024,
pageouts: fields.Pageouts ?? 0,
relevantProcesses: processList()
.filter((process) =>
/opencode|tsgo|tsserver|vtsls|eslintServer|tailwindcss-language-server/.test(process.command),
)
.sort((a, b) => b.rssKB - a.rssKB)
.map(processSummary),
topCpuProcesses: processList()
.sort((a, b) => b.cpuPercent - a.cpuPercent)
.slice(0, 15)
.map(processSummary),
topMemoryProcesses: processList()
.sort((a, b) => b.rssKB - a.rssKB)
.slice(0, 15)
.map(processSummary),
}
}
function processSummary(process: ReturnType<typeof processList>[number]) {
return {
pid: process.pid,
ppid: process.ppid,
rssMB: process.rssKB / 1024,
cpuPercent: process.cpuPercent,
command: process.command,
}
}
function processList() {
return text(["ps", "-axo", "pid=,ppid=,rss=,%cpu=,command="])
.split("\n")
.map((line) => line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+([\d.]+)\s+(.*)$/))
.filter((match): match is RegExpMatchArray => match !== null)
.map((match) => ({
pid: Number(match[1]),
ppid: Number(match[2]),
rssKB: Number(match[3]),
cpuPercent: Number(match[4]),
command: match[5],
}))
}
function text(command: string[]) {
return Bun.spawnSync(command).stdout.toString()
}
function sum(values: number[]) {
return values.reduce((total, value) => total + value, 0)
}
function average(values: number[]) {
if (values.length === 0) return 0
return sum(values) / values.length
}
function signed(value: number) {
return `${value >= 0 ? "+" : ""}${value.toFixed(0)}`
}