Skip to content

Commit aa2239d

Browse files
authored
add automatic heap snapshots for high-memory cli processes (#20788)
1 parent 8daeacc commit aa2239d

4 files changed

Lines changed: 66 additions & 0 deletions

File tree

packages/opencode/src/cli/cmd/tui/worker.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Flag } from "@/flag/flag"
1313
import { setTimeout as sleep } from "node:timers/promises"
1414
import { writeHeapSnapshot } from "node:v8"
1515
import { WorkspaceID } from "@/control-plane/schema"
16+
import { Heap } from "@/cli/heap"
1617

1718
await Log.init({
1819
print: process.argv.includes("--print-logs"),
@@ -23,6 +24,8 @@ await Log.init({
2324
})(),
2425
})
2526

27+
Heap.start()
28+
2629
process.on("unhandledRejection", (e) => {
2730
Log.Default.error("rejection", {
2831
e: e instanceof Error ? e.message : e,

packages/opencode/src/cli/heap.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import path from "path"
2+
import { writeHeapSnapshot } from "node:v8"
3+
import { Flag } from "@/flag/flag"
4+
import { Global } from "@/global"
5+
import { Log } from "@/util/log"
6+
7+
const log = Log.create({ service: "heap" })
8+
const MINUTE = 60_000
9+
const LIMIT = 2 * 1024 * 1024 * 1024
10+
11+
export namespace Heap {
12+
let timer: Timer | undefined
13+
let lock = false
14+
let armed = true
15+
16+
export function start() {
17+
if (!Flag.OPENCODE_AUTO_HEAP_SNAPSHOT) return
18+
if (timer) return
19+
20+
const run = async () => {
21+
if (lock) return
22+
23+
const stat = process.memoryUsage()
24+
if (stat.rss <= LIMIT) {
25+
armed = true
26+
return
27+
}
28+
if (!armed) return
29+
30+
lock = true
31+
armed = false
32+
const file = path.join(
33+
Global.Path.log,
34+
`heap-${process.pid}-${new Date().toISOString().replace(/[:.]/g, "")}.heapsnapshot`,
35+
)
36+
log.warn("heap usage exceeded limit", {
37+
rss: stat.rss,
38+
heap: stat.heapUsed,
39+
file,
40+
})
41+
42+
await Promise.resolve()
43+
.then(() => writeHeapSnapshot(file))
44+
.catch((err) => {
45+
log.error("failed to write heap snapshot", {
46+
error: err instanceof Error ? err.message : String(err),
47+
file,
48+
})
49+
})
50+
51+
lock = false
52+
}
53+
54+
timer = setInterval(() => {
55+
void run()
56+
}, MINUTE)
57+
timer.unref?.()
58+
}
59+
}

packages/opencode/src/flag/flag.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ function falsy(key: string) {
1212

1313
export namespace Flag {
1414
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
15+
export const OPENCODE_AUTO_HEAP_SNAPSHOT = truthy("OPENCODE_AUTO_HEAP_SNAPSHOT")
1516
export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"]
1617
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
1718
export declare const OPENCODE_PURE: boolean

packages/opencode/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { JsonMigration } from "./storage/json-migration"
3535
import { Database } from "./storage/db"
3636
import { errorMessage } from "./util/error"
3737
import { PluginCommand } from "./cli/cmd/plug"
38+
import { Heap } from "./cli/heap"
3839

3940
process.on("unhandledRejection", (e) => {
4041
Log.Default.error("rejection", {
@@ -96,6 +97,8 @@ const cli = yargs(args)
9697
})(),
9798
})
9899

100+
Heap.start()
101+
99102
process.env.AGENT = "1"
100103
process.env.OPENCODE = "1"
101104
process.env.OPENCODE_PID = String(process.pid)

0 commit comments

Comments
 (0)