-
-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove end-of-stream blocking #92
Conversation
index.js
Outdated
// the stream data. It does not include extraneous zeroes to trim at the end. | ||
// The underlying `ArrayBuffer` does allocate a number of bytes that is a power | ||
// of 2, but those bytes are only visible after calling `ArrayBuffer.resize()`. | ||
const resizeArrayBufferFast = (contents, length) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would name this resizeArrayBuffer
and the other one resizeArrayBufferSlow
as this one is the one we will keep in the future. It makes it slightly easier to remove the old one when we target Node.js 20.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in bcb87e7 👍
Co-authored-by: Sindre Sorhus <[email protected]>
Fixed 👍 |
Currently,
get-stream
aggregates all chunks in an array. Once fully consumed, all chunks are concatenated at the end.This final concatenation is slow and CPU-bound. On my machine, for
getStream()
, it is roughly a third of the CPU time spent. It is even half of it forgetStreamAsBuffer()
/getStreamAsArrayBuffer()
. If that final concatenation is happening on a big stream, this can hold up the event loop for a relatively long time. This can result in perceivable freezes by users, which removes some of the benefits of using streams.Instead, this PR finds a way to remove that final concatenation, and spend that CPU time when each chunk is processed instead. In other terms, it spreads that load over the whole time the stream is being consumed. Since streams are almost always I/O-bound (filesystem or network), that additional CPU time spent on each chunk is not noticed since the bottleneck is I/O (i.e. waiting for the next chunk).
This PR implements this by shifting the main strategy from storing each chunk in an array, to computing the final value incrementally instead. For example, instead of pushing to an array of strings then calling
array.join('')
, it keeps only a single string, which is appended with+=
on each chunk. This turns out to be actually ~30% faster (on my machine), most likely becausearray.push()
needs to allocate more memory at specific size intervals, which is slow. As a side note, this is also how core Node.js is implementingtext()
.For
Buffer
/ArrayBuffer
, the implementation is a little more involved to make sure it is as fast as possible (i.e. doing as few memory allocations and copies as possible). There are also two strategies, sinceArrayBuffer.resize()
provides with some significant performance boost, but is unfortunately only supported in Node 20 and a couple of browsers.This PR comes also with the following enhancement: when the stream is so big that it is larger than the max length for a string/
Buffer
/ArrayBuffer
, we currently truncateerror.bufferedData
to make sure it fits within the max length. The current implementation returns only half oferror.bufferedData
. This PR returns all bytes that have been read so far instead, so users will get as much information as possible inerror.bufferedData
.Apart from that last point, the user-visible behavior is identical to before this PR, this is just performance tweaking.
Finally, spreading the load as described above seems to decrease the probability of the process crashing on big streams. 2 automated tests had been previously disabled on CI for this reason. This PR re-enables them on CI, as they now work correctly.