122 lines
3.3 KiB
TypeScript
122 lines
3.3 KiB
TypeScript
import { Global } from "@/global"
|
|
import { Installation } from "@/installation"
|
|
import { stats } from "@/util/queue"
|
|
import { Log } from "@/util/log"
|
|
import { Filesystem } from "@/util/filesystem"
|
|
import { appendFile, mkdir } from "fs/promises"
|
|
import { writeHeapSnapshot } from "node:v8"
|
|
import path from "path"
|
|
|
|
const log = Log.create({ service: "memory" })
|
|
|
|
const root = process.env.OPENCODE_DEBUG_DIR ?? path.join(Global.Path.state, "debug")
|
|
const file = path.join(root, "memory.jsonl")
|
|
const snap = path.join(root, "snapshots")
|
|
|
|
export namespace Memory {
|
|
export function start(name: string) {
|
|
if (!Installation.isLocal()) return () => {}
|
|
|
|
let busy = false
|
|
let last = 0
|
|
const every = num("OPENCODE_DEBUG_MEMORY_INTERVAL_MS") ?? 10_000
|
|
const limit = (num("OPENCODE_DEBUG_MEMORY_RSS_MB") ?? 1_500) * 1024 * 1024
|
|
const cool = num("OPENCODE_DEBUG_MEMORY_COOLDOWN_MS") ?? 5 * 60 * 1000
|
|
|
|
const tick = async (kind: "start" | "sample") => {
|
|
if (busy) return
|
|
busy = true
|
|
|
|
try {
|
|
const now = Date.now()
|
|
const mem = process.memoryUsage()
|
|
const q = stats()
|
|
.filter((item) => item.size > 0 || item.max > 0)
|
|
.sort((a, b) => b.size - a.size || b.max - a.max)
|
|
.slice(0, 10)
|
|
const row = {
|
|
kind: "sample",
|
|
time: new Date(now).toISOString(),
|
|
name,
|
|
pid: process.pid,
|
|
rss: mem.rss,
|
|
heap: mem.heapUsed,
|
|
ext: mem.external,
|
|
buf: mem.arrayBuffers,
|
|
queues: q,
|
|
}
|
|
|
|
await line(row)
|
|
|
|
if (kind === "start" || mem.rss < limit || now - last < cool) return
|
|
|
|
last = now
|
|
const tag = stamp(now)
|
|
const heap = path.join(snap, `${tag}-${name}.heapsnapshot`)
|
|
await mkdir(snap, { recursive: true })
|
|
writeHeapSnapshot(heap)
|
|
|
|
const meta = {
|
|
kind: "snapshot",
|
|
time: row.time,
|
|
name,
|
|
pid: process.pid,
|
|
trigger: {
|
|
type: "rss",
|
|
limit,
|
|
value: mem.rss,
|
|
},
|
|
memory: mem,
|
|
queues: q,
|
|
}
|
|
|
|
await Filesystem.writeJson(path.join(snap, `${tag}-${name}.json`), meta)
|
|
await line({ ...meta, heap })
|
|
log.warn("memory snapshot written", {
|
|
name,
|
|
pid: process.pid,
|
|
rss_mb: mb(mem.rss),
|
|
limit_mb: mb(limit),
|
|
heap,
|
|
})
|
|
} catch (err) {
|
|
log.warn("memory monitor failed", {
|
|
name,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
})
|
|
} finally {
|
|
busy = false
|
|
}
|
|
}
|
|
|
|
const timer = setInterval(() => {
|
|
void tick("sample")
|
|
}, every)
|
|
timer.unref?.()
|
|
void tick("start")
|
|
|
|
return () => {
|
|
clearInterval(timer)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function line(input: unknown) {
|
|
await mkdir(root, { recursive: true })
|
|
await appendFile(file, JSON.stringify(input) + "\n")
|
|
}
|
|
|
|
function num(key: string) {
|
|
const value = process.env[key]
|
|
if (!value) return undefined
|
|
const parsed = Number(value)
|
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined
|
|
}
|
|
|
|
function mb(value: number) {
|
|
return Math.round((value / 1024 / 1024) * 10) / 10
|
|
}
|
|
|
|
function stamp(now: number) {
|
|
return new Date(now).toISOString().replaceAll(":", "-").replaceAll(".", "-")
|
|
}
|