From aa2239d5decac0002de2ddd3d8f8c7f6ccccd760 Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 2 Apr 2026 22:34:33 -0400 Subject: [PATCH] add automatic heap snapshots for high-memory cli processes (#20788) --- packages/opencode/src/cli/cmd/tui/worker.ts | 3 ++ packages/opencode/src/cli/heap.ts | 59 +++++++++++++++++++++ packages/opencode/src/flag/flag.ts | 1 + packages/opencode/src/index.ts | 3 ++ 4 files changed, 66 insertions(+) create mode 100644 packages/opencode/src/cli/heap.ts diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index a83645d892..643676e348 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -13,6 +13,7 @@ import { Flag } from "@/flag/flag" import { setTimeout as sleep } from "node:timers/promises" import { writeHeapSnapshot } from "node:v8" import { WorkspaceID } from "@/control-plane/schema" +import { Heap } from "@/cli/heap" await Log.init({ print: process.argv.includes("--print-logs"), @@ -23,6 +24,8 @@ await Log.init({ })(), }) +Heap.start() + process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { e: e instanceof Error ? e.message : e, diff --git a/packages/opencode/src/cli/heap.ts b/packages/opencode/src/cli/heap.ts new file mode 100644 index 0000000000..bb5a3d0937 --- /dev/null +++ b/packages/opencode/src/cli/heap.ts @@ -0,0 +1,59 @@ +import path from "path" +import { writeHeapSnapshot } from "node:v8" +import { Flag } from "@/flag/flag" +import { Global } from "@/global" +import { Log } from "@/util/log" + +const log = Log.create({ service: "heap" }) +const MINUTE = 60_000 +const LIMIT = 2 * 1024 * 1024 * 1024 + +export namespace Heap { + let timer: Timer | undefined + let lock = false + let armed = true + + export function start() { + if (!Flag.OPENCODE_AUTO_HEAP_SNAPSHOT) return + if (timer) return + + const run = async () => { + if (lock) return + + const stat = process.memoryUsage() + if (stat.rss <= LIMIT) { + armed = true + return + } + if (!armed) return + + lock = true + armed = false + const file = path.join( + Global.Path.log, + `heap-${process.pid}-${new Date().toISOString().replace(/[:.]/g, "")}.heapsnapshot`, + ) + log.warn("heap usage exceeded limit", { + rss: stat.rss, + heap: stat.heapUsed, + file, + }) + + await Promise.resolve() + .then(() => writeHeapSnapshot(file)) + .catch((err) => { + log.error("failed to write heap snapshot", { + error: err instanceof Error ? err.message : String(err), + file, + }) + }) + + lock = false + } + + timer = setInterval(() => { + void run() + }, MINUTE) + timer.unref?.() + } +} diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 27190f2eb2..1ac52dd17f 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -12,6 +12,7 @@ function falsy(key: string) { export namespace Flag { export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE") + export const OPENCODE_AUTO_HEAP_SNAPSHOT = truthy("OPENCODE_AUTO_HEAP_SNAPSHOT") export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"] export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"] export declare const OPENCODE_PURE: boolean diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index bb14e0588a..1fa027abf9 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -35,6 +35,7 @@ import { JsonMigration } from "./storage/json-migration" import { Database } from "./storage/db" import { errorMessage } from "./util/error" import { PluginCommand } from "./cli/cmd/plug" +import { Heap } from "./cli/heap" process.on("unhandledRejection", (e) => { Log.Default.error("rejection", { @@ -96,6 +97,8 @@ const cli = yargs(args) })(), }) + Heap.start() + process.env.AGENT = "1" process.env.OPENCODE = "1" process.env.OPENCODE_PID = String(process.pid)