fix: drain performance entry buffer to prevent OOM in long Ink sessions#522
Merged
will-lamerton merged 1 commit intoMay 19, 2026
Conversation
…ano-Collective#521) React 19's react-reconciler (dev build, shipped in node_modules) calls performance.measure() and performance.mark() on every render to emit user-timing entries for React DevTools. Node's built-in fetch (undici) similarly pushes a resource-timing entry per HTTP request. Both write to the same global performance entry buffer, which is never drained because nanocoder doesn't run a PerformanceObserver. Over long workflows with many subagent runs the buffer accumulates millions of entries. Node logs MaxPerformanceEntryBufferExceededWarning at the 1M mark, then V8 spends increasing time in mark-compact GC, and the process eventually crashes: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory Fix: install an unref'd 30s interval in the Ink render path that cFix: install an unref'd 30s interval in the Ink render path that cFix: install an unref'd 30s interval in the Ink render path that cFix: install an unref'd 30s interval in the Ink render path that cF/utFix: install an unref'd 30s interval in the Ink render path than sourceFix: install an unref'd 30s interval in the Ink render path thative \Fix: install an unref'd 30s interval in the Ink render path that cFix and is unaffected. Closes Nano-Collective#521
3 tasks
Contributor
There was a problem hiding this comment.
Pull request overview
Prevents long-running Ink (React) terminal sessions from accumulating unbounded Node perf_hooks performance entries (marks/measures/resource timings), which can eventually lead to heap OOM crashes during workflows with many renders/subagent runs.
Changes:
- Added a periodic “performance entry buffer guard” utility that clears marks/measures (and resource timings when supported).
- Installed the guard in the Ink CLI path immediately before rendering the app.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| source/utils/perf-buffer.ts | New helper that installs an unref()’d interval to clear the global performance entry buffer periodically. |
| source/cli.tsx | Calls installPerfBufferGuard() in the Ink render path to prevent long-session buffer growth. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+23
to
+25
| * `mark` / `measure` entries. We don't read them anywhere in the app, so this | ||
| * is lossless from our perspective, and it caps the buffer to at most one | ||
| * interval's worth of entries instead of letting it grow unbounded. |
Comment on lines
+28
to
+36
| const DEFAULT_INTERVAL_MS = 30_000; | ||
|
|
||
| let installed = false; | ||
|
|
||
| export function installPerfBufferGuard( | ||
| intervalMs: number = DEFAULT_INTERVAL_MS, | ||
| ): void { | ||
| if (installed) return; | ||
| if ( |
Member
|
Great fix - thank you :) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes the JavaScript heap OOM crash that occurs during long workflows with many subagent runs. Installs a periodic housekeeping timer that drains Node's global performance entry buffer (marks, measures, and resource timings), preventing it from growing unbounded over the lifetime of the process.
Closes #521
Root Cause
Ink renders via
react-reconciler@0.33— the development build, which is what ships innode_modules. In development mode React callsperformance.measure()andperformance.mark()on every render to emit user-timing DevTools entries. Node's built-in fetch (undici) similarly pushes a resource-timing entry per HTTP request. Both APIs write to the same global performance entry buffer (perf_hooks).There is no
PerformanceObserverin nanocoder that would drain the buffer, and there is no React DevTools to consume the entries, so they accumulate silently.At the 1 M entry mark Node logs:
After hours of operation (many subagent runs → many renders) the buffer holds millions of objects, V8 mark-compact GC becomes progressively less effective, and the process crashes:
Approach
source/utils/perf-buffer.ts— new standalone helper that installs anunref'd 30-secondsetIntervalwhich calls:performance.clearMarks()performance.clearMeasures()performance.clearResourceTimings()Using
timer.unref()means the interval never keeps the event loop alive if the app has otherwise finished.source/cli.tsx— callsinstallPerfBufferGuard()immediately beforerender(<App />)so the guard is active for every Ink session (both interactive andrunmode). The--plain/--version/--help/ login fast-paths are unaffected.The entries are never read by nanocoder itself, so draining them is entirely lossless.
Type of Change
Testing
Automated Tests
pnpm test:typespasses with no errors).spec.ts/tsxfilesManual Testing
Checklist