Skip to content

Commit c0fceaa

Browse files
Stephen BelangerCommit Bot
authored andcommitted
Reland "[api] JSFunction PromiseHook for v8::Context"
This is a reland of d5457f5 after a speculative revert. Additionally it fixes an issue with throwing promise hooks. Original change's description: > [api] JSFunction PromiseHook for v8::Context > > This will enable Node.js to get much better performance from async_hooks > as currently PromiseHook delegates to C++ for the hook function and then > Node.js delegates it right back to JavaScript, introducing several > unnecessary barrier hops in code that gets called very, very frequently > in modern, promise-heavy applications. > > This API mirrors the form of the original C++ function based PromiseHook > API, however it is intentionally separate to allow it to use JSFunctions > triggered within generated code to, as much as possible, avoid entering > runtime functions entirely. > > Because PromiseHook has internal use also, beyond just the Node.js use, > I have opted to leave the existing API intact and keep this separate to > avoid conflicting with any possible behaviour expectations of other API > users. > > The design ideas for this new API stemmed from discussion with some V8 > team members at a previous Node.js Diagnostics Summit hosted by Google > in Munich, and the relevant documentation of the discussion can be found > here: https://docs.google.com/document/d/1g8OrG5lMIUhRn1zbkutgY83MiTSMx-0NHDs8Bf-nXxM/edit#heading=h.w1bavzz80l1e > > A summary of the reasons for why this new design is important can be > found here: https://docs.google.com/document/d/1vtgoT4_kjgOr-Bl605HR2T6_SC-C8uWzYaOPDK5pmRo/edit?usp=sharing > > Bug: v8:11025 > Change-Id: I0b403b00c37d3020b5af07b654b860659d3a7697 > Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2759188 > Reviewed-by: Marja Hölttä <marja@chromium.org> > Reviewed-by: Camillo Bruni <cbruni@chromium.org> > Reviewed-by: Anton Bikineev <bikineev@chromium.org> > Reviewed-by: Igor Sheludko <ishell@chromium.org> > Commit-Queue: Camillo Bruni <cbruni@chromium.org> > Cr-Commit-Position: refs/heads/master@{#73858} Bug: v8:11025 Bug: chromium:1197475 Change-Id: I73a71e97d9c3dff89a2b092c3fe4adff81ede8ef Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2823917 Reviewed-by: Marja Hölttä <marja@chromium.org> Reviewed-by: Igor Sheludko <ishell@chromium.org> Reviewed-by: Anton Bikineev <bikineev@chromium.org> Reviewed-by: Camillo Bruni <cbruni@chromium.org> Commit-Queue: Camillo Bruni <cbruni@chromium.org> Cr-Commit-Position: refs/heads/master@{#74071}
1 parent 6e4769b commit c0fceaa

31 files changed

Lines changed: 781 additions & 130 deletions

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ Seo Sanghyeon <sanxiyn@gmail.com>
211211
Shawn Anastasio <shawnanastasio@gmail.com>
212212
Shawn Presser <shawnpresser@gmail.com>
213213
Stefan Penner <stefan.penner@gmail.com>
214+
Stephen Belanger <stephen.belanger@datadoghq.com>
214215
Sylvestre Ledru <sledru@mozilla.com>
215216
Taketoshi Aono <brn@b6n.ch>
216217
Tao Liqiang <taolq@outlook.com>

include/v8.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10589,6 +10589,18 @@ class V8_EXPORT Context : public Data {
1058910589
*/
1059010590
void SetContinuationPreservedEmbedderData(Local<Value> context);
1059110591

10592+
/**
10593+
* Set or clear hooks to be invoked for promise lifecycle operations.
10594+
* To clear a hook, set it to an empty v8::Function. Each function will
10595+
* receive the observed promise as the first argument. If a chaining
10596+
* operation is used on a promise, the init will additionally receive
10597+
* the parent promise as the second argument.
10598+
*/
10599+
void SetPromiseHooks(Local<Function> init_hook,
10600+
Local<Function> before_hook,
10601+
Local<Function> after_hook,
10602+
Local<Function> resolve_hook);
10603+
1059210604
/**
1059310605
* Stack-allocated class which sets the execution context for all
1059410606
* operations executed within a local scope.

src/api/api.cc

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6262,6 +6262,45 @@ void Context::SetContinuationPreservedEmbedderData(Local<Value> data) {
62626262
*i::Handle<i::HeapObject>::cast(Utils::OpenHandle(*data)));
62636263
}
62646264

6265+
void v8::Context::SetPromiseHooks(Local<Function> init_hook,
6266+
Local<Function> before_hook,
6267+
Local<Function> after_hook,
6268+
Local<Function> resolve_hook) {
6269+
i::Handle<i::Context> context = Utils::OpenHandle(this);
6270+
i::Isolate* isolate = context->GetIsolate();
6271+
6272+
i::Handle<i::Object> init = isolate->factory()->undefined_value();
6273+
i::Handle<i::Object> before = isolate->factory()->undefined_value();
6274+
i::Handle<i::Object> after = isolate->factory()->undefined_value();
6275+
i::Handle<i::Object> resolve = isolate->factory()->undefined_value();
6276+
6277+
bool has_hook = false;
6278+
6279+
if (!init_hook.IsEmpty()) {
6280+
init = Utils::OpenHandle(*init_hook);
6281+
has_hook = true;
6282+
}
6283+
if (!before_hook.IsEmpty()) {
6284+
before = Utils::OpenHandle(*before_hook);
6285+
has_hook = true;
6286+
}
6287+
if (!after_hook.IsEmpty()) {
6288+
after = Utils::OpenHandle(*after_hook);
6289+
has_hook = true;
6290+
}
6291+
if (!resolve_hook.IsEmpty()) {
6292+
resolve = Utils::OpenHandle(*resolve_hook);
6293+
has_hook = true;
6294+
}
6295+
6296+
isolate->SetHasContextPromiseHooks(has_hook);
6297+
6298+
context->native_context().set_promise_hook_init_function(*init);
6299+
context->native_context().set_promise_hook_before_function(*before);
6300+
context->native_context().set_promise_hook_after_function(*after);
6301+
context->native_context().set_promise_hook_resolve_function(*resolve);
6302+
}
6303+
62656304
MaybeLocal<Context> metrics::Recorder::GetContext(
62666305
Isolate* isolate, metrics::Recorder::ContextId id) {
62676306
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);

src/builtins/builtins-async-function-gen.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,14 @@ TF_BUILTIN(AsyncFunctionEnter, AsyncFunctionBuiltinsAssembler) {
157157
StoreObjectFieldNoWriteBarrier(
158158
async_function_object, JSAsyncFunctionObject::kPromiseOffset, promise);
159159

160+
RunContextPromiseHookInit(context, promise, UndefinedConstant());
161+
160162
// Fire promise hooks if enabled and push the Promise under construction
161163
// in an async function on the catch prediction stack to handle exceptions
162164
// thrown before the first await.
163165
Label if_instrumentation(this, Label::kDeferred),
164166
if_instrumentation_done(this);
165-
Branch(IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(),
167+
Branch(IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(),
166168
&if_instrumentation, &if_instrumentation_done);
167169
BIND(&if_instrumentation);
168170
{

src/builtins/builtins-async-gen.cc

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,11 @@ TNode<Object> AsyncBuiltinsAssembler::AwaitOld(
9797

9898
TVARIABLE(HeapObject, var_throwaway, UndefinedConstant());
9999

100-
// Deal with PromiseHooks and debug support in the runtime. This
101-
// also allocates the throwaway promise, which is only needed in
102-
// case of PromiseHooks or debugging.
103-
Label if_debugging(this, Label::kDeferred), do_resolve_promise(this);
104-
Branch(IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(),
105-
&if_debugging, &do_resolve_promise);
106-
BIND(&if_debugging);
107-
var_throwaway =
108-
CAST(CallRuntime(Runtime::kAwaitPromisesInitOld, context, value, promise,
109-
outer_promise, on_reject, is_predicted_as_caught));
110-
Goto(&do_resolve_promise);
111-
BIND(&do_resolve_promise);
100+
RunContextPromiseHookInit(context, promise, outer_promise);
101+
102+
InitAwaitPromise(Runtime::kAwaitPromisesInitOld, context, value, promise,
103+
outer_promise, on_reject, is_predicted_as_caught,
104+
&var_throwaway);
112105

113106
// Perform ! Call(promiseCapability.[[Resolve]], undefined, « promise »).
114107
CallBuiltin(Builtins::kResolvePromise, context, promise, value);
@@ -168,21 +161,46 @@ TNode<Object> AsyncBuiltinsAssembler::AwaitOptimized(
168161

169162
TVARIABLE(HeapObject, var_throwaway, UndefinedConstant());
170163

164+
InitAwaitPromise(Runtime::kAwaitPromisesInit, context, promise, promise,
165+
outer_promise, on_reject, is_predicted_as_caught,
166+
&var_throwaway);
167+
168+
return CallBuiltin(Builtins::kPerformPromiseThen, native_context, promise,
169+
on_resolve, on_reject, var_throwaway.value());
170+
}
171+
172+
void AsyncBuiltinsAssembler::InitAwaitPromise(
173+
Runtime::FunctionId id, TNode<Context> context, TNode<Object> value,
174+
TNode<Object> promise, TNode<Object> outer_promise,
175+
TNode<HeapObject> on_reject, TNode<Oddball> is_predicted_as_caught,
176+
TVariable<HeapObject>* var_throwaway) {
171177
// Deal with PromiseHooks and debug support in the runtime. This
172178
// also allocates the throwaway promise, which is only needed in
173179
// case of PromiseHooks or debugging.
174-
Label if_debugging(this, Label::kDeferred), do_perform_promise_then(this);
175-
Branch(IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(),
176-
&if_debugging, &do_perform_promise_then);
180+
Label if_debugging(this, Label::kDeferred),
181+
if_promise_hook(this, Label::kDeferred),
182+
not_debugging(this),
183+
do_nothing(this);
184+
TNode<Uint32T> promiseHookFlags = PromiseHookFlags();
185+
Branch(IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(
186+
promiseHookFlags), &if_debugging, &not_debugging);
177187
BIND(&if_debugging);
178-
var_throwaway =
179-
CAST(CallRuntime(Runtime::kAwaitPromisesInit, context, promise, promise,
188+
*var_throwaway =
189+
CAST(CallRuntime(id, context, value, promise,
180190
outer_promise, on_reject, is_predicted_as_caught));
181-
Goto(&do_perform_promise_then);
182-
BIND(&do_perform_promise_then);
183-
184-
return CallBuiltin(Builtins::kPerformPromiseThen, native_context, promise,
185-
on_resolve, on_reject, var_throwaway.value());
191+
Goto(&do_nothing);
192+
BIND(&not_debugging);
193+
194+
// This call to NewJSPromise is to keep behaviour parity with what happens
195+
// in Runtime::kAwaitPromisesInit above if native hooks are set. It will
196+
// create a throwaway promise that will trigger an init event and will get
197+
// passed into Builtins::kPerformPromiseThen below.
198+
Branch(IsContextPromiseHookEnabled(promiseHookFlags), &if_promise_hook,
199+
&do_nothing);
200+
BIND(&if_promise_hook);
201+
*var_throwaway = NewJSPromise(context, promise);
202+
Goto(&do_nothing);
203+
BIND(&do_nothing);
186204
}
187205

188206
TNode<Object> AsyncBuiltinsAssembler::Await(

src/builtins/builtins-async-gen.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ class AsyncBuiltinsAssembler : public PromiseBuiltinsAssembler {
6262
TNode<SharedFunctionInfo> on_resolve_sfi,
6363
TNode<SharedFunctionInfo> on_reject_sfi,
6464
TNode<Oddball> is_predicted_as_caught);
65+
66+
void InitAwaitPromise(
67+
Runtime::FunctionId id, TNode<Context> context, TNode<Object> value,
68+
TNode<Object> promise, TNode<Object> outer_promise,
69+
TNode<HeapObject> on_reject, TNode<Oddball> is_predicted_as_caught,
70+
TVariable<HeapObject>* var_throwaway);
6571
};
6672

6773
} // namespace internal

src/builtins/builtins-async-generator-gen.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ TF_BUILTIN(AsyncGeneratorResolve, AsyncGeneratorBuiltinsAssembler) {
518518
// the "promiseResolve" hook would not be fired otherwise.
519519
Label if_fast(this), if_slow(this, Label::kDeferred), return_promise(this);
520520
GotoIfForceSlowPath(&if_slow);
521-
GotoIf(IsPromiseHookEnabled(), &if_slow);
521+
GotoIf(IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate(), &if_slow);
522522
Branch(IsPromiseThenProtectorCellInvalid(), &if_slow, &if_fast);
523523

524524
BIND(&if_fast);

src/builtins/builtins-microtask-queue-gen.cc

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ class MicrotaskQueueBuiltinsAssembler : public CodeStubAssembler {
4646
void EnterMicrotaskContext(TNode<Context> native_context);
4747
void RewindEnteredContext(TNode<IntPtrT> saved_entered_context_count);
4848

49+
void RunAllPromiseHooks(PromiseHookType type, TNode<Context> context,
50+
TNode<HeapObject> promise_or_capability);
4951
void RunPromiseHook(Runtime::FunctionId id, TNode<Context> context,
50-
TNode<HeapObject> promise_or_capability);
52+
TNode<HeapObject> promise_or_capability,
53+
TNode<Uint32T> promiseHookFlags);
5154
};
5255

5356
TNode<RawPtrT> MicrotaskQueueBuiltinsAssembler::GetMicrotaskQueue(
@@ -199,7 +202,7 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask(
199202
const TNode<Object> thenable = LoadObjectField(
200203
microtask, PromiseResolveThenableJobTask::kThenableOffset);
201204

202-
RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context,
205+
RunAllPromiseHooks(PromiseHookType::kBefore, microtask_context,
203206
CAST(promise_to_resolve));
204207

205208
{
@@ -208,7 +211,7 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask(
208211
promise_to_resolve, thenable, then);
209212
}
210213

211-
RunPromiseHook(Runtime::kPromiseHookAfter, microtask_context,
214+
RunAllPromiseHooks(PromiseHookType::kAfter, microtask_context,
212215
CAST(promise_to_resolve));
213216

214217
RewindEnteredContext(saved_entered_context_count);
@@ -243,8 +246,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask(
243246
BIND(&preserved_data_done);
244247

245248
// Run the promise before/debug hook if enabled.
246-
RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context,
247-
promise_or_capability);
249+
RunAllPromiseHooks(PromiseHookType::kBefore, microtask_context,
250+
promise_or_capability);
248251

249252
{
250253
ScopedExceptionHandler handler(this, &if_exception, &var_exception);
@@ -253,8 +256,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask(
253256
}
254257

255258
// Run the promise after/debug hook if enabled.
256-
RunPromiseHook(Runtime::kPromiseHookAfter, microtask_context,
257-
promise_or_capability);
259+
RunAllPromiseHooks(PromiseHookType::kAfter, microtask_context,
260+
promise_or_capability);
258261

259262
Label preserved_data_reset_done(this);
260263
GotoIf(IsUndefined(preserved_embedder_data), &preserved_data_reset_done);
@@ -296,8 +299,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask(
296299
BIND(&preserved_data_done);
297300

298301
// Run the promise before/debug hook if enabled.
299-
RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context,
300-
promise_or_capability);
302+
RunAllPromiseHooks(PromiseHookType::kBefore, microtask_context,
303+
promise_or_capability);
301304

302305
{
303306
ScopedExceptionHandler handler(this, &if_exception, &var_exception);
@@ -306,8 +309,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask(
306309
}
307310

308311
// Run the promise after/debug hook if enabled.
309-
RunPromiseHook(Runtime::kPromiseHookAfter, microtask_context,
310-
promise_or_capability);
312+
RunAllPromiseHooks(PromiseHookType::kAfter, microtask_context,
313+
promise_or_capability);
311314

312315
Label preserved_data_reset_done(this);
313316
GotoIf(IsUndefined(preserved_embedder_data), &preserved_data_reset_done);
@@ -465,12 +468,43 @@ void MicrotaskQueueBuiltinsAssembler::RewindEnteredContext(
465468
saved_entered_context_count);
466469
}
467470

471+
void MicrotaskQueueBuiltinsAssembler::RunAllPromiseHooks(
472+
PromiseHookType type, TNode<Context> context,
473+
TNode<HeapObject> promise_or_capability) {
474+
Label hook(this, Label::kDeferred), done_hook(this);
475+
TNode<Uint32T> promiseHookFlags = PromiseHookFlags();
476+
Branch(IsAnyPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(
477+
promiseHookFlags), &hook, &done_hook);
478+
BIND(&hook);
479+
{
480+
switch (type) {
481+
case PromiseHookType::kBefore:
482+
RunContextPromiseHookBefore(context, promise_or_capability,
483+
promiseHookFlags);
484+
RunPromiseHook(Runtime::kPromiseHookBefore, context,
485+
promise_or_capability, promiseHookFlags);
486+
break;
487+
case PromiseHookType::kAfter:
488+
RunContextPromiseHookAfter(context, promise_or_capability,
489+
promiseHookFlags);
490+
RunPromiseHook(Runtime::kPromiseHookAfter, context,
491+
promise_or_capability, promiseHookFlags);
492+
break;
493+
default:
494+
UNREACHABLE();
495+
}
496+
Goto(&done_hook);
497+
}
498+
BIND(&done_hook);
499+
}
500+
468501
void MicrotaskQueueBuiltinsAssembler::RunPromiseHook(
469502
Runtime::FunctionId id, TNode<Context> context,
470-
TNode<HeapObject> promise_or_capability) {
503+
TNode<HeapObject> promise_or_capability,
504+
TNode<Uint32T> promiseHookFlags) {
471505
Label hook(this, Label::kDeferred), done_hook(this);
472-
Branch(IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(), &hook,
473-
&done_hook);
506+
Branch(IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(
507+
promiseHookFlags), &hook, &done_hook);
474508
BIND(&hook);
475509
{
476510
// Get to the underlying JSPromise instance.

src/builtins/cast.tq

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,12 @@ Cast<Undefined|Callable>(o: HeapObject): Undefined|Callable
386386
return HeapObjectToCallable(o) otherwise CastError;
387387
}
388388

389+
Cast<Undefined|JSFunction>(o: HeapObject): Undefined|JSFunction
390+
labels CastError {
391+
if (o == Undefined) return Undefined;
392+
return Cast<JSFunction>(o) otherwise CastError;
393+
}
394+
389395
macro Cast<T : type extends Symbol>(o: Symbol): T labels CastError;
390396
Cast<PublicSymbol>(s: Symbol): PublicSymbol labels CastError {
391397
if (s.flags.is_private) goto CastError;

src/builtins/promise-abstract-operations.tq

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ FulfillPromise(implicit context: Context)(
196196
// Assert: The value of promise.[[PromiseState]] is "pending".
197197
assert(promise.Status() == PromiseState::kPending);
198198

199+
RunContextPromiseHookResolve(promise);
200+
199201
// 2. Let reactions be promise.[[PromiseFulfillReactions]].
200202
const reactions =
201203
UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
@@ -214,17 +216,24 @@ FulfillPromise(implicit context: Context)(
214216
}
215217

216218
extern macro PromiseBuiltinsAssembler::
217-
IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(): bool;
219+
IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(): bool;
220+
221+
extern macro PromiseBuiltinsAssembler::
222+
IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(uint32):
223+
bool;
218224

219225
// https://tc39.es/ecma262/#sec-rejectpromise
220226
transitioning builtin
221227
RejectPromise(implicit context: Context)(
222228
promise: JSPromise, reason: JSAny, debugEvent: Boolean): JSAny {
229+
const promiseHookFlags = PromiseHookFlags();
230+
223231
// If promise hook is enabled or the debugger is active, let
224232
// the runtime handle this operation, which greatly reduces
225233
// the complexity here and also avoids a couple of back and
226234
// forth between JavaScript and C++ land.
227-
if (IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() ||
235+
if (IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(
236+
promiseHookFlags) ||
228237
!promise.HasHandler()) {
229238
// 7. If promise.[[PromiseIsHandled]] is false, perform
230239
// HostPromiseRejectionTracker(promise, "reject").
@@ -233,6 +242,8 @@ RejectPromise(implicit context: Context)(
233242
return runtime::RejectPromise(promise, reason, debugEvent);
234243
}
235244

245+
RunContextPromiseHookResolve(promise, promiseHookFlags);
246+
236247
// 2. Let reactions be promise.[[PromiseRejectReactions]].
237248
const reactions =
238249
UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);

0 commit comments

Comments
 (0)