Skip to content

Commit 0ff7249

Browse files
committed
stream: speed up reads and iteration over default WHATWG streams
Skip the size algorithm call and its result validation when the size algorithm is the default one, and stop re-running the full ShouldCallPull predicate at per-chunk call sites where its inputs are already established. readable-async-iterator type=normal: +16.6% (***) readable-read-buffered: +13.9% to +27.5% (**/***) pipe-to: +3.7% to +6.0% (15/16 configs significant) Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 7036199 commit 0ff7249

1 file changed

Lines changed: 51 additions & 9 deletions

File tree

lib/internal/webstreams/readablestream.js

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -599,8 +599,12 @@ class ReadableStream {
599599
!controller[kState].queue.length) {
600600
readableStreamDefaultControllerClearAlgorithms(controller);
601601
readableStreamClose(stream);
602-
} else {
603-
readableStreamDefaultControllerCallPullIfNeeded(controller);
602+
} else if (!controller[kState].closeRequested &&
603+
controller[kState].started &&
604+
controller[kState].highWaterMark -
605+
controller[kState].queueTotalSize > 0) {
606+
// Reduced ShouldCallPull, as in the read() fast path.
607+
readableStreamDefaultControllerPull(controller);
604608
}
605609

606610
return PromiseResolve({ done: false, value: chunk });
@@ -949,8 +953,14 @@ class ReadableStreamDefaultReader {
949953
if (controller[kState].closeRequested && !controller[kState].queue.length) {
950954
readableStreamDefaultControllerClearAlgorithms(controller);
951955
readableStreamClose(stream);
952-
} else {
953-
readableStreamDefaultControllerCallPullIfNeeded(controller);
956+
} else if (!controller[kState].closeRequested &&
957+
controller[kState].started &&
958+
controller[kState].highWaterMark -
959+
controller[kState].queueTotalSize > 0) {
960+
// ShouldCallPull reduced to the conditions not already
961+
// established on this path: the state is readable and the
962+
// queue was non-empty, so no read requests can be parked.
963+
readableStreamDefaultControllerPull(controller);
954964
}
955965

956966
return PromiseResolve({ done: false, value: chunk });
@@ -2524,13 +2534,28 @@ function readableStreamDefaultControllerEnqueue(controller, chunk) {
25242534
reader[kState] !== undefined &&
25252535
reader[kType] === 'ReadableStreamDefaultReader' &&
25262536
reader[kState].readRequests.length) {
2537+
// Fulfilling a read request can run user code synchronously (the
2538+
// pipeTo read request invokes the sink's write algorithm), so the
2539+
// full ShouldCallPull predicate has to be re-evaluated afterwards.
25272540
readableStreamFulfillReadRequest(stream, chunk, false);
25282541
} else if (controllerState.sizeAlgorithm === defaultSizeAlgorithm) {
25292542
// The internal default size algorithm is never observable by user
25302543
// code, always returns 1, and cannot throw: enqueue with the
2531-
// constant instead of calling it.
2532-
enqueueValueWithSize(controller, chunk, 1);
2544+
// constant size instead of calling it or validating the result.
2545+
// No user code runs between the guards at the top of this function
2546+
// and this point, so ShouldCallPull reduces to the started flag and
2547+
// the desired size (this branch implies no parked read requests,
2548+
// ruling out the reader arm of the predicate).
2549+
materializeQueue(controllerState).pushPair(chunk, 1);
2550+
controllerState.queueTotalSize++;
2551+
if (controllerState.started &&
2552+
controllerState.highWaterMark - controllerState.queueTotalSize > 0) {
2553+
readableStreamDefaultControllerPull(controller);
2554+
}
2555+
return;
25332556
} else {
2557+
// The user-supplied size algorithm may run arbitrary code, so the
2558+
// full ShouldCallPull predicate has to be re-evaluated afterwards.
25342559
try {
25352560
const chunkSize =
25362561
FunctionPrototypeCall(
@@ -2599,6 +2624,13 @@ function readableStreamDefaultControllerShouldCallPull(controller) {
25992624
function readableStreamDefaultControllerCallPullIfNeeded(controller) {
26002625
if (!readableStreamDefaultControllerShouldCallPull(controller))
26012626
return;
2627+
readableStreamDefaultControllerPull(controller);
2628+
}
2629+
2630+
// The ShouldCallPull half of CallPullIfNeeded, split out so that callers
2631+
// that have already established the predicate from state in scope (the
2632+
// enqueue path above) can skip re-running it.
2633+
function readableStreamDefaultControllerPull(controller) {
26022634
if (controller[kState].pulling) {
26032635
controller[kState].pullAgain = true;
26042636
return;
@@ -2659,14 +2691,24 @@ function readableStreamDefaultControllerPullSteps(controller, readRequest) {
26592691
if (controller[kState].closeRequested && !queue.length) {
26602692
readableStreamDefaultControllerClearAlgorithms(controller);
26612693
readableStreamClose(stream);
2662-
} else {
2663-
readableStreamDefaultControllerCallPullIfNeeded(controller);
2694+
} else if (!controller[kState].closeRequested &&
2695+
controller[kState].started &&
2696+
controller[kState].highWaterMark -
2697+
controller[kState].queueTotalSize > 0) {
2698+
// Reduced ShouldCallPull: the state is known to be readable and the
2699+
// queue was non-empty, so no read requests can be parked.
2700+
readableStreamDefaultControllerPull(controller);
26642701
}
26652702
readRequest[kChunk](chunk);
26662703
return;
26672704
}
26682705
readableStreamAddReadRequest(stream, readRequest);
2669-
readableStreamDefaultControllerCallPullIfNeeded(controller);
2706+
// Reduced ShouldCallPull: the state is known to be readable, an empty
2707+
// queue in that state implies close has not been requested (close with
2708+
// an empty queue closes the stream immediately), and the read request
2709+
// parked above already satisfies the reader-with-pending-reads arm.
2710+
if (controller[kState].started)
2711+
readableStreamDefaultControllerPull(controller);
26702712
}
26712713

26722714
function setupReadableStreamDefaultController(

0 commit comments

Comments
 (0)