Skip to content

Commit 4e75b71

Browse files
authored
stream: trim TransformStream construction costs
Constructing a TransformStream allocated five closures for the sink and source algorithms of its two sides, and each side adopted the start promise through a wrapper promise plus a thenable job, so a stream without a start() left about three kilobytes pending in the microtask queue until the started steps ran. Per-stream creation bursts spend most of their time copying that graph through the scavenger. The sink and source algorithms are now shared functions that reach the transform stream through a field on their controller state (the controller is passed to the close, abort and cancel algorithms for that), and the post-start steps of both sides are delivered by one reaction chain on a shared promise, taking the same microtask hops as the spec's start promise adoption: three after construction for a non-thenable start result, two after the adopting promise settles for a thenable one. The readable and writable transfer state records are materialized on first transfer instead of per stream. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #66643 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Mattias Buelens <mattias@buelens.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 62ca1ba commit 4e75b71

4 files changed

Lines changed: 168 additions & 79 deletions

File tree

‎lib/internal/webstreams/readablestream.js‎

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ const {
111111
extractSizeAlgorithm,
112112
getNonWritablePropertyDescriptor,
113113
isBrandCheck,
114+
kDeferredStart,
114115
kEmptyQueue,
115116
kParkedAlgorithmResult,
116117
kPendingPromise,
@@ -521,10 +522,11 @@ class ReadableStream {
521522
[kTransfer]() {
522523
if (!isReadableStream(this))
523524
throw new ERR_INVALID_THIS('ReadableStream');
525+
const transfer = readableStreamTransferState(this);
524526
if (this.locked) {
525-
this[kState].transfer.port1?.close();
526-
this[kState].transfer.port1 = undefined;
527-
this[kState].transfer.port2 = undefined;
527+
transfer.port1?.close();
528+
transfer.port1 = undefined;
529+
transfer.port2 = undefined;
528530
throw new DOMException(
529531
'Cannot transfer a locked ReadableStream',
530532
'DataCloneError');
@@ -533,24 +535,23 @@ class ReadableStream {
533535
const {
534536
writable,
535537
promise,
536-
} = lazyTransfer().newCrossRealmWritableSink(
537-
this,
538-
this[kState].transfer.port1);
538+
} = lazyTransfer().newCrossRealmWritableSink(this, transfer.port1);
539539

540-
this[kState].transfer.writable = writable;
541-
this[kState].transfer.promise = promise;
540+
transfer.writable = writable;
541+
transfer.promise = promise;
542542

543543
return {
544-
data: { port: this[kState].transfer.port2 },
544+
data: { port: transfer.port2 },
545545
deserializeInfo:
546546
'internal/webstreams/readablestream:TransferredReadableStream',
547547
};
548548
}
549549

550550
[kTransferList]() {
551551
const { port1, port2 } = new MessageChannel();
552-
this[kState].transfer.port1 = port1;
553-
this[kState].transfer.port2 = port2;
552+
const transfer = readableStreamTransferState(this);
553+
transfer.port1 = port1;
554+
transfer.port2 = port2;
554555
return [ port2 ];
555556
}
556557

@@ -1415,10 +1416,15 @@ class ReadableStreamState {
14151416
state = 'readable';
14161417
storedError = undefined;
14171418
controller = undefined;
1418-
transfer = new ReadableStreamTransferState();
1419+
// Materialized on transfer, see readableStreamTransferState().
1420+
transfer = undefined;
14191421
}
14201422
ObjectSetPrototypeOf(ReadableStreamState.prototype, null);
14211423

1424+
function readableStreamTransferState(stream) {
1425+
return stream[kState].transfer ??= new ReadableStreamTransferState();
1426+
}
1427+
14221428
function createReadableStreamState() {
14231429
return new ReadableStreamState();
14241430
}
@@ -2961,7 +2967,7 @@ function readableStreamDefaultControllerError(controller, error) {
29612967

29622968
function readableStreamDefaultControllerCancelSteps(controller, reason) {
29632969
resetQueue(controller);
2964-
const result = controller[kState].cancelAlgorithm(reason);
2970+
const result = controller[kState].cancelAlgorithm(reason, controller);
29652971
readableStreamDefaultControllerClearAlgorithms(controller);
29662972
return result;
29672973
}
@@ -3019,10 +3025,16 @@ function setupReadableStreamDefaultController(
30193025
started: false,
30203026
sizeAlgorithm,
30213027
stream,
3028+
// The TransformStream this controller is the source of, if any; its
3029+
// shared source algorithms reach the stream through it.
3030+
transformStream: undefined,
30223031
};
30233032
stream[kState].controller = controller;
30243033

30253034
const startResult = startAlgorithm();
3035+
if (startResult === kDeferredStart)
3036+
return;
3037+
30263038
// A non-thenable start result guarantees fulfillment, and no .then
30273039
// lookup on it is observable.
30283040
const startFulfilled = startResult === null ||
@@ -3036,12 +3048,7 @@ function setupReadableStreamDefaultController(
30363048
return;
30373049
}
30383050

3039-
const started = () => {
3040-
controller[kState].started = true;
3041-
assert(!controller[kState].pulling);
3042-
assert(!controller[kState].pullAgain);
3043-
readableStreamDefaultControllerCallPullIfNeeded(controller);
3044-
};
3051+
const started = () => readableStreamDefaultControllerStarted(controller);
30453052

30463053
if (startFulfilled) {
30473054
// The post-start step runs at the exact microtask position the
@@ -3058,6 +3065,15 @@ function setupReadableStreamDefaultController(
30583065
(error) => readableStreamDefaultControllerError(controller, error));
30593066
}
30603067

3068+
// The post-start steps; transform streams deliver them directly (see
3069+
// transformStreamStart).
3070+
function readableStreamDefaultControllerStarted(controller) {
3071+
controller[kState].started = true;
3072+
assert(!controller[kState].pulling);
3073+
assert(!controller[kState].pullAgain);
3074+
readableStreamDefaultControllerCallPullIfNeeded(controller);
3075+
}
3076+
30613077
function setupReadableStreamDefaultControllerFromSource(
30623078
stream,
30633079
source,
@@ -4033,6 +4049,7 @@ module.exports = {
40334049
readableStreamDefaultControllerError,
40344050
readableStreamDefaultControllerCancelSteps,
40354051
readableStreamDefaultControllerPullSteps,
4052+
readableStreamDefaultControllerStarted,
40364053
setupReadableStreamDefaultController,
40374054
setupReadableStreamDefaultControllerFromSource,
40384055
readableByteStreamControllerClose,

‎lib/internal/webstreams/transformstream.js‎

Lines changed: 74 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const {
44
FunctionPrototypeCall,
55
ObjectDefineProperties,
66
ObjectSetPrototypeOf,
7+
Promise,
78
PromisePrototypeThen,
89
PromiseReject,
910
PromiseResolve,
@@ -48,6 +49,7 @@ const {
4849
createPromiseCallback1Param,
4950
createRawCallback2Params,
5051
customInspect,
52+
deferredStartAlgorithm,
5153
extractHighWaterMark,
5254
extractSizeAlgorithm,
5355
getNonWritablePropertyDescriptor,
@@ -68,11 +70,14 @@ const {
6870
readableStreamDefaultControllerError,
6971
readableStreamDefaultControllerGetDesiredSize,
7072
readableStreamDefaultControllerHasBackpressure,
73+
readableStreamDefaultControllerStarted,
7174
} = require('internal/webstreams/readablestream');
7275

7376
const {
7477
createWritableStream,
7578
writableStreamDefaultControllerErrorIfNeeded,
79+
writableStreamDefaultControllerStarted,
80+
writableStreamDefaultControllerStartFailed,
7681
} = require('internal/webstreams/writablestream');
7782

7883
const assert = require('internal/assert');
@@ -159,28 +164,20 @@ class TransformStream {
159164
extractHighWaterMark(writableHighWaterMark, 1);
160165
const actualWritableSize = extractSizeAlgorithm(writableSize);
161166

162-
// Without a start() the start promise is already resolved by the time
163-
// the readable and writable sides adopt it, so the shared resolved
164-
// promise stands in for the record.
165-
const startPromise = start !== undefined ? PromiseWithResolvers() : undefined;
166-
167167
initializeTransformStream(
168168
this,
169-
startPromise,
170169
actualWritableHighWaterMark,
171170
actualWritableSize,
172171
actualReadableHighWaterMark,
173172
actualReadableSize);
174173

175174
setupTransformStreamDefaultControllerFromTransformer(this, transformer);
176175

177-
if (start !== undefined) {
178-
startPromise.resolve(
179-
FunctionPrototypeCall(
180-
start,
181-
transformer,
182-
this[kState].controller));
183-
}
176+
transformStreamStart(
177+
this,
178+
start === undefined ?
179+
undefined :
180+
FunctionPrototypeCall(start, transformer, this[kState].controller));
184181
}
185182

186183
/**
@@ -380,38 +377,35 @@ function defaultTransformAlgorithm(chunk, controller) {
380377
transformStreamDefaultControllerEnqueue(controller, chunk);
381378
}
382379

383-
function resolvedStartAlgorithm() {
384-
return kResolvedPromise;
385-
}
386-
387380
function initializeTransformStream(
388381
stream,
389-
startPromise,
390382
writableHighWaterMark,
391383
writableSizeAlgorithm,
392384
readableHighWaterMark,
393385
readableSizeAlgorithm) {
394386

395-
const startAlgorithm = startPromise === undefined ?
396-
resolvedStartAlgorithm :
397-
() => startPromise.promise;
398-
387+
// The sink and source algorithms are shared functions that reach the
388+
// transform stream through their controller, and the post-start step
389+
// is delivered by transformStreamStart, so no per-stream closures or
390+
// start promises are allocated here.
399391
const writable = createWritableStream(
400-
startAlgorithm,
401-
(chunk) => transformStreamDefaultSinkWriteAlgorithm(stream, chunk),
402-
() => transformStreamDefaultSinkCloseAlgorithm(stream),
403-
(reason) => transformStreamDefaultSinkAbortAlgorithm(stream, reason),
392+
deferredStartAlgorithm,
393+
transformStreamDefaultSinkWriteAlgorithm,
394+
transformStreamDefaultSinkCloseAlgorithm,
395+
transformStreamDefaultSinkAbortAlgorithm,
404396
writableHighWaterMark,
405397
writableSizeAlgorithm,
406398
);
399+
writable[kState].controller[kState].transformStream = stream;
407400

408401
const readable = createReadableStream(
409-
startAlgorithm,
410-
() => transformStreamDefaultSourcePullAlgorithm(stream),
411-
(reason) => transformStreamDefaultSourceCancelAlgorithm(stream, reason),
402+
deferredStartAlgorithm,
403+
transformStreamDefaultSourcePullAlgorithm,
404+
transformStreamDefaultSourceCancelAlgorithm,
412405
readableHighWaterMark,
413406
readableSizeAlgorithm,
414407
);
408+
readable[kState].controller[kState].transformStream = stream;
415409

416410
const state = new TransformStreamState();
417411
state.readable = readable;
@@ -421,6 +415,43 @@ function initializeTransformStream(
421415
transformStreamSetBackpressure(stream, true);
422416
}
423417

418+
// Delivers the post-start step of both sides. The spec resolves the start
419+
// promise with the transformer's start result at the end of construction
420+
// and each side then adopts it through a wrapper promise, so with a
421+
// non-thenable start result the started steps run three microtasks after
422+
// construction, and with a thenable one two microtasks after the adopting
423+
// promise settles. The same hops are taken as reactions on one shared
424+
// promise instead of a promise record plus two wrapper promises.
425+
function transformStreamStart(stream, startResult) {
426+
const state = stream[kState];
427+
const started = () => {
428+
writableStreamDefaultControllerStarted(state.writable[kState].controller);
429+
readableStreamDefaultControllerStarted(state.readable[kState].controller);
430+
};
431+
if (startResult === null ||
432+
(typeof startResult !== 'object' && typeof startResult !== 'function')) {
433+
PromisePrototypeThen(
434+
PromisePrototypeThen(
435+
PromisePrototypeThen(kResolvedPromise, undefined, undefined),
436+
undefined,
437+
undefined),
438+
started);
439+
return;
440+
}
441+
const startPromise = new Promise((resolve) => resolve(startResult));
442+
PromisePrototypeThen(
443+
PromisePrototypeThen(startPromise, undefined, undefined),
444+
started,
445+
(error) => {
446+
writableStreamDefaultControllerStartFailed(
447+
state.writable[kState].controller,
448+
error);
449+
readableStreamDefaultControllerError(
450+
state.readable[kState].controller,
451+
error);
452+
});
453+
}
454+
424455
function transformStreamError(stream, error) {
425456
const {
426457
readable,
@@ -609,7 +640,8 @@ function transformStreamDefaultControllerTerminate(controller) {
609640
new ERR_INVALID_STATE.TypeError('TransformStream has been terminated'));
610641
}
611642

612-
function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
643+
function transformStreamDefaultSinkWriteAlgorithm(chunk, writableController) {
644+
const stream = writableController[kState].transformStream;
613645
const state = stream[kState];
614646
const {
615647
writable,
@@ -656,7 +688,10 @@ function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
656688
return transformStreamDefaultControllerPerformTransform(controller, chunk);
657689
}
658690

659-
async function transformStreamDefaultSinkAbortAlgorithm(stream, reason) {
691+
async function transformStreamDefaultSinkAbortAlgorithm(
692+
reason,
693+
writableController) {
694+
const stream = writableController[kState].transformStream;
660695
const {
661696
controller,
662697
readable,
@@ -690,7 +725,8 @@ async function transformStreamDefaultSinkAbortAlgorithm(stream, reason) {
690725
return controller[kState].finishPromise;
691726
}
692727

693-
function transformStreamDefaultSinkCloseAlgorithm(stream) {
728+
function transformStreamDefaultSinkCloseAlgorithm(writableController) {
729+
const stream = writableController[kState].transformStream;
694730
const {
695731
readable,
696732
controller,
@@ -720,7 +756,8 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
720756
return controller[kState].finishPromise;
721757
}
722758

723-
function transformStreamDefaultSourcePullAlgorithm(stream) {
759+
function transformStreamDefaultSourcePullAlgorithm(readableController) {
760+
const stream = readableController[kState].transformStream;
724761
const state = stream[kState];
725762
assert(state.backpressure);
726763
transformStreamSetBackpressure(stream, false);
@@ -732,7 +769,10 @@ function transformStreamDefaultSourcePullAlgorithm(stream) {
732769
return kParkedAlgorithmResult;
733770
}
734771

735-
function transformStreamDefaultSourceCancelAlgorithm(stream, reason) {
772+
function transformStreamDefaultSourceCancelAlgorithm(
773+
reason,
774+
readableController) {
775+
const stream = readableController[kState].transformStream;
736776
const {
737777
controller,
738778
writable,

‎lib/internal/webstreams/util.js‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,15 @@ const kResolvedPromise = PromiseResolve();
357357
// (see the transform stream source pull algorithm).
358358
const kParkedAlgorithmResult = Symbol('kParkedAlgorithmResult');
359359

360+
// Start algorithm of internal streams whose creator delivers the
361+
// post-start step itself (see transformStreamStart): the controller setup
362+
// leaves `started` unset when the start result is this sentinel.
363+
const kDeferredStart = Symbol('kDeferredStart');
364+
365+
function deferredStartAlgorithm() {
366+
return kDeferredStart;
367+
}
368+
360369
// Wires the (possibly non-thenable) result of an underlying algorithm
361370
// callback to its fulfilled/rejected continuations. A non-thenable result
362371
// means fulfillment is guaranteed and no then() lookup is observable, so
@@ -461,13 +470,15 @@ module.exports = {
461470
createRawCallback2Params,
462471
customInspect,
463472
defaultSizeAlgorithm,
473+
deferredStartAlgorithm,
464474
dequeueValue,
465475
enqueueValueWithSize,
466476
extractHighWaterMark,
467477
extractSizeAlgorithm,
468478
getNonWritablePropertyDescriptor,
469479
isBrandCheck,
470480
isPromisePending,
481+
kDeferredStart,
471482
kEmptyQueue,
472483
kParkedAlgorithmResult,
473484
kPendingPromise,

0 commit comments

Comments
 (0)