From 50dd62ed96e745a087c9289a76721e5f5e0fecbb Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Tue, 20 Apr 2021 18:10:05 -0700 Subject: deps: V8: backport c0fceaa0669b MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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ä > Reviewed-by: Camillo Bruni > Reviewed-by: Anton Bikineev > Reviewed-by: Igor Sheludko > Commit-Queue: Camillo Bruni > 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ä Reviewed-by: Igor Sheludko Reviewed-by: Anton Bikineev Reviewed-by: Camillo Bruni Commit-Queue: Camillo Bruni 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 Reviewed-By: Gus Caplan Reviewed-By: Vladimir de Turckheim Reviewed-By: Gerhard Stöbich Reviewed-By: James M Snell --- deps/v8/AUTHORS | 1 + deps/v8/include/v8.h | 12 + deps/v8/src/api/api.cc | 39 ++++ .../v8/src/builtins/builtins-async-function-gen.cc | 4 +- deps/v8/src/builtins/builtins-async-gen.cc | 62 ++++-- deps/v8/src/builtins/builtins-async-gen.h | 6 + .../src/builtins/builtins-async-generator-gen.cc | 2 +- .../src/builtins/builtins-microtask-queue-gen.cc | 62 ++++-- deps/v8/src/builtins/cast.tq | 6 + .../v8/src/builtins/promise-abstract-operations.tq | 15 +- deps/v8/src/builtins/promise-all.tq | 2 +- deps/v8/src/builtins/promise-constructor.tq | 7 +- deps/v8/src/builtins/promise-jobs.tq | 2 +- deps/v8/src/builtins/promise-misc.tq | 122 ++++++++++- deps/v8/src/builtins/promise-resolve.tq | 2 +- deps/v8/src/codegen/code-stub-assembler.cc | 61 ++++-- deps/v8/src/codegen/code-stub-assembler.h | 40 +++- deps/v8/src/codegen/external-reference.cc | 20 +- deps/v8/src/codegen/external-reference.h | 6 +- deps/v8/src/d8/d8.cc | 22 ++ deps/v8/src/d8/d8.h | 2 + deps/v8/src/execution/isolate.cc | 41 +++- deps/v8/src/execution/isolate.h | 47 +++- deps/v8/src/heap/factory.cc | 3 +- deps/v8/src/objects/contexts.cc | 48 ++++ deps/v8/src/objects/contexts.h | 8 + deps/v8/src/objects/contexts.tq | 6 + deps/v8/src/objects/objects.cc | 8 +- deps/v8/src/runtime/runtime-promise.cc | 8 +- deps/v8/test/cctest/test-code-stub-assembler.cc | 3 +- deps/v8/test/mjsunit/promise-hooks.js | 244 +++++++++++++++++++++ 31 files changed, 781 insertions(+), 130 deletions(-) create mode 100644 deps/v8/test/mjsunit/promise-hooks.js (limited to 'deps/v8') diff --git a/deps/v8/AUTHORS b/deps/v8/AUTHORS index aa6d32302b..a27cf5ef0a 100644 --- a/deps/v8/AUTHORS +++ b/deps/v8/AUTHORS @@ -209,6 +209,7 @@ Seo Sanghyeon Shawn Anastasio Shawn Presser Stefan Penner +Stephen Belanger Sylvestre Ledru Taketoshi Aono Tao Liqiang diff --git a/deps/v8/include/v8.h b/deps/v8/include/v8.h index 083bf5e123..e4448db191 100644 --- a/deps/v8/include/v8.h +++ b/deps/v8/include/v8.h @@ -10794,6 +10794,18 @@ class V8_EXPORT Context : public Data { */ 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 a56b7e1a7d..ca73b58a60 100644 --- a/deps/v8/src/api/api.cc +++ b/deps/v8/src/api/api.cc @@ -6147,6 +6147,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); +} + MaybeLocal metrics::Recorder::GetContext( Isolate* isolate, metrics::Recorder::ContextId id) { i::Isolate* i_isolate = reinterpret_cast(isolate); diff --git a/deps/v8/src/builtins/builtins-async-function-gen.cc b/deps/v8/src/builtins/builtins-async-function-gen.cc index 49b00caa04..1644997ed0 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 fa05e9b32a..1a660abece 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 833e78d45d..34b7a0ce1d 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 374b13dd63..5d053063ff 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 9f16186d13..1ec9e350f6 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( @@ -199,7 +202,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)); { @@ -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, + 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/cast.tq b/deps/v8/src/builtins/cast.tq index b490055a19..2bec3d86be 100644 --- a/deps/v8/src/builtins/cast.tq +++ b/deps/v8/src/builtins/cast.tq @@ -386,6 +386,12 @@ Cast(o: HeapObject): Undefined|Callable return HeapObjectToCallable(o) otherwise CastError; } +Cast(o: HeapObject): Undefined|JSFunction + labels CastError { + if (o == Undefined) return Undefined; + return Cast(o) otherwise CastError; +} + macro Cast(o: Symbol): T labels CastError; Cast(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 b7a1b571e6..0e435afad9 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 41dee8b9e7..294c5e911c 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(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 3c5a5e560d..b5f7292a77 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( FastNewObject(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 80e98f373b..6fa81dcd28 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 67e5e38687..c6661e3717 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(maybeHook) otherwise unreachable; + 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( + 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, + promiseOrCapability: JSPromise|PromiseCapability, flags: uint32) { + if (!IsContextPromiseHookEnabled(flags)) return; + const maybeHook = *NativeContextSlot(slot); + if (IsUndefined(maybeHook)) return; + + const hook = Cast(maybeHook) otherwise unreachable; + + 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 +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 e933dfbae0..3125054e87 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 76ee8c2d06..415291dd73 100644 --- a/deps/v8/src/codegen/code-stub-assembler.cc +++ b/deps/v8/src/codegen/code-stub-assembler.cc @@ -13391,35 +13391,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 03af2cc5e2..702792d788 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); // 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 bf75ff3d12..88ce90f4fd 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 d44b8e801c..0cd80ca6f1 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 999e8c2b96..63bcdf871f 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& 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()); @@ -2582,6 +2596,14 @@ Local Shell::CreateD8Template(Isolate* isolate) { d8_template->Set(isolate, "log", log_template); } + { + Local promise_template = ObjectTemplate::New(isolate); + promise_template->Set( + isolate, "setHooks", + FunctionTemplate::New(isolate, SetPromiseHooks, Local(), + Local(), 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 a9f6f3bc8b..6ea9681d69 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& 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 6f7efe3986..7fa6d7774d 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 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); switch (type) { case PromiseHookType::kResolve: return; diff --git a/deps/v8/src/execution/isolate.h b/deps/v8/src/execution/isolate.h index cd819c5087..e08195674f 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
(&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() { @@ -1460,6 +1466,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); @@ -1697,6 +1706,13 @@ class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory { } #endif + 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(); @@ -1780,6 +1796,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: @@ -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 main_thread_local_isolate_; diff --git a/deps/v8/src/heap/factory.cc b/deps/v8/src/heap/factory.cc index bd7ec8915c..ca9d316e2b 100644 --- a/deps/v8/src/heap/factory.cc +++ b/deps/v8/src/heap/factory.cc @@ -3377,7 +3377,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 af73bf0256..771fbea40b 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 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 47784cf405..0a67d52d5e 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 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 604852c24e..ff427629ab 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, STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX: Slot, + PROMISE_HOOK_INIT_FUNCTION_INDEX: Slot, + PROMISE_HOOK_BEFORE_FUNCTION_INDEX: Slot, + PROMISE_HOOK_AFTER_FUNCTION_INDEX: Slot, + PROMISE_HOOK_RESOLVE_FUNCTION_INDEX: + Slot, + CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX: Slot, BOUND_FUNCTION_WITH_CONSTRUCTOR_MAP_INDEX: Slot, diff --git a/deps/v8/src/objects/objects.cc b/deps/v8/src/objects/objects.cc index d9cb7486be..697ffdec08 100644 --- a/deps/v8/src/objects/objects.cc +++ b/deps/v8/src/objects/objects.cc @@ -5254,8 +5254,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()); @@ -5290,8 +5290,8 @@ MaybeHandle JSPromise::Resolve(Handle promise, DCHECK( !reinterpret_cast(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 c1ee96facc..54adc4c920 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 07e6c768f5..1ba26a81b8 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 result = diff --git a/deps/v8/test/mjsunit/promise-hooks.js b/deps/v8/test/mjsunit/promise-hooks.js new file mode 100644 index 0000000000..9e13206a52 --- /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(); -- cgit v1.2.1