Skip to content

Commit ba231e2

Browse files
committed
stream: trim webstream setup and teardown work
Short-lived streams (create, a few chunks, close) pay a fixed cost per stream that dominates once the per-chunk path is lean: * The queue ring buffer grew after a push filled it, so the initial 8-slot ring held only three (value, size) pairs and a four-chunk stream reallocated every time. Growing before the push lets the ring hold four pairs. * pipeTo observed the source's closed promise with two reactions and, on teardown, let the reader and writer release paths probe and reject promise records that only the pipe could have observed. One reaction pair now watches the source, and finalize drops the records before release. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 2c76a1e commit ba231e2

3 files changed

Lines changed: 75 additions & 65 deletions

File tree

lib/internal/webstreams/readablestream.js

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,6 +1595,13 @@ function readableStreamPipeTo(
15951595
// tells us that the promise must be rejected even
15961596
// when error is undefine.
15971597
function finalize(rejected, error) {
1598+
// The pipe is the only observer of the reader's and writer's promise
1599+
// records (including the ready hook installed by parkOnReady), and
1600+
// it is done with them: dropping them lets release skip the
1601+
// pending-promise probes and the rejections nothing would handle.
1602+
writer[kState].ready = undefined;
1603+
writer[kState].close = undefined;
1604+
reader[kState].close = undefined;
15981605
writableStreamDefaultWriterRelease(writer);
15991606
readableStreamReaderGenericRelease(reader);
16001607
if (signal !== undefined)
@@ -1692,12 +1699,6 @@ function readableStreamPipeTo(
16921699
PromisePrototypeThen(promise, undefined, action);
16931700
}
16941701

1695-
function watchClosed(stream, promise, action) {
1696-
if (stream[kState].state === 'closed')
1697-
action();
1698-
else
1699-
PromisePrototypeThen(promise, action, () => {});
1700-
}
17011702

17021703
// The pump loop is callback-driven to avoid per-iteration promise
17031704
// allocations. At most one read is in flight at a time, so one read
@@ -1828,15 +1829,34 @@ function readableStreamPipeTo(
18281829

18291830
pump();
18301831

1831-
watchErrored(source, readerClosedPromise(reader).promise, (error) => {
1832+
function onSourceErrored(error) {
18321833
if (!preventAbort) {
18331834
return shutdownWithAnAction(
18341835
() => writableStreamAbort(dest, error),
18351836
true,
18361837
error);
18371838
}
18381839
shutdown(true, error);
1839-
});
1840+
}
1841+
1842+
function onSourceClosed() {
1843+
if (!preventClose) {
1844+
return shutdownWithAnAction(
1845+
() => writableStreamDefaultWriterCloseWithErrorPropagation(writer));
1846+
}
1847+
shutdown();
1848+
}
1849+
1850+
// The spec installs the source-errored watcher before the dest-errored
1851+
// one and the source-closed watcher last; a source that is already
1852+
// errored is handled before the dest watcher is installed, and an
1853+
// already-closed source after it, as before.
1854+
if (source[kState].state === 'errored') {
1855+
onSourceErrored(source[kState].storedError);
1856+
} else if (source[kState].state !== 'closed') {
1857+
PromisePrototypeThen(
1858+
readerClosedPromise(reader).promise, onSourceClosed, onSourceErrored);
1859+
}
18401860

18411861
watchErrored(dest, writerClosedPromise(writer).promise, (error) => {
18421862
if (!preventCancel) {
@@ -1848,13 +1868,8 @@ function readableStreamPipeTo(
18481868
shutdown(true, error);
18491869
});
18501870

1851-
watchClosed(source, readerClosedPromise(reader).promise, () => {
1852-
if (!preventClose) {
1853-
return shutdownWithAnAction(
1854-
() => writableStreamDefaultWriterCloseWithErrorPropagation(writer));
1855-
}
1856-
shutdown();
1857-
});
1871+
if (source[kState].state === 'closed')
1872+
onSourceClosed();
18581873

18591874
if (writableStreamCloseQueuedOrInFlight(dest) ||
18601875
dest[kState].state === 'closed') {
@@ -2864,29 +2879,27 @@ function setupReadableStreamDefaultController(
28642879

28652880
const startResult = startAlgorithm();
28662881

2882+
const started = () => {
2883+
controller[kState].started = true;
2884+
assert(!controller[kState].pulling);
2885+
assert(!controller[kState].pullAgain);
2886+
readableStreamDefaultControllerCallPullIfNeeded(controller);
2887+
};
2888+
28672889
if (startResult === null ||
28682890
(typeof startResult !== 'object' && typeof startResult !== 'function')) {
28692891
// Non-thenable start result: fulfillment is guaranteed and no .then
2870-
// lookup on the result is observable, so run the post-start step
2871-
// directly at the exact microtask position the promise reaction
2872-
// would have had, skipping two promise allocations.
2873-
queueMicrotask(() => {
2874-
controller[kState].started = true;
2875-
assert(!controller[kState].pulling);
2876-
assert(!controller[kState].pullAgain);
2877-
readableStreamDefaultControllerCallPullIfNeeded(controller);
2878-
});
2892+
// lookup on the result is observable, so the post-start step runs at
2893+
// the exact microtask position the promise reaction would have had.
2894+
queueMicrotask(started);
28792895
return;
28802896
}
28812897

2898+
// The wrapper promise matches the reference implementation's
2899+
// promiseResolvedWith(), whose extra microtask hops WPT relies on.
28822900
PromisePrototypeThen(
28832901
new Promise((r) => r(startResult)),
2884-
() => {
2885-
controller[kState].started = true;
2886-
assert(!controller[kState].pulling);
2887-
assert(!controller[kState].pullAgain);
2888-
readableStreamDefaultControllerCallPullIfNeeded(controller);
2889-
},
2902+
started,
28902903
(error) => readableStreamDefaultControllerError(controller, error));
28912904
}
28922905

@@ -3748,26 +3761,23 @@ function setupReadableByteStreamController(
37483761

37493762
const startResult = startAlgorithm();
37503763

3764+
const started = () => {
3765+
controller[kState].started = true;
3766+
assert(!controller[kState].pulling);
3767+
assert(!controller[kState].pullAgain);
3768+
readableByteStreamControllerCallPullIfNeeded(controller);
3769+
};
3770+
3771+
// See setupReadableStreamDefaultController.
37513772
if (startResult === null ||
37523773
(typeof startResult !== 'object' && typeof startResult !== 'function')) {
3753-
// See setupReadableStreamDefaultController.
3754-
queueMicrotask(() => {
3755-
controller[kState].started = true;
3756-
assert(!controller[kState].pulling);
3757-
assert(!controller[kState].pullAgain);
3758-
readableByteStreamControllerCallPullIfNeeded(controller);
3759-
});
3774+
queueMicrotask(started);
37603775
return;
37613776
}
37623777

37633778
PromisePrototypeThen(
37643779
new Promise((r) => r(startResult)),
3765-
() => {
3766-
controller[kState].started = true;
3767-
assert(!controller[kState].pulling);
3768-
assert(!controller[kState].pullAgain);
3769-
readableByteStreamControllerCallPullIfNeeded(controller);
3770-
},
3780+
started,
37713781
(error) => readableByteStreamControllerError(controller, error));
37723782
}
37733783

lib/internal/webstreams/util.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,12 @@ class Queue {
179179
// Single-slot entries (readable byte controller chunk records).
180180

181181
push(entry) {
182+
if (this.length === this.list.length)
183+
this.grow();
182184
const tail = this.tail;
183185
this.list[tail] = entry;
184186
this.tail = (tail + 1) & this.capacityMask;
185187
this.length++;
186-
if (this.tail === this.head)
187-
this.grow();
188188
}
189189

190190
shift() {
@@ -207,14 +207,14 @@ class Queue {
207207
// never need to wrap.
208208

209209
pushPair(value, size) {
210+
if (this.length * 2 === this.list.length)
211+
this.grow();
210212
const tail = this.tail;
211213
const list = this.list;
212214
list[tail] = value;
213215
list[tail + 1] = size;
214216
this.tail = (tail + 2) & this.capacityMask;
215217
this.length++;
216-
if (this.tail === this.head)
217-
this.grow();
218218
}
219219

220220
// Returns the dequeued value; the size of the same entry is left in
@@ -237,9 +237,11 @@ class Queue {
237237
return this.list[this.head];
238238
}
239239

240-
// The ring is completely full (the post-push tail caught up with the
241-
// head): double the capacity, re-linearizing from the head so index
242-
// arithmetic stays trivial.
240+
// The ring is completely full (the tail has caught up with the head, so
241+
// the next push would overwrite the oldest entry): double the capacity,
242+
// re-linearizing from the head so index arithmetic stays trivial.
243+
// Growing before the push rather than after it lets the initial 8-slot
244+
// ring hold four (value, size) pairs without reallocating.
243245
grow() {
244246
const list = this.list;
245247
const capacity = list.length;

lib/internal/webstreams/writablestream.js

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,29 +1379,27 @@ function setupWritableStreamDefaultController(
13791379

13801380
const startResult = startAlgorithm();
13811381

1382+
const started = () => {
1383+
assert(stream[kState].state === 'writable' ||
1384+
stream[kState].state === 'erroring');
1385+
controller[kState].started = true;
1386+
writableStreamDefaultControllerAdvanceQueueIfNeeded(controller);
1387+
};
1388+
13821389
if (startResult === null ||
13831390
(typeof startResult !== 'object' && typeof startResult !== 'function')) {
13841391
// Non-thenable start result: fulfillment is guaranteed and no .then
1385-
// lookup on the result is observable, so run the post-start step
1386-
// directly at the exact microtask position the promise reaction
1387-
// would have had, skipping two promise allocations.
1388-
queueMicrotask(() => {
1389-
assert(stream[kState].state === 'writable' ||
1390-
stream[kState].state === 'erroring');
1391-
controller[kState].started = true;
1392-
writableStreamDefaultControllerAdvanceQueueIfNeeded(controller);
1393-
});
1392+
// lookup on the result is observable, so the post-start step runs at
1393+
// the exact microtask position the promise reaction would have had.
1394+
queueMicrotask(started);
13941395
return;
13951396
}
13961397

1398+
// The wrapper promise matches the reference implementation's
1399+
// promiseResolvedWith(), whose extra microtask hops WPT relies on.
13971400
PromisePrototypeThen(
13981401
new Promise((r) => r(startResult)),
1399-
() => {
1400-
assert(stream[kState].state === 'writable' ||
1401-
stream[kState].state === 'erroring');
1402-
controller[kState].started = true;
1403-
writableStreamDefaultControllerAdvanceQueueIfNeeded(controller);
1404-
},
1402+
started,
14051403
(error) => {
14061404
assert(stream[kState].state === 'writable' ||
14071405
stream[kState].state === 'erroring');

0 commit comments

Comments
 (0)