Commit f7e0c81

Browse files
mcollinaaduh95
authored andcommitted
stream: cut promise churn in webstreams hot paths
Three related reductions on the per-chunk paths: Wrap user sink.write and source.pull callbacks without coercing their result into a promise. When the callback returns a non-thenable (the common synchronous case), fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had, skipping the implicit async-wrapper promise per chunk. Thenable results go through PromiseResolve(), which matches the spec's "a promise resolved with" conversion (identity for native promises). Park pipeTo's pump on backpressure by installing a record that duck-types the writer's lazily-materialized [[readyPromise]] record and whose resolve function is the pump continuation itself. Backpressure clearing then resumes the pump directly instead of materializing a fresh promise record plus reaction per flip, and the pump no longer schedules a microtask per batch. writableStreamUpdateBackpressure publishes the new backpressure state before resolving the ready record so the pump observes the updated value. Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths with a reaction on the shared resolved promise, which enqueues the continuation at the same position without the per-call scheduling overhead. pipe-to improves by 8-14% across all benchmark configurations, with readable-read and tee also improving in spot runs. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65138 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 562168f commit f7e0c81

3 files changed

Lines changed: 126 additions & 25 deletions

File tree

β€Žlib/internal/webstreams/readablestream.jsβ€Ž

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
cloneAsUint8Array,
102102
copyArrayBuffer,
103103
createPromiseCallback1Param,
104+
createRawCallback1Param,
104105
customInspect,
105106
defaultSizeAlgorithm,
106107
dequeueValue,
@@ -110,6 +111,7 @@ const {
110111
getNonWritablePropertyDescriptor,
111112
isBrandCheck,
112113
kEmptyQueue,
114+
kResolvedPromise,
113115
kState,
114116
kType,
115117
lazyTransfer,
@@ -121,6 +123,7 @@ const {
121123
resetQueue,
122124
resolvedRecord,
123125
setPromiseHandled,
126+
thenAlgorithmResult,
124127
}=require('internal/webstreams/util');
125128

126129
const{
@@ -137,7 +140,6 @@ const {
137140
writableStreamDefaultWriterRelease,
138141
writableStreamDefaultWriterWriteWithRequest,
139142
writerClosedPromise,
140-
writerReadyPromise,
141143
}=require('internal/webstreams/writablestream');
142144

143145
const{ Buffer }=require('buffer');
@@ -1664,11 +1666,31 @@ function readableStreamPipeTo(
16641666
// the chunk travels through `pendingChunk`.
16651667
letpendingChunk;
16661668
letreadRequest;
1669+
letreadyHook;
16671670

16681671
// Ready promise rejection is handled by the destination-errored
16691672
// watcher.
16701673
functionignoreReadyRejection(){}
16711674

1675+
// Parks the pump on the destination's backpressure by installing a
1676+
// record that duck-types the writer's lazily-materialized
1677+
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
1678+
// when backpressure clears (after publishing the new backpressure
1679+
// state), which re-enters the pump directly instead of rotating a
1680+
// fresh promise record plus reaction per flip. The pipe holds the only
1681+
// reference to the writer, so the record is never observable as a real
1682+
// ready promise; the erroring/release paths probe `promise` via
1683+
// isPromisePending() and call `reject`, so it carries a real
1684+
// forever-pending promise and a no-op reject.
1685+
functionparkOnReady(){
1686+
readyHook??={
1687+
promise: newPromise(nonOpCallback),
1688+
resolve: pump,
1689+
reject: ignoreReadyRejection,
1690+
};
1691+
writer[kState].ready=readyHook;
1692+
}
1693+
16721694
functionforwardChunk(){
16731695
constchunk=pendingChunk;
16741696
pendingChunk=undefined;
@@ -1680,10 +1702,7 @@ function readableStreamPipeTo(
16801702
if(shuttingDown)return;
16811703

16821704
if(dest[kState].backpressure){
1683-
PromisePrototypeThen(
1684-
writerReadyPromise(writer).promise,
1685-
pump,
1686-
ignoreReadyRejection);
1705+
parkOnReady();
16871706
return;
16881707
}
16891708

@@ -1728,9 +1747,18 @@ function readableStreamPipeTo(
17281747
return;
17291748
}
17301749

1731-
// Yield to microtask queue between batches to allow events/signals
1732-
// to fire
1733-
queueMicrotask(pump);
1750+
// Park on backpressure directly: the ready hook resumes the pump
1751+
// when a completed write clears it.
1752+
if(dest[kState].backpressure){
1753+
parkOnReady();
1754+
return;
1755+
}
1756+
1757+
// Yield to the microtask queue between batches so completed-write
1758+
// reactions and events/signals fire; a shared resolved promise
1759+
// enqueues the continuation at the same position as queueMicrotask
1760+
// without the per-batch scheduling overhead.
1761+
PromisePrototypeThen(kResolvedPromise,pump);
17341762
return;
17351763
}
17361764

@@ -1742,7 +1770,7 @@ function readableStreamPipeTo(
17421770
// synchronous write during enqueue(). See WHATWG Streams spec
17431771
// "ReadableStreamPipeTo" step 15's "chunk steps".
17441772
pendingChunk=chunk;
1745-
queueMicrotask(forwardChunk);
1773+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
17461774
},
17471775
[kClose](){},
17481776
[kError](){},
@@ -1857,7 +1885,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
18571885
// The microtask is required by the spec (ReadableStreamTee's
18581886
// "chunk steps" queue one).
18591887
pendingChunk=value;
1860-
queueMicrotask(forwardChunk);
1888+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
18611889
},
18621890
[kClose](){
18631891
// The `process.nextTick()` is not part of the spec.
@@ -2010,7 +2038,7 @@ function readableByteStreamTee(stream) {
20102038
defaultReadRequest??={
20112039
[kChunk](chunk){
20122040
pendingChunk=chunk;
2013-
queueMicrotask(forwardChunk);
2041+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
20142042
},
20152043
[kClose](){
20162044
reading=false;
@@ -2699,8 +2727,18 @@ function readableStreamDefaultControllerPull(controller) {
26992727
controller[kState].pullRejected=
27002728
(error)=>readableStreamDefaultControllerError(controller,error);
27012729
}
2702-
PromisePrototypeThen(
2703-
controller[kState].pullAlgorithm(controller),
2730+
// The pull algorithm may be a raw callback (a wrapped user source.pull
2731+
// returns its result uncoerced; a synchronous throw surfaces here) or an
2732+
// internal algorithm that always returns a promise; thenAlgorithmResult
2733+
// handles both.
2734+
letresult;
2735+
try{
2736+
result=controller[kState].pullAlgorithm(controller);
2737+
}catch(error){
2738+
result=PromiseReject(error);
2739+
}
2740+
thenAlgorithmResult(
2741+
result,
27042742
controller[kState].pullFulfilled,
27052743
controller[kState].pullRejected);
27062744
}
@@ -2826,7 +2864,7 @@ function setupReadableStreamDefaultControllerFromSource(
28262864
FunctionPrototypeBind(start,source,controller) :
28272865
nonOpStart;
28282866
constpullAlgorithm=pull ?
2829-
createPromiseCallback1Param('source.pull',pull,source) :
2867+
createRawCallback1Param('source.pull',pull,source) :
28302868
nonOpPull;
28312869
constcancelAlgorithm=cancel ?
28322870
createPromiseCallback1Param('source.cancel',cancel,source) :
@@ -3519,8 +3557,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
35193557
controller[kState].pullRejected=
35203558
(error)=>readableByteStreamControllerError(controller,error);
35213559
}
3522-
PromisePrototypeThen(
3523-
controller[kState].pullAlgorithm(controller),
3560+
// See readableStreamDefaultControllerPull for the raw-callback contract.
3561+
letresult;
3562+
try{
3563+
result=controller[kState].pullAlgorithm(controller);
3564+
}catch(error){
3565+
result=PromiseReject(error);
3566+
}
3567+
thenAlgorithmResult(
3568+
result,
35243569
controller[kState].pullFulfilled,
35253570
controller[kState].pullRejected);
35263571
}
@@ -3700,7 +3745,7 @@ function setupReadableByteStreamControllerFromSource(
37003745
FunctionPrototypeBind(start,source,controller) :
37013746
nonOpStart;
37023747
constpullAlgorithm=pull ?
3703-
createPromiseCallback1Param('source.pull',pull,source) :
3748+
createRawCallback1Param('source.pull',pull,source) :
37043749
nonOpPull;
37053750
constcancelAlgorithm=cancel ?
37063751
createPromiseCallback1Param('source.cancel',cancel,source) :

β€Žlib/internal/webstreams/util.jsβ€Ž

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
338338
returnasync()=>FunctionPrototypeCall(fn,thisArg);
339339
}
340340

341+
// Raw variants that skip the async wrapper's implicit result promise.
342+
// Consumers of a raw callback invoke it inside try/catch and route the
343+
// result through thenAlgorithmResult() below.
344+
functioncreateRawCallback1Param(name,fn,thisArg){
345+
validateFunction(fn,name);
346+
return(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
347+
}
348+
349+
functioncreateRawCallback2Params(name,fn,thisArg){
350+
validateFunction(fn,name);
351+
return(arg1,arg2)=>FunctionPrototypeCall(fn,thisArg,arg1,arg2);
352+
}
353+
354+
// A single shared, forever-resolved promise used to enqueue a reaction at
355+
// the next microtask checkpoint without allocating a fresh promise.
356+
constkResolvedPromise=PromiseResolve();
357+
358+
// Wires the (possibly non-thenable) result of an underlying algorithm
359+
// callback to its fulfilled/rejected continuations. A non-thenable result
360+
// means fulfillment is guaranteed and no then() lookup is observable, so
361+
// the fulfillment step is enqueued directly at the exact microtask
362+
// position the coerced promise's reaction would have had, skipping the
363+
// per-chunk promise allocation. For thenable results PromiseResolve()
364+
// matches the spec's "a promise resolved with" conversion (identity for
365+
// native promises).
366+
functionthenAlgorithmResult(result,onFulfilled,onRejected){
367+
if(result===null||
368+
(typeofresult!=='object'&&typeofresult!=='function')){
369+
PromisePrototypeThen(kResolvedPromise,onFulfilled);
370+
}else{
371+
PromisePrototypeThen(PromiseResolve(result),onFulfilled,onRejected);
372+
}
373+
}
374+
341375
functioncreatePromiseCallback1Param(name,fn,thisArg){
342376
validateFunction(fn,name);
343377
returnasync(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
@@ -386,11 +420,14 @@ async function nonOpFlush() {}
386420

387421
functionnonOpStart(){}
388422

389-
asyncfunctionnonOpPull(){}
423+
// nonOpPull and nonOpWrite are raw callbacks (see createRawCallback*):
424+
// their non-thenable return takes the allocation-free fast path in
425+
// thenAlgorithmResult().
426+
functionnonOpPull(){}
390427

391428
asyncfunctionnonOpCancel(){}
392429

393-
asyncfunctionnonOpWrite(){}
430+
functionnonOpWrite(){}
394431

395432
lettransfer;
396433
functionlazyTransfer(){
@@ -411,6 +448,8 @@ module.exports = {
411448
createPromiseCallbackNoParams,
412449
createPromiseCallback1Param,
413450
createPromiseCallback2Params,
451+
createRawCallback1Param,
452+
createRawCallback2Params,
414453
customInspect,
415454
defaultSizeAlgorithm,
416455
dequeueValue,
@@ -421,6 +460,7 @@ module.exports = {
421460
isBrandCheck,
422461
isPromisePending,
423462
kEmptyQueue,
463+
kResolvedPromise,
424464
kState,
425465
kType,
426466
lazyTransfer,
@@ -435,4 +475,5 @@ module.exports = {
435475
resetQueue,
436476
resolvedRecord,
437477
setPromiseHandled,
478+
thenAlgorithmResult,
438479
};

β€Žlib/internal/webstreams/writablestream.jsβ€Ž

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const {
5656
Queue,
5757
createPromiseCallbackNoParams,
5858
createPromiseCallback1Param,
59-
createPromiseCallback2Params,
59+
createRawCallback2Params,
6060
customInspect,
6161
defaultSizeAlgorithm,
6262
dequeueValue,
@@ -78,6 +78,7 @@ const {
7878
resetQueue,
7979
resolvedRecord,
8080
setPromiseHandled,
81+
thenAlgorithmResult,
8182
}=require('internal/webstreams/util');
8283

8384
const{
@@ -769,7 +770,12 @@ function writableStreamUpdateBackpressure(controller, streamState) {
769770
constbackpressure=
770771
controllerState.highWaterMark-controllerState.queueTotalSize<=0;
771772
constwriter=streamState.writer;
772-
if(writer!==undefined&&streamState.backpressure!==backpressure){
773+
constchanged=streamState.backpressure!==backpressure;
774+
// The state field is published before the ready record is resolved so
775+
// that a ready resolve hook (pipeTo's pump continuation) observes the
776+
// new value.
777+
streamState.backpressure=backpressure;
778+
if(writer!==undefined&&changed){
773779
if(backpressure){
774780
// The spec replaces [[readyPromise]] with a fresh pending promise;
775781
// dropping the cache lets the next observation derive it.
@@ -778,7 +784,6 @@ function writableStreamUpdateBackpressure(controller, streamState) {
778784
writer[kState].ready?.resolve();
779785
}
780786
}
781-
streamState.backpressure=backpressure;
782787
}
783788

784789
functionwritableStreamStartErroring(stream,reason){
@@ -1197,8 +1202,18 @@ function writableStreamDefaultControllerProcessWrite(controller, chunk) {
11971202
};
11981203
}
11991204

1200-
PromisePrototypeThen(
1201-
writeAlgorithm(chunk,controller),
1205+
// The write algorithm may be a raw callback (a wrapped user sink.write
1206+
// returns its result uncoerced; a synchronous throw surfaces here) or an
1207+
// internal algorithm that always returns a promise; thenAlgorithmResult
1208+
// handles both.
1209+
letresult;
1210+
try{
1211+
result=writeAlgorithm(chunk,controller);
1212+
}catch(error){
1213+
result=PromiseReject(error);
1214+
}
1215+
thenAlgorithmResult(
1216+
result,
12021217
controller[kState].writeFulfilled,
12031218
controller[kState].writeRejected);
12041219
}
@@ -1323,7 +1338,7 @@ function setupWritableStreamDefaultControllerFromSink(
13231338
FunctionPrototypeBind(start,sink,controller) :
13241339
nonOpStart;
13251340
constwriteAlgorithm=write ?
1326-
createPromiseCallback2Params('sink.write',write,sink) :
1341+
createRawCallback2Params('sink.write',write,sink) :
13271342
nonOpWrite;
13281343
constcloseAlgorithm=close ?
13291344
createPromiseCallbackNoParams('sink.close',close,sink) :

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit f7e0c81

Browse files
mcollinaaduh95
authored andcommitted
stream: cut promise churn in webstreams hot paths
Three related reductions on the per-chunk paths: Wrap user sink.write and source.pull callbacks without coercing their result into a promise. When the callback returns a non-thenable (the common synchronous case), fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had, skipping the implicit async-wrapper promise per chunk. Thenable results go through PromiseResolve(), which matches the spec's "a promise resolved with" conversion (identity for native promises). Park pipeTo's pump on backpressure by installing a record that duck-types the writer's lazily-materialized [[readyPromise]] record and whose resolve function is the pump continuation itself. Backpressure clearing then resumes the pump directly instead of materializing a fresh promise record plus reaction per flip, and the pump no longer schedules a microtask per batch. writableStreamUpdateBackpressure publishes the new backpressure state before resolving the ready record so the pump observes the updated value. Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths with a reaction on the shared resolved promise, which enqueues the continuation at the same position without the per-call scheduling overhead. pipe-to improves by 8-14% across all benchmark configurations, with readable-read and tee also improving in spot runs. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65138 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 562168f commit f7e0c81

3 files changed

Lines changed: 126 additions & 25 deletions

File tree

β€Žlib/internal/webstreams/readablestream.jsβ€Ž

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
cloneAsUint8Array,
102102
copyArrayBuffer,
103103
createPromiseCallback1Param,
104+
createRawCallback1Param,
104105
customInspect,
105106
defaultSizeAlgorithm,
106107
dequeueValue,
@@ -110,6 +111,7 @@ const {
110111
getNonWritablePropertyDescriptor,
111112
isBrandCheck,
112113
kEmptyQueue,
114+
kResolvedPromise,
113115
kState,
114116
kType,
115117
lazyTransfer,
@@ -121,6 +123,7 @@ const {
121123
resetQueue,
122124
resolvedRecord,
123125
setPromiseHandled,
126+
thenAlgorithmResult,
124127
}=require('internal/webstreams/util');
125128

126129
const{
@@ -137,7 +140,6 @@ const {
137140
writableStreamDefaultWriterRelease,
138141
writableStreamDefaultWriterWriteWithRequest,
139142
writerClosedPromise,
140-
writerReadyPromise,
141143
}=require('internal/webstreams/writablestream');
142144

143145
const{ Buffer }=require('buffer');
@@ -1664,11 +1666,31 @@ function readableStreamPipeTo(
16641666
// the chunk travels through `pendingChunk`.
16651667
letpendingChunk;
16661668
letreadRequest;
1669+
letreadyHook;
16671670

16681671
// Ready promise rejection is handled by the destination-errored
16691672
// watcher.
16701673
functionignoreReadyRejection(){}
16711674

1675+
// Parks the pump on the destination's backpressure by installing a
1676+
// record that duck-types the writer's lazily-materialized
1677+
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
1678+
// when backpressure clears (after publishing the new backpressure
1679+
// state), which re-enters the pump directly instead of rotating a
1680+
// fresh promise record plus reaction per flip. The pipe holds the only
1681+
// reference to the writer, so the record is never observable as a real
1682+
// ready promise; the erroring/release paths probe `promise` via
1683+
// isPromisePending() and call `reject`, so it carries a real
1684+
// forever-pending promise and a no-op reject.
1685+
functionparkOnReady(){
1686+
readyHook??={
1687+
promise: newPromise(nonOpCallback),
1688+
resolve: pump,
1689+
reject: ignoreReadyRejection,
1690+
};
1691+
writer[kState].ready=readyHook;
1692+
}
1693+
16721694
functionforwardChunk(){
16731695
constchunk=pendingChunk;
16741696
pendingChunk=undefined;
@@ -1680,10 +1702,7 @@ function readableStreamPipeTo(
16801702
if(shuttingDown)return;
16811703

16821704
if(dest[kState].backpressure){
1683-
PromisePrototypeThen(
1684-
writerReadyPromise(writer).promise,
1685-
pump,
1686-
ignoreReadyRejection);
1705+
parkOnReady();
16871706
return;
16881707
}
16891708

@@ -1728,9 +1747,18 @@ function readableStreamPipeTo(
17281747
return;
17291748
}
17301749

1731-
// Yield to microtask queue between batches to allow events/signals
1732-
// to fire
1733-
queueMicrotask(pump);
1750+
// Park on backpressure directly: the ready hook resumes the pump
1751+
// when a completed write clears it.
1752+
if(dest[kState].backpressure){
1753+
parkOnReady();
1754+
return;
1755+
}
1756+
1757+
// Yield to the microtask queue between batches so completed-write
1758+
// reactions and events/signals fire; a shared resolved promise
1759+
// enqueues the continuation at the same position as queueMicrotask
1760+
// without the per-batch scheduling overhead.
1761+
PromisePrototypeThen(kResolvedPromise,pump);
17341762
return;
17351763
}
17361764

@@ -1742,7 +1770,7 @@ function readableStreamPipeTo(
17421770
// synchronous write during enqueue(). See WHATWG Streams spec
17431771
// "ReadableStreamPipeTo" step 15's "chunk steps".
17441772
pendingChunk=chunk;
1745-
queueMicrotask(forwardChunk);
1773+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
17461774
},
17471775
[kClose](){},
17481776
[kError](){},
@@ -1857,7 +1885,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
18571885
// The microtask is required by the spec (ReadableStreamTee's
18581886
// "chunk steps" queue one).
18591887
pendingChunk=value;
1860-
queueMicrotask(forwardChunk);
1888+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
18611889
},
18621890
[kClose](){
18631891
// The `process.nextTick()` is not part of the spec.
@@ -2010,7 +2038,7 @@ function readableByteStreamTee(stream) {
20102038
defaultReadRequest??={
20112039
[kChunk](chunk){
20122040
pendingChunk=chunk;
2013-
queueMicrotask(forwardChunk);
2041+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
20142042
},
20152043
[kClose](){
20162044
reading=false;
@@ -2699,8 +2727,18 @@ function readableStreamDefaultControllerPull(controller) {
26992727
controller[kState].pullRejected=
27002728
(error)=>readableStreamDefaultControllerError(controller,error);
27012729
}
2702-
PromisePrototypeThen(
2703-
controller[kState].pullAlgorithm(controller),
2730+
// The pull algorithm may be a raw callback (a wrapped user source.pull
2731+
// returns its result uncoerced; a synchronous throw surfaces here) or an
2732+
// internal algorithm that always returns a promise; thenAlgorithmResult
2733+
// handles both.
2734+
letresult;
2735+
try{
2736+
result=controller[kState].pullAlgorithm(controller);
2737+
}catch(error){
2738+
result=PromiseReject(error);
2739+
}
2740+
thenAlgorithmResult(
2741+
result,
27042742
controller[kState].pullFulfilled,
27052743
controller[kState].pullRejected);
27062744
}
@@ -2826,7 +2864,7 @@ function setupReadableStreamDefaultControllerFromSource(
28262864
FunctionPrototypeBind(start,source,controller) :
28272865
nonOpStart;
28282866
constpullAlgorithm=pull ?
2829-
createPromiseCallback1Param('source.pull',pull,source) :
2867+
createRawCallback1Param('source.pull',pull,source) :
28302868
nonOpPull;
28312869
constcancelAlgorithm=cancel ?
28322870
createPromiseCallback1Param('source.cancel',cancel,source) :
@@ -3519,8 +3557,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
35193557
controller[kState].pullRejected=
35203558
(error)=>readableByteStreamControllerError(controller,error);
35213559
}
3522-
PromisePrototypeThen(
3523-
controller[kState].pullAlgorithm(controller),
3560+
// See readableStreamDefaultControllerPull for the raw-callback contract.
3561+
letresult;
3562+
try{
3563+
result=controller[kState].pullAlgorithm(controller);
3564+
}catch(error){
3565+
result=PromiseReject(error);
3566+
}
3567+
thenAlgorithmResult(
3568+
result,
35243569
controller[kState].pullFulfilled,
35253570
controller[kState].pullRejected);
35263571
}
@@ -3700,7 +3745,7 @@ function setupReadableByteStreamControllerFromSource(
37003745
FunctionPrototypeBind(start,source,controller) :
37013746
nonOpStart;
37023747
constpullAlgorithm=pull ?
3703-
createPromiseCallback1Param('source.pull',pull,source) :
3748+
createRawCallback1Param('source.pull',pull,source) :
37043749
nonOpPull;
37053750
constcancelAlgorithm=cancel ?
37063751
createPromiseCallback1Param('source.cancel',cancel,source) :

β€Žlib/internal/webstreams/util.jsβ€Ž

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
338338
returnasync()=>FunctionPrototypeCall(fn,thisArg);
339339
}
340340

341+
// Raw variants that skip the async wrapper's implicit result promise.
342+
// Consumers of a raw callback invoke it inside try/catch and route the
343+
// result through thenAlgorithmResult() below.
344+
functioncreateRawCallback1Param(name,fn,thisArg){
345+
validateFunction(fn,name);
346+
return(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
347+
}
348+
349+
functioncreateRawCallback2Params(name,fn,thisArg){
350+
validateFunction(fn,name);
351+
return(arg1,arg2)=>FunctionPrototypeCall(fn,thisArg,arg1,arg2);
352+
}
353+
354+
// A single shared, forever-resolved promise used to enqueue a reaction at
355+
// the next microtask checkpoint without allocating a fresh promise.
356+
constkResolvedPromise=PromiseResolve();
357+
358+
// Wires the (possibly non-thenable) result of an underlying algorithm
359+
// callback to its fulfilled/rejected continuations. A non-thenable result
360+
// means fulfillment is guaranteed and no then() lookup is observable, so
361+
// the fulfillment step is enqueued directly at the exact microtask
362+
// position the coerced promise's reaction would have had, skipping the
363+
// per-chunk promise allocation. For thenable results PromiseResolve()
364+
// matches the spec's "a promise resolved with" conversion (identity for
365+
// native promises).
366+
functionthenAlgorithmResult(result,onFulfilled,onRejected){
367+
if(result===null||
368+
(typeofresult!=='object'&&typeofresult!=='function')){
369+
PromisePrototypeThen(kResolvedPromise,onFulfilled);
370+
}else{
371+
PromisePrototypeThen(PromiseResolve(result),onFulfilled,onRejected);
372+
}
373+
}
374+
341375
functioncreatePromiseCallback1Param(name,fn,thisArg){
342376
validateFunction(fn,name);
343377
returnasync(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
@@ -386,11 +420,14 @@ async function nonOpFlush() {}
386420

387421
functionnonOpStart(){}
388422

389-
asyncfunctionnonOpPull(){}
423+
// nonOpPull and nonOpWrite are raw callbacks (see createRawCallback*):
424+
// their non-thenable return takes the allocation-free fast path in
425+
// thenAlgorithmResult().
426+
functionnonOpPull(){}
390427

391428
asyncfunctionnonOpCancel(){}
392429

393-
asyncfunctionnonOpWrite(){}
430+
functionnonOpWrite(){}
394431

395432
lettransfer;
396433
functionlazyTransfer(){
@@ -411,6 +448,8 @@ module.exports = {
411448
createPromiseCallbackNoParams,
412449
createPromiseCallback1Param,
413450
createPromiseCallback2Params,
451+
createRawCallback1Param,
452+
createRawCallback2Params,
414453
customInspect,
415454
defaultSizeAlgorithm,
416455
dequeueValue,
@@ -421,6 +460,7 @@ module.exports = {
421460
isBrandCheck,
422461
isPromisePending,
423462
kEmptyQueue,
463+
kResolvedPromise,
424464
kState,
425465
kType,
426466
lazyTransfer,
@@ -435,4 +475,5 @@ module.exports = {
435475
resetQueue,
436476
resolvedRecord,
437477
setPromiseHandled,
478+
thenAlgorithmResult,
438479
};

β€Žlib/internal/webstreams/writablestream.jsβ€Ž

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const {
5656
Queue,
5757
createPromiseCallbackNoParams,
5858
createPromiseCallback1Param,
59-
createPromiseCallback2Params,
59+
createRawCallback2Params,
6060
customInspect,
6161
defaultSizeAlgorithm,
6262
dequeueValue,
@@ -78,6 +78,7 @@ const {
7878
resetQueue,
7979
resolvedRecord,
8080
setPromiseHandled,
81+
thenAlgorithmResult,
8182
}=require('internal/webstreams/util');
8283

8384
const{
@@ -769,7 +770,12 @@ function writableStreamUpdateBackpressure(controller, streamState) {
769770
constbackpressure=
770771
controllerState.highWaterMark-controllerState.queueTotalSize<=0;
771772
constwriter=streamState.writer;
772-
if(writer!==undefined&&streamState.backpressure!==backpressure){
773+
constchanged=streamState.backpressure!==backpressure;
774+
// The state field is published before the ready record is resolved so
775+
// that a ready resolve hook (pipeTo's pump continuation) observes the
776+
// new value.
777+
streamState.backpressure=backpressure;
778+
if(writer!==undefined&&changed){
773779
if(backpressure){
774780
// The spec replaces [[readyPromise]] with a fresh pending promise;
775781
// dropping the cache lets the next observation derive it.
@@ -778,7 +784,6 @@ function writableStreamUpdateBackpressure(controller, streamState) {
778784
writer[kState].ready?.resolve();
779785
}
780786
}
781-
streamState.backpressure=backpressure;
782787
}
783788

784789
functionwritableStreamStartErroring(stream,reason){
@@ -1197,8 +1202,18 @@ function writableStreamDefaultControllerProcessWrite(controller, chunk) {
11971202
};
11981203
}
11991204

1200-
PromisePrototypeThen(
1201-
writeAlgorithm(chunk,controller),
1205+
// The write algorithm may be a raw callback (a wrapped user sink.write
1206+
// returns its result uncoerced; a synchronous throw surfaces here) or an
1207+
// internal algorithm that always returns a promise; thenAlgorithmResult
1208+
// handles both.
1209+
letresult;
1210+
try{
1211+
result=writeAlgorithm(chunk,controller);
1212+
}catch(error){
1213+
result=PromiseReject(error);
1214+
}
1215+
thenAlgorithmResult(
1216+
result,
12021217
controller[kState].writeFulfilled,
12031218
controller[kState].writeRejected);
12041219
}
@@ -1323,7 +1338,7 @@ function setupWritableStreamDefaultControllerFromSink(
13231338
FunctionPrototypeBind(start,sink,controller) :
13241339
nonOpStart;
13251340
constwriteAlgorithm=write ?
1326-
createPromiseCallback2Params('sink.write',write,sink) :
1341+
createRawCallback2Params('sink.write',write,sink) :
13271342
nonOpWrite;
13281343
constcloseAlgorithm=close ?
13291344
createPromiseCallbackNoParams('sink.close',close,sink) :

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit f7e0c81

Browse files
mcollinaaduh95
authored andcommitted
stream: cut promise churn in webstreams hot paths
Three related reductions on the per-chunk paths: Wrap user sink.write and source.pull callbacks without coercing their result into a promise. When the callback returns a non-thenable (the common synchronous case), fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had, skipping the implicit async-wrapper promise per chunk. Thenable results go through PromiseResolve(), which matches the spec's "a promise resolved with" conversion (identity for native promises). Park pipeTo's pump on backpressure by installing a record that duck-types the writer's lazily-materialized [[readyPromise]] record and whose resolve function is the pump continuation itself. Backpressure clearing then resumes the pump directly instead of materializing a fresh promise record plus reaction per flip, and the pump no longer schedules a microtask per batch. writableStreamUpdateBackpressure publishes the new backpressure state before resolving the ready record so the pump observes the updated value. Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths with a reaction on the shared resolved promise, which enqueues the continuation at the same position without the per-call scheduling overhead. pipe-to improves by 8-14% across all benchmark configurations, with readable-read and tee also improving in spot runs. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65138 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 562168f commit f7e0c81

3 files changed

Lines changed: 126 additions & 25 deletions

File tree

β€Žlib/internal/webstreams/readablestream.jsβ€Ž

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
cloneAsUint8Array,
102102
copyArrayBuffer,
103103
createPromiseCallback1Param,
104+
createRawCallback1Param,
104105
customInspect,
105106
defaultSizeAlgorithm,
106107
dequeueValue,
@@ -110,6 +111,7 @@ const {
110111
getNonWritablePropertyDescriptor,
111112
isBrandCheck,
112113
kEmptyQueue,
114+
kResolvedPromise,
113115
kState,
114116
kType,
115117
lazyTransfer,
@@ -121,6 +123,7 @@ const {
121123
resetQueue,
122124
resolvedRecord,
123125
setPromiseHandled,
126+
thenAlgorithmResult,
124127
}=require('internal/webstreams/util');
125128

126129
const{
@@ -137,7 +140,6 @@ const {
137140
writableStreamDefaultWriterRelease,
138141
writableStreamDefaultWriterWriteWithRequest,
139142
writerClosedPromise,
140-
writerReadyPromise,
141143
}=require('internal/webstreams/writablestream');
142144

143145
const{ Buffer }=require('buffer');
@@ -1664,11 +1666,31 @@ function readableStreamPipeTo(
16641666
// the chunk travels through `pendingChunk`.
16651667
letpendingChunk;
16661668
letreadRequest;
1669+
letreadyHook;
16671670

16681671
// Ready promise rejection is handled by the destination-errored
16691672
// watcher.
16701673
functionignoreReadyRejection(){}
16711674

1675+
// Parks the pump on the destination's backpressure by installing a
1676+
// record that duck-types the writer's lazily-materialized
1677+
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
1678+
// when backpressure clears (after publishing the new backpressure
1679+
// state), which re-enters the pump directly instead of rotating a
1680+
// fresh promise record plus reaction per flip. The pipe holds the only
1681+
// reference to the writer, so the record is never observable as a real
1682+
// ready promise; the erroring/release paths probe `promise` via
1683+
// isPromisePending() and call `reject`, so it carries a real
1684+
// forever-pending promise and a no-op reject.
1685+
functionparkOnReady(){
1686+
readyHook??={
1687+
promise: newPromise(nonOpCallback),
1688+
resolve: pump,
1689+
reject: ignoreReadyRejection,
1690+
};
1691+
writer[kState].ready=readyHook;
1692+
}
1693+
16721694
functionforwardChunk(){
16731695
constchunk=pendingChunk;
16741696
pendingChunk=undefined;
@@ -1680,10 +1702,7 @@ function readableStreamPipeTo(
16801702
if(shuttingDown)return;
16811703

16821704
if(dest[kState].backpressure){
1683-
PromisePrototypeThen(
1684-
writerReadyPromise(writer).promise,
1685-
pump,
1686-
ignoreReadyRejection);
1705+
parkOnReady();
16871706
return;
16881707
}
16891708

@@ -1728,9 +1747,18 @@ function readableStreamPipeTo(
17281747
return;
17291748
}
17301749

1731-
// Yield to microtask queue between batches to allow events/signals
1732-
// to fire
1733-
queueMicrotask(pump);
1750+
// Park on backpressure directly: the ready hook resumes the pump
1751+
// when a completed write clears it.
1752+
if(dest[kState].backpressure){
1753+
parkOnReady();
1754+
return;
1755+
}
1756+
1757+
// Yield to the microtask queue between batches so completed-write
1758+
// reactions and events/signals fire; a shared resolved promise
1759+
// enqueues the continuation at the same position as queueMicrotask
1760+
// without the per-batch scheduling overhead.
1761+
PromisePrototypeThen(kResolvedPromise,pump);
17341762
return;
17351763
}
17361764

@@ -1742,7 +1770,7 @@ function readableStreamPipeTo(
17421770
// synchronous write during enqueue(). See WHATWG Streams spec
17431771
// "ReadableStreamPipeTo" step 15's "chunk steps".
17441772
pendingChunk=chunk;
1745-
queueMicrotask(forwardChunk);
1773+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
17461774
},
17471775
[kClose](){},
17481776
[kError](){},
@@ -1857,7 +1885,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
18571885
// The microtask is required by the spec (ReadableStreamTee's
18581886
// "chunk steps" queue one).
18591887
pendingChunk=value;
1860-
queueMicrotask(forwardChunk);
1888+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
18611889
},
18621890
[kClose](){
18631891
// The `process.nextTick()` is not part of the spec.
@@ -2010,7 +2038,7 @@ function readableByteStreamTee(stream) {
20102038
defaultReadRequest??={
20112039
[kChunk](chunk){
20122040
pendingChunk=chunk;
2013-
queueMicrotask(forwardChunk);
2041+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
20142042
},
20152043
[kClose](){
20162044
reading=false;
@@ -2699,8 +2727,18 @@ function readableStreamDefaultControllerPull(controller) {
26992727
controller[kState].pullRejected=
27002728
(error)=>readableStreamDefaultControllerError(controller,error);
27012729
}
2702-
PromisePrototypeThen(
2703-
controller[kState].pullAlgorithm(controller),
2730+
// The pull algorithm may be a raw callback (a wrapped user source.pull
2731+
// returns its result uncoerced; a synchronous throw surfaces here) or an
2732+
// internal algorithm that always returns a promise; thenAlgorithmResult
2733+
// handles both.
2734+
letresult;
2735+
try{
2736+
result=controller[kState].pullAlgorithm(controller);
2737+
}catch(error){
2738+
result=PromiseReject(error);
2739+
}
2740+
thenAlgorithmResult(
2741+
result,
27042742
controller[kState].pullFulfilled,
27052743
controller[kState].pullRejected);
27062744
}
@@ -2826,7 +2864,7 @@ function setupReadableStreamDefaultControllerFromSource(
28262864
FunctionPrototypeBind(start,source,controller) :
28272865
nonOpStart;
28282866
constpullAlgorithm=pull ?
2829-
createPromiseCallback1Param('source.pull',pull,source) :
2867+
createRawCallback1Param('source.pull',pull,source) :
28302868
nonOpPull;
28312869
constcancelAlgorithm=cancel ?
28322870
createPromiseCallback1Param('source.cancel',cancel,source) :
@@ -3519,8 +3557,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
35193557
controller[kState].pullRejected=
35203558
(error)=>readableByteStreamControllerError(controller,error);
35213559
}
3522-
PromisePrototypeThen(
3523-
controller[kState].pullAlgorithm(controller),
3560+
// See readableStreamDefaultControllerPull for the raw-callback contract.
3561+
letresult;
3562+
try{
3563+
result=controller[kState].pullAlgorithm(controller);
3564+
}catch(error){
3565+
result=PromiseReject(error);
3566+
}
3567+
thenAlgorithmResult(
3568+
result,
35243569
controller[kState].pullFulfilled,
35253570
controller[kState].pullRejected);
35263571
}
@@ -3700,7 +3745,7 @@ function setupReadableByteStreamControllerFromSource(
37003745
FunctionPrototypeBind(start,source,controller) :
37013746
nonOpStart;
37023747
constpullAlgorithm=pull ?
3703-
createPromiseCallback1Param('source.pull',pull,source) :
3748+
createRawCallback1Param('source.pull',pull,source) :
37043749
nonOpPull;
37053750
constcancelAlgorithm=cancel ?
37063751
createPromiseCallback1Param('source.cancel',cancel,source) :

β€Žlib/internal/webstreams/util.jsβ€Ž

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
338338
returnasync()=>FunctionPrototypeCall(fn,thisArg);
339339
}
340340

341+
// Raw variants that skip the async wrapper's implicit result promise.
342+
// Consumers of a raw callback invoke it inside try/catch and route the
343+
// result through thenAlgorithmResult() below.
344+
functioncreateRawCallback1Param(name,fn,thisArg){
345+
validateFunction(fn,name);
346+
return(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
347+
}
348+
349+
functioncreateRawCallback2Params(name,fn,thisArg){
350+
validateFunction(fn,name);
351+
return(arg1,arg2)=>FunctionPrototypeCall(fn,thisArg,arg1,arg2);
352+
}
353+
354+
// A single shared, forever-resolved promise used to enqueue a reaction at
355+
// the next microtask checkpoint without allocating a fresh promise.
356+
constkResolvedPromise=PromiseResolve();
357+
358+
// Wires the (possibly non-thenable) result of an underlying algorithm
359+
// callback to its fulfilled/rejected continuations. A non-thenable result
360+
// means fulfillment is guaranteed and no then() lookup is observable, so
361+
// the fulfillment step is enqueued directly at the exact microtask
362+
// position the coerced promise's reaction would have had, skipping the
363+
// per-chunk promise allocation. For thenable results PromiseResolve()
364+
// matches the spec's "a promise resolved with" conversion (identity for
365+
// native promises).
366+
functionthenAlgorithmResult(result,onFulfilled,onRejected){
367+
if(result===null||
368+
(typeofresult!=='object'&&typeofresult!=='function')){
369+
PromisePrototypeThen(kResolvedPromise,onFulfilled);
370+
}else{
371+
PromisePrototypeThen(PromiseResolve(result),onFulfilled,onRejected);
372+
}
373+
}
374+
341375
functioncreatePromiseCallback1Param(name,fn,thisArg){
342376
validateFunction(fn,name);
343377
returnasync(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
@@ -386,11 +420,14 @@ async function nonOpFlush() {}
386420

387421
functionnonOpStart(){}
388422

389-
asyncfunctionnonOpPull(){}
423+
// nonOpPull and nonOpWrite are raw callbacks (see createRawCallback*):
424+
// their non-thenable return takes the allocation-free fast path in
425+
// thenAlgorithmResult().
426+
functionnonOpPull(){}
390427

391428
asyncfunctionnonOpCancel(){}
392429

393-
asyncfunctionnonOpWrite(){}
430+
functionnonOpWrite(){}
394431

395432
lettransfer;
396433
functionlazyTransfer(){
@@ -411,6 +448,8 @@ module.exports = {
411448
createPromiseCallbackNoParams,
412449
createPromiseCallback1Param,
413450
createPromiseCallback2Params,
451+
createRawCallback1Param,
452+
createRawCallback2Params,
414453
customInspect,
415454
defaultSizeAlgorithm,
416455
dequeueValue,
@@ -421,6 +460,7 @@ module.exports = {
421460
isBrandCheck,
422461
isPromisePending,
423462
kEmptyQueue,
463+
kResolvedPromise,
424464
kState,
425465
kType,
426466
lazyTransfer,
@@ -435,4 +475,5 @@ module.exports = {
435475
resetQueue,
436476
resolvedRecord,
437477
setPromiseHandled,
478+
thenAlgorithmResult,
438479
};

β€Žlib/internal/webstreams/writablestream.jsβ€Ž

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const {
5656
Queue,
5757
createPromiseCallbackNoParams,
5858
createPromiseCallback1Param,
59-
createPromiseCallback2Params,
59+
createRawCallback2Params,
6060
customInspect,
6161
defaultSizeAlgorithm,
6262
dequeueValue,
@@ -78,6 +78,7 @@ const {
7878
resetQueue,
7979
resolvedRecord,
8080
setPromiseHandled,
81+
thenAlgorithmResult,
8182
}=require('internal/webstreams/util');
8283

8384
const{
@@ -769,7 +770,12 @@ function writableStreamUpdateBackpressure(controller, streamState) {
769770
constbackpressure=
770771
controllerState.highWaterMark-controllerState.queueTotalSize<=0;
771772
constwriter=streamState.writer;
772-
if(writer!==undefined&&streamState.backpressure!==backpressure){
773+
constchanged=streamState.backpressure!==backpressure;
774+
// The state field is published before the ready record is resolved so
775+
// that a ready resolve hook (pipeTo's pump continuation) observes the
776+
// new value.
777+
streamState.backpressure=backpressure;
778+
if(writer!==undefined&&changed){
773779
if(backpressure){
774780
// The spec replaces [[readyPromise]] with a fresh pending promise;
775781
// dropping the cache lets the next observation derive it.
@@ -778,7 +784,6 @@ function writableStreamUpdateBackpressure(controller, streamState) {
778784
writer[kState].ready?.resolve();
779785
}
780786
}
781-
streamState.backpressure=backpressure;
782787
}
783788

784789
functionwritableStreamStartErroring(stream,reason){
@@ -1197,8 +1202,18 @@ function writableStreamDefaultControllerProcessWrite(controller, chunk) {
11971202
};
11981203
}
11991204

1200-
PromisePrototypeThen(
1201-
writeAlgorithm(chunk,controller),
1205+
// The write algorithm may be a raw callback (a wrapped user sink.write
1206+
// returns its result uncoerced; a synchronous throw surfaces here) or an
1207+
// internal algorithm that always returns a promise; thenAlgorithmResult
1208+
// handles both.
1209+
letresult;
1210+
try{
1211+
result=writeAlgorithm(chunk,controller);
1212+
}catch(error){
1213+
result=PromiseReject(error);
1214+
}
1215+
thenAlgorithmResult(
1216+
result,
12021217
controller[kState].writeFulfilled,
12031218
controller[kState].writeRejected);
12041219
}
@@ -1323,7 +1338,7 @@ function setupWritableStreamDefaultControllerFromSink(
13231338
FunctionPrototypeBind(start,sink,controller) :
13241339
nonOpStart;
13251340
constwriteAlgorithm=write ?
1326-
createPromiseCallback2Params('sink.write',write,sink) :
1341+
createRawCallback2Params('sink.write',write,sink) :
13271342
nonOpWrite;
13281343
constcloseAlgorithm=close ?
13291344
createPromiseCallbackNoParams('sink.close',close,sink) :

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit f7e0c81

Browse files
mcollinaaduh95
authored andcommitted
stream: cut promise churn in webstreams hot paths
Three related reductions on the per-chunk paths: Wrap user sink.write and source.pull callbacks without coercing their result into a promise. When the callback returns a non-thenable (the common synchronous case), fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had, skipping the implicit async-wrapper promise per chunk. Thenable results go through PromiseResolve(), which matches the spec's "a promise resolved with" conversion (identity for native promises). Park pipeTo's pump on backpressure by installing a record that duck-types the writer's lazily-materialized [[readyPromise]] record and whose resolve function is the pump continuation itself. Backpressure clearing then resumes the pump directly instead of materializing a fresh promise record plus reaction per flip, and the pump no longer schedules a microtask per batch. writableStreamUpdateBackpressure publishes the new backpressure state before resolving the ready record so the pump observes the updated value. Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths with a reaction on the shared resolved promise, which enqueues the continuation at the same position without the per-call scheduling overhead. pipe-to improves by 8-14% across all benchmark configurations, with readable-read and tee also improving in spot runs. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65138 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 562168f commit f7e0c81

3 files changed

Lines changed: 126 additions & 25 deletions

File tree

β€Žlib/internal/webstreams/readablestream.jsβ€Ž

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
cloneAsUint8Array,
102102
copyArrayBuffer,
103103
createPromiseCallback1Param,
104+
createRawCallback1Param,
104105
customInspect,
105106
defaultSizeAlgorithm,
106107
dequeueValue,
@@ -110,6 +111,7 @@ const {
110111
getNonWritablePropertyDescriptor,
111112
isBrandCheck,
112113
kEmptyQueue,
114+
kResolvedPromise,
113115
kState,
114116
kType,
115117
lazyTransfer,
@@ -121,6 +123,7 @@ const {
121123
resetQueue,
122124
resolvedRecord,
123125
setPromiseHandled,
126+
thenAlgorithmResult,
124127
}=require('internal/webstreams/util');
125128

126129
const{
@@ -137,7 +140,6 @@ const {
137140
writableStreamDefaultWriterRelease,
138141
writableStreamDefaultWriterWriteWithRequest,
139142
writerClosedPromise,
140-
writerReadyPromise,
141143
}=require('internal/webstreams/writablestream');
142144

143145
const{ Buffer }=require('buffer');
@@ -1664,11 +1666,31 @@ function readableStreamPipeTo(
16641666
// the chunk travels through `pendingChunk`.
16651667
letpendingChunk;
16661668
letreadRequest;
1669+
letreadyHook;
16671670

16681671
// Ready promise rejection is handled by the destination-errored
16691672
// watcher.
16701673
functionignoreReadyRejection(){}
16711674

1675+
// Parks the pump on the destination's backpressure by installing a
1676+
// record that duck-types the writer's lazily-materialized
1677+
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
1678+
// when backpressure clears (after publishing the new backpressure
1679+
// state), which re-enters the pump directly instead of rotating a
1680+
// fresh promise record plus reaction per flip. The pipe holds the only
1681+
// reference to the writer, so the record is never observable as a real
1682+
// ready promise; the erroring/release paths probe `promise` via
1683+
// isPromisePending() and call `reject`, so it carries a real
1684+
// forever-pending promise and a no-op reject.
1685+
functionparkOnReady(){
1686+
readyHook??={
1687+
promise: newPromise(nonOpCallback),
1688+
resolve: pump,
1689+
reject: ignoreReadyRejection,
1690+
};
1691+
writer[kState].ready=readyHook;
1692+
}
1693+
16721694
functionforwardChunk(){
16731695
constchunk=pendingChunk;
16741696
pendingChunk=undefined;
@@ -1680,10 +1702,7 @@ function readableStreamPipeTo(
16801702
if(shuttingDown)return;
16811703

16821704
if(dest[kState].backpressure){
1683-
PromisePrototypeThen(
1684-
writerReadyPromise(writer).promise,
1685-
pump,
1686-
ignoreReadyRejection);
1705+
parkOnReady();
16871706
return;
16881707
}
16891708

@@ -1728,9 +1747,18 @@ function readableStreamPipeTo(
17281747
return;
17291748
}
17301749

1731-
// Yield to microtask queue between batches to allow events/signals
1732-
// to fire
1733-
queueMicrotask(pump);
1750+
// Park on backpressure directly: the ready hook resumes the pump
1751+
// when a completed write clears it.
1752+
if(dest[kState].backpressure){
1753+
parkOnReady();
1754+
return;
1755+
}
1756+
1757+
// Yield to the microtask queue between batches so completed-write
1758+
// reactions and events/signals fire; a shared resolved promise
1759+
// enqueues the continuation at the same position as queueMicrotask
1760+
// without the per-batch scheduling overhead.
1761+
PromisePrototypeThen(kResolvedPromise,pump);
17341762
return;
17351763
}
17361764

@@ -1742,7 +1770,7 @@ function readableStreamPipeTo(
17421770
// synchronous write during enqueue(). See WHATWG Streams spec
17431771
// "ReadableStreamPipeTo" step 15's "chunk steps".
17441772
pendingChunk=chunk;
1745-
queueMicrotask(forwardChunk);
1773+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
17461774
},
17471775
[kClose](){},
17481776
[kError](){},
@@ -1857,7 +1885,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
18571885
// The microtask is required by the spec (ReadableStreamTee's
18581886
// "chunk steps" queue one).
18591887
pendingChunk=value;
1860-
queueMicrotask(forwardChunk);
1888+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
18611889
},
18621890
[kClose](){
18631891
// The `process.nextTick()` is not part of the spec.
@@ -2010,7 +2038,7 @@ function readableByteStreamTee(stream) {
20102038
defaultReadRequest??={
20112039
[kChunk](chunk){
20122040
pendingChunk=chunk;
2013-
queueMicrotask(forwardChunk);
2041+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
20142042
},
20152043
[kClose](){
20162044
reading=false;
@@ -2699,8 +2727,18 @@ function readableStreamDefaultControllerPull(controller) {
26992727
controller[kState].pullRejected=
27002728
(error)=>readableStreamDefaultControllerError(controller,error);
27012729
}
2702-
PromisePrototypeThen(
2703-
controller[kState].pullAlgorithm(controller),
2730+
// The pull algorithm may be a raw callback (a wrapped user source.pull
2731+
// returns its result uncoerced; a synchronous throw surfaces here) or an
2732+
// internal algorithm that always returns a promise; thenAlgorithmResult
2733+
// handles both.
2734+
letresult;
2735+
try{
2736+
result=controller[kState].pullAlgorithm(controller);
2737+
}catch(error){
2738+
result=PromiseReject(error);
2739+
}
2740+
thenAlgorithmResult(
2741+
result,
27042742
controller[kState].pullFulfilled,
27052743
controller[kState].pullRejected);
27062744
}
@@ -2826,7 +2864,7 @@ function setupReadableStreamDefaultControllerFromSource(
28262864
FunctionPrototypeBind(start,source,controller) :
28272865
nonOpStart;
28282866
constpullAlgorithm=pull ?
2829-
createPromiseCallback1Param('source.pull',pull,source) :
2867+
createRawCallback1Param('source.pull',pull,source) :
28302868
nonOpPull;
28312869
constcancelAlgorithm=cancel ?
28322870
createPromiseCallback1Param('source.cancel',cancel,source) :
@@ -3519,8 +3557,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
35193557
controller[kState].pullRejected=
35203558
(error)=>readableByteStreamControllerError(controller,error);
35213559
}
3522-
PromisePrototypeThen(
3523-
controller[kState].pullAlgorithm(controller),
3560+
// See readableStreamDefaultControllerPull for the raw-callback contract.
3561+
letresult;
3562+
try{
3563+
result=controller[kState].pullAlgorithm(controller);
3564+
}catch(error){
3565+
result=PromiseReject(error);
3566+
}
3567+
thenAlgorithmResult(
3568+
result,
35243569
controller[kState].pullFulfilled,
35253570
controller[kState].pullRejected);
35263571
}
@@ -3700,7 +3745,7 @@ function setupReadableByteStreamControllerFromSource(
37003745
FunctionPrototypeBind(start,source,controller) :
37013746
nonOpStart;
37023747
constpullAlgorithm=pull ?
3703-
createPromiseCallback1Param('source.pull',pull,source) :
3748+
createRawCallback1Param('source.pull',pull,source) :
37043749
nonOpPull;
37053750
constcancelAlgorithm=cancel ?
37063751
createPromiseCallback1Param('source.cancel',cancel,source) :

β€Žlib/internal/webstreams/util.jsβ€Ž

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
338338
returnasync()=>FunctionPrototypeCall(fn,thisArg);
339339
}
340340

341+
// Raw variants that skip the async wrapper's implicit result promise.
342+
// Consumers of a raw callback invoke it inside try/catch and route the
343+
// result through thenAlgorithmResult() below.
344+
functioncreateRawCallback1Param(name,fn,thisArg){
345+
validateFunction(fn,name);
346+
return(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
347+
}
348+
349+
functioncreateRawCallback2Params(name,fn,thisArg){
350+
validateFunction(fn,name);
351+
return(arg1,arg2)=>FunctionPrototypeCall(fn,thisArg,arg1,arg2);
352+
}
353+
354+
// A single shared, forever-resolved promise used to enqueue a reaction at
355+
// the next microtask checkpoint without allocating a fresh promise.
356+
constkResolvedPromise=PromiseResolve();
357+
358+
// Wires the (possibly non-thenable) result of an underlying algorithm
359+
// callback to its fulfilled/rejected continuations. A non-thenable result
360+
// means fulfillment is guaranteed and no then() lookup is observable, so
361+
// the fulfillment step is enqueued directly at the exact microtask
362+
// position the coerced promise's reaction would have had, skipping the
363+
// per-chunk promise allocation. For thenable results PromiseResolve()
364+
// matches the spec's "a promise resolved with" conversion (identity for
365+
// native promises).
366+
functionthenAlgorithmResult(result,onFulfilled,onRejected){
367+
if(result===null||
368+
(typeofresult!=='object'&&typeofresult!=='function')){
369+
PromisePrototypeThen(kResolvedPromise,onFulfilled);
370+
}else{
371+
PromisePrototypeThen(PromiseResolve(result),onFulfilled,onRejected);
372+
}
373+
}
374+
341375
functioncreatePromiseCallback1Param(name,fn,thisArg){
342376
validateFunction(fn,name);
343377
returnasync(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
@@ -386,11 +420,14 @@ async function nonOpFlush() {}
386420

387421
functionnonOpStart(){}
388422

389-
asyncfunctionnonOpPull(){}
423+
// nonOpPull and nonOpWrite are raw callbacks (see createRawCallback*):
424+
// their non-thenable return takes the allocation-free fast path in
425+
// thenAlgorithmResult().
426+
functionnonOpPull(){}
390427

391428
asyncfunctionnonOpCancel(){}
392429

393-
asyncfunctionnonOpWrite(){}
430+
functionnonOpWrite(){}
394431

395432
lettransfer;
396433
functionlazyTransfer(){
@@ -411,6 +448,8 @@ module.exports = {
411448
createPromiseCallbackNoParams,
412449
createPromiseCallback1Param,
413450
createPromiseCallback2Params,
451+
createRawCallback1Param,
452+
createRawCallback2Params,
414453
customInspect,
415454
defaultSizeAlgorithm,
416455
dequeueValue,
@@ -421,6 +460,7 @@ module.exports = {
421460
isBrandCheck,
422461
isPromisePending,
423462
kEmptyQueue,
463+
kResolvedPromise,
424464
kState,
425465
kType,
426466
lazyTransfer,
@@ -435,4 +475,5 @@ module.exports = {
435475
resetQueue,
436476
resolvedRecord,
437477
setPromiseHandled,
478+
thenAlgorithmResult,
438479
};

β€Žlib/internal/webstreams/writablestream.jsβ€Ž

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const {
5656
Queue,
5757
createPromiseCallbackNoParams,
5858
createPromiseCallback1Param,
59-
createPromiseCallback2Params,
59+
createRawCallback2Params,
6060
customInspect,
6161
defaultSizeAlgorithm,
6262
dequeueValue,
@@ -78,6 +78,7 @@ const {
7878
resetQueue,
7979
resolvedRecord,
8080
setPromiseHandled,
81+
thenAlgorithmResult,
8182
}=require('internal/webstreams/util');
8283

8384
const{
@@ -769,7 +770,12 @@ function writableStreamUpdateBackpressure(controller, streamState) {
769770
constbackpressure=
770771
controllerState.highWaterMark-controllerState.queueTotalSize<=0;
771772
constwriter=streamState.writer;
772-
if(writer!==undefined&&streamState.backpressure!==backpressure){
773+
constchanged=streamState.backpressure!==backpressure;
774+
// The state field is published before the ready record is resolved so
775+
// that a ready resolve hook (pipeTo's pump continuation) observes the
776+
// new value.
777+
streamState.backpressure=backpressure;
778+
if(writer!==undefined&&changed){
773779
if(backpressure){
774780
// The spec replaces [[readyPromise]] with a fresh pending promise;
775781
// dropping the cache lets the next observation derive it.
@@ -778,7 +784,6 @@ function writableStreamUpdateBackpressure(controller, streamState) {
778784
writer[kState].ready?.resolve();
779785
}
780786
}
781-
streamState.backpressure=backpressure;
782787
}
783788

784789
functionwritableStreamStartErroring(stream,reason){
@@ -1197,8 +1202,18 @@ function writableStreamDefaultControllerProcessWrite(controller, chunk) {
11971202
};
11981203
}
11991204

1200-
PromisePrototypeThen(
1201-
writeAlgorithm(chunk,controller),
1205+
// The write algorithm may be a raw callback (a wrapped user sink.write
1206+
// returns its result uncoerced; a synchronous throw surfaces here) or an
1207+
// internal algorithm that always returns a promise; thenAlgorithmResult
1208+
// handles both.
1209+
letresult;
1210+
try{
1211+
result=writeAlgorithm(chunk,controller);
1212+
}catch(error){
1213+
result=PromiseReject(error);
1214+
}
1215+
thenAlgorithmResult(
1216+
result,
12021217
controller[kState].writeFulfilled,
12031218
controller[kState].writeRejected);
12041219
}
@@ -1323,7 +1338,7 @@ function setupWritableStreamDefaultControllerFromSink(
13231338
FunctionPrototypeBind(start,sink,controller) :
13241339
nonOpStart;
13251340
constwriteAlgorithm=write ?
1326-
createPromiseCallback2Params('sink.write',write,sink) :
1341+
createRawCallback2Params('sink.write',write,sink) :
13271342
nonOpWrite;
13281343
constcloseAlgorithm=close ?
13291344
createPromiseCallbackNoParams('sink.close',close,sink) :

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit f7e0c81

Browse files
mcollinaaduh95
authored andcommitted
stream: cut promise churn in webstreams hot paths
Three related reductions on the per-chunk paths: Wrap user sink.write and source.pull callbacks without coercing their result into a promise. When the callback returns a non-thenable (the common synchronous case), fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had, skipping the implicit async-wrapper promise per chunk. Thenable results go through PromiseResolve(), which matches the spec's "a promise resolved with" conversion (identity for native promises). Park pipeTo's pump on backpressure by installing a record that duck-types the writer's lazily-materialized [[readyPromise]] record and whose resolve function is the pump continuation itself. Backpressure clearing then resumes the pump directly instead of materializing a fresh promise record plus reaction per flip, and the pump no longer schedules a microtask per batch. writableStreamUpdateBackpressure publishes the new backpressure state before resolving the ready record so the pump observes the updated value. Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths with a reaction on the shared resolved promise, which enqueues the continuation at the same position without the per-call scheduling overhead. pipe-to improves by 8-14% across all benchmark configurations, with readable-read and tee also improving in spot runs. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65138 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 562168f commit f7e0c81

3 files changed

Lines changed: 126 additions & 25 deletions

File tree

β€Žlib/internal/webstreams/readablestream.jsβ€Ž

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
cloneAsUint8Array,
102102
copyArrayBuffer,
103103
createPromiseCallback1Param,
104+
createRawCallback1Param,
104105
customInspect,
105106
defaultSizeAlgorithm,
106107
dequeueValue,
@@ -110,6 +111,7 @@ const {
110111
getNonWritablePropertyDescriptor,
111112
isBrandCheck,
112113
kEmptyQueue,
114+
kResolvedPromise,
113115
kState,
114116
kType,
115117
lazyTransfer,
@@ -121,6 +123,7 @@ const {
121123
resetQueue,
122124
resolvedRecord,
123125
setPromiseHandled,
126+
thenAlgorithmResult,
124127
}=require('internal/webstreams/util');
125128

126129
const{
@@ -137,7 +140,6 @@ const {
137140
writableStreamDefaultWriterRelease,
138141
writableStreamDefaultWriterWriteWithRequest,
139142
writerClosedPromise,
140-
writerReadyPromise,
141143
}=require('internal/webstreams/writablestream');
142144

143145
const{ Buffer }=require('buffer');
@@ -1664,11 +1666,31 @@ function readableStreamPipeTo(
16641666
// the chunk travels through `pendingChunk`.
16651667
letpendingChunk;
16661668
letreadRequest;
1669+
letreadyHook;
16671670

16681671
// Ready promise rejection is handled by the destination-errored
16691672
// watcher.
16701673
functionignoreReadyRejection(){}
16711674

1675+
// Parks the pump on the destination's backpressure by installing a
1676+
// record that duck-types the writer's lazily-materialized
1677+
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
1678+
// when backpressure clears (after publishing the new backpressure
1679+
// state), which re-enters the pump directly instead of rotating a
1680+
// fresh promise record plus reaction per flip. The pipe holds the only
1681+
// reference to the writer, so the record is never observable as a real
1682+
// ready promise; the erroring/release paths probe `promise` via
1683+
// isPromisePending() and call `reject`, so it carries a real
1684+
// forever-pending promise and a no-op reject.
1685+
functionparkOnReady(){
1686+
readyHook??={
1687+
promise: newPromise(nonOpCallback),
1688+
resolve: pump,
1689+
reject: ignoreReadyRejection,
1690+
};
1691+
writer[kState].ready=readyHook;
1692+
}
1693+
16721694
functionforwardChunk(){
16731695
constchunk=pendingChunk;
16741696
pendingChunk=undefined;
@@ -1680,10 +1702,7 @@ function readableStreamPipeTo(
16801702
if(shuttingDown)return;
16811703

16821704
if(dest[kState].backpressure){
1683-
PromisePrototypeThen(
1684-
writerReadyPromise(writer).promise,
1685-
pump,
1686-
ignoreReadyRejection);
1705+
parkOnReady();
16871706
return;
16881707
}
16891708

@@ -1728,9 +1747,18 @@ function readableStreamPipeTo(
17281747
return;
17291748
}
17301749

1731-
// Yield to microtask queue between batches to allow events/signals
1732-
// to fire
1733-
queueMicrotask(pump);
1750+
// Park on backpressure directly: the ready hook resumes the pump
1751+
// when a completed write clears it.
1752+
if(dest[kState].backpressure){
1753+
parkOnReady();
1754+
return;
1755+
}
1756+
1757+
// Yield to the microtask queue between batches so completed-write
1758+
// reactions and events/signals fire; a shared resolved promise
1759+
// enqueues the continuation at the same position as queueMicrotask
1760+
// without the per-batch scheduling overhead.
1761+
PromisePrototypeThen(kResolvedPromise,pump);
17341762
return;
17351763
}
17361764

@@ -1742,7 +1770,7 @@ function readableStreamPipeTo(
17421770
// synchronous write during enqueue(). See WHATWG Streams spec
17431771
// "ReadableStreamPipeTo" step 15's "chunk steps".
17441772
pendingChunk=chunk;
1745-
queueMicrotask(forwardChunk);
1773+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
17461774
},
17471775
[kClose](){},
17481776
[kError](){},
@@ -1857,7 +1885,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
18571885
// The microtask is required by the spec (ReadableStreamTee's
18581886
// "chunk steps" queue one).
18591887
pendingChunk=value;
1860-
queueMicrotask(forwardChunk);
1888+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
18611889
},
18621890
[kClose](){
18631891
// The `process.nextTick()` is not part of the spec.
@@ -2010,7 +2038,7 @@ function readableByteStreamTee(stream) {
20102038
defaultReadRequest??={
20112039
[kChunk](chunk){
20122040
pendingChunk=chunk;
2013-
queueMicrotask(forwardChunk);
2041+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
20142042
},
20152043
[kClose](){
20162044
reading=false;
@@ -2699,8 +2727,18 @@ function readableStreamDefaultControllerPull(controller) {
26992727
controller[kState].pullRejected=
27002728
(error)=>readableStreamDefaultControllerError(controller,error);
27012729
}
2702-
PromisePrototypeThen(
2703-
controller[kState].pullAlgorithm(controller),
2730+
// The pull algorithm may be a raw callback (a wrapped user source.pull
2731+
// returns its result uncoerced; a synchronous throw surfaces here) or an
2732+
// internal algorithm that always returns a promise; thenAlgorithmResult
2733+
// handles both.
2734+
letresult;
2735+
try{
2736+
result=controller[kState].pullAlgorithm(controller);
2737+
}catch(error){
2738+
result=PromiseReject(error);
2739+
}
2740+
thenAlgorithmResult(
2741+
result,
27042742
controller[kState].pullFulfilled,
27052743
controller[kState].pullRejected);
27062744
}
@@ -2826,7 +2864,7 @@ function setupReadableStreamDefaultControllerFromSource(
28262864
FunctionPrototypeBind(start,source,controller) :
28272865
nonOpStart;
28282866
constpullAlgorithm=pull ?
2829-
createPromiseCallback1Param('source.pull',pull,source) :
2867+
createRawCallback1Param('source.pull',pull,source) :
28302868
nonOpPull;
28312869
constcancelAlgorithm=cancel ?
28322870
createPromiseCallback1Param('source.cancel',cancel,source) :
@@ -3519,8 +3557,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
35193557
controller[kState].pullRejected=
35203558
(error)=>readableByteStreamControllerError(controller,error);
35213559
}
3522-
PromisePrototypeThen(
3523-
controller[kState].pullAlgorithm(controller),
3560+
// See readableStreamDefaultControllerPull for the raw-callback contract.
3561+
letresult;
3562+
try{
3563+
result=controller[kState].pullAlgorithm(controller);
3564+
}catch(error){
3565+
result=PromiseReject(error);
3566+
}
3567+
thenAlgorithmResult(
3568+
result,
35243569
controller[kState].pullFulfilled,
35253570
controller[kState].pullRejected);
35263571
}
@@ -3700,7 +3745,7 @@ function setupReadableByteStreamControllerFromSource(
37003745
FunctionPrototypeBind(start,source,controller) :
37013746
nonOpStart;
37023747
constpullAlgorithm=pull ?
3703-
createPromiseCallback1Param('source.pull',pull,source) :
3748+
createRawCallback1Param('source.pull',pull,source) :
37043749
nonOpPull;
37053750
constcancelAlgorithm=cancel ?
37063751
createPromiseCallback1Param('source.cancel',cancel,source) :

β€Žlib/internal/webstreams/util.jsβ€Ž

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
338338
returnasync()=>FunctionPrototypeCall(fn,thisArg);
339339
}
340340

341+
// Raw variants that skip the async wrapper's implicit result promise.
342+
// Consumers of a raw callback invoke it inside try/catch and route the
343+
// result through thenAlgorithmResult() below.
344+
functioncreateRawCallback1Param(name,fn,thisArg){
345+
validateFunction(fn,name);
346+
return(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
347+
}
348+
349+
functioncreateRawCallback2Params(name,fn,thisArg){
350+
validateFunction(fn,name);
351+
return(arg1,arg2)=>FunctionPrototypeCall(fn,thisArg,arg1,arg2);
352+
}
353+
354+
// A single shared, forever-resolved promise used to enqueue a reaction at
355+
// the next microtask checkpoint without allocating a fresh promise.
356+
constkResolvedPromise=PromiseResolve();
357+
358+
// Wires the (possibly non-thenable) result of an underlying algorithm
359+
// callback to its fulfilled/rejected continuations. A non-thenable result
360+
// means fulfillment is guaranteed and no then() lookup is observable, so
361+
// the fulfillment step is enqueued directly at the exact microtask
362+
// position the coerced promise's reaction would have had, skipping the
363+
// per-chunk promise allocation. For thenable results PromiseResolve()
364+
// matches the spec's "a promise resolved with" conversion (identity for
365+
// native promises).
366+
functionthenAlgorithmResult(result,onFulfilled,onRejected){
367+
if(result===null||
368+
(typeofresult!=='object'&&typeofresult!=='function')){
369+
PromisePrototypeThen(kResolvedPromise,onFulfilled);
370+
}else{
371+
PromisePrototypeThen(PromiseResolve(result),onFulfilled,onRejected);
372+
}
373+
}
374+
341375
functioncreatePromiseCallback1Param(name,fn,thisArg){
342376
validateFunction(fn,name);
343377
returnasync(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
@@ -386,11 +420,14 @@ async function nonOpFlush() {}
386420

387421
functionnonOpStart(){}
388422

389-
asyncfunctionnonOpPull(){}
423+
// nonOpPull and nonOpWrite are raw callbacks (see createRawCallback*):
424+
// their non-thenable return takes the allocation-free fast path in
425+
// thenAlgorithmResult().
426+
functionnonOpPull(){}
390427

391428
asyncfunctionnonOpCancel(){}
392429

393-
asyncfunctionnonOpWrite(){}
430+
functionnonOpWrite(){}
394431

395432
lettransfer;
396433
functionlazyTransfer(){
@@ -411,6 +448,8 @@ module.exports = {
411448
createPromiseCallbackNoParams,
412449
createPromiseCallback1Param,
413450
createPromiseCallback2Params,
451+
createRawCallback1Param,
452+
createRawCallback2Params,
414453
customInspect,
415454
defaultSizeAlgorithm,
416455
dequeueValue,
@@ -421,6 +460,7 @@ module.exports = {
421460
isBrandCheck,
422461
isPromisePending,
423462
kEmptyQueue,
463+
kResolvedPromise,
424464
kState,
425465
kType,
426466
lazyTransfer,
@@ -435,4 +475,5 @@ module.exports = {
435475
resetQueue,
436476
resolvedRecord,
437477
setPromiseHandled,
478+
thenAlgorithmResult,
438479
};

β€Žlib/internal/webstreams/writablestream.jsβ€Ž

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const {
5656
Queue,
5757
createPromiseCallbackNoParams,
5858
createPromiseCallback1Param,
59-
createPromiseCallback2Params,
59+
createRawCallback2Params,
6060
customInspect,
6161
defaultSizeAlgorithm,
6262
dequeueValue,
@@ -78,6 +78,7 @@ const {
7878
resetQueue,
7979
resolvedRecord,
8080
setPromiseHandled,
81+
thenAlgorithmResult,
8182
}=require('internal/webstreams/util');
8283

8384
const{
@@ -769,7 +770,12 @@ function writableStreamUpdateBackpressure(controller, streamState) {
769770
constbackpressure=
770771
controllerState.highWaterMark-controllerState.queueTotalSize<=0;
771772
constwriter=streamState.writer;
772-
if(writer!==undefined&&streamState.backpressure!==backpressure){
773+
constchanged=streamState.backpressure!==backpressure;
774+
// The state field is published before the ready record is resolved so
775+
// that a ready resolve hook (pipeTo's pump continuation) observes the
776+
// new value.
777+
streamState.backpressure=backpressure;
778+
if(writer!==undefined&&changed){
773779
if(backpressure){
774780
// The spec replaces [[readyPromise]] with a fresh pending promise;
775781
// dropping the cache lets the next observation derive it.
@@ -778,7 +784,6 @@ function writableStreamUpdateBackpressure(controller, streamState) {
778784
writer[kState].ready?.resolve();
779785
}
780786
}
781-
streamState.backpressure=backpressure;
782787
}
783788

784789
functionwritableStreamStartErroring(stream,reason){
@@ -1197,8 +1202,18 @@ function writableStreamDefaultControllerProcessWrite(controller, chunk) {
11971202
};
11981203
}
11991204

1200-
PromisePrototypeThen(
1201-
writeAlgorithm(chunk,controller),
1205+
// The write algorithm may be a raw callback (a wrapped user sink.write
1206+
// returns its result uncoerced; a synchronous throw surfaces here) or an
1207+
// internal algorithm that always returns a promise; thenAlgorithmResult
1208+
// handles both.
1209+
letresult;
1210+
try{
1211+
result=writeAlgorithm(chunk,controller);
1212+
}catch(error){
1213+
result=PromiseReject(error);
1214+
}
1215+
thenAlgorithmResult(
1216+
result,
12021217
controller[kState].writeFulfilled,
12031218
controller[kState].writeRejected);
12041219
}
@@ -1323,7 +1338,7 @@ function setupWritableStreamDefaultControllerFromSink(
13231338
FunctionPrototypeBind(start,sink,controller) :
13241339
nonOpStart;
13251340
constwriteAlgorithm=write ?
1326-
createPromiseCallback2Params('sink.write',write,sink) :
1341+
createRawCallback2Params('sink.write',write,sink) :
13271342
nonOpWrite;
13281343
constcloseAlgorithm=close ?
13291344
createPromiseCallbackNoParams('sink.close',close,sink) :

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit f7e0c81

Browse files
mcollinaaduh95
authored andcommitted
stream: cut promise churn in webstreams hot paths
Three related reductions on the per-chunk paths: Wrap user sink.write and source.pull callbacks without coercing their result into a promise. When the callback returns a non-thenable (the common synchronous case), fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had, skipping the implicit async-wrapper promise per chunk. Thenable results go through PromiseResolve(), which matches the spec's "a promise resolved with" conversion (identity for native promises). Park pipeTo's pump on backpressure by installing a record that duck-types the writer's lazily-materialized [[readyPromise]] record and whose resolve function is the pump continuation itself. Backpressure clearing then resumes the pump directly instead of materializing a fresh promise record plus reaction per flip, and the pump no longer schedules a microtask per batch. writableStreamUpdateBackpressure publishes the new backpressure state before resolving the ready record so the pump observes the updated value. Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths with a reaction on the shared resolved promise, which enqueues the continuation at the same position without the per-call scheduling overhead. pipe-to improves by 8-14% across all benchmark configurations, with readable-read and tee also improving in spot runs. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65138 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 562168f commit f7e0c81

3 files changed

Lines changed: 126 additions & 25 deletions

File tree

β€Žlib/internal/webstreams/readablestream.jsβ€Ž

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
cloneAsUint8Array,
102102
copyArrayBuffer,
103103
createPromiseCallback1Param,
104+
createRawCallback1Param,
104105
customInspect,
105106
defaultSizeAlgorithm,
106107
dequeueValue,
@@ -110,6 +111,7 @@ const {
110111
getNonWritablePropertyDescriptor,
111112
isBrandCheck,
112113
kEmptyQueue,
114+
kResolvedPromise,
113115
kState,
114116
kType,
115117
lazyTransfer,
@@ -121,6 +123,7 @@ const {
121123
resetQueue,
122124
resolvedRecord,
123125
setPromiseHandled,
126+
thenAlgorithmResult,
124127
}=require('internal/webstreams/util');
125128

126129
const{
@@ -137,7 +140,6 @@ const {
137140
writableStreamDefaultWriterRelease,
138141
writableStreamDefaultWriterWriteWithRequest,
139142
writerClosedPromise,
140-
writerReadyPromise,
141143
}=require('internal/webstreams/writablestream');
142144

143145
const{ Buffer }=require('buffer');
@@ -1664,11 +1666,31 @@ function readableStreamPipeTo(
16641666
// the chunk travels through `pendingChunk`.
16651667
letpendingChunk;
16661668
letreadRequest;
1669+
letreadyHook;
16671670

16681671
// Ready promise rejection is handled by the destination-errored
16691672
// watcher.
16701673
functionignoreReadyRejection(){}
16711674

1675+
// Parks the pump on the destination's backpressure by installing a
1676+
// record that duck-types the writer's lazily-materialized
1677+
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
1678+
// when backpressure clears (after publishing the new backpressure
1679+
// state), which re-enters the pump directly instead of rotating a
1680+
// fresh promise record plus reaction per flip. The pipe holds the only
1681+
// reference to the writer, so the record is never observable as a real
1682+
// ready promise; the erroring/release paths probe `promise` via
1683+
// isPromisePending() and call `reject`, so it carries a real
1684+
// forever-pending promise and a no-op reject.
1685+
functionparkOnReady(){
1686+
readyHook??={
1687+
promise: newPromise(nonOpCallback),
1688+
resolve: pump,
1689+
reject: ignoreReadyRejection,
1690+
};
1691+
writer[kState].ready=readyHook;
1692+
}
1693+
16721694
functionforwardChunk(){
16731695
constchunk=pendingChunk;
16741696
pendingChunk=undefined;
@@ -1680,10 +1702,7 @@ function readableStreamPipeTo(
16801702
if(shuttingDown)return;
16811703

16821704
if(dest[kState].backpressure){
1683-
PromisePrototypeThen(
1684-
writerReadyPromise(writer).promise,
1685-
pump,
1686-
ignoreReadyRejection);
1705+
parkOnReady();
16871706
return;
16881707
}
16891708

@@ -1728,9 +1747,18 @@ function readableStreamPipeTo(
17281747
return;
17291748
}
17301749

1731-
// Yield to microtask queue between batches to allow events/signals
1732-
// to fire
1733-
queueMicrotask(pump);
1750+
// Park on backpressure directly: the ready hook resumes the pump
1751+
// when a completed write clears it.
1752+
if(dest[kState].backpressure){
1753+
parkOnReady();
1754+
return;
1755+
}
1756+
1757+
// Yield to the microtask queue between batches so completed-write
1758+
// reactions and events/signals fire; a shared resolved promise
1759+
// enqueues the continuation at the same position as queueMicrotask
1760+
// without the per-batch scheduling overhead.
1761+
PromisePrototypeThen(kResolvedPromise,pump);
17341762
return;
17351763
}
17361764

@@ -1742,7 +1770,7 @@ function readableStreamPipeTo(
17421770
// synchronous write during enqueue(). See WHATWG Streams spec
17431771
// "ReadableStreamPipeTo" step 15's "chunk steps".
17441772
pendingChunk=chunk;
1745-
queueMicrotask(forwardChunk);
1773+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
17461774
},
17471775
[kClose](){},
17481776
[kError](){},
@@ -1857,7 +1885,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
18571885
// The microtask is required by the spec (ReadableStreamTee's
18581886
// "chunk steps" queue one).
18591887
pendingChunk=value;
1860-
queueMicrotask(forwardChunk);
1888+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
18611889
},
18621890
[kClose](){
18631891
// The `process.nextTick()` is not part of the spec.
@@ -2010,7 +2038,7 @@ function readableByteStreamTee(stream) {
20102038
defaultReadRequest??={
20112039
[kChunk](chunk){
20122040
pendingChunk=chunk;
2013-
queueMicrotask(forwardChunk);
2041+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
20142042
},
20152043
[kClose](){
20162044
reading=false;
@@ -2699,8 +2727,18 @@ function readableStreamDefaultControllerPull(controller) {
26992727
controller[kState].pullRejected=
27002728
(error)=>readableStreamDefaultControllerError(controller,error);
27012729
}
2702-
PromisePrototypeThen(
2703-
controller[kState].pullAlgorithm(controller),
2730+
// The pull algorithm may be a raw callback (a wrapped user source.pull
2731+
// returns its result uncoerced; a synchronous throw surfaces here) or an
2732+
// internal algorithm that always returns a promise; thenAlgorithmResult
2733+
// handles both.
2734+
letresult;
2735+
try{
2736+
result=controller[kState].pullAlgorithm(controller);
2737+
}catch(error){
2738+
result=PromiseReject(error);
2739+
}
2740+
thenAlgorithmResult(
2741+
result,
27042742
controller[kState].pullFulfilled,
27052743
controller[kState].pullRejected);
27062744
}
@@ -2826,7 +2864,7 @@ function setupReadableStreamDefaultControllerFromSource(
28262864
FunctionPrototypeBind(start,source,controller) :
28272865
nonOpStart;
28282866
constpullAlgorithm=pull ?
2829-
createPromiseCallback1Param('source.pull',pull,source) :
2867+
createRawCallback1Param('source.pull',pull,source) :
28302868
nonOpPull;
28312869
constcancelAlgorithm=cancel ?
28322870
createPromiseCallback1Param('source.cancel',cancel,source) :
@@ -3519,8 +3557,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
35193557
controller[kState].pullRejected=
35203558
(error)=>readableByteStreamControllerError(controller,error);
35213559
}
3522-
PromisePrototypeThen(
3523-
controller[kState].pullAlgorithm(controller),
3560+
// See readableStreamDefaultControllerPull for the raw-callback contract.
3561+
letresult;
3562+
try{
3563+
result=controller[kState].pullAlgorithm(controller);
3564+
}catch(error){
3565+
result=PromiseReject(error);
3566+
}
3567+
thenAlgorithmResult(
3568+
result,
35243569
controller[kState].pullFulfilled,
35253570
controller[kState].pullRejected);
35263571
}
@@ -3700,7 +3745,7 @@ function setupReadableByteStreamControllerFromSource(
37003745
FunctionPrototypeBind(start,source,controller) :
37013746
nonOpStart;
37023747
constpullAlgorithm=pull ?
3703-
createPromiseCallback1Param('source.pull',pull,source) :
3748+
createRawCallback1Param('source.pull',pull,source) :
37043749
nonOpPull;
37053750
constcancelAlgorithm=cancel ?
37063751
createPromiseCallback1Param('source.cancel',cancel,source) :

β€Žlib/internal/webstreams/util.jsβ€Ž

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
338338
returnasync()=>FunctionPrototypeCall(fn,thisArg);
339339
}
340340

341+
// Raw variants that skip the async wrapper's implicit result promise.
342+
// Consumers of a raw callback invoke it inside try/catch and route the
343+
// result through thenAlgorithmResult() below.
344+
functioncreateRawCallback1Param(name,fn,thisArg){
345+
validateFunction(fn,name);
346+
return(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
347+
}
348+
349+
functioncreateRawCallback2Params(name,fn,thisArg){
350+
validateFunction(fn,name);
351+
return(arg1,arg2)=>FunctionPrototypeCall(fn,thisArg,arg1,arg2);
352+
}
353+
354+
// A single shared, forever-resolved promise used to enqueue a reaction at
355+
// the next microtask checkpoint without allocating a fresh promise.
356+
constkResolvedPromise=PromiseResolve();
357+
358+
// Wires the (possibly non-thenable) result of an underlying algorithm
359+
// callback to its fulfilled/rejected continuations. A non-thenable result
360+
// means fulfillment is guaranteed and no then() lookup is observable, so
361+
// the fulfillment step is enqueued directly at the exact microtask
362+
// position the coerced promise's reaction would have had, skipping the
363+
// per-chunk promise allocation. For thenable results PromiseResolve()
364+
// matches the spec's "a promise resolved with" conversion (identity for
365+
// native promises).
366+
functionthenAlgorithmResult(result,onFulfilled,onRejected){
367+
if(result===null||
368+
(typeofresult!=='object'&&typeofresult!=='function')){
369+
PromisePrototypeThen(kResolvedPromise,onFulfilled);
370+
}else{
371+
PromisePrototypeThen(PromiseResolve(result),onFulfilled,onRejected);
372+
}
373+
}
374+
341375
functioncreatePromiseCallback1Param(name,fn,thisArg){
342376
validateFunction(fn,name);
343377
returnasync(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
@@ -386,11 +420,14 @@ async function nonOpFlush() {}
386420

387421
functionnonOpStart(){}
388422

389-
asyncfunctionnonOpPull(){}
423+
// nonOpPull and nonOpWrite are raw callbacks (see createRawCallback*):
424+
// their non-thenable return takes the allocation-free fast path in
425+
// thenAlgorithmResult().
426+
functionnonOpPull(){}
390427

391428
asyncfunctionnonOpCancel(){}
392429

393-
asyncfunctionnonOpWrite(){}
430+
functionnonOpWrite(){}
394431

395432
lettransfer;
396433
functionlazyTransfer(){
@@ -411,6 +448,8 @@ module.exports = {
411448
createPromiseCallbackNoParams,
412449
createPromiseCallback1Param,
413450
createPromiseCallback2Params,
451+
createRawCallback1Param,
452+
createRawCallback2Params,
414453
customInspect,
415454
defaultSizeAlgorithm,
416455
dequeueValue,
@@ -421,6 +460,7 @@ module.exports = {
421460
isBrandCheck,
422461
isPromisePending,
423462
kEmptyQueue,
463+
kResolvedPromise,
424464
kState,
425465
kType,
426466
lazyTransfer,
@@ -435,4 +475,5 @@ module.exports = {
435475
resetQueue,
436476
resolvedRecord,
437477
setPromiseHandled,
478+
thenAlgorithmResult,
438479
};

β€Žlib/internal/webstreams/writablestream.jsβ€Ž

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const {
5656
Queue,
5757
createPromiseCallbackNoParams,
5858
createPromiseCallback1Param,
59-
createPromiseCallback2Params,
59+
createRawCallback2Params,
6060
customInspect,
6161
defaultSizeAlgorithm,
6262
dequeueValue,
@@ -78,6 +78,7 @@ const {
7878
resetQueue,
7979
resolvedRecord,
8080
setPromiseHandled,
81+
thenAlgorithmResult,
8182
}=require('internal/webstreams/util');
8283

8384
const{
@@ -769,7 +770,12 @@ function writableStreamUpdateBackpressure(controller, streamState) {
769770
constbackpressure=
770771
controllerState.highWaterMark-controllerState.queueTotalSize<=0;
771772
constwriter=streamState.writer;
772-
if(writer!==undefined&&streamState.backpressure!==backpressure){
773+
constchanged=streamState.backpressure!==backpressure;
774+
// The state field is published before the ready record is resolved so
775+
// that a ready resolve hook (pipeTo's pump continuation) observes the
776+
// new value.
777+
streamState.backpressure=backpressure;
778+
if(writer!==undefined&&changed){
773779
if(backpressure){
774780
// The spec replaces [[readyPromise]] with a fresh pending promise;
775781
// dropping the cache lets the next observation derive it.
@@ -778,7 +784,6 @@ function writableStreamUpdateBackpressure(controller, streamState) {
778784
writer[kState].ready?.resolve();
779785
}
780786
}
781-
streamState.backpressure=backpressure;
782787
}
783788

784789
functionwritableStreamStartErroring(stream,reason){
@@ -1197,8 +1202,18 @@ function writableStreamDefaultControllerProcessWrite(controller, chunk) {
11971202
};
11981203
}
11991204

1200-
PromisePrototypeThen(
1201-
writeAlgorithm(chunk,controller),
1205+
// The write algorithm may be a raw callback (a wrapped user sink.write
1206+
// returns its result uncoerced; a synchronous throw surfaces here) or an
1207+
// internal algorithm that always returns a promise; thenAlgorithmResult
1208+
// handles both.
1209+
letresult;
1210+
try{
1211+
result=writeAlgorithm(chunk,controller);
1212+
}catch(error){
1213+
result=PromiseReject(error);
1214+
}
1215+
thenAlgorithmResult(
1216+
result,
12021217
controller[kState].writeFulfilled,
12031218
controller[kState].writeRejected);
12041219
}
@@ -1323,7 +1338,7 @@ function setupWritableStreamDefaultControllerFromSink(
13231338
FunctionPrototypeBind(start,sink,controller) :
13241339
nonOpStart;
13251340
constwriteAlgorithm=write ?
1326-
createPromiseCallback2Params('sink.write',write,sink) :
1341+
createRawCallback2Params('sink.write',write,sink) :
13271342
nonOpWrite;
13281343
constcloseAlgorithm=close ?
13291344
createPromiseCallbackNoParams('sink.close',close,sink) :

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit f7e0c81

Browse files
mcollinaaduh95
authored andcommitted
stream: cut promise churn in webstreams hot paths
Three related reductions on the per-chunk paths: Wrap user sink.write and source.pull callbacks without coercing their result into a promise. When the callback returns a non-thenable (the common synchronous case), fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had, skipping the implicit async-wrapper promise per chunk. Thenable results go through PromiseResolve(), which matches the spec's "a promise resolved with" conversion (identity for native promises). Park pipeTo's pump on backpressure by installing a record that duck-types the writer's lazily-materialized [[readyPromise]] record and whose resolve function is the pump continuation itself. Backpressure clearing then resumes the pump directly instead of materializing a fresh promise record plus reaction per flip, and the pump no longer schedules a microtask per batch. writableStreamUpdateBackpressure publishes the new backpressure state before resolving the ready record so the pump observes the updated value. Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths with a reaction on the shared resolved promise, which enqueues the continuation at the same position without the per-call scheduling overhead. pipe-to improves by 8-14% across all benchmark configurations, with readable-read and tee also improving in spot runs. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65138 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 562168f commit f7e0c81

3 files changed

Lines changed: 126 additions & 25 deletions

File tree

β€Žlib/internal/webstreams/readablestream.jsβ€Ž

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
cloneAsUint8Array,
102102
copyArrayBuffer,
103103
createPromiseCallback1Param,
104+
createRawCallback1Param,
104105
customInspect,
105106
defaultSizeAlgorithm,
106107
dequeueValue,
@@ -110,6 +111,7 @@ const {
110111
getNonWritablePropertyDescriptor,
111112
isBrandCheck,
112113
kEmptyQueue,
114+
kResolvedPromise,
113115
kState,
114116
kType,
115117
lazyTransfer,
@@ -121,6 +123,7 @@ const {
121123
resetQueue,
122124
resolvedRecord,
123125
setPromiseHandled,
126+
thenAlgorithmResult,
124127
}=require('internal/webstreams/util');
125128

126129
const{
@@ -137,7 +140,6 @@ const {
137140
writableStreamDefaultWriterRelease,
138141
writableStreamDefaultWriterWriteWithRequest,
139142
writerClosedPromise,
140-
writerReadyPromise,
141143
}=require('internal/webstreams/writablestream');
142144

143145
const{ Buffer }=require('buffer');
@@ -1664,11 +1666,31 @@ function readableStreamPipeTo(
16641666
// the chunk travels through `pendingChunk`.
16651667
letpendingChunk;
16661668
letreadRequest;
1669+
letreadyHook;
16671670

16681671
// Ready promise rejection is handled by the destination-errored
16691672
// watcher.
16701673
functionignoreReadyRejection(){}
16711674

1675+
// Parks the pump on the destination's backpressure by installing a
1676+
// record that duck-types the writer's lazily-materialized
1677+
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
1678+
// when backpressure clears (after publishing the new backpressure
1679+
// state), which re-enters the pump directly instead of rotating a
1680+
// fresh promise record plus reaction per flip. The pipe holds the only
1681+
// reference to the writer, so the record is never observable as a real
1682+
// ready promise; the erroring/release paths probe `promise` via
1683+
// isPromisePending() and call `reject`, so it carries a real
1684+
// forever-pending promise and a no-op reject.
1685+
functionparkOnReady(){
1686+
readyHook??={
1687+
promise: newPromise(nonOpCallback),
1688+
resolve: pump,
1689+
reject: ignoreReadyRejection,
1690+
};
1691+
writer[kState].ready=readyHook;
1692+
}
1693+
16721694
functionforwardChunk(){
16731695
constchunk=pendingChunk;
16741696
pendingChunk=undefined;
@@ -1680,10 +1702,7 @@ function readableStreamPipeTo(
16801702
if(shuttingDown)return;
16811703

16821704
if(dest[kState].backpressure){
1683-
PromisePrototypeThen(
1684-
writerReadyPromise(writer).promise,
1685-
pump,
1686-
ignoreReadyRejection);
1705+
parkOnReady();
16871706
return;
16881707
}
16891708

@@ -1728,9 +1747,18 @@ function readableStreamPipeTo(
17281747
return;
17291748
}
17301749

1731-
// Yield to microtask queue between batches to allow events/signals
1732-
// to fire
1733-
queueMicrotask(pump);
1750+
// Park on backpressure directly: the ready hook resumes the pump
1751+
// when a completed write clears it.
1752+
if(dest[kState].backpressure){
1753+
parkOnReady();
1754+
return;
1755+
}
1756+
1757+
// Yield to the microtask queue between batches so completed-write
1758+
// reactions and events/signals fire; a shared resolved promise
1759+
// enqueues the continuation at the same position as queueMicrotask
1760+
// without the per-batch scheduling overhead.
1761+
PromisePrototypeThen(kResolvedPromise,pump);
17341762
return;
17351763
}
17361764

@@ -1742,7 +1770,7 @@ function readableStreamPipeTo(
17421770
// synchronous write during enqueue(). See WHATWG Streams spec
17431771
// "ReadableStreamPipeTo" step 15's "chunk steps".
17441772
pendingChunk=chunk;
1745-
queueMicrotask(forwardChunk);
1773+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
17461774
},
17471775
[kClose](){},
17481776
[kError](){},
@@ -1857,7 +1885,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
18571885
// The microtask is required by the spec (ReadableStreamTee's
18581886
// "chunk steps" queue one).
18591887
pendingChunk=value;
1860-
queueMicrotask(forwardChunk);
1888+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
18611889
},
18621890
[kClose](){
18631891
// The `process.nextTick()` is not part of the spec.
@@ -2010,7 +2038,7 @@ function readableByteStreamTee(stream) {
20102038
defaultReadRequest??={
20112039
[kChunk](chunk){
20122040
pendingChunk=chunk;
2013-
queueMicrotask(forwardChunk);
2041+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
20142042
},
20152043
[kClose](){
20162044
reading=false;
@@ -2699,8 +2727,18 @@ function readableStreamDefaultControllerPull(controller) {
26992727
controller[kState].pullRejected=
27002728
(error)=>readableStreamDefaultControllerError(controller,error);
27012729
}
2702-
PromisePrototypeThen(
2703-
controller[kState].pullAlgorithm(controller),
2730+
// The pull algorithm may be a raw callback (a wrapped user source.pull
2731+
// returns its result uncoerced; a synchronous throw surfaces here) or an
2732+
// internal algorithm that always returns a promise; thenAlgorithmResult
2733+
// handles both.
2734+
letresult;
2735+
try{
2736+
result=controller[kState].pullAlgorithm(controller);
2737+
}catch(error){
2738+
result=PromiseReject(error);
2739+
}
2740+
thenAlgorithmResult(
2741+
result,
27042742
controller[kState].pullFulfilled,
27052743
controller[kState].pullRejected);
27062744
}
@@ -2826,7 +2864,7 @@ function setupReadableStreamDefaultControllerFromSource(
28262864
FunctionPrototypeBind(start,source,controller) :
28272865
nonOpStart;
28282866
constpullAlgorithm=pull ?
2829-
createPromiseCallback1Param('source.pull',pull,source) :
2867+
createRawCallback1Param('source.pull',pull,source) :
28302868
nonOpPull;
28312869
constcancelAlgorithm=cancel ?
28322870
createPromiseCallback1Param('source.cancel',cancel,source) :
@@ -3519,8 +3557,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
35193557
controller[kState].pullRejected=
35203558
(error)=>readableByteStreamControllerError(controller,error);
35213559
}
3522-
PromisePrototypeThen(
3523-
controller[kState].pullAlgorithm(controller),
3560+
// See readableStreamDefaultControllerPull for the raw-callback contract.
3561+
letresult;
3562+
try{
3563+
result=controller[kState].pullAlgorithm(controller);
3564+
}catch(error){
3565+
result=PromiseReject(error);
3566+
}
3567+
thenAlgorithmResult(
3568+
result,
35243569
controller[kState].pullFulfilled,
35253570
controller[kState].pullRejected);
35263571
}
@@ -3700,7 +3745,7 @@ function setupReadableByteStreamControllerFromSource(
37003745
FunctionPrototypeBind(start,source,controller) :
37013746
nonOpStart;
37023747
constpullAlgorithm=pull ?
3703-
createPromiseCallback1Param('source.pull',pull,source) :
3748+
createRawCallback1Param('source.pull',pull,source) :
37043749
nonOpPull;
37053750
constcancelAlgorithm=cancel ?
37063751
createPromiseCallback1Param('source.cancel',cancel,source) :

β€Žlib/internal/webstreams/util.jsβ€Ž

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
338338
returnasync()=>FunctionPrototypeCall(fn,thisArg);
339339
}
340340

341+
// Raw variants that skip the async wrapper's implicit result promise.
342+
// Consumers of a raw callback invoke it inside try/catch and route the
343+
// result through thenAlgorithmResult() below.
344+
functioncreateRawCallback1Param(name,fn,thisArg){
345+
validateFunction(fn,name);
346+
return(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
347+
}
348+
349+
functioncreateRawCallback2Params(name,fn,thisArg){
350+
validateFunction(fn,name);
351+
return(arg1,arg2)=>FunctionPrototypeCall(fn,thisArg,arg1,arg2);
352+
}
353+
354+
// A single shared, forever-resolved promise used to enqueue a reaction at
355+
// the next microtask checkpoint without allocating a fresh promise.
356+
constkResolvedPromise=PromiseResolve();
357+
358+
// Wires the (possibly non-thenable) result of an underlying algorithm
359+
// callback to its fulfilled/rejected continuations. A non-thenable result
360+
// means fulfillment is guaranteed and no then() lookup is observable, so
361+
// the fulfillment step is enqueued directly at the exact microtask
362+
// position the coerced promise's reaction would have had, skipping the
363+
// per-chunk promise allocation. For thenable results PromiseResolve()
364+
// matches the spec's "a promise resolved with" conversion (identity for
365+
// native promises).
366+
functionthenAlgorithmResult(result,onFulfilled,onRejected){
367+
if(result===null||
368+
(typeofresult!=='object'&&typeofresult!=='function')){
369+
PromisePrototypeThen(kResolvedPromise,onFulfilled);
370+
}else{
371+
PromisePrototypeThen(PromiseResolve(result),onFulfilled,onRejected);
372+
}
373+
}
374+
341375
functioncreatePromiseCallback1Param(name,fn,thisArg){
342376
validateFunction(fn,name);
343377
returnasync(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
@@ -386,11 +420,14 @@ async function nonOpFlush() {}
386420

387421
functionnonOpStart(){}
388422

389-
asyncfunctionnonOpPull(){}
423+
// nonOpPull and nonOpWrite are raw callbacks (see createRawCallback*):
424+
// their non-thenable return takes the allocation-free fast path in
425+
// thenAlgorithmResult().
426+
functionnonOpPull(){}
390427

391428
asyncfunctionnonOpCancel(){}
392429

393-
asyncfunctionnonOpWrite(){}
430+
functionnonOpWrite(){}
394431

395432
lettransfer;
396433
functionlazyTransfer(){
@@ -411,6 +448,8 @@ module.exports = {
411448
createPromiseCallbackNoParams,
412449
createPromiseCallback1Param,
413450
createPromiseCallback2Params,
451+
createRawCallback1Param,
452+
createRawCallback2Params,
414453
customInspect,
415454
defaultSizeAlgorithm,
416455
dequeueValue,
@@ -421,6 +460,7 @@ module.exports = {
421460
isBrandCheck,
422461
isPromisePending,
423462
kEmptyQueue,
463+
kResolvedPromise,
424464
kState,
425465
kType,
426466
lazyTransfer,
@@ -435,4 +475,5 @@ module.exports = {
435475
resetQueue,
436476
resolvedRecord,
437477
setPromiseHandled,
478+
thenAlgorithmResult,
438479
};

β€Žlib/internal/webstreams/writablestream.jsβ€Ž

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const {
5656
Queue,
5757
createPromiseCallbackNoParams,
5858
createPromiseCallback1Param,
59-
createPromiseCallback2Params,
59+
createRawCallback2Params,
6060
customInspect,
6161
defaultSizeAlgorithm,
6262
dequeueValue,
@@ -78,6 +78,7 @@ const {
7878
resetQueue,
7979
resolvedRecord,
8080
setPromiseHandled,
81+
thenAlgorithmResult,
8182
}=require('internal/webstreams/util');
8283

8384
const{
@@ -769,7 +770,12 @@ function writableStreamUpdateBackpressure(controller, streamState) {
769770
constbackpressure=
770771
controllerState.highWaterMark-controllerState.queueTotalSize<=0;
771772
constwriter=streamState.writer;
772-
if(writer!==undefined&&streamState.backpressure!==backpressure){
773+
constchanged=streamState.backpressure!==backpressure;
774+
// The state field is published before the ready record is resolved so
775+
// that a ready resolve hook (pipeTo's pump continuation) observes the
776+
// new value.
777+
streamState.backpressure=backpressure;
778+
if(writer!==undefined&&changed){
773779
if(backpressure){
774780
// The spec replaces [[readyPromise]] with a fresh pending promise;
775781
// dropping the cache lets the next observation derive it.
@@ -778,7 +784,6 @@ function writableStreamUpdateBackpressure(controller, streamState) {
778784
writer[kState].ready?.resolve();
779785
}
780786
}
781-
streamState.backpressure=backpressure;
782787
}
783788

784789
functionwritableStreamStartErroring(stream,reason){
@@ -1197,8 +1202,18 @@ function writableStreamDefaultControllerProcessWrite(controller, chunk) {
11971202
};
11981203
}
11991204

1200-
PromisePrototypeThen(
1201-
writeAlgorithm(chunk,controller),
1205+
// The write algorithm may be a raw callback (a wrapped user sink.write
1206+
// returns its result uncoerced; a synchronous throw surfaces here) or an
1207+
// internal algorithm that always returns a promise; thenAlgorithmResult
1208+
// handles both.
1209+
letresult;
1210+
try{
1211+
result=writeAlgorithm(chunk,controller);
1212+
}catch(error){
1213+
result=PromiseReject(error);
1214+
}
1215+
thenAlgorithmResult(
1216+
result,
12021217
controller[kState].writeFulfilled,
12031218
controller[kState].writeRejected);
12041219
}
@@ -1323,7 +1338,7 @@ function setupWritableStreamDefaultControllerFromSink(
13231338
FunctionPrototypeBind(start,sink,controller) :
13241339
nonOpStart;
13251340
constwriteAlgorithm=write ?
1326-
createPromiseCallback2Params('sink.write',write,sink) :
1341+
createRawCallback2Params('sink.write',write,sink) :
13271342
nonOpWrite;
13281343
constcloseAlgorithm=close ?
13291344
createPromiseCallbackNoParams('sink.close',close,sink) :

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit f7e0c81

Browse files
mcollinaaduh95
authored andcommitted
stream: cut promise churn in webstreams hot paths
Three related reductions on the per-chunk paths: Wrap user sink.write and source.pull callbacks without coercing their result into a promise. When the callback returns a non-thenable (the common synchronous case), fulfillment is guaranteed and no then() lookup is observable, so the fulfilled reaction is enqueued through a single shared resolved promise at the exact microtask position the coerced promise's reaction would have had, skipping the implicit async-wrapper promise per chunk. Thenable results go through PromiseResolve(), which matches the spec's "a promise resolved with" conversion (identity for native promises). Park pipeTo's pump on backpressure by installing a record that duck-types the writer's lazily-materialized [[readyPromise]] record and whose resolve function is the pump continuation itself. Backpressure clearing then resumes the pump directly instead of materializing a fresh promise record plus reaction per flip, and the pump no longer schedules a microtask per batch. writableStreamUpdateBackpressure publishes the new backpressure state before resolving the ready record so the pump observes the updated value. Replace queueMicrotask() on the pipeTo and tee chunk-forwarding paths with a reaction on the shared resolved promise, which enqueues the continuation at the same position without the per-call scheduling overhead. pipe-to improves by 8-14% across all benchmark configurations, with readable-read and tee also improving in spot runs. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65138 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 562168f commit f7e0c81

3 files changed

Lines changed: 126 additions & 25 deletions

File tree

β€Žlib/internal/webstreams/readablestream.jsβ€Ž

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const {
101101
cloneAsUint8Array,
102102
copyArrayBuffer,
103103
createPromiseCallback1Param,
104+
createRawCallback1Param,
104105
customInspect,
105106
defaultSizeAlgorithm,
106107
dequeueValue,
@@ -110,6 +111,7 @@ const {
110111
getNonWritablePropertyDescriptor,
111112
isBrandCheck,
112113
kEmptyQueue,
114+
kResolvedPromise,
113115
kState,
114116
kType,
115117
lazyTransfer,
@@ -121,6 +123,7 @@ const {
121123
resetQueue,
122124
resolvedRecord,
123125
setPromiseHandled,
126+
thenAlgorithmResult,
124127
}=require('internal/webstreams/util');
125128

126129
const{
@@ -137,7 +140,6 @@ const {
137140
writableStreamDefaultWriterRelease,
138141
writableStreamDefaultWriterWriteWithRequest,
139142
writerClosedPromise,
140-
writerReadyPromise,
141143
}=require('internal/webstreams/writablestream');
142144

143145
const{ Buffer }=require('buffer');
@@ -1664,11 +1666,31 @@ function readableStreamPipeTo(
16641666
// the chunk travels through `pendingChunk`.
16651667
letpendingChunk;
16661668
letreadRequest;
1669+
letreadyHook;
16671670

16681671
// Ready promise rejection is handled by the destination-errored
16691672
// watcher.
16701673
functionignoreReadyRejection(){}
16711674

1675+
// Parks the pump on the destination's backpressure by installing a
1676+
// record that duck-types the writer's lazily-materialized
1677+
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
1678+
// when backpressure clears (after publishing the new backpressure
1679+
// state), which re-enters the pump directly instead of rotating a
1680+
// fresh promise record plus reaction per flip. The pipe holds the only
1681+
// reference to the writer, so the record is never observable as a real
1682+
// ready promise; the erroring/release paths probe `promise` via
1683+
// isPromisePending() and call `reject`, so it carries a real
1684+
// forever-pending promise and a no-op reject.
1685+
functionparkOnReady(){
1686+
readyHook??={
1687+
promise: newPromise(nonOpCallback),
1688+
resolve: pump,
1689+
reject: ignoreReadyRejection,
1690+
};
1691+
writer[kState].ready=readyHook;
1692+
}
1693+
16721694
functionforwardChunk(){
16731695
constchunk=pendingChunk;
16741696
pendingChunk=undefined;
@@ -1680,10 +1702,7 @@ function readableStreamPipeTo(
16801702
if(shuttingDown)return;
16811703

16821704
if(dest[kState].backpressure){
1683-
PromisePrototypeThen(
1684-
writerReadyPromise(writer).promise,
1685-
pump,
1686-
ignoreReadyRejection);
1705+
parkOnReady();
16871706
return;
16881707
}
16891708

@@ -1728,9 +1747,18 @@ function readableStreamPipeTo(
17281747
return;
17291748
}
17301749

1731-
// Yield to microtask queue between batches to allow events/signals
1732-
// to fire
1733-
queueMicrotask(pump);
1750+
// Park on backpressure directly: the ready hook resumes the pump
1751+
// when a completed write clears it.
1752+
if(dest[kState].backpressure){
1753+
parkOnReady();
1754+
return;
1755+
}
1756+
1757+
// Yield to the microtask queue between batches so completed-write
1758+
// reactions and events/signals fire; a shared resolved promise
1759+
// enqueues the continuation at the same position as queueMicrotask
1760+
// without the per-batch scheduling overhead.
1761+
PromisePrototypeThen(kResolvedPromise,pump);
17341762
return;
17351763
}
17361764

@@ -1742,7 +1770,7 @@ function readableStreamPipeTo(
17421770
// synchronous write during enqueue(). See WHATWG Streams spec
17431771
// "ReadableStreamPipeTo" step 15's "chunk steps".
17441772
pendingChunk=chunk;
1745-
queueMicrotask(forwardChunk);
1773+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
17461774
},
17471775
[kClose](){},
17481776
[kError](){},
@@ -1857,7 +1885,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
18571885
// The microtask is required by the spec (ReadableStreamTee's
18581886
// "chunk steps" queue one).
18591887
pendingChunk=value;
1860-
queueMicrotask(forwardChunk);
1888+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
18611889
},
18621890
[kClose](){
18631891
// The `process.nextTick()` is not part of the spec.
@@ -2010,7 +2038,7 @@ function readableByteStreamTee(stream) {
20102038
defaultReadRequest??={
20112039
[kChunk](chunk){
20122040
pendingChunk=chunk;
2013-
queueMicrotask(forwardChunk);
2041+
PromisePrototypeThen(kResolvedPromise,forwardChunk);
20142042
},
20152043
[kClose](){
20162044
reading=false;
@@ -2699,8 +2727,18 @@ function readableStreamDefaultControllerPull(controller) {
26992727
controller[kState].pullRejected=
27002728
(error)=>readableStreamDefaultControllerError(controller,error);
27012729
}
2702-
PromisePrototypeThen(
2703-
controller[kState].pullAlgorithm(controller),
2730+
// The pull algorithm may be a raw callback (a wrapped user source.pull
2731+
// returns its result uncoerced; a synchronous throw surfaces here) or an
2732+
// internal algorithm that always returns a promise; thenAlgorithmResult
2733+
// handles both.
2734+
letresult;
2735+
try{
2736+
result=controller[kState].pullAlgorithm(controller);
2737+
}catch(error){
2738+
result=PromiseReject(error);
2739+
}
2740+
thenAlgorithmResult(
2741+
result,
27042742
controller[kState].pullFulfilled,
27052743
controller[kState].pullRejected);
27062744
}
@@ -2826,7 +2864,7 @@ function setupReadableStreamDefaultControllerFromSource(
28262864
FunctionPrototypeBind(start,source,controller) :
28272865
nonOpStart;
28282866
constpullAlgorithm=pull ?
2829-
createPromiseCallback1Param('source.pull',pull,source) :
2867+
createRawCallback1Param('source.pull',pull,source) :
28302868
nonOpPull;
28312869
constcancelAlgorithm=cancel ?
28322870
createPromiseCallback1Param('source.cancel',cancel,source) :
@@ -3519,8 +3557,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
35193557
controller[kState].pullRejected=
35203558
(error)=>readableByteStreamControllerError(controller,error);
35213559
}
3522-
PromisePrototypeThen(
3523-
controller[kState].pullAlgorithm(controller),
3560+
// See readableStreamDefaultControllerPull for the raw-callback contract.
3561+
letresult;
3562+
try{
3563+
result=controller[kState].pullAlgorithm(controller);
3564+
}catch(error){
3565+
result=PromiseReject(error);
3566+
}
3567+
thenAlgorithmResult(
3568+
result,
35243569
controller[kState].pullFulfilled,
35253570
controller[kState].pullRejected);
35263571
}
@@ -3700,7 +3745,7 @@ function setupReadableByteStreamControllerFromSource(
37003745
FunctionPrototypeBind(start,source,controller) :
37013746
nonOpStart;
37023747
constpullAlgorithm=pull ?
3703-
createPromiseCallback1Param('source.pull',pull,source) :
3748+
createRawCallback1Param('source.pull',pull,source) :
37043749
nonOpPull;
37053750
constcancelAlgorithm=cancel ?
37063751
createPromiseCallback1Param('source.cancel',cancel,source) :

β€Žlib/internal/webstreams/util.jsβ€Ž

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
338338
returnasync()=>FunctionPrototypeCall(fn,thisArg);
339339
}
340340

341+
// Raw variants that skip the async wrapper's implicit result promise.
342+
// Consumers of a raw callback invoke it inside try/catch and route the
343+
// result through thenAlgorithmResult() below.
344+
functioncreateRawCallback1Param(name,fn,thisArg){
345+
validateFunction(fn,name);
346+
return(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
347+
}
348+
349+
functioncreateRawCallback2Params(name,fn,thisArg){
350+
validateFunction(fn,name);
351+
return(arg1,arg2)=>FunctionPrototypeCall(fn,thisArg,arg1,arg2);
352+
}
353+
354+
// A single shared, forever-resolved promise used to enqueue a reaction at
355+
// the next microtask checkpoint without allocating a fresh promise.
356+
constkResolvedPromise=PromiseResolve();
357+
358+
// Wires the (possibly non-thenable) result of an underlying algorithm
359+
// callback to its fulfilled/rejected continuations. A non-thenable result
360+
// means fulfillment is guaranteed and no then() lookup is observable, so
361+
// the fulfillment step is enqueued directly at the exact microtask
362+
// position the coerced promise's reaction would have had, skipping the
363+
// per-chunk promise allocation. For thenable results PromiseResolve()
364+
// matches the spec's "a promise resolved with" conversion (identity for
365+
// native promises).
366+
functionthenAlgorithmResult(result,onFulfilled,onRejected){
367+
if(result===null||
368+
(typeofresult!=='object'&&typeofresult!=='function')){
369+
PromisePrototypeThen(kResolvedPromise,onFulfilled);
370+
}else{
371+
PromisePrototypeThen(PromiseResolve(result),onFulfilled,onRejected);
372+
}
373+
}
374+
341375
functioncreatePromiseCallback1Param(name,fn,thisArg){
342376
validateFunction(fn,name);
343377
returnasync(arg)=>FunctionPrototypeCall(fn,thisArg,arg);
@@ -386,11 +420,14 @@ async function nonOpFlush() {}
386420

387421
functionnonOpStart(){}
388422

389-
asyncfunctionnonOpPull(){}
423+
// nonOpPull and nonOpWrite are raw callbacks (see createRawCallback*):
424+
// their non-thenable return takes the allocation-free fast path in
425+
// thenAlgorithmResult().
426+
functionnonOpPull(){}
390427

391428
asyncfunctionnonOpCancel(){}
392429

393-
asyncfunctionnonOpWrite(){}
430+
functionnonOpWrite(){}
394431

395432
lettransfer;
396433
functionlazyTransfer(){
@@ -411,6 +448,8 @@ module.exports = {
411448
createPromiseCallbackNoParams,
412449
createPromiseCallback1Param,
413450
createPromiseCallback2Params,
451+
createRawCallback1Param,
452+
createRawCallback2Params,
414453
customInspect,
415454
defaultSizeAlgorithm,
416455
dequeueValue,
@@ -421,6 +460,7 @@ module.exports = {
421460
isBrandCheck,
422461
isPromisePending,
423462
kEmptyQueue,
463+
kResolvedPromise,
424464
kState,
425465
kType,
426466
lazyTransfer,
@@ -435,4 +475,5 @@ module.exports = {
435475
resetQueue,
436476
resolvedRecord,
437477
setPromiseHandled,
478+
thenAlgorithmResult,
438479
};

β€Žlib/internal/webstreams/writablestream.jsβ€Ž

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const {
5656
Queue,
5757
createPromiseCallbackNoParams,
5858
createPromiseCallback1Param,
59-
createPromiseCallback2Params,
59+
createRawCallback2Params,
6060
customInspect,
6161
defaultSizeAlgorithm,
6262
dequeueValue,
@@ -78,6 +78,7 @@ const {
7878
resetQueue,
7979
resolvedRecord,
8080
setPromiseHandled,
81+
thenAlgorithmResult,
8182
}=require('internal/webstreams/util');
8283

8384
const{
@@ -769,7 +770,12 @@ function writableStreamUpdateBackpressure(controller, streamState) {
769770
constbackpressure=
770771
controllerState.highWaterMark-controllerState.queueTotalSize<=0;
771772
constwriter=streamState.writer;
772-
if(writer!==undefined&&streamState.backpressure!==backpressure){
773+
constchanged=streamState.backpressure!==backpressure;
774+
// The state field is published before the ready record is resolved so
775+
// that a ready resolve hook (pipeTo's pump continuation) observes the
776+
// new value.
777+
streamState.backpressure=backpressure;
778+
if(writer!==undefined&&changed){
773779
if(backpressure){
774780
// The spec replaces [[readyPromise]] with a fresh pending promise;
775781
// dropping the cache lets the next observation derive it.
@@ -778,7 +784,6 @@ function writableStreamUpdateBackpressure(controller, streamState) {
778784
writer[kState].ready?.resolve();
779785
}
780786
}
781-
streamState.backpressure=backpressure;
782787
}
783788

784789
functionwritableStreamStartErroring(stream,reason){
@@ -1197,8 +1202,18 @@ function writableStreamDefaultControllerProcessWrite(controller, chunk) {
11971202
};
11981203
}
11991204

1200-
PromisePrototypeThen(
1201-
writeAlgorithm(chunk,controller),
1205+
// The write algorithm may be a raw callback (a wrapped user sink.write
1206+
// returns its result uncoerced; a synchronous throw surfaces here) or an
1207+
// internal algorithm that always returns a promise; thenAlgorithmResult
1208+
// handles both.
1209+
letresult;
1210+
try{
1211+
result=writeAlgorithm(chunk,controller);
1212+
}catch(error){
1213+
result=PromiseReject(error);
1214+
}
1215+
thenAlgorithmResult(
1216+
result,
12021217
controller[kState].writeFulfilled,
12031218
controller[kState].writeRejected);
12041219
}
@@ -1323,7 +1338,7 @@ function setupWritableStreamDefaultControllerFromSink(
13231338
FunctionPrototypeBind(start,sink,controller) :
13241339
nonOpStart;
13251340
constwriteAlgorithm=write ?
1326-
createPromiseCallback2Params('sink.write',write,sink) :
1341+
createRawCallback2Params('sink.write',write,sink) :
13271342
nonOpWrite;
13281343
constcloseAlgorithm=close ?
13291344
createPromiseCallbackNoParams('sink.close',close,sink) :

0 commit comments

Comments
Β (0)