| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- #!/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)}`
- }
|