Skip to content

Commit cea71e0

Browse files
mcollinaclaude
andcommitted
src: cache readable controller pull-reaction functions
The per-pull hot path allocated two V8 Functions on every CallPullIfNeeded (the fulfil/reject reactions for the user pull() promise), via Function::New per chunk. Cache them per-controller (Data == the controller wrapper), created on first pull and reused thereafter; reset in ClearAlgorithms to break the controller<->wrapper cycle on terminal states. Effect (vs JS baseline, 30 runs): readable-read byob -42% -> -5%, normal -77% -> -49%, async-iterator -81% -> -64%, pipe-to -81% -> -75%. WPT streams (1404) + 39 webstream parallel tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e665464 commit cea71e0

2 files changed

Lines changed: 51 additions & 3 deletions

File tree

src/streams/readable_stream.cc

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ void ReactRejected(const FunctionCallbackInfo<Value>& args) {
184184
}
185185

186186
// Attaches fulfill/reject reactions that re-enter C++. The reaction functions
187-
// carry the controller wrapper as their Data.
187+
// carry the controller wrapper as their Data. Used by cold paths (start);
188+
// the per-pull hot path uses ThenReactCached below.
188189
void ThenReact(Environment* env,
189190
Local<Promise> promise,
190191
Local<Object> controller_obj,
@@ -200,12 +201,38 @@ void ThenReact(Environment* env,
200201
USE(promise->Then(context, ff, rj));
201202
}
202203

204+
// Like ThenReact, but reuses the controller's cached reaction functions
205+
// (creating them on first use), so the per-pull path allocates no V8 Function.
206+
void ThenReactCached(Environment* env,
207+
Local<Promise> promise,
208+
Local<Object> controller_obj,
209+
v8::FunctionCallback on_fulfilled,
210+
v8::Global<Function>* ff_slot,
211+
v8::Global<Function>* rj_slot) {
212+
Isolate* isolate = env->isolate();
213+
Local<Context> context = env->context();
214+
Local<Function> ff = ff_slot->Get(isolate);
215+
if (ff.IsEmpty()) {
216+
if (!Function::New(context, on_fulfilled, controller_obj).ToLocal(&ff))
217+
return;
218+
ff_slot->Reset(isolate, ff);
219+
}
220+
Local<Function> rj = rj_slot->Get(isolate);
221+
if (rj.IsEmpty()) {
222+
if (!Function::New(context, ReactRejected, controller_obj).ToLocal(&rj))
223+
return;
224+
rj_slot->Reset(isolate, rj);
225+
}
226+
USE(promise->Then(context, ff, rj));
227+
}
228+
203229
// A function that returns undefined; used both to map a promise's fulfilment
204230
// value to undefined and as a no-op rejection handler.
205231
void Noop(const FunctionCallbackInfo<Value>& args) {}
206232

207233
// Marks a promise as handled to avoid spurious unhandled-rejection warnings on
208234
// internal promises, mirroring `PromisePrototypeThen(p, undefined, () => {})`.
235+
// Cold path (close/error/release), so the per-call Function is acceptable.
209236
void MarkHandled(Environment* env, Local<Promise> promise) {
210237
Local<Context> context = env->context();
211238
Local<Function> noop;
@@ -478,13 +505,19 @@ void ReadableStreamDefaultController::CallPullIfNeeded() {
478505
return;
479506
// The pull algorithm is wrapped as an async function, so it returns a Promise.
480507
if (!result->IsPromise()) return;
481-
ThenReact(env, result.As<Promise>(), controller_obj, ReactPullFulfilled);
508+
ThenReactCached(env, result.As<Promise>(), controller_obj, ReactPullFulfilled,
509+
&on_pull_fulfilled_, &on_rejected_);
482510
}
483511

484512
void ReadableStreamDefaultController::ClearAlgorithms() {
485513
pull_algorithm_.Reset();
486514
cancel_algorithm_.Reset();
487515
size_algorithm_.Reset();
516+
// Break the controller<->wrapper cycle created by the cached reaction
517+
// functions (their Data is this controller's wrapper). No further pulls occur
518+
// after the algorithms are cleared, so they will not be recreated.
519+
on_pull_fulfilled_.Reset();
520+
on_rejected_.Reset();
488521
}
489522

490523
void ReadableStreamDefaultController::CloseInternal() {
@@ -1450,12 +1483,16 @@ void ReadableByteStreamController::CallPullIfNeeded() {
14501483
if (!pull->Call(context, Undefined(isolate), 1, argv).ToLocal(&result))
14511484
return;
14521485
if (!result->IsPromise()) return;
1453-
ThenReact(env, result.As<Promise>(), controller_obj, ReactPullFulfilled);
1486+
ThenReactCached(env, result.As<Promise>(), controller_obj, ReactPullFulfilled,
1487+
&on_pull_fulfilled_, &on_rejected_);
14541488
}
14551489

14561490
void ReadableByteStreamController::ClearAlgorithms() {
14571491
pull_algorithm_.Reset();
14581492
cancel_algorithm_.Reset();
1493+
// Break the controller<->wrapper cycle from the cached reaction functions.
1494+
on_pull_fulfilled_.Reset();
1495+
on_rejected_.Reset();
14591496
}
14601497

14611498
void ReadableByteStreamController::InvalidateBYOBRequest() {

src/streams/readable_stream.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ class ReadableStreamDefaultController final : public StreamBaseObject {
126126
v8::Global<v8::Function> pull_algorithm_;
127127
v8::Global<v8::Function> cancel_algorithm_;
128128
v8::Global<v8::Function> size_algorithm_;
129+
130+
// Cached promise-reaction functions for the pull hot path (Data == this
131+
// controller's wrapper). Created once on first pull and reused for every
132+
// subsequent pull, so the per-pull path allocates no V8 Function.
133+
v8::Global<v8::Function> on_pull_fulfilled_;
134+
v8::Global<v8::Function> on_rejected_;
129135
};
130136

131137
// ReadableStreamDefaultReader — holds the queue of pending read requests as
@@ -350,6 +356,11 @@ class ReadableByteStreamController final : public StreamBaseObject {
350356

351357
v8::Global<v8::Function> pull_algorithm_;
352358
v8::Global<v8::Function> cancel_algorithm_;
359+
360+
// Cached promise-reaction functions for the pull hot path (Data == this
361+
// controller's wrapper). Created once on first pull and reused thereafter.
362+
v8::Global<v8::Function> on_pull_fulfilled_;
363+
v8::Global<v8::Function> on_rejected_;
353364
};
354365

355366
// ReadableStreamBYOBRequest — a thin view onto the controller's first pending

0 commit comments

Comments
 (0)