Files
node/test/parallel/test-webstreams-queue-wraparound.js
Yagiz Nizipli 1912893c29 stream: use ring buffer for WHATWG stream queues
The [[queue]] backing every default readable/writable controller was a
plain array of { value, size } wrappers consumed with
ArrayPrototypeShift, so each buffered chunk allocated a wrapper object
and each dequeue moved (or forced the engine to re-linearize) the
remaining elements; the byte controller queue paid the same shift cost
for its chunk descriptor records.

Replace the array with a power-of-two ring buffer. Default controller
queues store each entry as (value, size) in two consecutive slots, so
the per-chunk wrapper allocation disappears; the byte controller keeps
its descriptor records (they are mutated in place at the head) in
single slots. Controllers start from (and are reset to) a shared
immutable empty queue, so constructing a stream allocates no queue
storage until a chunk is actually buffered. Enqueues measured by the
internal default size algorithm (never observable by user code, always
returns 1, cannot throw) skip the algorithm call and its try/catch
entirely.

The layout mirrors what Bun/WebKit use for the same spec structure:
[[queue]] as a ring-buffer deque (WTF::Deque in Bun's
src/jsc/bindings/webcore/streams/StreamQueue.h), the pure-JS ring
buffer in Bun's src/js/internal/fifo.ts, and the trivial-size-algorithm
bypass in
src/jsc/bindings/webcore/streams/JSReadableStreamDefaultController.cpp.

benchmark/compare.js against the unmodified baseline (30-run capture
plus an independent 15-run repeat, Welch t-test, all p < 1e-5):
webstreams/pipe-to.js +12-17% across all sixteen high-water-mark
configurations, readable-read-buffered +20% (bufferSize=1) to +49%
(bufferSize=1000), readable-async-iterator +21%. No stable significant
regression across the rest of the webstreams suite: the creation.js and
readable-read.js deltas seen in the full-suite capture disappear in
isolated 60-run rechecks.

Refs: https://github.com/oven-sh/bun/blob/main/src/js/internal/fifo.ts
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Assisted-by: Grok (Grok Build)
PR-URL: https://github.com/nodejs/node/pull/64312
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
2026-07-08 15:05:45 +00:00

179 lines
5.2 KiB
JavaScript

'use strict';
const common = require('../common');
const assert = require('assert');
// Regression test for the ring-buffer [[queue]] backing store used by the
// WHATWG stream controllers: exercises growth, wrap-around, drain-rewind
// and shrink behavior through the public API only.
async function testDeepDefaultQueue() {
// Queue 3000 chunks before reading anything: the backing ring must grow
// (and re-linearize) repeatedly, then fully drain (shrink on rewind).
const rs = new ReadableStream({
start(controller) {
for (let i = 0; i < 3000; i++)
controller.enqueue(i);
controller.close();
},
}, { highWaterMark: Infinity });
const reader = rs.getReader();
for (let i = 0; i < 3000; i++) {
const { value, done } = await reader.read();
assert.strictEqual(done, false);
assert.strictEqual(value, i);
}
const { done } = await reader.read();
assert.strictEqual(done, true);
}
async function testInterleavedWraparound() {
// Interleave enqueues and reads so head/tail wrap around the ring many
// times without ever growing it, including repeated empty transitions.
let enqueued = 0;
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
},
});
const reader = rs.getReader();
let expected = 0;
for (let round = 0; round < 500; round++) {
const burst = (round % 3) + 1;
for (let i = 0; i < burst; i++)
controller.enqueue(enqueued++);
for (let i = 0; i < burst; i++) {
const { value, done } = await reader.read();
assert.strictEqual(done, false);
assert.strictEqual(value, expected++);
}
}
assert.strictEqual(expected, enqueued);
}
async function testCustomSizesSurviveQueueing() {
// Fractional and zero sizes must round-trip through the queue exactly
// (desiredSize accounting depends on the stored per-chunk size).
const sizes = [0.25, 0, 1.75, 3, 0.5];
const chunks = ['a', 'b', 'c', 'd', 'e'];
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
},
}, {
highWaterMark: 10,
size(chunk) {
return sizes[chunks.indexOf(chunk)];
},
});
let expectedDesired = 10;
for (let i = 0; i < chunks.length; i++) {
controller.enqueue(chunks[i]);
expectedDesired -= sizes[i];
assert.strictEqual(controller.desiredSize, expectedDesired);
}
const reader = rs.getReader();
for (let i = 0; i < chunks.length; i++) {
const { value } = await reader.read();
assert.strictEqual(value, chunks[i]);
expectedDesired += sizes[i];
assert.strictEqual(controller.desiredSize, expectedDesired);
}
}
async function testByteQueueUnevenReads() {
// Enqueue 7-byte chunks but read through 4-byte BYOB views: the byte
// queue's head record is partially consumed in place across reads.
const total = 7 * 300;
let written = 0;
const rs = new ReadableStream({
type: 'bytes',
pull(controller) {
if (written >= total) {
controller.close();
return;
}
const chunk = new Uint8Array(7);
for (let i = 0; i < 7; i++)
chunk[i] = (written + i) % 251;
written += 7;
controller.enqueue(chunk);
},
});
const reader = rs.getReader({ mode: 'byob' });
let read = 0;
while (read < total) {
const { value, done } = await reader.read(new Uint8Array(4));
if (done) break;
for (let i = 0; i < value.length; i++) {
assert.strictEqual(value[i], (read + i) % 251,
`byte ${read + i} mismatch`);
}
read += value.length;
}
assert.strictEqual(read, total);
}
async function testDeepByteQueueBurst() {
// Fill the byte queue with 2500 records before the default reader
// drains it, forcing ring growth and the drain-to-empty rewind.
const rs = new ReadableStream({
type: 'bytes',
start(controller) {
for (let i = 0; i < 2500; i++)
controller.enqueue(new Uint8Array([i & 0xff]));
controller.close();
},
});
const reader = rs.getReader();
for (let i = 0; i < 2500; i++) {
const { value, done } = await reader.read();
assert.strictEqual(done, false);
assert.strictEqual(value[0], i & 0xff);
}
const { done } = await reader.read();
assert.strictEqual(done, true);
}
async function testWritableQueueBackpressureDrain() {
// Buffer thousands of writes behind a slow sink, then let it drain:
// the writable controller queue grows, wraps, and empties while every
// chunk is delivered in order.
const received = [];
let release;
const gate = new Promise((resolve) => { release = resolve; });
const ws = new WritableStream({
async write(chunk) {
if (received.length === 0) await gate;
received.push(chunk);
},
}, { highWaterMark: 1 });
const writer = ws.getWriter();
const writes = [];
for (let i = 0; i < 2000; i++)
writes.push(writer.write(i));
release();
await writer.close();
await Promise.all(writes);
assert.strictEqual(received.length, 2000);
for (let i = 0; i < 2000; i++)
assert.strictEqual(received[i], i);
}
(async () => {
await testDeepDefaultQueue();
await testInterleavedWraparound();
await testCustomSizesSurviveQueueing();
await testByteQueueUnevenReads();
await testDeepByteQueueBurst();
await testWritableQueueBackpressureDrain();
})().then(common.mustCall());