fix(cli): drain perf_hooks buffer to stop Ink dev-mode heap leak

Ink/react-reconciler in dev mode emits performance.mark/measure() on every
render. Without a PerformanceObserver, Node retains every entry in the User
Timing buffer forever — ~165 renders/sec in the TUI accumulate ~600MB of
PerformanceMeasure objects + diff strings ("Components ⚛", "Changed Props",
"– children", etc.) over 20-30min, which is what was OOM-crashing long dev
sessions. A 30s interval calling clearMeasures()/clearMarks() drains it.

Also teach scripts/dev-with-memory.mjs to forward --inspect /
--inspect-brk, plus auto-add --heapsnapshot-near-heap-limit=3 so any future
heap-limit OOM auto-dumps. The script now invokes node directly with tsx's
preflight + loader (resolved via require.resolve) instead of going through
the .bin/tsx wrapper, so the inspector attaches to the real app process
instead of a parent wrapper that exits before serving the websocket.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-25 08:10:40 -07:00
parent 3c1fe7ee73
commit 2ae7091049
2 changed files with 37 additions and 13 deletions

View File

@@ -15,6 +15,7 @@ import { existsSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } fro
import { createRequire } from "node:module";
import { join, dirname } from "node:path";
import { tmpdir } from "node:os";
import { performance } from "node:perf_hooks";
// @ts-expect-error -- Bun-only global; undefined in Node
const isBunBinary = typeof Bun !== "undefined" && !!Bun.embeddedFiles;
@@ -59,6 +60,14 @@ function configurePiPackage(): void {
configurePiPackage();
// Drain Node's User Timing buffer. Ink (react-reconciler) in dev mode emits
// performance.mark()/measure() on every render; entries accumulate forever
// without an observer, retaining ~600MB after 20-30min of TUI rendering.
setInterval(() => {
performance.clearMeasures();
performance.clearMarks();
}, 30_000).unref();
/**
* Load `.env` (and `.env.local`) from the current working directory into
* process.env so that secrets like FUSION_DAEMON_TOKEN can live in a local