Repository navigation
stream: Readable batched iteration #34207
Copy link
Copy link
Closed as not planned
Labels
staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.streamIssues and PRs related to Node.js streams.Issues and PRs related to Node.js streams.
Description
This is a continuation of #34035 and the promises session we had on OpenJS about async iteration performance of streams. One alternative discussed was to batch reading.
I was thinking we could do something along the lines of:
async function* createBatchedAsyncIterator(stream, batchLen) {
let callback = nop;
function next(resolve) {
if (this === stream) {
callback();
callback = nop;
} else {
callback = resolve;
}
}
stream
.on('readable', next)
.on('error', next)
.on('end', next)
.on('close', next);
try {
const state = stream._readableState;
while (true) {
let buffer;
while (true) {
const chunk = stream.read();
if (chunk === null) break;
if (!buffer) buffer = [];
buffer.push(chunk);
if (batchLen && buffer.length >= batchLen) break;
}
if (buffer) {
yield buffer;
} else if (state.errored) {
throw state.errored;
} else if (state.ended) {
break;
} else if (state.closed) {
// TODO(ronag): ERR_PREMATURE_CLOSE?
break;
} else {
await new Promise(next);
}
}
} catch (err) {
destroyImpl.destroyer(stream, err);
throw err;
} finally {
destroyImpl.destroyer(stream, null);
}
}
Readable.batched = function (stream, batchLen) {
return createBatchedAsyncIterator(stream, batchLen);
}Which would make the following possible:
// Concurrency
for await (const requests of Readable.batched(stream, 128)) {
// Process in parallel with concurrency limit of 128.
await Promise.all(requests.map(dispatch))
}
// Speed
for await (const requests of Readable.batched(stream, 128)) {
for (const request of requests) {
// All in the same tick
}
}It's still not perfect since if one element takes very long it would reduce concurrency. However, it would still be a step forward. Also reducing the async iteration overhead.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.streamIssues and PRs related to Node.js streams.Issues and PRs related to Node.js streams.
Activity
ronag commentedon Jul 4, 2020
@nodejs/streams @benjamingr
ronag commentedon Jul 4, 2020
Another possible implementation with maybe better performance:
stream.read(0); const buffer = state.length > 0 ? state.buffer.splice(0, batchLen) : null; if (buffer) yield buffer; else ...Though then it becomes a question on whether it's an array or just an iterable.
mcollina commentedon Jul 5, 2020
I think this is great.
what do you think @jasnell?
ronag commentedon Jul 5, 2020
Giving this further thought, If you want there always to be max concurrency we could also go for a pattern like this:
Or something...
ronag commentedon Jul 5, 2020
Anyway, going back to to original proposal I think it's a good idea to put the stream parameter as the last argument, i.e.
Readable.batched(128, stream)to allow for extensions such as the previous post.mcollina commentedon Jul 5, 2020
I would use an option-object instead.
addaleax commentedon Jul 5, 2020
How would this effectively differ from increasing the readable HWM? The
Readable.batched()here would trade off memory usage for speed, which is also what the HWM does, but increasing the HWM doesn’t increase latency whereas this would, right?ronag commentedon Jul 5, 2020
Would differ quite a bit in objectMode.
We would reduce the number of yield invocation, e.g. with batch=128 you would do yield once instead of 128 times. Most of the overhead of async iterators comes from this. Also remember that between each item there has to be a tick, so we would also reduce the number of ticks from 128, to 1.
It would allow the users to work on several items in parallel. Currently async iterators serializes everything and forces the user to work on one item at a time.
What I suggest here would basically bring the HWM buffer from the stream directly into the iteration.
imcotton commentedon Jul 8, 2020
Just want to pitch in my 2 cents, instead of fixed batch size, how about accepting size dynamically?
The syntax is actually quite simple:
Based on that, Async Generator could also comes in play:
More realistic examples are like 1) bitcoin block parser, or 2) socket5 client negotiator.
If you were interested, the underlying implementations are only around 60 lines of code, not sure it would be a good fit in core, would love to have any feedback, thanks.
edit: code sample correction
ronag commentedon Jul 8, 2020
I'm not sure I see how that differs from my example?
vs
imcotton commentedon Jul 8, 2020
Its
read :: number -> Promise<Buffer>part I'm referring to, thereadsignature.ronag commentedon Jul 8, 2020
Ah, I think that's a different topic though?
imcotton commentedon Jul 8, 2020
Sorry I don't quite follow, the topic here been around batch reading methodology and its implementation, no?
ronag commentedon Jul 29, 2020
Here is another case where this would be useful:
jimmywarting commentedon Dec 7, 2020
whatwg talked about having a extendable queuingStrategy
ReadableStream.from(asyncIterable, queuingStrategy = {})mcollina commentedon Dec 7, 2020
@jimmywarting that would not help in this case. A significant part of the cost is having a promise per chunk. Essentially we'll need to have an alternative async iteration implementation that returns an array.
In
node/lib/internal/streams/readable.js
Line 1105 in 537da19
benjamingr commentedon Dec 7, 2020
I'm honestly fine with just landing one of the APIs Robert mentioned above as experimental and going from there they seem adequate.
mcollina commentedon Dec 7, 2020
I think we should just add this in.
github-actions commentedon Jun 27, 2026
This issue has been marked as stale due to 210 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.
github-actions commentedon Jul 28, 2026
This issue has been automatically closed after 30 days of inactivity following its stale status (no activity for a total of 120 days).
If this is still relevant, feel free to reopen it or leave a comment with additional details so we can continue the discussion.