chore: add typecheck profiling (#35622)
This commit is contained in:
parent
bf01264661
commit
984430c97d
4 changed files with 242 additions and 1 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -13,6 +13,7 @@ tmp
|
||||||
dist
|
dist
|
||||||
ts-dist
|
ts-dist
|
||||||
.turbo
|
.turbo
|
||||||
|
.typecheck-profiles
|
||||||
**/.serena
|
**/.serena
|
||||||
.serena/
|
.serena/
|
||||||
**/.omo
|
**/.omo
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@
|
||||||
"lint": "oxlint",
|
"lint": "oxlint",
|
||||||
"lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/core/src packages/server/src packages/protocol/src packages/cli/src",
|
"lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/core/src packages/server/src packages/protocol/src packages/cli/src",
|
||||||
"test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml",
|
"test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml",
|
||||||
"typecheck": "bun turbo typecheck",
|
"typecheck": "bun turbo typecheck --concurrency=3",
|
||||||
|
"typecheck:profile": "bun script/profile-typecheck.ts",
|
||||||
|
"typecheck:profile:packages": "bun script/profile-typecheck-packages.ts",
|
||||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||||
"postinstall": "bun run --cwd packages/core fix-node-pty",
|
"postinstall": "bun run --cwd packages/core fix-node-pty",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
|
|
|
||||||
66
script/profile-typecheck-packages.ts
Normal file
66
script/profile-typecheck-packages.ts
Normal 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
172
script/profile-typecheck.ts
Normal 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)}`
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue