Skip to content

Commit 31fb1fe

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 fa8ad29 commit 31fb1fe

1 file changed

Lines changed: 53 additions & 7 deletions

File tree

lib/internal/webstreams/readablestream.js

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -597,8 +597,12 @@ class ReadableStream {
597597
!controller[kState].queue.length) {
598598
readableStreamDefaultControllerClearAlgorithms(controller);
599599
readableStreamClose(stream);
600-
} else {
601-
readableStreamDefaultControllerCallPullIfNeeded(controller);
600+
} else if (!controller[kState].closeRequested &&
601+
controller[kState].started &&
602+
controller[kState].highWaterMark -
603+
controller[kState].queueTotalSize > 0) {
604+
// Reduced ShouldCallPull, as in the read() fast path.
605+
readableStreamDefaultControllerPull(controller);
602606
}
603607

604608
return PromiseResolve({ done: false, value: chunk });
@@ -947,8 +951,14 @@ class ReadableStreamDefaultReader {
947951
if (controller[kState].closeRequested && !controller[kState].queue.length) {
948952
readableStreamDefaultControllerClearAlgorithms(controller);
949953
readableStreamClose(stream);
950-
} else {
951-
readableStreamDefaultControllerCallPullIfNeeded(controller);
954+
} else if (!controller[kState].closeRequested &&
955+
controller[kState].started &&
956+
controller[kState].highWaterMark -
957+
controller[kState].queueTotalSize > 0) {
958+
// ShouldCallPull reduced to the conditions not already
959+
// established on this path: the state is readable and the
960+
// queue was non-empty, so no read requests can be parked.
961+
readableStreamDefaultControllerPull(controller);
952962
}
953963

954964
return PromiseResolve({ done: false, value: chunk });
@@ -2522,8 +2532,27 @@ function readableStreamDefaultControllerEnqueue(controller, chunk) {
25222532
reader[kState] !== undefined &&
25232533
reader[kType] === 'ReadableStreamDefaultReader' &&
25242534
reader[kState].readRequests.length) {
2535+
// Fulfilling a read request can run user code synchronously (the
2536+
// pipeTo read request invokes the sink's write algorithm), so the
2537+
// full ShouldCallPull predicate has to be re-evaluated afterwards.
25252538
readableStreamFulfillReadRequest(stream, chunk, false);
2539+
} else if (controllerState.sizeAlgorithm === defaultSizeAlgorithm) {
2540+
// The default size algorithm always returns 1, so the call and the
2541+
// size validation in enqueueValueWithSize can be skipped entirely.
2542+
// No user code runs between the guards at the top of this function
2543+
// and this point, so ShouldCallPull reduces to the started flag and
2544+
// the desired size (this branch implies no parked read requests,
2545+
// ruling out the reader arm of the predicate).
2546+
ArrayPrototypePush(controllerState.queue, { value: chunk, size: 1 });
2547+
controllerState.queueTotalSize++;
2548+
if (controllerState.started &&
2549+
controllerState.highWaterMark - controllerState.queueTotalSize > 0) {
2550+
readableStreamDefaultControllerPull(controller);
2551+
}
2552+
return;
25262553
} else {
2554+
// The user-supplied size algorithm may run arbitrary code, so the
2555+
// full ShouldCallPull predicate has to be re-evaluated afterwards.
25272556
try {
25282557
const chunkSize =
25292558
FunctionPrototypeCall(
@@ -2592,6 +2621,13 @@ function readableStreamDefaultControllerShouldCallPull(controller) {
25922621
function readableStreamDefaultControllerCallPullIfNeeded(controller) {
25932622
if (!readableStreamDefaultControllerShouldCallPull(controller))
25942623
return;
2624+
readableStreamDefaultControllerPull(controller);
2625+
}
2626+
2627+
// The ShouldCallPull half of CallPullIfNeeded, split out so that callers
2628+
// that have already established the predicate from state in scope (the
2629+
// enqueue path above) can skip re-running it.
2630+
function readableStreamDefaultControllerPull(controller) {
25952631
if (controller[kState].pulling) {
25962632
controller[kState].pullAgain = true;
25972633
return;
@@ -2652,14 +2688,24 @@ function readableStreamDefaultControllerPullSteps(controller, readRequest) {
26522688
if (controller[kState].closeRequested && !queue.length) {
26532689
readableStreamDefaultControllerClearAlgorithms(controller);
26542690
readableStreamClose(stream);
2655-
} else {
2656-
readableStreamDefaultControllerCallPullIfNeeded(controller);
2691+
} else if (!controller[kState].closeRequested &&
2692+
controller[kState].started &&
2693+
controller[kState].highWaterMark -
2694+
controller[kState].queueTotalSize > 0) {
2695+
// Reduced ShouldCallPull: the state is known to be readable and the
2696+
// queue was non-empty, so no read requests can be parked.
2697+
readableStreamDefaultControllerPull(controller);
26572698
}
26582699
readRequest[kChunk](chunk);
26592700
return;
26602701
}
26612702
readableStreamAddReadRequest(stream, readRequest);
2662-
readableStreamDefaultControllerCallPullIfNeeded(controller);
2703+
// Reduced ShouldCallPull: the state is known to be readable, an empty
2704+
// queue in that state implies close has not been requested (close with
2705+
// an empty queue closes the stream immediately), and the read request
2706+
// parked above already satisfies the reader-with-pending-reads arm.
2707+
if (controller[kState].started)
2708+
readableStreamDefaultControllerPull(controller);
26632709
}
26642710

26652711
function setupReadableStreamDefaultController(

0 commit comments

Comments
 (0)