mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-10 05:08:32 +00:00
172 lines
5.9 KiB
TypeScript
172 lines
5.9 KiB
TypeScript
#!/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)}`
|
|
}
|