diff --git a/common.gypi b/common.gypi index fda54611734358..3126f43aa975f3 100644 --- a/common.gypi +++ b/common.gypi @@ -36,7 +36,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.76', + 'v8_embedder_string': '-node.77', ##### V8 defaults for Node.js ##### diff --git a/deps/v8/AUTHORS b/deps/v8/AUTHORS index 13a915c130b7a2..a2c09adfc83135 100644 --- a/deps/v8/AUTHORS +++ b/deps/v8/AUTHORS @@ -190,6 +190,7 @@ Seo Sanghyeon Shawn Anastasio Shawn Presser Stefan Penner +Stephen Belanger Sylvestre Ledru Taketoshi Aono Teddy Katz diff --git a/deps/v8/include/v8.h b/deps/v8/include/v8.h index fe31e4cac2823b..92d3623df54506 100644 --- a/deps/v8/include/v8.h +++ b/deps/v8/include/v8.h @@ -10488,6 +10488,18 @@ class V8_EXPORT Context { */ void SetContinuationPreservedEmbedderData(Local 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 init_hook, + Local before_hook, + Local after_hook, + Local 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 3b6226b0f438d3..b462697bc3dd16 100644 --- a/deps/v8/src/api/api.cc +++ b/deps/v8/src/api/api.cc @@ -6078,6 +6078,45 @@ void Context::SetContinuationPreservedEmbedderData(Local data) { *i::Handle::cast(Utils::OpenHandle(*data))); } +void v8::Context::SetPromiseHooks(Local init_hook, + Local before_hook, + Local after_hook, + Local resolve_hook) { + i::Handle context = Utils::OpenHandle(this); + i::Isolate* isolate = context->GetIsolate(); + + i::Handle init = isolate->factory()->undefined_value(); + i::Handle before = isolate->factory()->undefined_value(); + i::Handle after = isolate->factory()->undefined_value(); + i::Handle 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); +} + namespace { i::Address* GetSerializedDataFromFixedArray(i::Isolate* isolate, i::FixedArray list, size_t index) { diff --git a/deps/v8/src/builtins/builtins-async-function-gen.cc b/deps/v8/src/builtins/builtins-async-function-gen.cc index e84442295cfc90..7b557d183ea2c8 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 383289fd0f3ea4..38bb304a20f195 100644 --- a/deps/v8/src/builtins/builtins-async-gen.cc +++ b/deps/v8/src/builtins/builtins-async-gen.cc @@ -99,18 +99,11 @@ TNode 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 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, TNode value, + TNode promise, TNode outer_promise, + TNode on_reject, TNode is_predicted_as_caught, + TVariable* 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 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 AsyncBuiltinsAssembler::Await( diff --git a/deps/v8/src/builtins/builtins-async-gen.h b/deps/v8/src/builtins/builtins-async-gen.h index 833e78d45d5be0..34b7a0ce1d654c 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 on_resolve_sfi, TNode on_reject_sfi, TNode is_predicted_as_caught); + + void InitAwaitPromise( + Runtime::FunctionId id, TNode context, TNode value, + TNode promise, TNode outer_promise, + TNode on_reject, TNode is_predicted_as_caught, + TVariable* 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 2b6d72088083b8..89cb64c0ec1f47 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 1da6f54c82057b..534f2528a4cf1a 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 native_context); void RewindEnteredContext(TNode saved_entered_context_count); + void RunAllPromiseHooks(PromiseHookType type, TNode context, + TNode promise_or_capability); void RunPromiseHook(Runtime::FunctionId id, TNode context, - TNode promise_or_capability); + TNode promise_or_capability, + TNode promiseHookFlags); }; TNode MicrotaskQueueBuiltinsAssembler::GetMicrotaskQueue( @@ -198,7 +201,7 @@ void MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask( const TNode thenable = LoadObjectField( microtask, PromiseResolveThenableJobTask::kThenableOffset); - RunPromiseHook(Runtime::kPromiseHookBefore, microtask_context, + RunAllPromiseHooks(PromiseHookType::kBefore, microtask_context, CAST(promise_to_resolve)); { @@ -207,7 +210,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); @@ -242,8 +245,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); @@ -252,8 +255,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); @@ -295,8 +298,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); @@ -305,8 +308,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); @@ -464,12 +467,43 @@ void MicrotaskQueueBuiltinsAssembler::RewindEnteredContext( saved_entered_context_count); } +void MicrotaskQueueBuiltinsAssembler::RunAllPromiseHooks( + PromiseHookType type, TNode context, + TNode promise_or_capability) { + Label hook(this, Label::kDeferred), done_hook(this); + TNode 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, - TNode promise_or_capability) { + TNode promise_or_capability, + TNode 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/promise-abstract-operations.tq b/deps/v8/src/builtins/promise-abstract-operations.tq index 9cf6da102b8eec..c04db9f5e38849 100644 --- a/deps/v8/src/builtins/promise-abstract-operations.tq +++ b/deps/v8/src/builtins/promise-abstract-operations.tq @@ -186,6 +186,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); @@ -204,17 +206,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"). @@ -223,6 +232,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 b7fad88f6fc891..0e914aef4057b4 100644 --- a/deps/v8/src/builtins/promise-all.tq +++ b/deps/v8/src/builtins/promise-all.tq @@ -234,7 +234,7 @@ Reject(Object) { // PerformPromiseThen), since this is only necessary for DevTools and // PromiseHooks. if (promiseResolveFunction != Undefined || - IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() || + IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() || IsPromiseSpeciesProtectorCellInvalid() || Is(nextValue) || !IsPromiseThenLookupChainIntact( nativeContext, UnsafeCast(nextValue).map)) { diff --git a/deps/v8/src/builtins/promise-constructor.tq b/deps/v8/src/builtins/promise-constructor.tq index dbf1fe2f4ded26..8b29d28713595b 100644 --- a/deps/v8/src/builtins/promise-constructor.tq +++ b/deps/v8/src/builtins/promise-constructor.tq @@ -40,7 +40,8 @@ extern macro ConstructorBuiltinsAssembler::EmitFastNewObject( Context, JSFunction, JSReceiver): JSObject; extern macro -PromiseBuiltinsAssembler::IsPromiseHookEnabledOrHasAsyncEventDelegate(): bool; +PromiseBuiltinsAssembler::IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate( + uint32): bool; // https://tc39.es/ecma262/#sec-promise-executor transitioning javascript builtin @@ -74,9 +75,7 @@ PromiseConstructor( result = UnsafeCast(EmitFastNewObject( context, promiseFun, UnsafeCast(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 6c64baf22d19ba..50bcd395288051 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 = nativeContext[NativeContextSlot::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 7ed2f7909a88c1..3c2863077307d5 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,107 @@ macro NewPromiseRejectReactionJobTask(implicit context: Context)( }; } +@export +transitioning macro RunContextPromiseHookInit(implicit context: Context)( + promise: JSPromise, parent: Object) { + const nativeContext: NativeContext = LoadNativeContext(context); + const hook = Cast( + nativeContext[NativeContextSlot::PROMISE_HOOK_INIT_FUNCTION_INDEX]) + otherwise return; + const parentObject = Is(parent) ? Cast(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( + NativeContextSlot::PROMISE_HOOK_RESOLVE_FUNCTION_INDEX, promise, + PromiseHookFlags()); +} + +@export +transitioning macro RunContextPromiseHookResolve(implicit context: Context)( + promise: JSPromise, flags: uint32) { + RunContextPromiseHook( + NativeContextSlot::PROMISE_HOOK_RESOLVE_FUNCTION_INDEX, promise, flags); +} + +@export +transitioning macro RunContextPromiseHookBefore(implicit context: Context)( + promiseOrCapability: JSPromise|PromiseCapability) { + RunContextPromiseHook( + NativeContextSlot::PROMISE_HOOK_BEFORE_FUNCTION_INDEX, promiseOrCapability, + PromiseHookFlags()); +} + +@export +transitioning macro RunContextPromiseHookBefore(implicit context: Context)( + promiseOrCapability: JSPromise|PromiseCapability, flags: uint32) { + RunContextPromiseHook( + NativeContextSlot::PROMISE_HOOK_BEFORE_FUNCTION_INDEX, promiseOrCapability, + flags); +} + +@export +transitioning macro RunContextPromiseHookAfter(implicit context: Context)( + promiseOrCapability: JSPromise|PromiseCapability) { + RunContextPromiseHook( + NativeContextSlot::PROMISE_HOOK_AFTER_FUNCTION_INDEX, promiseOrCapability, + PromiseHookFlags()); +} + +@export +transitioning macro RunContextPromiseHookAfter(implicit context: Context)( + promiseOrCapability: JSPromise|PromiseCapability, flags: uint32) { + RunContextPromiseHook( + NativeContextSlot::PROMISE_HOOK_AFTER_FUNCTION_INDEX, promiseOrCapability, + flags); +} + +transitioning macro RunContextPromiseHook(implicit context: Context)( + slot: NativeContextSlot, + promiseOrCapability: JSPromise|PromiseCapability, flags: uint32) { + if (!IsContextPromiseHookEnabled(flags)) return; + const nativeContext: NativeContext = LoadNativeContext(context); + const hook = Cast(nativeContext[slot]) otherwise return; + + let promise: JSPromise; + typeswitch (promiseOrCapability) { + case (jspromise: JSPromise): { + promise = jspromise; + } + case (capability: PromiseCapability): { + promise = Cast(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 +210,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 +232,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 dbb60720c04416..cb66f6ef0c87d6 100644 --- a/deps/v8/src/builtins/promise-resolve.tq +++ b/deps/v8/src/builtins/promise-resolve.tq @@ -95,7 +95,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 50fb563244cee4..e330d99f0389d9 100644 --- a/deps/v8/src/codegen/code-stub-assembler.cc +++ b/deps/v8/src/codegen/code-stub-assembler.cc @@ -12737,35 +12737,56 @@ TNode CodeStubAssembler::IsDebugActive() { return Word32NotEqual(is_debug_active, Int32Constant(0)); } -TNode CodeStubAssembler::IsPromiseHookEnabled() { - const TNode promise_hook = Load( - ExternalConstant(ExternalReference::promise_hook_address(isolate()))); - return WordNotEqual(promise_hook, IntPtrConstant(0)); -} - TNode CodeStubAssembler::HasAsyncEventDelegate() { const TNode async_event_delegate = Load(ExternalConstant( ExternalReference::async_event_delegate_address(isolate()))); return WordNotEqual(async_event_delegate, IntPtrConstant(0)); } -TNode CodeStubAssembler::IsPromiseHookEnabledOrHasAsyncEventDelegate() { - const TNode promise_hook_or_async_event_delegate = - Load(ExternalConstant( - ExternalReference::promise_hook_or_async_event_delegate_address( - isolate()))); - return Word32NotEqual(promise_hook_or_async_event_delegate, Int32Constant(0)); +TNode CodeStubAssembler::PromiseHookFlags() { + return Load(ExternalConstant( + ExternalReference::promise_hook_flags_address(isolate()))); +} + +TNode CodeStubAssembler::IsAnyPromiseHookEnabled(TNode flags) { + uint32_t mask = Isolate::PromiseHookFields::HasContextPromiseHook::kMask | + Isolate::PromiseHookFields::HasIsolatePromiseHook::kMask; + return IsSetWord32(flags, mask); +} + +TNode CodeStubAssembler::IsContextPromiseHookEnabled( + TNode flags) { + return IsSetWord32(flags); +} + +TNode CodeStubAssembler:: + IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate(TNode flags) { + uint32_t mask = Isolate::PromiseHookFields::HasIsolatePromiseHook::kMask | + Isolate::PromiseHookFields::HasAsyncEventDelegate::kMask; + return IsSetWord32(flags, mask); +} + +TNode CodeStubAssembler:: + IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + TNode flags) { + uint32_t mask = Isolate::PromiseHookFields::HasIsolatePromiseHook::kMask | + Isolate::PromiseHookFields::HasAsyncEventDelegate::kMask | + Isolate::PromiseHookFields::IsDebugActive::kMask; + return IsSetWord32(flags, mask); +} + +TNode CodeStubAssembler:: + IsAnyPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + TNode flags) { + return Word32NotEqual(flags, Int32Constant(0)); } TNode CodeStubAssembler:: - IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() { - const TNode promise_hook_or_debug_is_active_or_async_event_delegate = - Load(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 flags) { + uint32_t mask = Isolate::PromiseHookFields::HasContextPromiseHook::kMask | + Isolate::PromiseHookFields::HasIsolatePromiseHook::kMask | + Isolate::PromiseHookFields::HasAsyncEventDelegate::kMask; + return IsSetWord32(flags, mask); } TNode CodeStubAssembler::LoadBuiltin(TNode builtin_id) { diff --git a/deps/v8/src/codegen/code-stub-assembler.h b/deps/v8/src/codegen/code-stub-assembler.h index 23b99377d8dccc..bcb88afe0ca804 100644 --- a/deps/v8/src/codegen/code-stub-assembler.h +++ b/deps/v8/src/codegen/code-stub-assembler.h @@ -3716,10 +3716,44 @@ class V8_EXPORT_PRIVATE CodeStubAssembler TNode context); // Promise helpers - TNode IsPromiseHookEnabled(); + TNode PromiseHookFlags(); TNode HasAsyncEventDelegate(); - TNode IsPromiseHookEnabledOrHasAsyncEventDelegate(); - TNode IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(); + TNode IsContextPromiseHookEnabled(TNode flags); + TNode IsContextPromiseHookEnabled() { + return IsContextPromiseHookEnabled(PromiseHookFlags()); + } + TNode IsAnyPromiseHookEnabled(TNode flags); + TNode IsAnyPromiseHookEnabled() { + return IsAnyPromiseHookEnabled(PromiseHookFlags()); + } + TNode IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate( + TNode flags); + TNode IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate() { + return IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate( + PromiseHookFlags()); + } + TNode + IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + TNode flags); + TNode + IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() { + return IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + PromiseHookFlags()); + } + TNode IsAnyPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + TNode flags); + TNode + IsAnyPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() { + return IsAnyPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate( + PromiseHookFlags()); + } + TNode IsAnyPromiseHookEnabledOrHasAsyncEventDelegate( + TNode flags); + TNode + IsAnyPromiseHookEnabledOrHasAsyncEventDelegate() { + return IsAnyPromiseHookEnabledOrHasAsyncEventDelegate( + PromiseHookFlags()); + } // for..in helpers void CheckPrototypeEnumCache(TNode receiver, diff --git a/deps/v8/src/codegen/external-reference.cc b/deps/v8/src/codegen/external-reference.cc index 5c2c63e816ca76..512b64e531b4d1 100644 --- a/deps/v8/src/codegen/external-reference.cc +++ b/deps/v8/src/codegen/external-reference.cc @@ -699,6 +699,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()); } @@ -708,21 +713,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 f42a7d74861a4f..b1ad43a8df8f21 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 fe1bb58e4a94fa..523b30666353f0 100644 --- a/deps/v8/src/d8/d8.cc +++ b/deps/v8/src/d8/d8.cc @@ -1393,6 +1393,20 @@ void Shell::AsyncHooksTriggerAsyncId( PerIsolateData::Get(isolate)->GetAsyncHooks()->GetTriggerAsyncId())); } +void Shell::SetPromiseHooks(const v8::FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + HandleScope handle_scope(isolate); + + context->SetPromiseHooks( + args[0]->IsFunction() ? args[0].As() : Local(), + args[1]->IsFunction() ? args[1].As() : Local(), + args[2]->IsFunction() ? args[2].As() : Local(), + args[3]->IsFunction() ? args[3].As() : Local()); + + args.GetReturnValue().Set(v8::Undefined(isolate)); +} + void WriteToFile(FILE* file, const v8::FunctionCallbackInfo& args) { for (int i = 0; i < args.Length(); i++) { HandleScope handle_scope(args.GetIsolate()); @@ -1971,7 +1985,14 @@ Local Shell::CreateGlobalTemplate(Isolate* isolate) { FunctionTemplate::New(isolate, AsyncHooksTriggerAsyncId)); global_template->Set(isolate, "async_hooks", async_hooks_templ); } - + { + Local promise_template = ObjectTemplate::New(isolate); + promise_template->Set( + isolate, "setHooks", + FunctionTemplate::New(isolate, SetPromiseHooks, Local(), + Local(), 4)); + global_template->Set(isolate, "promise", promise_template); + } return global_template; } diff --git a/deps/v8/src/d8/d8.h b/deps/v8/src/d8/d8.h index bd49b81fd02ec6..b4fbde5a9d6449 100644 --- a/deps/v8/src/d8/d8.h +++ b/deps/v8/src/d8/d8.h @@ -369,6 +369,8 @@ class Shell : public i::AllStatic { static void AsyncHooksTriggerAsyncId( const v8::FunctionCallbackInfo& args); + static void SetPromiseHooks(const v8::FunctionCallbackInfo& args); + static void Print(const v8::FunctionCallbackInfo& args); static void PrintErr(const v8::FunctionCallbackInfo& args); static void Write(const v8::FunctionCallbackInfo& args); diff --git a/deps/v8/src/execution/isolate.cc b/deps/v8/src/execution/isolate.cc index ff940d9da662d5..f99f369bbf3d3e 100644 --- a/deps/v8/src/execution/isolate.cc +++ b/deps/v8/src/execution/isolate.cc @@ -3887,19 +3887,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 { @@ -4088,17 +4092,30 @@ void Isolate::SetPromiseHook(PromiseHook hook) { PromiseHookStateUpdated(); } +void Isolate::RunAllPromiseHooks(PromiseHookType type, + Handle promise, + Handle parent) { + if (HasContextPromiseHooks()) { + native_context()->RunPromiseHook(type, promise, parent); + } + if (HasIsolatePromiseHooks() || HasAsyncEventDelegate()) { + RunPromiseHook(type, promise, parent); + } +} + void Isolate::RunPromiseHook(PromiseHookType type, Handle promise, Handle 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 promise) { - if (!async_event_delegate_) return; + if (!HasAsyncEventDelegate()) return; + DCHECK(async_event_delegate_ != nullptr); if (type == PromiseHookType::kResolve) return; if (type == PromiseHookType::kBefore) { diff --git a/deps/v8/src/execution/isolate.h b/deps/v8/src/execution/isolate.h index 624a5849699336..8537db5b99642c 100644 --- a/deps/v8/src/execution/isolate.h +++ b/deps/v8/src/execution/isolate.h @@ -1323,21 +1323,27 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory { } #endif - Address promise_hook_address() { - return reinterpret_cast
(&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
(&async_event_delegate_); + bool HasContextPromiseHooks() const { + return PromiseHookFields::HasContextPromiseHook::decode( + promise_hook_flags_); } - Address promise_hook_or_async_event_delegate_address() { - return reinterpret_cast
(&promise_hook_or_async_event_delegate_); + Address promise_hook_flags_address() { + return reinterpret_cast
(&promise_hook_flags_); } - Address promise_hook_or_debug_is_active_or_async_event_delegate_address() { - return reinterpret_cast
( - &promise_hook_or_debug_is_active_or_async_event_delegate_); + Address promise_hook_address() { + return reinterpret_cast
(&promise_hook_); + } + + Address async_event_delegate_address() { + return reinterpret_cast
(&async_event_delegate_); } Address handle_scope_implementer_address() { @@ -1355,6 +1361,9 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory { void SetPromiseHook(PromiseHook hook); void RunPromiseHook(PromiseHookType type, Handle promise, Handle parent); + void RunAllPromiseHooks(PromiseHookType type, Handle promise, + Handle parent); + void UpdatePromiseHookProtector(); void PromiseHookStateUpdated(); void AddDetachedContext(Handle context); @@ -1541,6 +1550,13 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory { bool RequiresCodeRange() const; + struct PromiseHookFields { + using HasContextPromiseHook = base::BitField; + using HasIsolatePromiseHook = HasContextPromiseHook::Next; + using HasAsyncEventDelegate = HasIsolatePromiseHook::Next; + using IsDebugActive = HasAsyncEventDelegate::Next; + }; + private: explicit Isolate(std::unique_ptr isolate_allocator); ~Isolate(); @@ -1623,6 +1639,16 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory { void RunPromiseHookForAsyncEventDelegate(PromiseHookType type, Handle 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: @@ -1840,8 +1866,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; v8::Isolate::AbortOnUncaughtExceptionCallback diff --git a/deps/v8/src/heap/factory.cc b/deps/v8/src/heap/factory.cc index ba78b952a4e980..4b42a6312a1946 100644 --- a/deps/v8/src/heap/factory.cc +++ b/deps/v8/src/heap/factory.cc @@ -3559,7 +3559,8 @@ Handle Factory::NewJSPromiseWithoutHook() { Handle Factory::NewJSPromise() { Handle 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 686a3c689ef5cf..e7c3ed12e0e049 100644 --- a/deps/v8/src/objects/contexts.cc +++ b/deps/v8/src/objects/contexts.cc @@ -510,5 +510,53 @@ STATIC_ASSERT(NativeContext::kSize == (Context::SizeFor(NativeContext::NATIVE_CONTEXT_SLOTS) + kSystemPointerSize)); +void NativeContext::RunPromiseHook(PromiseHookType type, + Handle promise, + Handle 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 hook(isolate->native_context()->get(contextSlot), isolate); + if (hook->IsUndefined()) return; + + int argc = type == PromiseHookType::kInit ? 2 : 1; + Handle argv[2] = { + Handle::cast(promise), + parent + }; + + Handle receiver = isolate->global_proxy(); + + if (Execution::Call(isolate, hook, receiver, argc, argv).is_null()) { + DCHECK(isolate->has_pending_exception()); + Handle exception(isolate->pending_exception(), isolate); + + MessageLocation* no_location = nullptr; + Handle 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 06f742281ad2b8..7420bbe4463d35 100644 --- a/deps/v8/src/objects/contexts.h +++ b/deps/v8/src/objects/contexts.h @@ -195,6 +195,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) \ @@ -694,6 +699,9 @@ class NativeContext : public Context { void IncrementErrorsThrown(); int GetErrorsThrown(); + void RunPromiseHook(PromiseHookType type, Handle promise, + Handle 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 bae4fd60df9cfc..9c1ae09c1ce68a 100644 --- a/deps/v8/src/objects/contexts.tq +++ b/deps/v8/src/objects/contexts.tq @@ -51,6 +51,11 @@ extern enum NativeContextSlot extends intptr constexpr 'Context::Field' { PROMISE_PROTOTYPE_INDEX, STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX, + PROMISE_HOOK_INIT_FUNCTION_INDEX, + PROMISE_HOOK_BEFORE_FUNCTION_INDEX, + PROMISE_HOOK_AFTER_FUNCTION_INDEX, + PROMISE_HOOK_RESOLVE_FUNCTION_INDEX, + CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX, ... } diff --git a/deps/v8/src/objects/objects.cc b/deps/v8/src/objects/objects.cc index 009b09284e6ab9..6864ffdb883309 100644 --- a/deps/v8/src/objects/objects.cc +++ b/deps/v8/src/objects/objects.cc @@ -5962,8 +5962,8 @@ Handle JSPromise::Reject(Handle 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()); @@ -5996,8 +5996,8 @@ MaybeHandle JSPromise::Resolve(Handle promise, Handle resolution) { Isolate* const isolate = promise->GetIsolate(); - 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 4d1c5ea9d2a79f..92a0d8d779a226 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 AwaitPromisesInitCommon(Isolate* isolate, // hook for the throwaway promise (passing the {promise} as its // parent). Handle 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 d0b91de7d2da1d..f9b92207a0c248 100644 --- a/deps/v8/test/cctest/test-code-stub-assembler.cc +++ b/deps/v8/test/cctest/test-code-stub-assembler.cc @@ -2430,7 +2430,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 result = diff --git a/deps/v8/test/mjsunit/promise-hooks.js b/deps/v8/test/mjsunit/promise-hooks.js new file mode 100644 index 00000000000000..9e13206a525f95 --- /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();