Skip to content

Commit 2e06ee2

Browse files
committed
stream: release only stream/iter reads that can be cancelled while pending
Since b809ad3 every kNextUncancellable read of a from() async source registered a hook that releases the read when the normalization is cancelled while it is pending. pipeTo() without a signal never cancels then, and paid for the hook on every read: 2-5% on async pipes to a write-only sink, measured in isolation. Releasing is now asked for with kNextUncancellable(true), by the callers that can cancel while a read is pending (pull() pipelines, pipeTo() with a signal, the consumers with a signal). pipeTo() without a signal reads as it did before b809ad3, with the same handlers. pipeTo() of an async source to a write-only sink, no signal, 16-byte chunks, isolated: 0.95-0.97x before this change, 0.98-1.01x after, against the parent of b809ad3. Assisted-by: OpenCode
1 parent 46cc4c9 commit 2e06ee2

3 files changed

Lines changed: 47 additions & 28 deletions

File tree

‎lib/internal/streams/iter/consumers.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ async function collectAsync(source, signal, limit) {
178178
const uncancellable = iterator[kNextUncancellable] !== undefined;
179179
for (;;) {
180180
const result = await (uncancellable ?
181-
iterator[kNextUncancellable]() : iterator.next());
181+
iterator[kNextUncancellable](true) : iterator.next());
182182
if (result.done || state.aborted) return;
183183
try {
184184
recordBatch(result.value);

‎lib/internal/streams/iter/from.js‎

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,14 @@ const kNextSyncBatch = Symbol('kNextSyncBatch');
153153

154154
// The method of an iterator returned by from() for an async iterable that
155155
// reads like next(), for internal callers that stop reading when they
156-
// cancel the normalization (pipeTo(), pull() pipelines, consumers): a
157-
// return() or throw() while a read is pending closes the source at once,
158-
// as for next(), but does not settle the pending read early. That read
159-
// settles when the source's next() does, rejecting with the cancellation
160-
// reason, and the caller ignores it. It saves the read a promise of its
161-
// own: see createAsyncSourceNormalizer().
156+
// cancel the normalization (pipeTo(), pull() pipelines, consumers). It
157+
// saves the read a promise of its own: see createAsyncSourceNormalizer().
158+
// With `releasable` true, a return() or throw() while a read is pending
159+
// closes the source at once, as for next(), but does not settle the
160+
// pending read early: the read settles when the source's next() does,
161+
// rejecting with the cancellation reason, and the caller ignores it.
162+
// Without it, the caller must not cancel while a read is pending (pipeTo()
163+
// without a signal). The first call decides for all reads.
162164
const kNextUncancellable = Symbol('kNextUncancellable');
163165

164166
// Methods of the iterator returned by yieldNormalizationAbortable(), used by
@@ -190,8 +192,8 @@ function createNormalizationIterator(createIterator) {
190192
cancelNormalization(context, error, true);
191193
return FunctionPrototypeCall(iterator.throw, iterator, error);
192194
},
193-
[kNextUncancellable]() {
194-
return iterator[kNextUncancellable]();
195+
[kNextUncancellable](releasable) {
196+
return iterator[kNextUncancellable](releasable);
195197
},
196198
[SymbolAsyncIterator]() {
197199
return this;
@@ -843,9 +845,13 @@ function createAsyncSourceNormalizer(source, context) {
843845
let valueBatches = null;
844846
// Whether the source is read with kNextUncancellable: set by the
845847
// kNextUncancellable method, for callers that ignore a read pending when
846-
// they cancel the normalization. readReleased: whether a cancellation has
847-
// released such a read (see onUncancellableCancelled()).
848+
// they cancel the normalization. releasableReads: whether the caller
849+
// can cancel while a read is pending, so that a cancellation releases it
850+
// (pipeTo() without a signal cannot, and its reads skip that).
851+
// readReleased: whether a cancellation has released such a read (see
852+
// onUncancellableCancelled()).
848853
let uncancellable = false;
854+
let releasableReads = false;
849855
let readReleased = false;
850856
// Whether the source is read with kRead, and the settling functions of
851857
// the read's promise while it is pending.
@@ -923,8 +929,6 @@ function createAsyncSourceNormalizer(source, context) {
923929
// A kReadUncancellable result: handled as the result of the source's
924930
// next() is, in the same reaction.
925931
function onUncancellableResult(result) {
926-
if (readReleased) throw context.reason;
927-
context.resolve = null;
928932
let iterResult;
929933
try {
930934
iterResult = iterator[kToIterResult](result);
@@ -935,18 +939,29 @@ function createAsyncSourceNormalizer(source, context) {
935939
}
936940

937941
function onUncancellableError(error) {
938-
if (readReleased) throw context.reason;
939-
context.resolve = null;
940942
iterator[kReadFailed]();
941943
return fail(error);
942944
}
943945

944-
// context.resolve while a kNextUncancellable read is pending: the caller
945-
// ignores that read, so it is released rather than waited for. The
946-
// normalization ends, the source is closed at once, as for a cancelled
947-
// kRead (errors ignored), and the operations queued behind the read,
948-
// such as the return() that cancelled it, run now. The read settles when
949-
// the source's next() does, rejecting with the cancellation reason.
946+
// The same, for a releasable read.
947+
function onReleasableResult(result) {
948+
if (readReleased) throw context.reason;
949+
context.resolve = null;
950+
return onUncancellableResult(result);
951+
}
952+
953+
function onReleasableError(error) {
954+
if (readReleased) throw context.reason;
955+
context.resolve = null;
956+
return onUncancellableError(error);
957+
}
958+
959+
// context.resolve while a releasable kNextUncancellable read is pending:
960+
// the caller ignores that read, so it is released rather than waited for.
961+
// The normalization ends, the source is closed at once, as for a
962+
// cancelled kRead (errors ignored), and the operations queued behind the
963+
// read, such as the return() that cancelled it, run now. The read settles
964+
// when the source's next() does, rejecting with the cancellation reason.
950965
function onUncancellableCancelled() {
951966
context.resolve = null;
952967
readReleased = true;
@@ -955,7 +970,7 @@ function createAsyncSourceNormalizer(source, context) {
955970
settled();
956971
}
957972

958-
function readUncancellable() {
973+
function readReleasable() {
959974
const cancelledBefore = context.cancelled;
960975
const read = iterator[kReadUncancellable]();
961976
if (!cancelledBefore) {
@@ -966,8 +981,7 @@ function createAsyncSourceNormalizer(source, context) {
966981
context.resolve = onUncancellableCancelled;
967982
}
968983
}
969-
return PromisePrototypeThen(read, onUncancellableResult,
970-
onUncancellableError);
984+
return PromisePrototypeThen(read, onReleasableResult, onReleasableError);
971985
}
972986

973987
// The kRead handlers: the result is handled in the reaction to the
@@ -995,7 +1009,11 @@ function createAsyncSourceNormalizer(source, context) {
9951009
}
9961010

9971011
function pullSource() {
998-
if (uncancellable) return readUncancellable();
1012+
if (uncancellable) {
1013+
if (releasableReads) return readReleasable();
1014+
return PromisePrototypeThen(iterator[kReadUncancellable](),
1015+
onUncancellableResult, onUncancellableError);
1016+
}
9991017
if (readable) {
10001018
// One promise for the read, which a cancellation rejects, instead of
10011019
// next()'s promise and a reaction to it.
@@ -1090,8 +1108,9 @@ function createAsyncSourceNormalizer(source, context) {
10901108
next() { return run(doNext); },
10911109
return(value) { return run(doReturn, value); },
10921110
throw(error) { return run(doThrow, error); },
1093-
[kNextUncancellable]() {
1111+
[kNextUncancellable](releasable = false) {
10941112
uncancellable = true;
1113+
releasableReads = releasable;
10951114
return run(doNext);
10961115
},
10971116
}, null);

‎lib/internal/streams/iter/pull.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,7 +1270,7 @@ class PipelineSource {
12701270
this.#uncancellable = false;
12711271
}
12721272
}
1273-
if (this.#uncancellable) return this.#iterator[kNextUncancellable]();
1273+
if (this.#uncancellable) return this.#iterator[kNextUncancellable](true);
12741274
return this.#iterator.next();
12751275
}
12761276

@@ -2099,7 +2099,7 @@ async function pipeTo(source, ...args) {
20992099
const uncancellable = iterator[kNextUncancellable] !== undefined;
21002100
for (;;) {
21012101
const result = await (uncancellable ?
2102-
iterator[kNextUncancellable]() : iterator.next());
2102+
iterator[kNextUncancellable](true) : iterator.next());
21032103
if (result.done || abortState.aborted) return;
21042104
try {
21052105
const p = writeBatch(result.value);

0 commit comments

Comments
 (0)