buffer: fix Blob.stream() leaking source buffer#63577
Open
semimikoh wants to merge 1 commit into
Open
Conversation
Blob.prototype.stream() registered a wakeup callback on the underlying source's start() and never released it. The strong Reader::wakeup_ handle kept the reader -- and through it the blob's DataQueue and backing store -- reachable as a GC root, so the source buffer leaked on every stream() call. On Node 26+, streaming a 1 MiB blob 300 times retained ~300 MiB in process.memoryUsage().arrayBuffers while the V8 heap stayed small. Register the wakeup lazily in pull() and clear it on every terminal or idle path (EOS, error, cancel, backpressure), mirroring the cleanup already done by the async iterator path. The strong handle now only lives while a pull is in flight, so the reader and its backing store become collectable once the stream finishes, errors, is cancelled, or goes idle under backpressure. Fixes: nodejs#63574 Signed-off-by: semimikoh <ejffjeosms@gmail.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #63577 +/- ##
=======================================
Coverage 90.31% 90.32%
=======================================
Files 730 730
Lines 234463 234476 +13
Branches 43908 43921 +13
=======================================
+ Hits 211763 211795 +32
+ Misses 14419 14417 -2
+ Partials 8281 8264 -17
🚀 New features to boost your workflow:
|
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.
Summary
Blob.prototype.stream()permanently retained the blob's source bufferon Node 26+. RSS /
arrayBuffersclimbed without bound while the V8heap stayed small, making it hard to diagnose. This is a regression from
the QUIC
DataQueue/Blob::Readerwork that introduced thewakeup_mechanism.
Root cause
createBlobReaderStream()registered the C++ wakeup callback in thestream source's
start()and never cleared it.Reader::wakeup_is astrong
v8::Global<v8::Function>, so it anchored a cycle as a GC root:This matches the maintainer diagnosis in #63574 ("
Reader::wakeup_iskeeping the stream reference alive").
Fix
Register the wakeup lazily in
pull()(only while a read is in flight)and clear it via
reader.setWakeup(undefined)on every terminal/idlepath — EOS, error, cancel, and backpressure — mirroring the
finally { reader.setWakeup(undefined) }already used bycreateBlobReaderIterable. The async-iterator andarrayBuffer()pathsdo not use the wakeup and are unchanged.
Test
Adds
test/parallel/test-blob-stream-gc.js, which streams a 1 MiB blobmany times (unused, cancelled, and fully drained) and asserts
process.memoryUsage().arrayBuffersdoes not grow unbounded.Fixes: #63574