Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/opencode/src/cli/cmd/tui/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -23,6 +24,8 @@ await Log.init({
})(),
})

Heap.start()

process.on("unhandledRejection", (e) => {
Log.Default.error("rejection", {
e: e instanceof Error ? e.message : e,
Expand Down
67 changes: 67 additions & 0 deletions packages/opencode/src/cli/heap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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?.()
}

export function stop() {
if (!timer) return
clearInterval(timer)
timer = undefined
lock = false
armed = true
}
}
12 changes: 12 additions & 0 deletions packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function falsy(key: string) {

export namespace Flag {
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
export declare const OPENCODE_AUTO_HEAP_SNAPSHOT: boolean
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
Expand Down Expand Up @@ -87,6 +88,17 @@ export namespace Flag {
}
}

// Dynamic getter for OPENCODE_AUTO_HEAP_SNAPSHOT
// This must be evaluated at access time, not module load time,
// because tests may set this env var at runtime.
Object.defineProperty(Flag, "OPENCODE_AUTO_HEAP_SNAPSHOT", {
get() {
return truthy("OPENCODE_AUTO_HEAP_SNAPSHOT")
},
enumerable: true,
configurable: false,
})

// Dynamic getter for OPENCODE_DISABLE_PROJECT_CONFIG
// This must be evaluated at access time, not module load time,
// because external tooling may set this env var at runtime
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down Expand Up @@ -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)
Expand Down
Loading