Repository navigation
Expand file tree
/
Copy pathprofile-typecheck.ts
More file actions
172 lines (157 loc) · 5.87 KB
/
Copy pathprofile-typecheck.ts
File metadata and controls
172 lines (157 loc) · 5.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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)),
peakTscRelatedProcesses: Math.max(0, ...active.map((sample) => sample.tscRelatedProcesses)),
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.peakTscRelatedProcesses} tsc-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,
tscRelatedProcesses: tree.filter((process) => /\btsc\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|\btsc\b|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)}`
}