diff options
author | Stephen Belanger <stephen.belanger@datadoghq.com> | 2021-04-21 04:10:05 +0300 |
---|---|---|
committer | Michaël Zasso <targos@protonmail.com> | 2021-05-17 10:11:31 +0300 |
commit | 827ae0553850bc85fe77e649062efa82e7a1ec19 (patch) | |
tree | 886149ac6a4f1625c23236492b4d9f42112c169e /deps | |
parent | 95db7d5afcb475bde501b3add712d68fc6d5ea39 (diff) |
deps: V8: backport c0fceaa0669b
Original commit message:
Reland "[api] JSFunction PromiseHook for v8::Context"
This is a reland of d5457f5fb7ea05ca05a697599ffa50d35c1ae3c7
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}
Refs: https://github.com/v8/v8/commit/c0fceaa0669b39136c9e780f278e2596d71b4e8a
PR-URL: https://github.com/nodejs/node/pull/36394
Reviewed-By: Bryan English <bryan@bryanenglish.com>
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com>
Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
Reviewed-By: James M Snell <jasnell@gmail.com>
Diffstat (limited to 'deps')
31 files changed, 781 insertions, 130 deletions
diff --git a/deps/v8/AUTHORS b/deps/v8/AUTHORS index aa6d32302b3..a27cf5ef0a1 100644 --- a/deps/v8/AUTHORS +++ b/deps/v8/AUTHORS @@ -209,6 +209,7 @@ Seo Sanghyeon <sanxiyn@gmail.com> Shawn Anastasio <shawnanastasio@gmail.com> Shawn Presser <shawnpresser@gmail.com> Stefan Penner <stefan.penner@gmail.com> +Stephen Belanger <stephen.belanger@datadoghq.com> Sylvestre Ledru <sledru@mozilla.com> Taketoshi Aono <brn@b6n.ch> Tao Liqiang <taolq@outlook.com> diff --git a/deps/v8/include/v8.h b/deps/v8/include/v8.h index 083bf5e1232..e4448db1910 100644 --- a/deps/v8/include/v8.h +++ b/deps/v8/include/v8.h @@ -10795,6 +10795,18 @@ class V8_EXPORT Context : public Data { void SetContinuationPreservedEmbedderData(Local<Value> context); /** + * Set or clear hooks to be invoked for promise lifecycle operations. + * To clear a hook, set it to an empty v8::Function. Each function will + * receive the observed promise as the first argument. If a chaining + * operation is used on a promise, the init will additionally receive + * the parent promise as the second argument. + */ + void SetPromiseHooks(Local<Function> init_hook, + Local<Function> before_hook, + Local<Function> after_hook, + Local<Function> resolve_hook); + + /** * Stack-allocated class which sets the execution context for all * operations executed within a local scope. */ diff --git a/deps/v8/src/api/api.cc b/deps/v8/src/api/api.cc index a56b7e1a7d7..ca73b58a60b 100644 --- a/deps/v8/src/api/api.cc +++ b/deps/v8/src/api/api.cc @@ -6147,6 +6147,45 @@ void Context::SetContinuationPreservedEmbedderData(Local<Value> data) { *i::Handle<i::HeapObject>::cast(Utils::OpenHandle(*data))); } +void v8::Context::SetPromiseHooks(Local<Function> init_hook, + Local<Function> before_hook, + Local<Function> after_hook, + Local<Function> resolve_hook) { + i::Handle<i::Context> context = Utils::OpenHandle(this); + i::Isolate* isolate = context->GetIsolate(); + + i::Handle<i::Object> init = isolate->factory()->undefined_value(); + i::Handle<i::Object> before = isolate->factory()->undefined_value(); + i::Handle<i::Object> after = isolate->factory()->undefined_value(); + i::Handle<i::Object> resolve = isolate->factory()->undefined_value(); + + bool has_hook = false; + + if (!init_hook.IsEmpty()) { + init = Utils::OpenHandle(*init_hook); + has_hook = true; + } + if (!before_hook.IsEmpty()) { + before = Utils::OpenHandle(*before_hook); + has_hook = true; + } + if (!after_hook.IsEmpty()) { + after = Utils::OpenHandle(*after_hook); + has_hook = true; + } + if (!resolve_hook.IsEmpty()) { + resolve = Utils::OpenHandle(*resolve_hook); + has_hook = true; + } + + isolate->SetHasContextPromiseHooks(has_hook); + + context->native_context().set_promise_hook_init_function(*init); + context->native_context().set_promise_hook_before_function(*before); + context->native_context().set_promise_hook_after_function(*after); + context->native_context().set_promise_hook_resolve_function(*resolve); +} + MaybeLocal<Context> metrics::Recorder::GetContext( Isolate* isolate, metrics::Recorder::ContextId id) { i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate); diff --git a/deps/v8/src/builtins/builtins-async-function-gen.cc b/deps/v8/src/builtins/builtins-async-function-gen.cc index 49b00caa048..1644997ed01 100644 --- a/deps/v8/src/builtins/builtins-async-function-gen.cc +++ b/deps/v8/src/builtins/builtins-async-function-gen.cc @@ -157,12 +157,14 @@ TF_BUILTIN(AsyncFunctionEnter, AsyncFunctionBuiltinsAssembler) { StoreObjectFieldNoWriteBarrier( async_function_object, JSAsyncFunctionObject::kPromiseOffset, promise); + RunContextPromiseHookInit(context, promise, UndefinedConstant()); + // Fire promise hooks if enabled and push the Promise under construction // in an async function on the catch prediction stack to handle exceptions // thrown before the first await. Label if_instrumentation(this, Label::kDeferred), if_instrumentation_done(this); - Branch(IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(), + Branch(IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(), &if_instrumentation, &if_instrumentation_done); BIND(&if_instrumentation); { diff --git a/deps/v8/src/builtins/builtins-async-gen.cc b/deps/v8/src/builtins/builtins-async-gen.cc index fa05e9b32ad..1a660abece1 100644 --- a/deps/v8/src/builtins/builtins-async-gen.cc +++ b/deps/v8/src/builtins/builtins-async-gen.cc @@ -99,18 +99,11 @@ TNode<Object> AsyncBuiltinsAssembler::AwaitOld( TVARIABLE(HeapObject, var_throwaway, UndefinedConstant()); - // Deal with PromiseHooks and debug support in the runtime. This - // also allocates the throwaway promise, which is only needed in - // case of PromiseHooks or debugging. - Label if_debugging(this, Label::kDeferred), do_resolve_promise(this); - Branch(IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(), - &if_debugging, &do_resolve_promise); - BIND(&if_debugging); - var_throwaway = - CAST(CallRuntime(Runtime::kAwaitPromisesInitOld, context, value, promise, - outer_promise, on_reject, is_predicted_as_caught)); - Goto(&do_resolve_promise); - BIND(&do_resolve_promise); + RunContextPromiseHookInit(context, promise, outer_promise); + + InitAwaitPromise(Runtime::kAwaitPromisesInitOld, context, value, promise, + outer_promise, on_reject, is_predicted_as_caught, + &var_throwaway); // Perform ! Call(promiseCapability.[[Resolve]], undefined, « promise »). CallBuiltin(Builtins::kResolvePromise, context, promise, value); @@ -170,21 +163,46 @@ TNode<Object> AsyncBuiltinsAssembler::AwaitOptimized( TVARIABLE(HeapObject, var_throwaway, UndefinedConstant()); + InitAwaitPromise(Runtime::kAwaitPromisesInit, context, promise, promise, + outer_promise, on_reject, is_predicted_as_caught, + &var_throwaway); + + return CallBuiltin(Builtins::kPerformPromiseThen, native_context, promise, + on_resolve, on_reject, var_throwaway.value()); +} + +void AsyncBuiltinsAssembler::InitAwaitPromise( + Runtime::FunctionId id, TNode<Context> context, TNode<Object> value, + TNode<Object> promise, TNode<Object> outer_promise, + TNode<HeapObject> on_reject, TNode<Oddball> is_predicted_as_caught, + TVariable<HeapObject>* var_throwaway) { // Deal with PromiseHooks and debug support in the runtime. This // also allocates the throwaway promise, which is only needed in // case of PromiseHooks or debugging. - Label if_debugging(this, Label::kDeferred), do_perform_promise_then(this); - Branch(IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(), - &if_debugging, &do_perform_promise_then); + Label if_debugging(this, Label::kDeferred), + if_promise_hook(this, Label::kDeferred), + not_debugging(this), + do_nothing(this); + TNode<Uint32T> promiseHookFlags = PromiseHookFlags(); + Branch(IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + promiseHookFlags), &if_debugging, ¬_debugging); BIND(&if_debugging); - var_throwaway = - CAST(CallRuntime(Runtime::kAwaitPromisesInit, context, promise, promise, + *var_throwaway = + CAST(CallRuntime(id, context, value, promise, outer_promise, on_reject, is_predicted_as_caught)); - Goto(&do_perform_promise_then); - BIND(&do_perform_promise_then); - - return CallBuiltin(Builtins::kPerformPromiseThen, native_context, promise, - on_resolve, on_reject, var_throwaway.value()); + Goto(&do_nothing); + BIND(¬_debugging); + + // This call to NewJSPromise is to keep behaviour parity with what happens + // in Runtime::kAwaitPromisesInit above if native hooks are set. It will + // create a throwaway promise that will trigger an init event and will get + // passed into Builtins::kPerformPromiseThen below. + Branch(IsContextPromiseHookEnabled(promiseHookFlags), &if_promise_hook, + &do_nothing); + BIND(&if_promise_hook); + *var_throwaway = NewJSPromise(context, promise); + Goto(&do_nothing); + BIND(&do_nothing); } TNode<Object> AsyncBuiltinsAssembler::Await( diff --git a/deps/v8/src/builtins/builtins-async-gen.h b/deps/v8/src/builtins/builtins-async-gen.h index 833e78d45d5..34b7a0ce1d6 100644 --- a/deps/v8/src/builtins/builtins-async-gen.h +++ b/deps/v8/src/builtins/builtins-async-gen.h @@ -62,6 +62,12 @@ class AsyncBuiltinsAssembler : public PromiseBuiltinsAssembler { TNode<SharedFunctionInfo> on_resolve_sfi, TNode<SharedFunctionInfo> on_reject_sfi, TNode<Oddball> is_predicted_as_caught); + + void InitAwaitPromise( + Runtime::FunctionId id, TNode<Context> context, TNode<Object> value, + TNode<Object> promise, TNode<Object> outer_promise, + TNode<HeapObject> on_reject, TNode<Oddball> is_predicted_as_caught, + TVariable<HeapObject>* var_throwaway); }; } // namespace internal diff --git a/deps/v8/src/builtins/builtins-async-generator-gen.cc b/deps/v8/src/builtins/builtins-async-generator-gen.cc index 374b13dd63f..5d053063ff9 100644 --- a/deps/v8/src/builtins/builtins-async-generator-gen.cc +++ b/deps/v8/src/builtins/builtins-async-generator-gen.cc @@ -520,7 +520,7 @@ TF_BUILTIN(AsyncGeneratorResolve, AsyncGeneratorBuiltinsAssembler) { // the "promiseResolve" hook would not be fired otherwise. Label if_fast(this), if_slow(this, Label::kDeferred), return_promise(this); GotoIfForceSlowPath(&if_slow); - GotoIf(IsPromiseHookEnabled(), &if_slow); + GotoIf(IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate(), &if_slow); Branch(IsPromiseThenProtectorCellInvalid(), &if_slow, &if_fast); BIND(&if_fast); diff --git a/deps/v8/src/builtins/builtins-microtask-queue-gen.cc b/deps/v8/src/builtins/builtins-microtask-queue-gen.cc index 9f16186d13b..1ec9e350f67 100644 --- a/deps/v8/src/builtins/builtins-microtask-queue-gen.cc +++ b/deps/v8/src/builtins/builtins-microtask-queue-gen.cc @@ -46,8 +46,11 @@ class MicrotaskQueueBuiltinsAssembler : public CodeStubAssembler { void EnterMicrotaskContext(TNode<Context> native_context); void RewindEnteredContext(TNode<IntPtrT> saved_entered_context_count); + void RunAllPromiseHooks(PromiseHookType type, TNode<Context> context, + TNode<HeapObject> promise_or_capability); void RunPromiseHook(Runtime::FunctionId id, TNode<Context> context, - TNode<HeapObject> promise_or_capability); + TNode<HeapObject> promise_or_capability, + TNode<Uint32T> promiseHookFlags); }; TNode<RawPtrT> MicrotaskQueueBuiltinsAssembler::GetMicrotaskQueue( @@ -199,7 +202,7 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( const TNode<Object> thenable = LoadObjectField( microtask, PromiseResolveThenableJobTask::kThenableOffset); - RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context, + RunAllPromiseHooks(PromiseHookType::kBefore, microtask_context, CAST(promise_to_resolve)); { @@ -208,7 +211,7 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( promise_to_resolve, thenable, then); } - RunPromiseHook(Runtime::kPromiseHookAfter, microtask_context, + RunAllPromiseHooks(PromiseHookType::kAfter, microtask_context, CAST(promise_to_resolve)); RewindEnteredContext(saved_entered_context_count); @@ -243,8 +246,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( BIND(&preserved_data_done); // Run the promise before/debug hook if enabled. - RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context, - promise_or_capability); + RunAllPromiseHooks(PromiseHookType::kBefore, microtask_context, + promise_or_capability); { ScopedExceptionHandler handler(this, &if_exception, &var_exception); @@ -253,8 +256,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( } // Run the promise after/debug hook if enabled. - RunPromiseHook(Runtime::kPromiseHookAfter, microtask_context, - promise_or_capability); + RunAllPromiseHooks(PromiseHookType::kAfter, microtask_context, + promise_or_capability); Label preserved_data_reset_done(this); GotoIf(IsUndefined(preserved_embedder_data), &preserved_data_reset_done); @@ -296,8 +299,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( BIND(&preserved_data_done); // Run the promise before/debug hook if enabled. - RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context, - promise_or_capability); + RunAllPromiseHooks(PromiseHookType::kBefore, microtask_context, + promise_or_capability); { ScopedExceptionHandler handler(this, &if_exception, &var_exception); @@ -306,8 +309,8 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( } // Run the promise after/debug hook if enabled. - RunPromiseHook(Runtime::kPromiseHookAfter, microtask_context, - promise_or_capability); + RunAllPromiseHooks(PromiseHookType::kAfter, microtask_context, + promise_or_capability); Label preserved_data_reset_done(this); GotoIf(IsUndefined(preserved_embedder_data), &preserved_data_reset_done); @@ -465,12 +468,43 @@ void MicrotaskQueueBuiltinsAssembler::RewindEnteredContext( saved_entered_context_count); } +void MicrotaskQueueBuiltinsAssembler::RunAllPromiseHooks( + PromiseHookType type, TNode<Context> context, + TNode<HeapObject> promise_or_capability) { + Label hook(this, Label::kDeferred), done_hook(this); + TNode<Uint32T> promiseHookFlags = PromiseHookFlags(); + Branch(IsAnyPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + promiseHookFlags), &hook, &done_hook); + BIND(&hook); + { + switch (type) { + case PromiseHookType::kBefore: + RunContextPromiseHookBefore(context, promise_or_capability, + promiseHookFlags); + RunPromiseHook(Runtime::kPromiseHookBefore, context, + promise_or_capability, promiseHookFlags); + break; + case PromiseHookType::kAfter: + RunContextPromiseHookAfter(context, promise_or_capability, + promiseHookFlags); + RunPromiseHook(Runtime::kPromiseHookAfter, context, + promise_or_capability, promiseHookFlags); + break; + default: + UNREACHABLE(); + } + Goto(&done_hook); + } + BIND(&done_hook); +} + void MicrotaskQueueBuiltinsAssembler::RunPromiseHook( Runtime::FunctionId id, TNode<Context> context, - TNode<HeapObject> promise_or_capability) { + TNode<HeapObject> promise_or_capability, + TNode<Uint32T> promiseHookFlags) { Label hook(this, Label::kDeferred), done_hook(this); - Branch(IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(), &hook, - &done_hook); + Branch(IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + promiseHookFlags), &hook, &done_hook); BIND(&hook); { // Get to the underlying JSPromise instance. diff --git a/deps/v8/src/builtins/cast.tq b/deps/v8/src/builtins/cast.tq index b490055a19e..2bec3d86be0 100644 --- a/deps/v8/src/builtins/cast.tq +++ b/deps/v8/src/builtins/cast.tq @@ -386,6 +386,12 @@ Cast<Undefined|Callable>(o: HeapObject): Undefined|Callable return HeapObjectToCallable(o) otherwise CastError; } +Cast<Undefined|JSFunction>(o: HeapObject): Undefined|JSFunction + labels CastError { + if (o == Undefined) return Undefined; + return Cast<JSFunction>(o) otherwise CastError; +} + macro Cast<T : type extends Symbol>(o: Symbol): T labels CastError; Cast<PublicSymbol>(s: Symbol): PublicSymbol labels CastError { if (s.flags.is_private) goto CastError; diff --git a/deps/v8/src/builtins/promise-abstract-operations.tq b/deps/v8/src/builtins/promise-abstract-operations.tq index b7a1b571e64..0e435afad9b 100644 --- a/deps/v8/src/builtins/promise-abstract-operations.tq +++ b/deps/v8/src/builtins/promise-abstract-operations.tq @@ -196,6 +196,8 @@ FulfillPromise(implicit context: Context)( // Assert: The value of promise.[[PromiseState]] is "pending". assert(promise.Status() == PromiseState::kPending); + RunContextPromiseHookResolve(promise); + // 2. Let reactions be promise.[[PromiseFulfillReactions]]. const reactions = UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result); @@ -214,17 +216,24 @@ FulfillPromise(implicit context: Context)( } extern macro PromiseBuiltinsAssembler:: - IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(): bool; + IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(): bool; + +extern macro PromiseBuiltinsAssembler:: + IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(uint32): + bool; // https://tc39.es/ecma262/#sec-rejectpromise transitioning builtin RejectPromise(implicit context: Context)( promise: JSPromise, reason: JSAny, debugEvent: Boolean): JSAny { + const promiseHookFlags = PromiseHookFlags(); + // If promise hook is enabled or the debugger is active, let // the runtime handle this operation, which greatly reduces // the complexity here and also avoids a couple of back and // forth between JavaScript and C++ land. - if (IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() || + if (IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + promiseHookFlags) || !promise.HasHandler()) { // 7. If promise.[[PromiseIsHandled]] is false, perform // HostPromiseRejectionTracker(promise, "reject"). @@ -233,6 +242,8 @@ RejectPromise(implicit context: Context)( return runtime::RejectPromise(promise, reason, debugEvent); } + RunContextPromiseHookResolve(promise, promiseHookFlags); + // 2. Let reactions be promise.[[PromiseRejectReactions]]. const reactions = UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result); diff --git a/deps/v8/src/builtins/promise-all.tq b/deps/v8/src/builtins/promise-all.tq index 41dee8b9e76..294c5e911c9 100644 --- a/deps/v8/src/builtins/promise-all.tq +++ b/deps/v8/src/builtins/promise-all.tq @@ -232,7 +232,7 @@ Reject(Object) { // PerformPromiseThen), since this is only necessary for DevTools and // PromiseHooks. if (promiseResolveFunction != Undefined || - IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() || + IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() || IsPromiseSpeciesProtectorCellInvalid() || Is<Smi>(nextValue) || !IsPromiseThenLookupChainIntact( nativeContext, UnsafeCast<HeapObject>(nextValue).map)) { diff --git a/deps/v8/src/builtins/promise-constructor.tq b/deps/v8/src/builtins/promise-constructor.tq index 3c5a5e560d4..b5f7292a77c 100644 --- a/deps/v8/src/builtins/promise-constructor.tq +++ b/deps/v8/src/builtins/promise-constructor.tq @@ -40,7 +40,8 @@ extern macro ConstructorBuiltinsAssembler::FastNewObject( Context, JSFunction, JSReceiver): JSObject; extern macro -PromiseBuiltinsAssembler::IsPromiseHookEnabledOrHasAsyncEventDelegate(): bool; +PromiseBuiltinsAssembler::IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate( + uint32): bool; // https://tc39.es/ecma262/#sec-promise-executor transitioning javascript builtin @@ -73,9 +74,7 @@ PromiseConstructor( result = UnsafeCast<JSPromise>( FastNewObject(context, promiseFun, UnsafeCast<JSReceiver>(newTarget))); PromiseInit(result); - if (IsPromiseHookEnabledOrHasAsyncEventDelegate()) { - runtime::PromiseHookInit(result, Undefined); - } + RunAnyPromiseHookInit(result, Undefined); } const isDebugActive = IsDebugActive(); diff --git a/deps/v8/src/builtins/promise-jobs.tq b/deps/v8/src/builtins/promise-jobs.tq index 80e98f373b9..6fa81dcd286 100644 --- a/deps/v8/src/builtins/promise-jobs.tq +++ b/deps/v8/src/builtins/promise-jobs.tq @@ -25,7 +25,7 @@ PromiseResolveThenableJob(implicit context: Context)( const promiseThen = *NativeContextSlot(ContextSlot::PROMISE_THEN_INDEX); const thenableMap = thenable.map; if (TaggedEqual(then, promiseThen) && IsJSPromiseMap(thenableMap) && - !IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() && + !IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() && IsPromiseSpeciesLookupChainIntact(nativeContext, thenableMap)) { // We know that the {thenable} is a JSPromise, which doesn't require // any special treatment and that {then} corresponds to the initial diff --git a/deps/v8/src/builtins/promise-misc.tq b/deps/v8/src/builtins/promise-misc.tq index 67e5e38687d..c6661e37173 100644 --- a/deps/v8/src/builtins/promise-misc.tq +++ b/deps/v8/src/builtins/promise-misc.tq @@ -8,6 +8,9 @@ namespace runtime { extern transitioning runtime AllowDynamicFunction(implicit context: Context)(JSAny): JSAny; + +extern transitioning runtime +ReportMessageFromMicrotask(implicit context: Context)(JSAny): JSAny; } // Unsafe functions that should be used very carefully. @@ -17,6 +20,12 @@ extern macro PromiseBuiltinsAssembler::ZeroOutEmbedderOffsets(JSPromise): void; extern macro PromiseBuiltinsAssembler::AllocateJSPromise(Context): HeapObject; } +extern macro +PromiseBuiltinsAssembler::IsContextPromiseHookEnabled(uint32): bool; + +extern macro +PromiseBuiltinsAssembler::PromiseHookFlags(): uint32; + namespace promise { extern macro IsFunctionWithPrototypeSlotMap(Map): bool; @@ -90,6 +99,110 @@ macro NewPromiseRejectReactionJobTask(implicit context: Context)( }; } +@export +transitioning macro RunContextPromiseHookInit(implicit context: Context)( + promise: JSPromise, parent: Object) { + const maybeHook = *NativeContextSlot( + ContextSlot::PROMISE_HOOK_INIT_FUNCTION_INDEX); + if (IsUndefined(maybeHook)) return; + + const hook = Cast<JSFunction>(maybeHook) otherwise unreachable; + const parentObject = Is<JSPromise>(parent) ? Cast<JSPromise>(parent) + otherwise unreachable: Undefined; + + try { + Call(context, hook, Undefined, promise, parentObject); + } catch (e) { + runtime::ReportMessageFromMicrotask(e); + } +} + +@export +transitioning macro RunContextPromiseHookResolve(implicit context: Context)( + promise: JSPromise) { + RunContextPromiseHook( + ContextSlot::PROMISE_HOOK_RESOLVE_FUNCTION_INDEX, promise, + PromiseHookFlags()); +} + +@export +transitioning macro RunContextPromiseHookResolve(implicit context: Context)( + promise: JSPromise, flags: uint32) { + RunContextPromiseHook( + ContextSlot::PROMISE_HOOK_RESOLVE_FUNCTION_INDEX, promise, flags); +} + +@export +transitioning macro RunContextPromiseHookBefore(implicit context: Context)( + promiseOrCapability: JSPromise|PromiseCapability) { + RunContextPromiseHook( + ContextSlot::PROMISE_HOOK_BEFORE_FUNCTION_INDEX, promiseOrCapability, + PromiseHookFlags()); +} + +@export +transitioning macro RunContextPromiseHookBefore(implicit context: Context)( + promiseOrCapability: JSPromise|PromiseCapability, flags: uint32) { + RunContextPromiseHook( + ContextSlot::PROMISE_HOOK_BEFORE_FUNCTION_INDEX, promiseOrCapability, + flags); +} + +@export +transitioning macro RunContextPromiseHookAfter(implicit context: Context)( + promiseOrCapability: JSPromise|PromiseCapability) { + RunContextPromiseHook( + ContextSlot::PROMISE_HOOK_AFTER_FUNCTION_INDEX, promiseOrCapability, + PromiseHookFlags()); +} + +@export +transitioning macro RunContextPromiseHookAfter(implicit context: Context)( + promiseOrCapability: JSPromise|PromiseCapability, flags: uint32) { + RunContextPromiseHook( + ContextSlot::PROMISE_HOOK_AFTER_FUNCTION_INDEX, promiseOrCapability, + flags); +} + +transitioning macro RunContextPromiseHook(implicit context: Context)( + slot: Slot<NativeContext, Undefined|JSFunction>, + promiseOrCapability: JSPromise|PromiseCapability, flags: uint32) { + if (!IsContextPromiseHookEnabled(flags)) return; + const maybeHook = *NativeContextSlot(slot); + if (IsUndefined(maybeHook)) return; + + const hook = Cast<JSFunction>(maybeHook) otherwise unreachable; + + let promise: JSPromise; + typeswitch (promiseOrCapability) { + case (jspromise: JSPromise): { + promise = jspromise; + } + case (capability: PromiseCapability): { + promise = Cast<JSPromise>(capability.promise) otherwise return; + } + } + + try { + Call(context, hook, Undefined, promise); + } catch (e) { + runtime::ReportMessageFromMicrotask(e); + } +} + +transitioning macro RunAnyPromiseHookInit(implicit context: Context)( + promise: JSPromise, parent: Object) { + const promiseHookFlags = PromiseHookFlags(); + // Fast return if no hooks are set. + if (promiseHookFlags == 0) return; + if (IsContextPromiseHookEnabled(promiseHookFlags)) { + RunContextPromiseHookInit(promise, parent); + } + if (IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate(promiseHookFlags)) { + runtime::PromiseHookInit(promise, parent); + } +} + // These allocate and initialize a promise with pending state and // undefined fields. // @@ -100,9 +213,7 @@ transitioning macro NewJSPromise(implicit context: Context)(parent: Object): JSPromise { const instance = InnerNewJSPromise(); PromiseInit(instance); - if (IsPromiseHookEnabledOrHasAsyncEventDelegate()) { - runtime::PromiseHookInit(instance, parent); - } + RunAnyPromiseHookInit(instance, parent); return instance; } @@ -124,10 +235,7 @@ transitioning macro NewJSPromise(implicit context: Context)( instance.reactions_or_result = result; instance.SetStatus(status); promise_internal::ZeroOutEmbedderOffsets(instance); - - if (IsPromiseHookEnabledOrHasAsyncEventDelegate()) { - runtime::PromiseHookInit(instance, Undefined); - } + RunAnyPromiseHookInit(instance, Undefined); return instance; } diff --git a/deps/v8/src/builtins/promise-resolve.tq b/deps/v8/src/builtins/promise-resolve.tq index e933dfbae0a..3125054e87a 100644 --- a/deps/v8/src/builtins/promise-resolve.tq +++ b/deps/v8/src/builtins/promise-resolve.tq @@ -97,7 +97,7 @@ ResolvePromise(implicit context: Context)( // We also let the runtime handle it if promise == resolution. // We can use pointer comparison here, since the {promise} is guaranteed // to be a JSPromise inside this function and thus is reference comparable. - if (IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() || + if (IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() || TaggedEqual(promise, resolution)) deferred { return runtime::ResolvePromise(promise, resolution); diff --git a/deps/v8/src/codegen/code-stub-assembler.cc b/deps/v8/src/codegen/code-stub-assembler.cc index 76ee8c2d065..415291dd736 100644 --- a/deps/v8/src/codegen/code-stub-assembler.cc +++ b/deps/v8/src/codegen/code-stub-assembler.cc @@ -13391,35 +13391,56 @@ TNode<BoolT> CodeStubAssembler::IsDebugActive() { return Word32NotEqual(is_debug_active, Int32Constant(0)); } -TNode<BoolT> CodeStubAssembler::IsPromiseHookEnabled() { - const TNode<RawPtrT> promise_hook = Load<RawPtrT>( - ExternalConstant(ExternalReference::promise_hook_address(isolate()))); - return WordNotEqual(promise_hook, IntPtrConstant(0)); -} - TNode<BoolT> CodeStubAssembler::HasAsyncEventDelegate() { const TNode<RawPtrT> async_event_delegate = Load<RawPtrT>(ExternalConstant( ExternalReference::async_event_delegate_address(isolate()))); return WordNotEqual(async_event_delegate, IntPtrConstant(0)); } -TNode<BoolT> CodeStubAssembler::IsPromiseHookEnabledOrHasAsyncEventDelegate() { - const TNode<Uint8T> promise_hook_or_async_event_delegate = - Load<Uint8T>(ExternalConstant( - ExternalReference::promise_hook_or_async_event_delegate_address( - isolate()))); - return Word32NotEqual(promise_hook_or_async_event_delegate, Int32Constant(0)); +TNode<Uint32T> CodeStubAssembler::PromiseHookFlags() { + return Load<Uint32T>(ExternalConstant( + ExternalReference::promise_hook_flags_address(isolate()))); +} + +TNode<BoolT> CodeStubAssembler::IsAnyPromiseHookEnabled(TNode<Uint32T> flags) { + uint32_t mask = Isolate::PromiseHookFields::HasContextPromiseHook::kMask | + Isolate::PromiseHookFields::HasIsolatePromiseHook::kMask; + return IsSetWord32(flags, mask); +} + +TNode<BoolT> CodeStubAssembler::IsContextPromiseHookEnabled( + TNode<Uint32T> flags) { + return IsSetWord32<Isolate::PromiseHookFields::HasContextPromiseHook>(flags); +} + +TNode<BoolT> CodeStubAssembler:: + IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate(TNode<Uint32T> flags) { + uint32_t mask = Isolate::PromiseHookFields::HasIsolatePromiseHook::kMask | + Isolate::PromiseHookFields::HasAsyncEventDelegate::kMask; + return IsSetWord32(flags, mask); +} + +TNode<BoolT> CodeStubAssembler:: + IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + TNode<Uint32T> flags) { + uint32_t mask = Isolate::PromiseHookFields::HasIsolatePromiseHook::kMask | + Isolate::PromiseHookFields::HasAsyncEventDelegate::kMask | + Isolate::PromiseHookFields::IsDebugActive::kMask; + return IsSetWord32(flags, mask); +} + +TNode<BoolT> CodeStubAssembler:: + IsAnyPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + TNode<Uint32T> flags) { + return Word32NotEqual(flags, Int32Constant(0)); } TNode<BoolT> CodeStubAssembler:: - IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() { - const TNode<Uint8T> promise_hook_or_debug_is_active_or_async_event_delegate = - Load<Uint8T>(ExternalConstant( - ExternalReference:: - promise_hook_or_debug_is_active_or_async_event_delegate_address( - isolate()))); - return Word32NotEqual(promise_hook_or_debug_is_active_or_async_event_delegate, - Int32Constant(0)); + IsAnyPromiseHookEnabledOrHasAsyncEventDelegate(TNode<Uint32T> flags) { + uint32_t mask = Isolate::PromiseHookFields::HasContextPromiseHook::kMask | + Isolate::PromiseHookFields::HasIsolatePromiseHook::kMask | + Isolate::PromiseHookFields::HasAsyncEventDelegate::kMask; + return IsSetWord32(flags, mask); } TNode<Code> CodeStubAssembler::LoadBuiltin(TNode<Smi> builtin_id) { diff --git a/deps/v8/src/codegen/code-stub-assembler.h b/deps/v8/src/codegen/code-stub-assembler.h index 03af2cc5e2d..702792d7888 100644 --- a/deps/v8/src/codegen/code-stub-assembler.h +++ b/deps/v8/src/codegen/code-stub-assembler.h @@ -3497,10 +3497,44 @@ class V8_EXPORT_PRIVATE CodeStubAssembler TNode<Context> context); // Promise helpers - TNode<BoolT> IsPromiseHookEnabled(); + TNode<Uint32T> PromiseHookFlags(); TNode<BoolT> HasAsyncEventDelegate(); - TNode<BoolT> IsPromiseHookEnabledOrHasAsyncEventDelegate(); - TNode<BoolT> IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(); + TNode<BoolT> IsContextPromiseHookEnabled(TNode<Uint32T> flags); + TNode<BoolT> IsContextPromiseHookEnabled() { + return IsContextPromiseHookEnabled(PromiseHookFlags()); + } + TNode<BoolT> IsAnyPromiseHookEnabled(TNode<Uint32T> flags); + TNode<BoolT> IsAnyPromiseHookEnabled() { + return IsAnyPromiseHookEnabled(PromiseHookFlags()); + } + TNode<BoolT> IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate( + TNode<Uint32T> flags); + TNode<BoolT> IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate() { + return IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate( + PromiseHookFlags()); + } + TNode<BoolT> + IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + TNode<Uint32T> flags); + TNode<BoolT> + IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() { + return IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + PromiseHookFlags()); + } + TNode<BoolT> IsAnyPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + TNode<Uint32T> flags); + TNode<BoolT> + IsAnyPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() { + return IsAnyPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + PromiseHookFlags()); + } + TNode<BoolT> IsAnyPromiseHookEnabledOrHasAsyncEventDelegate( + TNode<Uint32T> flags); + TNode<BoolT> + IsAnyPromiseHookEnabledOrHasAsyncEventDelegate() { + return IsAnyPromiseHookEnabledOrHasAsyncEventDelegate( + PromiseHookFlags()); + } // for..in helpers void CheckPrototypeEnumCache(TNode<JSReceiver> receiver, diff --git a/deps/v8/src/codegen/external-reference.cc b/deps/v8/src/codegen/external-reference.cc index bf75ff3d122..88ce90f4fdc 100644 --- a/deps/v8/src/codegen/external-reference.cc +++ b/deps/v8/src/codegen/external-reference.cc @@ -925,6 +925,11 @@ ExternalReference ExternalReference::cpu_features() { return ExternalReference(&CpuFeatures::supported_); } +ExternalReference ExternalReference::promise_hook_flags_address( + Isolate* isolate) { + return ExternalReference(isolate->promise_hook_flags_address()); +} + ExternalReference ExternalReference::promise_hook_address(Isolate* isolate) { return ExternalReference(isolate->promise_hook_address()); } @@ -934,21 +939,6 @@ ExternalReference ExternalReference::async_event_delegate_address( return ExternalReference(isolate->async_event_delegate_address()); } -ExternalReference -ExternalReference::promise_hook_or_async_event_delegate_address( - Isolate* isolate) { - return ExternalReference( - isolate->promise_hook_or_async_event_delegate_address()); -} - -ExternalReference ExternalReference:: - promise_hook_or_debug_is_active_or_async_event_delegate_address( - Isolate* isolate) { - return ExternalReference( - isolate - ->promise_hook_or_debug_is_active_or_async_event_delegate_address()); -} - ExternalReference ExternalReference::debug_execution_mode_address( Isolate* isolate) { return ExternalReference(isolate->debug_execution_mode_address()); diff --git a/deps/v8/src/codegen/external-reference.h b/deps/v8/src/codegen/external-reference.h index d44b8e801c2..0cd80ca6f1b 100644 --- a/deps/v8/src/codegen/external-reference.h +++ b/deps/v8/src/codegen/external-reference.h @@ -50,13 +50,9 @@ class StatsCounter; V(handle_scope_limit_address, "HandleScope::limit") \ V(scheduled_exception_address, "Isolate::scheduled_exception") \ V(address_of_pending_message_obj, "address_of_pending_message_obj") \ + V(promise_hook_flags_address, "Isolate::promise_hook_flags_address()") \ V(promise_hook_address, "Isolate::promise_hook_address()") \ V(async_event_delegate_address, "Isolate::async_event_delegate_address()") \ - V(promise_hook_or_async_event_delegate_address, \ - "Isolate::promise_hook_or_async_event_delegate_address()") \ - V(promise_hook_or_debug_is_active_or_async_event_delegate_address, \ - "Isolate::promise_hook_or_debug_is_active_or_async_event_delegate_" \ - "address()") \ V(debug_execution_mode_address, "Isolate::debug_execution_mode_address()") \ V(debug_is_active_address, "Debug::is_active_address()") \ V(debug_hook_on_function_call_address, \ diff --git a/deps/v8/src/d8/d8.cc b/deps/v8/src/d8/d8.cc index 999e8c2b964..63bcdf871f1 100644 --- a/deps/v8/src/d8/d8.cc +++ b/deps/v8/src/d8/d8.cc @@ -1792,6 +1792,20 @@ void Shell::AsyncHooksTriggerAsyncId( PerIsolateData::Get(isolate)->GetAsyncHooks()->GetTriggerAsyncId())); } +void Shell::SetPromiseHooks(const v8::FunctionCallbackInfo<v8::Value>& args) { + Isolate* isolate = args.GetIsolate(); + Local<Context> context = isolate->GetCurrentContext(); + HandleScope handle_scope(isolate); + + context->SetPromiseHooks( + args[0]->IsFunction() ? args[0].As<Function>() : Local<Function>(), + args[1]->IsFunction() ? args[1].As<Function>() : Local<Function>(), + args[2]->IsFunction() ? args[2].As<Function>() : Local<Function>(), + args[3]->IsFunction() ? args[3].As<Function>() : Local<Function>()); + + args.GetReturnValue().Set(v8::Undefined(isolate)); +} + void WriteToFile(FILE* file, const v8::FunctionCallbackInfo<v8::Value>& args) { for (int i = 0; i < args.Length(); i++) { HandleScope handle_scope(args.GetIsolate()); @@ -2582,6 +2596,14 @@ Local<ObjectTemplate> Shell::CreateD8Template(Isolate* isolate) { d8_template->Set(isolate, "log", log_template); } + { + Local<ObjectTemplate> promise_template = ObjectTemplate::New(isolate); + promise_template->Set( + isolate, "setHooks", + FunctionTemplate::New(isolate, SetPromiseHooks, Local<Value>(), + Local<Signature>(), 4)); + d8_template->Set(isolate, "promise", promise_template); + } return d8_template; } diff --git a/deps/v8/src/d8/d8.h b/deps/v8/src/d8/d8.h index a9f6f3bc8b8..6ea9681d699 100644 --- a/deps/v8/src/d8/d8.h +++ b/deps/v8/src/d8/d8.h @@ -478,6 +478,8 @@ class Shell : public i::AllStatic { static void AsyncHooksTriggerAsyncId( const v8::FunctionCallbackInfo<v8::Value>& args); + static void SetPromiseHooks(const v8::FunctionCallbackInfo<v8::Value>& args); + static void Print(const v8::FunctionCallbackInfo<v8::Value>& args); static void PrintErr(const v8::FunctionCallbackInfo<v8::Value>& args); static void Write(const v8::FunctionCallbackInfo<v8::Value>& args); diff --git a/deps/v8/src/execution/isolate.cc b/deps/v8/src/execution/isolate.cc index 6f7efe39869..7fa6d7774da 100644 --- a/deps/v8/src/execution/isolate.cc +++ b/deps/v8/src/execution/isolate.cc @@ -4045,19 +4045,23 @@ void Isolate::FireCallCompletedCallback(MicrotaskQueue* microtask_queue) { } } -void Isolate::PromiseHookStateUpdated() { - bool promise_hook_or_async_event_delegate = - promise_hook_ || async_event_delegate_; - bool promise_hook_or_debug_is_active_or_async_event_delegate = - promise_hook_or_async_event_delegate || debug()->is_active(); - if (promise_hook_or_debug_is_active_or_async_event_delegate && - Protectors::IsPromiseHookIntact(this)) { +void Isolate::UpdatePromiseHookProtector() { + if (Protectors::IsPromiseHookIntact(this)) { HandleScope scope(this); Protectors::InvalidatePromiseHook(this); } - promise_hook_or_async_event_delegate_ = promise_hook_or_async_event_delegate; - promise_hook_or_debug_is_active_or_async_event_delegate_ = - promise_hook_or_debug_is_active_or_async_event_delegate; +} + +void Isolate::PromiseHookStateUpdated() { + promise_hook_flags_ = + (promise_hook_flags_ & PromiseHookFields::HasContextPromiseHook::kMask) | + PromiseHookFields::HasIsolatePromiseHook::encode(promise_hook_) | + PromiseHookFields::HasAsyncEventDelegate::encode(async_event_delegate_) | + PromiseHookFields::IsDebugActive::encode(debug()->is_active()); + + if (promise_hook_flags_ != 0) { + UpdatePromiseHookProtector(); + } } namespace { @@ -4357,17 +4361,30 @@ void Isolate::SetPromiseHook(PromiseHook hook) { PromiseHookStateUpdated(); } +void Isolate::RunAllPromiseHooks(PromiseHookType type, + Handle<JSPromise> promise, + Handle<Object> parent) { + if (HasContextPromiseHooks()) { + native_context()->RunPromiseHook(type, promise, parent); + } + if (HasIsolatePromiseHooks() || HasAsyncEventDelegate()) { + RunPromiseHook(type, promise, parent); + } +} + void Isolate::RunPromiseHook(PromiseHookType type, Handle<JSPromise> promise, Handle<Object> parent) { RunPromiseHookForAsyncEventDelegate(type, promise); - if (promise_hook_ == nullptr) return; + if (!HasIsolatePromiseHooks()) return; + DCHECK(promise_hook_ != nullptr); promise_hook_(type, v8::Utils::PromiseToLocal(promise), v8::Utils::ToLocal(parent)); } void Isolate::RunPromiseHookForAsyncEventDelegate(PromiseHookType type, Handle<JSPromise> promise) { - if (!async_event_delegate_) return; + if (!HasAsyncEventDelegate()) return; + DCHECK(async_event_delegate_ != nullptr); switch (type) { case PromiseHookType::kResolve: return; diff --git a/deps/v8/src/execution/isolate.h b/deps/v8/src/execution/isolate.h index cd819c5087f..e08195674fd 100644 --- a/deps/v8/src/execution/isolate.h +++ b/deps/v8/src/execution/isolate.h @@ -1428,21 +1428,27 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory { } #endif - Address promise_hook_address() { - return reinterpret_cast<Address>(&promise_hook_); + void SetHasContextPromiseHooks(bool context_promise_hook) { + promise_hook_flags_ = PromiseHookFields::HasContextPromiseHook::update( + promise_hook_flags_, context_promise_hook); + PromiseHookStateUpdated(); } - Address async_event_delegate_address() { - return reinterpret_cast<Address>(&async_event_delegate_); + bool HasContextPromiseHooks() const { + return PromiseHookFields::HasContextPromiseHook::decode( + promise_hook_flags_); } - Address promise_hook_or_async_event_delegate_address() { - return reinterpret_cast<Address>(&promise_hook_or_async_event_delegate_); + Address promise_hook_flags_address() { + return reinterpret_cast<Address>(&promise_hook_flags_); } - Address promise_hook_or_debug_is_active_or_async_event_delegate_address() { - return reinterpret_cast<Address>( - &promise_hook_or_debug_is_active_or_async_event_delegate_); + Address promise_hook_address() { + return reinterpret_cast<Address>(&promise_hook_); + } + + Address async_event_delegate_address() { + return reinterpret_cast<Address>(&async_event_delegate_); } Address handle_scope_implementer_address() { @@ -1460,6 +1466,9 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory { void SetPromiseHook(PromiseHook hook); void RunPromiseHook(PromiseHookType type, Handle<JSPromise> promise, Handle<Object> parent); + void RunAllPromiseHooks(PromiseHookType type, Handle<JSPromise> promise, + Handle<Object> parent); + void UpdatePromiseHookProtector(); void PromiseHookStateUpdated(); void AddDetachedContext(Handle<Context> context); @@ -1697,6 +1706,13 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory { } #endif + struct PromiseHookFields { + using HasContextPromiseHook = base::BitField<bool, 0, 1>; + using HasIsolatePromiseHook = HasContextPromiseHook::Next<bool, 1>; + using HasAsyncEventDelegate = HasIsolatePromiseHook::Next<bool, 1>; + using IsDebugActive = HasAsyncEventDelegate::Next<bool, 1>; + }; + private: explicit Isolate(std::unique_ptr<IsolateAllocator> isolate_allocator); ~Isolate(); @@ -1780,6 +1796,16 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory { void RunPromiseHookForAsyncEventDelegate(PromiseHookType type, Handle<JSPromise> promise); + bool HasIsolatePromiseHooks() const { + return PromiseHookFields::HasIsolatePromiseHook::decode( + promise_hook_flags_); + } + + bool HasAsyncEventDelegate() const { + return PromiseHookFields::HasAsyncEventDelegate::decode( + promise_hook_flags_); + } + const char* RAILModeName(RAILMode rail_mode) const { switch (rail_mode) { case PERFORMANCE_RESPONSE: @@ -2032,8 +2058,7 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory { debug::ConsoleDelegate* console_delegate_ = nullptr; debug::AsyncEventDelegate* async_event_delegate_ = nullptr; - bool promise_hook_or_async_event_delegate_ = false; - bool promise_hook_or_debug_is_active_or_async_event_delegate_ = false; + uint32_t promise_hook_flags_ = 0; int async_task_count_ = 0; std::unique_ptr<LocalIsolate> main_thread_local_isolate_; diff --git a/deps/v8/src/heap/factory.cc b/deps/v8/src/heap/factory.cc index bd7ec8915ca..ca9d316e2bb 100644 --- a/deps/v8/src/heap/factory.cc +++ b/deps/v8/src/heap/factory.cc @@ -3377,7 +3377,8 @@ Handle<JSPromise> Factory::NewJSPromiseWithoutHook() { Handle<JSPromise> Factory::NewJSPromise() { Handle<JSPromise> promise = NewJSPromiseWithoutHook(); - isolate()->RunPromiseHook(PromiseHookType::kInit, promise, undefined_value()); + isolate()->RunAllPromiseHooks(PromiseHookType::kInit, promise, + undefined_value()); return promise; } diff --git a/deps/v8/src/objects/contexts.cc b/deps/v8/src/objects/contexts.cc index af73bf02568..771fbea40bb 100644 --- a/deps/v8/src/objects/contexts.cc +++ b/deps/v8/src/objects/contexts.cc @@ -511,5 +511,53 @@ STATIC_ASSERT(NativeContext::kSize == (Context::SizeFor(NativeContext::NATIVE_CONTEXT_SLOTS) + kSystemPointerSize)); +void NativeContext::RunPromiseHook(PromiseHookType type, + Handle<JSPromise> promise, + Handle<Object> parent) { + Isolate* isolate = promise->GetIsolate(); + DCHECK(isolate->HasContextPromiseHooks()); + int contextSlot; + + switch (type) { + case PromiseHookType::kInit: + contextSlot = PROMISE_HOOK_INIT_FUNCTION_INDEX; + break; + case PromiseHookType::kResolve: + contextSlot = PROMISE_HOOK_RESOLVE_FUNCTION_INDEX; + break; + case PromiseHookType::kBefore: + contextSlot = PROMISE_HOOK_BEFORE_FUNCTION_INDEX; + break; + case PromiseHookType::kAfter: + contextSlot = PROMISE_HOOK_AFTER_FUNCTION_INDEX; + break; + default: + UNREACHABLE(); + } + + Handle<Object> hook(isolate->native_context()->get(contextSlot), isolate); + if (hook->IsUndefined()) return; + + int argc = type == PromiseHookType::kInit ? 2 : 1; + Handle<Object> argv[2] = { + Handle<Object>::cast(promise), + parent + }; + + Handle<Object> receiver = isolate->global_proxy(); + + if (Execution::Call(isolate, hook, receiver, argc, argv).is_null()) { + DCHECK(isolate->has_pending_exception()); + Handle<Object> exception(isolate->pending_exception(), isolate); + + MessageLocation* no_location = nullptr; + Handle<JSMessageObject> message = + isolate->CreateMessageOrAbort(exception, no_location); + MessageHandler::ReportMessage(isolate, no_location, message); + + isolate->clear_pending_exception(); + } +} + } // namespace internal } // namespace v8 diff --git a/deps/v8/src/objects/contexts.h b/deps/v8/src/objects/contexts.h index 47784cf4051..0a67d52d5ed 100644 --- a/deps/v8/src/objects/contexts.h +++ b/deps/v8/src/objects/contexts.h @@ -198,6 +198,11 @@ enum ContextLookupFlags { V(NUMBER_FUNCTION_INDEX, JSFunction, number_function) \ V(OBJECT_FUNCTION_INDEX, JSFunction, object_function) \ V(OBJECT_FUNCTION_PROTOTYPE_MAP_INDEX, Map, object_function_prototype_map) \ + V(PROMISE_HOOK_INIT_FUNCTION_INDEX, Object, promise_hook_init_function) \ + V(PROMISE_HOOK_BEFORE_FUNCTION_INDEX, Object, promise_hook_before_function) \ + V(PROMISE_HOOK_AFTER_FUNCTION_INDEX, Object, promise_hook_after_function) \ + V(PROMISE_HOOK_RESOLVE_FUNCTION_INDEX, Object, \ + promise_hook_resolve_function) \ V(PROXY_CALLABLE_MAP_INDEX, Map, proxy_callable_map) \ V(PROXY_CONSTRUCTOR_MAP_INDEX, Map, proxy_constructor_map) \ V(PROXY_FUNCTION_INDEX, JSFunction, proxy_function) \ @@ -692,6 +697,9 @@ class NativeContext : public Context { void IncrementErrorsThrown(); int GetErrorsThrown(); + void RunPromiseHook(PromiseHookType type, Handle<JSPromise> promise, + Handle<Object> parent); + private: STATIC_ASSERT(OffsetOfElementAt(EMBEDDER_DATA_INDEX) == Internals::kNativeContextEmbedderDataOffset); diff --git a/deps/v8/src/objects/contexts.tq b/deps/v8/src/objects/contexts.tq index 604852c24ea..ff427629abe 100644 --- a/deps/v8/src/objects/contexts.tq +++ b/deps/v8/src/objects/contexts.tq @@ -124,6 +124,12 @@ extern enum ContextSlot extends intptr constexpr 'Context::Field' { PROMISE_PROTOTYPE_INDEX: Slot<NativeContext, JSObject>, STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX: Slot<NativeContext, Map>, + PROMISE_HOOK_INIT_FUNCTION_INDEX: Slot<NativeContext, Undefined|JSFunction>, + PROMISE_HOOK_BEFORE_FUNCTION_INDEX: Slot<NativeContext, Undefined|JSFunction>, + PROMISE_HOOK_AFTER_FUNCTION_INDEX: Slot<NativeContext, Undefined|JSFunction>, + PROMISE_HOOK_RESOLVE_FUNCTION_INDEX: + Slot<NativeContext, Undefined|JSFunction>, + CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX: Slot<NativeContext, HeapObject>, BOUND_FUNCTION_WITH_CONSTRUCTOR_MAP_INDEX: Slot<NativeContext, Map>, diff --git a/deps/v8/src/objects/objects.cc b/deps/v8/src/objects/objects.cc index d9cb7486be8..697ffdec088 100644 --- a/deps/v8/src/objects/objects.cc +++ b/deps/v8/src/objects/objects.cc @@ -5254,8 +5254,8 @@ Handle<Object> JSPromise::Reject(Handle<JSPromise> promise, if (isolate->debug()->is_active()) MoveMessageToPromise(isolate, promise); if (debug_event) isolate->debug()->OnPromiseReject(promise, reason); - isolate->RunPromiseHook(PromiseHookType::kResolve, promise, - isolate->factory()->undefined_value()); + isolate->RunAllPromiseHooks(PromiseHookType::kResolve, promise, + isolate->factory()->undefined_value()); // 1. Assert: The value of promise.[[PromiseState]] is "pending". CHECK_EQ(Promise::kPending, promise->status()); @@ -5290,8 +5290,8 @@ MaybeHandle<Object> JSPromise::Resolve(Handle<JSPromise> promise, DCHECK( !reinterpret_cast<v8::Isolate*>(isolate)->GetCurrentContext().IsEmpty()); - isolate->RunPromiseHook(PromiseHookType::kResolve, promise, - isolate->factory()->undefined_value()); + isolate->RunAllPromiseHooks(PromiseHookType::kResolve, promise, + isolate->factory()->undefined_value()); // 7. If SameValue(resolution, promise) is true, then if (promise.is_identical_to(resolution)) { diff --git a/deps/v8/src/runtime/runtime-promise.cc b/deps/v8/src/runtime/runtime-promise.cc index c1ee96facca..54adc4c920f 100644 --- a/deps/v8/src/runtime/runtime-promise.cc +++ b/deps/v8/src/runtime/runtime-promise.cc @@ -29,8 +29,8 @@ RUNTIME_FUNCTION(Runtime_PromiseRejectEventFromStack) { // undefined, which we interpret as being a caught exception event. rejected_promise = isolate->GetPromiseOnStackOnThrow(); } - isolate->RunPromiseHook(PromiseHookType::kResolve, promise, - isolate->factory()->undefined_value()); + isolate->RunAllPromiseHooks(PromiseHookType::kResolve, promise, + isolate->factory()->undefined_value()); isolate->debug()->OnPromiseReject(rejected_promise, value); // Report only if we don't actually have a handler. @@ -142,7 +142,7 @@ Handle<JSPromise> AwaitPromisesInitCommon(Isolate* isolate, // hook for the throwaway promise (passing the {promise} as its // parent). Handle<JSPromise> throwaway = isolate->factory()->NewJSPromiseWithoutHook(); - isolate->RunPromiseHook(PromiseHookType::kInit, throwaway, promise); + isolate->RunAllPromiseHooks(PromiseHookType::kInit, throwaway, promise); // On inspector side we capture async stack trace and store it by // outer_promise->async_task_id when async function is suspended first time. @@ -204,7 +204,7 @@ RUNTIME_FUNCTION(Runtime_AwaitPromisesInitOld) { // Fire the init hook for the wrapper promise (that we created for the // {value} previously). - isolate->RunPromiseHook(PromiseHookType::kInit, promise, outer_promise); + isolate->RunAllPromiseHooks(PromiseHookType::kInit, promise, outer_promise); return *AwaitPromisesInitCommon(isolate, value, promise, outer_promise, reject_handler, is_predicted_as_caught); } diff --git a/deps/v8/test/cctest/test-code-stub-assembler.cc b/deps/v8/test/cctest/test-code-stub-assembler.cc index 07e6c768f54..1ba26a81b86 100644 --- a/deps/v8/test/cctest/test-code-stub-assembler.cc +++ b/deps/v8/test/cctest/test-code-stub-assembler.cc @@ -2595,7 +2595,8 @@ TEST(IsPromiseHookEnabled) { CodeStubAssembler m(asm_tester.state()); m.Return( - m.SelectBooleanConstant(m.IsPromiseHookEnabledOrHasAsyncEventDelegate())); + m.SelectBooleanConstant( + m.IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate())); FunctionTester ft(asm_tester.GenerateCode(), kNumParams); Handle<Object> result = diff --git a/deps/v8/test/mjsunit/promise-hooks.js b/deps/v8/test/mjsunit/promise-hooks.js new file mode 100644 index 00000000000..9e13206a525 --- /dev/null +++ b/deps/v8/test/mjsunit/promise-hooks.js @@ -0,0 +1,244 @@ +// Copyright 2020 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Flags: --allow-natives-syntax --opt --no-always-opt --no-stress-opt --deopt-every-n-times=0 --ignore-unhandled-promises + +let log = []; +let asyncId = 0; + +function logEvent (type, args) { + const promise = args[0]; + promise.asyncId = promise.asyncId || ++asyncId; + log.push({ + type, + promise, + parent: args[1], + argsLength: args.length + }) +} +function initHook(...args) { + logEvent('init', args); +} +function resolveHook(...args) { + logEvent('resolve', args); +} +function beforeHook(...args) { + logEvent('before', args); +} +function afterHook(...args) { + logEvent('after', args); +} + +function printLog(message) { + console.log(` --- ${message} --- `) + for (const event of log) { + console.log(JSON.stringify(event)) + } +} + +function assertNextEvent(type, args) { + const [ promiseOrId, parentOrId ] = args; + const nextEvent = log.shift(); + + assertEquals(type, nextEvent.type); + assertEquals(type === 'init' ? 2 : 1, nextEvent.argsLength); + + assertTrue(nextEvent.promise instanceof Promise); + if (promiseOrId instanceof Promise) { + assertEquals(promiseOrId, nextEvent.promise); + } else { + assertTrue(typeof promiseOrId === 'number'); + assertEquals(promiseOrId, nextEvent.promise?.asyncId); + } + + if (parentOrId instanceof Promise) { + assertEquals(parentOrId, nextEvent.parent); + assertTrue(nextEvent.parent instanceof Promise); + } else if (typeof parentOrId === 'number') { + assertEquals(parentOrId, nextEvent.parent?.asyncId); + assertTrue(nextEvent.parent instanceof Promise); + } else { + assertEquals(undefined, parentOrId); + assertEquals(undefined, nextEvent.parent); + } +} +function assertEmptyLog() { + assertEquals(0, log.length); + asyncId = 0; + log = []; +} + +// Verify basic log structure of different promise behaviours +function basicTest() { + d8.promise.setHooks(initHook, beforeHook, afterHook, resolveHook); + + // `new Promise(...)` triggers init event with correct promise + var done, p1 = new Promise(r => done = r); + %PerformMicrotaskCheckpoint(); + assertNextEvent('init', [ p1 ]); + assertEmptyLog(); + + // `promise.then(...)` triggers init event with correct promise and parent + var p2 = p1.then(() => { }); + %PerformMicrotaskCheckpoint(); + assertNextEvent('init', [ p2, p1 ]); + assertEmptyLog(); + + // `resolve(...)` triggers resolve event and any already attached continuations + done(); + %PerformMicrotaskCheckpoint(); + assertNextEvent('resolve', [ p1 ]); + assertNextEvent('before', [ p2 ]); + assertNextEvent('resolve', [ p2 ]); + assertNextEvent('after', [ p2 ]); + assertEmptyLog(); + + // `reject(...)` triggers the resolve event + var done, p3 = new Promise((_, r) => done = r); + done(); + %PerformMicrotaskCheckpoint(); + assertNextEvent('init', [ p3 ]); + assertNextEvent('resolve', [ p3 ]); + assertEmptyLog(); + + // `promise.catch(...)` triggers init event with correct promise and parent + // When the promise is already completed, the continuation should also run + // immediately at the next checkpoint. + var p4 = p3.catch(() => { }); + %PerformMicrotaskCheckpoint(); + assertNextEvent('init', [ p4, p3 ]); + assertNextEvent('before', [ p4 ]); + assertNextEvent('resolve', [ p4 ]); + assertNextEvent('after', [ p4 ]); + assertEmptyLog(); + + // Detach hooks + d8.promise.setHooks(); +} + +// Exceptions thrown in hook handlers should not raise or reject +function exceptions() { + function thrower() { + throw new Error('unexpected!'); + } + + // Init hook + d8.promise.setHooks(thrower); + assertDoesNotThrow(() => { + Promise.resolve() + .catch(assertUnreachable); + }); + %PerformMicrotaskCheckpoint(); + d8.promise.setHooks(); + + // Before hook + d8.promise.setHooks(undefined, thrower); + assertDoesNotThrow(() => { + Promise.resolve() + .then(() => {}) + .catch(assertUnreachable); + }); + %PerformMicrotaskCheckpoint(); + d8.promise.setHooks(); + + // After hook + d8.promise.setHooks(undefined, undefined, thrower); + assertDoesNotThrow(() => { + Promise.resolve() + .then(() => {}) + .catch(assertUnreachable); + }); + %PerformMicrotaskCheckpoint(); + d8.promise.setHooks(); + + // Resolve hook + d8.promise.setHooks(undefined, undefined, undefined, thrower); + assertDoesNotThrow(() => { + Promise.resolve() + .catch(assertUnreachable); + }); + %PerformMicrotaskCheckpoint(); + d8.promise.setHooks(); + + // Resolve hook for a reject + d8.promise.setHooks(undefined, undefined, undefined, thrower); + assertDoesNotThrow(() => { + Promise.reject() + .then(assertUnreachable) + .catch(); + }); + %PerformMicrotaskCheckpoint(); + d8.promise.setHooks(); +} + +// For now, expect the optimizer to bail out on async functions +// when context promise hooks are attached. +function optimizerBailout(test, verify) { + // Warm up test method + %PrepareFunctionForOptimization(test); + assertUnoptimized(test); + test(); + test(); + test(); + %PerformMicrotaskCheckpoint(); + + // Prove transition to optimized code when no hooks are present + assertUnoptimized(test); + %OptimizeFunctionOnNextCall(test); + test(); + assertOptimized(test); + %PerformMicrotaskCheckpoint(); + + // Verify that attaching hooks deopts the async function + d8.promise.setHooks(initHook, beforeHook, afterHook, resolveHook); + // assertUnoptimized(test); + + // Verify log structure of deoptimized call + %PrepareFunctionForOptimization(test); + test(); + %PerformMicrotaskCheckpoint(); + + verify(); + + // Optimize and verify log structure again + %OptimizeFunctionOnNextCall(test); + test(); + assertOptimized(test); + %PerformMicrotaskCheckpoint(); + + verify(); + + d8.promise.setHooks(); +} + +optimizerBailout(async () => { + await Promise.resolve(); +}, () => { + assertNextEvent('init', [ 1 ]); + assertNextEvent('init', [ 2 ]); + assertNextEvent('resolve', [ 2 ]); + assertNextEvent('init', [ 3, 2 ]); + assertNextEvent('before', [ 3 ]); + assertNextEvent('resolve', [ 1 ]); + assertNextEvent('resolve', [ 3 ]); + assertNextEvent('after', [ 3 ]); + assertEmptyLog(); +}); +optimizerBailout(async () => { + await { then (cb) { cb() } }; +}, () => { + assertNextEvent('init', [ 1 ]); + assertNextEvent('init', [ 2, 1 ]); + assertNextEvent('init', [ 3, 2 ]); + assertNextEvent('before', [ 2 ]); + assertNextEvent('resolve', [ 2 ]); + assertNextEvent('after', [ 2 ]); + assertNextEvent('before', [ 3 ]); + assertNextEvent('resolve', [ 1 ]); + assertNextEvent('resolve', [ 3 ]); + assertNextEvent('after', [ 3 ]); + assertEmptyLog(); +}); +basicTest(); +exceptions(); |