Skip to content

Commit 961f2ce

Browse files
committed
stream: avoid promise allocation for parked transform writes
A transform sink write arriving under backpressure parked the chunk together with a PromiseWithResolvers record whose promise was returned to the writable controller and later resolved with the perform-transform promise. The writable's write reactions already exist before the write algorithm runs, so the parked write can instead return the parked-result sentinel and have the continuation wire the perform-transform promise directly to those reactions, dropping the per-chunk promise record and the thenable adoption hop. Failures while erroring are delivered in a microtask, preserving the old rejection position. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 1e2212e commit 961f2ce

2 files changed

Lines changed: 82 additions & 19 deletions

File tree

lib/internal/webstreams/transformstream.js

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ class TransformStreamState {
264264
// Continuation slots replacing the spec's
265265
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
266266
pullPending = false;
267-
pendingWrite = undefined;
267+
pendingWriteParked = false;
268268
pendingWriteChunk = undefined;
269269
writeContinuation = undefined;
270270
}
@@ -465,7 +465,7 @@ function transformStreamSetBackpressure(stream, backpressure) {
465465
kResolvedPromise,
466466
state.readable[kState].controller[kState].pullFulfilled);
467467
}
468-
} else if (state.pendingWrite !== undefined) {
468+
} else if (state.pendingWriteParked) {
469469
PromisePrototypeThen(kResolvedPromise, state.writeContinuation);
470470
}
471471
}
@@ -609,35 +609,41 @@ function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
609609
} = state;
610610
assert(writable[kState].state === 'writable');
611611
if (state.backpressure) {
612-
// Park the chunk and one promise record; the backpressure -> false
613-
// flip delivers the cached continuation (see
614-
// transformStreamSetBackpressure) at the same microtask position as
615-
// the old [[backpressureChangePromise]] reaction. The continuation
616-
// resolves the sink promise with the perform-transform promise, so
617-
// adoption reproduces the old derived-chain settle depth exactly.
618-
// The writable dispatches a single write at a time, so one pending
619-
// slot suffices.
620-
assert(state.pendingWrite === undefined);
621-
const pendingWrite = PromiseWithResolvers();
622-
state.pendingWrite = pendingWrite;
612+
// Park the chunk; the backpressure -> false flip delivers the cached
613+
// continuation (see transformStreamSetBackpressure) at the same
614+
// microtask position as the old [[backpressureChangePromise]]
615+
// reaction. The continuation completes the parked write by wiring
616+
// the perform-transform promise directly to the writable
617+
// controller's write reactions (they exist: the controller creates
618+
// them before invoking the write algorithm), replacing the promise
619+
// record the old code allocated and resolved per parked chunk. The
620+
// writable dispatches a single write at a time, so one pending slot
621+
// suffices.
622+
assert(!state.pendingWriteParked);
623+
state.pendingWriteParked = true;
623624
state.pendingWriteChunk = chunk;
624625
state.writeContinuation ??= () => {
625-
const pending = state.pendingWrite;
626626
const pendingChunk = state.pendingWriteChunk;
627-
state.pendingWrite = undefined;
627+
state.pendingWriteParked = false;
628628
state.pendingWriteChunk = undefined;
629629
const writableState = state.writable[kState];
630+
const writableControllerState = writableState.controller[kState];
630631
if (writableState.state === 'erroring') {
631-
pending.reject(writableState.storedError);
632+
const error = writableState.storedError;
633+
PromisePrototypeThen(
634+
kResolvedPromise,
635+
() => writableControllerState.writeRejected(error));
632636
return;
633637
}
634638
assert(writableState.state === 'writable');
635-
pending.resolve(
639+
PromisePrototypeThen(
636640
transformStreamDefaultControllerPerformTransform(
637641
controller,
638-
pendingChunk));
642+
pendingChunk),
643+
writableControllerState.writeFulfilled,
644+
writableControllerState.writeRejected);
639645
};
640-
return pendingWrite.promise;
646+
return kParkedAlgorithmResult;
641647
}
642648
return transformStreamDefaultControllerPerformTransform(controller, chunk);
643649
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
require('../common');
4+
const { test } = require('node:test');
5+
const assert = require('node:assert');
6+
const { TransformStream } = require('stream/web');
7+
const { setImmediate } = require('timers/promises');
8+
9+
// A transform sink write arriving while the stream has backpressure is
10+
// parked until backpressure clears. These cases complete a parked write
11+
// while the writable side is already erroring.
12+
//
13+
// The setImmediate() lets the start algorithm settle so the write below
14+
// reaches the sink and parks (backpressure is set until the readable
15+
// side pulls).
16+
17+
test('readable.cancel() rejects a parked write with the cancel reason', async () => {
18+
const stream = new TransformStream();
19+
const writer = stream.writable.getWriter();
20+
await setImmediate();
21+
22+
const reason = new Error('cancelled');
23+
const write = writer.write('parked');
24+
await stream.readable.cancel(reason);
25+
await assert.rejects(write, (err) => err === reason);
26+
});
27+
28+
test('controller.error() rejects a parked write with the stored error', async () => {
29+
let controller;
30+
const stream = new TransformStream({
31+
start(c) { controller = c; },
32+
});
33+
const writer = stream.writable.getWriter();
34+
await setImmediate();
35+
36+
const reason = new Error('boom');
37+
const write = writer.write('parked');
38+
controller.error(reason);
39+
await assert.rejects(write, (err) => err === reason);
40+
await assert.rejects(writer.closed, (err) => err === reason);
41+
});
42+
43+
test('controller.terminate() rejects a parked write', async () => {
44+
let controller;
45+
const stream = new TransformStream({
46+
start(c) { controller = c; },
47+
});
48+
const writer = stream.writable.getWriter();
49+
await setImmediate();
50+
51+
const write = writer.write('parked');
52+
controller.terminate();
53+
await assert.rejects(write, {
54+
name: 'TypeError',
55+
message: /terminated/,
56+
});
57+
});

0 commit comments

Comments
 (0)