stream: drain WHATWG stream queues from a head index#64354
Conversation
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>
The default readable and writable controller queues were drained with ArrayPrototypeShift, which is O(queue length); reading a buffered queue one chunk at a time is therefore O(n^2). Dequeue through a moving head index instead so each read is O(1), dropping the consumed prefix in amortized O(1) once it reaches half of the backing array (past a small floor, so a short oscillating queue never pays a per-dequeue array mutation). readable-read-buffered: +14.8% to +35.4% (**/***) readable-async-iterator type=normal: +17.8% (***) pipe-to: +12.9% to +18.1% (all 16 configs ***) Signed-off-by: Matteo Collina <hello@matteocollina.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #64354 +/- ##
==========================================
- Coverage 90.24% 90.23% -0.01%
==========================================
Files 741 741
Lines 240976 241059 +83
Branches 45408 45419 +11
==========================================
+ Hits 217477 217530 +53
- Misses 15070 15099 +29
- Partials 8429 8430 +1
🚀 New features to boost your workflow:
|
| } else if (!controller[kState].closeRequested && | ||
| controller[kState].started && | ||
| controller[kState].highWaterMark - | ||
| controller[kState].queueTotalSize > 0) { |
There was a problem hiding this comment.
This is repeated more than once and is complicated enough to move into a utility function.
There was a problem hiding this comment.
Done in 50a13e9 — extracted readableStreamDefaultControllerPullIfNeededAfterDequeue(), now used at the read(), async-iterator next() and PullSteps dequeue arms.
| if (stream[kState].state === 'readable') { | ||
| if (isReadableStreamDefaultController(controller)) { | ||
| if (controller[kState].queue.length > 0) { | ||
| if (controller[kState].queueHead < controller[kState].queue.length) { |
There was a problem hiding this comment.
Likewise this... worth moving into a utility function
There was a problem hiding this comment.
Done in 50a13e9 — this now uses the queueLength() helper (as suggested above).
Address review feedback: move the `queue.length - queueHead` live-length computation into a queueLength() helper and the repeated post-dequeue ShouldCallPull predicate into readableStreamDefaultControllerPullIfNeededAfterDequeue(). Both are small enough to inline; benchmarks are unchanged (readable-read-buffered +11.9% to +30.5% vs the pre-head-index baseline, matching the inline form). Signed-off-by: Matteo Collina <hello@matteocollina.com>
|
Superseded by 1912893 ("stream: use ring buffer for WHATWG stream queues"), which replaces the array queue with a power-of-two ring buffer. That covers the O(n) shift this PR targeted and also drops the per-chunk { value, size } wrapper via two-slot (value, size) storage, so there is nothing left for the head-index approach to add. Closing. |
Drain the default readable and writable controller queues through a moving head index instead of
ArrayPrototypeShift(which is O(queue length), making a buffered drain O(n^2)). Each dequeue becomes O(1); the consumed prefix is dropped in amortized O(1).The first commit is #64320; only the last commit is new here.