mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-10 04:18:30 +00:00
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
|
||||
ts-dist
|
||||
.turbo
|
||||
.typecheck-profiles
|
||||
**/.serena
|
||||
.serena/
|
||||
**/.omo
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@
|
|||
"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",
|
||||
"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",
|
||||
"postinstall": "bun run --cwd packages/core fix-node-pty",
|
||||
"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