mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
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>
179 lines
5.2 KiB
JavaScript
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());
|