Skip to content

Report memory as a breakdown, and fix OOM detection - #70

Merged
Kikobeats merged 2 commits into
masterfrom
feat/memory-breakdown
Jul 17, 2026
Merged

Report memory as a breakdown, and fix OOM detection#70
Kikobeats merged 2 commits into
masterfrom
feat/memory-breakdown

Conversation

@Kikobeats

@Kikobeats Kikobeats commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Why

profiling.memory was a single process.memoryUsage().rss sample taken when the function returned. No single number is correct here, and this one was wrong in both directions:

  • It over-reports. A function that does () => 1 + 1 reported 43.3 MB, because that is the Node.js runtime baseline. Nothing was attributable to the function.
  • It under-reports. 300 MB of untouched typed arrays reported 44.9 MB, because pages that are never written to never become resident. The process is holding 300 MB that RSS cannot see.

It was also documented as "Peak RSS", which it is not: it is sampled once, in respond(), after the function settles. There is no high-water tracking.

What

profiling.memory becomes a breakdown, where each field answers a different question:

field meaning
total RSS of the isolate process, Node.js baseline included
used total minus the baseline measured before the function ran
heap V8 heap in use. The only field the memory limit bounds
external Off-heap Buffer/ArrayBuffer. Not bounded by the memory limit
// before
memory: 45449216                     // 43.3 MB, for a function that allocates nothing

// after
memory: { total: 45432832, used: 524288, heap: 4410880, external: 1742574 }

Measured across workloads, under memory: 16:

scenario old memory used heap external
() => 1 + 1 42.9 MB 0.0 MB 3.6 MB 1.4 MB
300 MB of touched Buffers 344.7 MB 301.8 MB 2.7 MB 301.4 MB
300 MB of untouched typed arrays 44.9 MB 2.1 MB 2.7 MB 301.4 MB

The last row is the case that justifies the split: the old number looks innocent while the process holds 300 MB. Only external catches it.

Bug fix: MemoryError never fired at a realistic limit

Included as a separate commit, because it stands on its own.

V8 only raises SIGTRAP when the heap limit is too small to boot the heap. Every realistic limit exhausts the heap through the normal OOM path and exits with SIGABRT, which was not checked, so it escaped as a raw ChildProcessError instead of MemoryError.

memory signal detected before
1 SIGTRAP yes
8 / 16 / 64 / 128 SIGABRT no

The existing test only covered memory: 1, the single value that traps, so the suite stayed green over a broken path. The abort is gated on V8's own out of memory message so an unrelated abort is not mislabelled, and there is a test asserting that.

Docs correction

The README's resource-limit example claimed 78 MB of Uint8Array trips { memory: 64 }. It does not. Typed arrays are off-heap, so it returns cleanly with external=79.7MB. Verified:

memory: 64 -> NO ERROR. heap=3.8MB external=79.7MB
memory: 16 -> NO ERROR. heap=2.9MB external=79.7MB
memory:  1 -> MemoryError: Out of memory

Replaced with on-heap growth, which the limit does bound, and added a note that memory bounds the heap only.

BREAKING CHANGE

profiling.memory is an object instead of a number. Callers reading it as a number want profiling.memory.used, or .total for the previous value.

Tests

40 passed. Both new tests were confirmed to fail against the old code before the fix (the OOM one fails with ChildProcessError), so they are real guards rather than green-by-construction.

🤖 Generated with Claude Code


Note

Medium Risk
Breaking API change for profiling.memory consumers and altered error classification for child process exits; core isolate profiling and limit behavior.

Overview
Breaking: profiling.memory is now an object (total, used, heap, external) instead of a single RSS number. The isolate template captures a pre-run RSS baseline and reports attributable used memory plus V8 heap and off-heap external.

OOM handling now treats SIGABRT with V8’s “out of memory” message as MemoryError, not only SIGTRAP (tiny limits). Unrelated aborts stay non-MemoryError.

README and the memory-limit example are updated: heap-bound string growth replaces the misleading typed-array demo, with a note that memory limits V8 heap only, not external.

Reviewed by Cursor Bugbot for commit 8f00253. Bugbot is set up for automated code reviews on this repo. Configure here.

Kikobeats and others added 2 commits July 17, 2026 09:49
V8 only traps (SIGTRAP) when the heap limit is too small to boot,
which is why `memory: 1` was detected. Every realistic limit
exhausts the heap through the normal OOM path and exits with
SIGABRT, so it escaped as a raw ChildProcessError and never
surfaced as MemoryError.

The existing test only covered `memory: 1`, the single value that
traps, so it passed while every production limit was broken.

Gate the abort on V8's own "out of memory" message so an unrelated
abort is not mislabelled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CTJQQb6p1UWdkireDjCVri
profiling.memory was a single RSS sample taken when the function
returned. No single number is correct: RSS over-reports (a no-op
function reported ~43MB of Node.js baseline) and under-reports
(300MB of untouched typed arrays stayed invisible, because pages
that are never written to never become resident).

Report the fields that answer different questions instead:

  total    RSS of the isolate process, baseline included
  used     total minus the pre-execution baseline
  heap     V8 heap, the only field the `memory` limit bounds
  external off-heap Buffer/ArrayBuffer, which the limit misses

Also correct the README resource-limit example, which claimed 78MB
of Uint8Array trips `memory: 64`. It does not: typed arrays are
off-heap, so it returns cleanly with external=79.7MB. Replaced with
on-heap growth, which the limit does bound.

BREAKING CHANGE: profiling.memory is an object instead of a number.
Callers reading it as a number should read profiling.memory.used
for the memory attributable to the function, or .total for the
previous RSS value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CTJQQb6p1UWdkireDjCVri
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 29565032313

Warning

No base build found for commit a0eb1f4 on master.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 95.163%

Details

  • Patch coverage: 10 of 10 lines across 2 files are fully covered (100%).

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 703
Covered Lines: 674
Line Coverage: 95.87%
Relevant Branches: 124
Covered Branches: 113
Branch Coverage: 91.13%
Branches in Coverage %: Yes
Coverage Strength: 49.64 hits per line

💛 - Coveralls

1 similar comment
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 29565032313

Warning

No base build found for commit a0eb1f4 on master.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 95.163%

Details

  • Patch coverage: 10 of 10 lines across 2 files are fully covered (100%).

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 703
Covered Lines: 674
Line Coverage: 95.87%
Relevant Branches: 124
Covered Branches: 113
Branch Coverage: 91.13%
Branches in Coverage %: Yes
Coverage Strength: 49.64 hits per line

💛 - Coveralls

@Kikobeats
Kikobeats merged commit fda9eb9 into master Jul 17, 2026
3 checks passed
@Kikobeats
Kikobeats deleted the feat/memory-breakdown branch July 17, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants