Commit e24b95dc authored by Benedikt Meurer's avatar Benedikt Meurer Committed by Commit Bot

[esnext] Implement "Editorial: refactor PerformPromiseThen to allow no capability".

This implements the editorial change in https://github.com/tc39/ecma262/pull/1146
which removes the need to allocate the throwaway promise in await (the throwaway
promise was not exposed to user JavaScript anyways, but this spec change allows
us to rely on this behavior). Now we still need the throwaway promise for proper
before and after events with both DevTools (which might change) and PromiseHooks.
So if either DevTools or PromiseHooks is on, we call into %AwaitPromisesInit,
which then allocates the throwaway promise and does all the other debugger/hooks
related setup.

This gives around 7% improvement on the doxbee-async-es2017-native and around 1-2%
on the parallel-async-es2017-native benchmarks.

Bug: v8:7253, v8:8285
Ref: tc39/ecma262#1146
Tbr: ulan@chromium.org
Change-Id: I972ba0538ec8c00808e95b183603025c7e55a6d3
Cq-Include-Trybots: luci.chromium.try:linux_chromium_headless_rel;luci.chromium.try:linux_chromium_rel_ng;master.tryserver.blink:linux_trusty_blink_rel
Reviewed-on: https://chromium-review.googlesource.com/c/1270798
Commit-Queue: Benedikt Meurer <bmeurer@chromium.org>
Reviewed-by: 's avatarBenedikt Meurer <bmeurer@chromium.org>
Reviewed-by: 's avatarMaya Lekova <mslekova@chromium.org>
Reviewed-by: 's avatarSathya Gunasekaran <gsathya@chromium.org>
Cr-Commit-Position: refs/heads/master@{#56506}
parent 5ac88053
......@@ -32,10 +32,8 @@ Node* AsyncBuiltinsAssembler::AwaitOld(Node* context, Node* generator,
static const int kWrappedPromiseOffset =
FixedArray::SizeFor(Context::MIN_CONTEXT_SLOTS);
static const int kThrowawayPromiseOffset =
kWrappedPromiseOffset + JSPromise::kSizeWithEmbedderFields;
static const int kResolveClosureOffset =
kThrowawayPromiseOffset + JSPromise::kSizeWithEmbedderFields;
kWrappedPromiseOffset + JSPromise::kSizeWithEmbedderFields;
static const int kRejectClosureOffset =
kResolveClosureOffset + JSFunction::kSizeWithoutPrototype;
static const int kTotalSize =
......@@ -81,80 +79,38 @@ Node* AsyncBuiltinsAssembler::AwaitOld(Node* context, Node* generator,
PromiseInit(wrapped_value);
}
Node* const throwaway = InnerAllocate(base, kThrowawayPromiseOffset);
{
// Initialize throwawayPromise
StoreMapNoWriteBarrier(throwaway, promise_map);
InitializeJSObjectFromMap(
throwaway, promise_map,
IntPtrConstant(JSPromise::kSizeWithEmbedderFields));
PromiseInit(throwaway);
}
// Initialize resolve handler
Node* const on_resolve = InnerAllocate(base, kResolveClosureOffset);
{
// Initialize resolve handler
InitializeNativeClosure(closure_context, native_context, on_resolve,
on_resolve_context_index);
}
InitializeNativeClosure(closure_context, native_context, on_resolve,
on_resolve_context_index);
// Initialize reject handler
Node* const on_reject = InnerAllocate(base, kRejectClosureOffset);
{
// Initialize reject handler
InitializeNativeClosure(closure_context, native_context, on_reject,
on_reject_context_index);
}
{
// Add PromiseHooks if needed
Label next(this);
GotoIfNot(IsPromiseHookEnabledOrHasAsyncEventDelegate(), &next);
CallRuntime(Runtime::kAwaitPromisesInit, context, wrapped_value,
outer_promise, throwaway);
Goto(&next);
BIND(&next);
}
InitializeNativeClosure(closure_context, native_context, on_reject,
on_reject_context_index);
VARIABLE(var_throwaway, MachineRepresentation::kTaggedPointer,
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);
GotoIf(IsDebugActive(), &if_debugging);
Branch(IsPromiseHookEnabledOrHasAsyncEventDelegate(), &if_debugging,
&do_resolve_promise);
BIND(&if_debugging);
var_throwaway.Bind(CallRuntime(Runtime::kAwaitPromisesInit, context, value,
wrapped_value, outer_promise, on_reject,
is_predicted_as_caught));
Goto(&do_resolve_promise);
BIND(&do_resolve_promise);
// Perform ! Call(promiseCapability.[[Resolve]], undefined, « promise »).
CallBuiltin(Builtins::kResolvePromise, context, wrapped_value, value);
// The Promise will be thrown away and not handled, but it shouldn't trigger
// unhandled reject events as its work is done
PromiseSetHasHandler(throwaway);
Label do_perform_promise_then(this);
GotoIfNot(IsDebugActive(), &do_perform_promise_then);
{
Label common(this);
GotoIf(TaggedIsSmi(value), &common);
GotoIfNot(HasInstanceType(value, JS_PROMISE_TYPE), &common);
{
// Mark the reject handler callback to be a forwarding edge, rather
// than a meaningful catch handler
Node* const key =
HeapConstant(factory()->promise_forwarding_handler_symbol());
SetPropertyStrict(CAST(context), CAST(on_reject), CAST(key),
TrueConstant());
GotoIf(IsFalse(is_predicted_as_caught), &common);
PromiseSetHandledHint(value);
}
Goto(&common);
BIND(&common);
// Mark the dependency to outer Promise in case the throwaway Promise is
// found on the Promise stack
CSA_SLOW_ASSERT(this, HasInstanceType(outer_promise, JS_PROMISE_TYPE));
Node* const key = HeapConstant(factory()->promise_handled_by_symbol());
SetPropertyStrict(CAST(context), CAST(throwaway), CAST(key),
CAST(outer_promise));
}
Goto(&do_perform_promise_then);
BIND(&do_perform_promise_then);
return CallBuiltin(Builtins::kPerformPromiseThen, context, wrapped_value,
on_resolve, on_reject, throwaway);
on_resolve, on_reject, var_throwaway.value());
}
Node* AsyncBuiltinsAssembler::AwaitOptimized(
......@@ -167,10 +123,8 @@ Node* AsyncBuiltinsAssembler::AwaitOptimized(
CSA_ASSERT(this, IsFunctionWithPrototypeSlotMap(LoadMap(promise_fun)));
CSA_ASSERT(this, IsConstructor(promise_fun));
static const int kThrowawayPromiseOffset =
FixedArray::SizeFor(Context::MIN_CONTEXT_SLOTS);
static const int kResolveClosureOffset =
kThrowawayPromiseOffset + JSPromise::kSizeWithEmbedderFields;
FixedArray::SizeFor(Context::MIN_CONTEXT_SLOTS);
static const int kRejectClosureOffset =
kResolveClosureOffset + JSFunction::kSizeWithoutPrototype;
static const int kTotalSize =
......@@ -199,84 +153,35 @@ Node* AsyncBuiltinsAssembler::AwaitOptimized(
closure_context, Context::NATIVE_CONTEXT_INDEX, native_context);
}
Node* const promise_map =
LoadObjectField(promise_fun, JSFunction::kPrototypeOrInitialMapOffset);
// Assert that the JSPromise map has an instance size is
// JSPromise::kSizeWithEmbedderFields.
CSA_ASSERT(this, WordEqual(LoadMapInstanceSizeInWords(promise_map),
IntPtrConstant(JSPromise::kSizeWithEmbedderFields /
kPointerSize)));
Node* const throwaway = InnerAllocate(base, kThrowawayPromiseOffset);
{
// Initialize throwawayPromise
StoreMapNoWriteBarrier(throwaway, promise_map);
InitializeJSObjectFromMap(
throwaway, promise_map,
IntPtrConstant(JSPromise::kSizeWithEmbedderFields));
PromiseInit(throwaway);
}
// Initialize resolve handler
Node* const on_resolve = InnerAllocate(base, kResolveClosureOffset);
{
// Initialize resolve handler
InitializeNativeClosure(closure_context, native_context, on_resolve,
on_resolve_context_index);
}
InitializeNativeClosure(closure_context, native_context, on_resolve,
on_resolve_context_index);
// Initialize reject handler
Node* const on_reject = InnerAllocate(base, kRejectClosureOffset);
{
// Initialize reject handler
InitializeNativeClosure(closure_context, native_context, on_reject,
on_reject_context_index);
}
{
// Add PromiseHooks if needed
Label next(this);
GotoIfNot(IsPromiseHookEnabledOrHasAsyncEventDelegate(), &next);
CallRuntime(Runtime::kAwaitPromisesInit, context, promise, outer_promise,
throwaway);
Goto(&next);
BIND(&next);
}
// The Promise will be thrown away and not handled, but it shouldn't trigger
// unhandled reject events as its work is done
PromiseSetHasHandler(throwaway);
Label do_perform_promise_then(this);
GotoIfNot(IsDebugActive(), &do_perform_promise_then);
{
Label common(this);
GotoIf(TaggedIsSmi(value), &common);
GotoIfNot(HasInstanceType(value, JS_PROMISE_TYPE), &common);
{
// Mark the reject handler callback to be a forwarding edge, rather
// than a meaningful catch handler
Node* const key =
HeapConstant(factory()->promise_forwarding_handler_symbol());
SetPropertyStrict(CAST(context), CAST(on_reject), CAST(key),
TrueConstant());
GotoIf(IsFalse(is_predicted_as_caught), &common);
PromiseSetHandledHint(value);
}
Goto(&common);
BIND(&common);
// Mark the dependency to outer Promise in case the throwaway Promise is
// found on the Promise stack
CSA_SLOW_ASSERT(this, HasInstanceType(outer_promise, JS_PROMISE_TYPE));
Node* const key = HeapConstant(factory()->promise_handled_by_symbol());
SetPropertyStrict(CAST(context), CAST(throwaway), CAST(key),
CAST(outer_promise));
}
InitializeNativeClosure(closure_context, native_context, on_reject,
on_reject_context_index);
VARIABLE(var_throwaway, MachineRepresentation::kTaggedPointer,
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_perform_promise_then(this);
GotoIf(IsDebugActive(), &if_debugging);
Branch(IsPromiseHookEnabledOrHasAsyncEventDelegate(), &if_debugging,
&do_perform_promise_then);
BIND(&if_debugging);
var_throwaway.Bind(CallRuntime(Runtime::kAwaitPromisesInit, 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, throwaway);
on_resolve, on_reject, var_throwaway.value());
}
Node* AsyncBuiltinsAssembler::Await(Node* context, Node* generator, Node* value,
......
......@@ -823,13 +823,15 @@ void InternalBuiltinsAssembler::RunPromiseHook(
BIND(&hook);
{
// Get to the underlying JSPromise instance.
Node* const promise = Select<HeapObject>(
IsJSPromise(promise_or_capability),
[=] { return promise_or_capability; },
TNode<HeapObject> promise = Select<HeapObject>(
IsPromiseCapability(promise_or_capability),
[=] {
return CAST(LoadObjectField(promise_or_capability,
PromiseCapability::kPromiseOffset));
});
},
[=] { return promise_or_capability; });
GotoIf(IsUndefined(promise), &done_hook);
CallRuntime(id, context, promise);
Goto(&done_hook);
}
......
......@@ -318,8 +318,11 @@ void PromiseBuiltinsAssembler::PerformPromiseThen(
Word32Or(IsCallable(on_fulfilled), IsUndefined(on_fulfilled)));
CSA_ASSERT(this, Word32Or(IsCallable(on_rejected), IsUndefined(on_rejected)));
CSA_ASSERT(this, TaggedIsNotSmi(result_promise_or_capability));
CSA_ASSERT(this, Word32Or(IsJSPromise(result_promise_or_capability),
IsPromiseCapability(result_promise_or_capability)));
CSA_ASSERT(
this,
Word32Or(Word32Or(IsJSPromise(result_promise_or_capability),
IsPromiseCapability(result_promise_or_capability)),
IsUndefined(result_promise_or_capability)));
Label if_pending(this), if_notpending(this), done(this);
Node* const status = PromiseStatus(promise);
......@@ -390,7 +393,8 @@ TF_BUILTIN(PerformPromiseThen, PromiseBuiltinsAssembler) {
Node* const result_promise = Parameter(Descriptor::kResultPromise);
CSA_ASSERT(this, TaggedIsNotSmi(result_promise));
CSA_ASSERT(this, IsJSPromise(result_promise));
CSA_ASSERT(
this, Word32Or(IsJSPromise(result_promise), IsUndefined(result_promise)));
PerformPromiseThen(context, promise, on_fulfilled, on_rejected,
result_promise);
......@@ -1154,11 +1158,14 @@ void PromiseBuiltinsAssembler::PromiseReactionJob(Node* context, Node* argument,
CSA_ASSERT(this, TaggedIsNotSmi(handler));
CSA_ASSERT(this, Word32Or(IsUndefined(handler), IsCallable(handler)));
CSA_ASSERT(this, TaggedIsNotSmi(promise_or_capability));
CSA_ASSERT(this, Word32Or(IsJSPromise(promise_or_capability),
IsPromiseCapability(promise_or_capability)));
CSA_ASSERT(this,
Word32Or(Word32Or(IsJSPromise(promise_or_capability),
IsPromiseCapability(promise_or_capability)),
IsUndefined(promise_or_capability)));
VARIABLE(var_handler_result, MachineRepresentation::kTagged, argument);
Label if_handler_callable(this), if_fulfill(this), if_reject(this);
Label if_handler_callable(this), if_fulfill(this), if_reject(this),
if_internal(this);
Branch(IsUndefined(handler),
type == PromiseReaction::kFulfill ? &if_fulfill : &if_reject,
&if_handler_callable);
......@@ -1170,7 +1177,16 @@ void PromiseBuiltinsAssembler::PromiseReactionJob(Node* context, Node* argument,
context, handler, UndefinedConstant(), argument);
GotoIfException(result, &if_reject, &var_handler_result);
var_handler_result.Bind(result);
Goto(&if_fulfill);
Branch(IsUndefined(promise_or_capability), &if_internal, &if_fulfill);
}
BIND(&if_internal);
{
// There's no [[Capability]] for this promise reaction job, which
// means that this is a specification-internal operation (aka await)
// where the result does not matter (see the specification change in
// https://github.com/tc39/ecma262/pull/1146 for details).
Return(UndefinedConstant());
}
BIND(&if_fulfill);
......
......@@ -911,6 +911,8 @@ class V8_EXPORT_PRIVATE Factory {
// Converts the given ToPrimitive hint to it's string representation.
Handle<String> ToPrimitiveHintString(ToPrimitiveHint hint);
Handle<JSPromise> NewJSPromiseWithoutHook(
PretenureFlag pretenure = NOT_TENURED);
Handle<JSPromise> NewJSPromise(PretenureFlag pretenure = NOT_TENURED);
Handle<CallHandlerInfo> NewCallHandlerInfo(bool has_no_side_effect = false);
......@@ -978,9 +980,6 @@ class V8_EXPORT_PRIVATE Factory {
Handle<JSArray> NewJSArray(ElementsKind elements_kind,
PretenureFlag pretenure = NOT_TENURED);
Handle<JSPromise> NewJSPromiseWithoutHook(
PretenureFlag pretenure = NOT_TENURED);
Handle<SharedFunctionInfo> NewSharedFunctionInfo(
MaybeHandle<String> name, MaybeHandle<HeapObject> maybe_function_data,
int maybe_builtin_index, FunctionKind kind = kNormalFunction);
......
......@@ -682,11 +682,14 @@ void CaptureAsyncStackTrace(Isolate* isolate, Handle<JSPromise> promise,
reaction->promise_or_capability(), isolate);
if (promise_or_capability->IsJSPromise()) {
promise = Handle<JSPromise>::cast(promise_or_capability);
} else {
} else if (promise_or_capability->IsPromiseCapability()) {
Handle<PromiseCapability> capability =
Handle<PromiseCapability>::cast(promise_or_capability);
if (!capability->promise()->IsJSPromise()) return;
promise = handle(JSPromise::cast(capability->promise()), isolate);
} else {
// Otherwise the {promise_or_capability} must be undefined here.
CHECK(promise_or_capability->IsUndefined(isolate));
}
}
}
......@@ -2273,21 +2276,23 @@ bool InternalPromiseHasUserDefinedRejectHandler(Isolate* isolate,
Handle<PromiseReaction> reaction = Handle<PromiseReaction>::cast(current);
Handle<HeapObject> promise_or_capability(
reaction->promise_or_capability(), isolate);
Handle<JSPromise> promise = Handle<JSPromise>::cast(
promise_or_capability->IsJSPromise()
? promise_or_capability
: handle(Handle<PromiseCapability>::cast(promise_or_capability)
->promise(),
isolate));
if (reaction->reject_handler()->IsUndefined(isolate)) {
if (InternalPromiseHasUserDefinedRejectHandler(isolate, promise)) {
return true;
}
} else {
Handle<JSReceiver> current_handler(
JSReceiver::cast(reaction->reject_handler()), isolate);
if (PromiseHandlerCheck(isolate, current_handler, promise)) {
return true;
if (!promise_or_capability->IsUndefined(isolate)) {
Handle<JSPromise> promise = Handle<JSPromise>::cast(
promise_or_capability->IsJSPromise()
? promise_or_capability
: handle(Handle<PromiseCapability>::cast(promise_or_capability)
->promise(),
isolate));
if (reaction->reject_handler()->IsUndefined(isolate)) {
if (InternalPromiseHasUserDefinedRejectHandler(isolate, promise)) {
return true;
}
} else {
Handle<JSReceiver> current_handler(
JSReceiver::cast(reaction->reject_handler()), isolate);
if (PromiseHandlerCheck(isolate, current_handler, promise)) {
return true;
}
}
}
current = handle(reaction->next(), isolate);
......
......@@ -1337,7 +1337,8 @@ void PromiseReactionJobTask::PromiseReactionJobTaskVerify(Isolate* isolate) {
CHECK(handler()->IsUndefined(isolate) || handler()->IsCallable());
VerifyHeapPointer(isolate, promise_or_capability());
CHECK(promise_or_capability()->IsJSPromise() ||
promise_or_capability()->IsPromiseCapability());
promise_or_capability()->IsPromiseCapability() ||
promise_or_capability()->IsUndefined(isolate));
}
void MicrotaskQueue::MicrotaskQueueVerify(Isolate* isolate) {
......@@ -1396,7 +1397,8 @@ void PromiseReaction::PromiseReactionVerify(Isolate* isolate) {
fulfill_handler()->IsCallable());
VerifyHeapPointer(isolate, promise_or_capability());
CHECK(promise_or_capability()->IsJSPromise() ||
promise_or_capability()->IsPromiseCapability());
promise_or_capability()->IsPromiseCapability() ||
promise_or_capability()->IsUndefined(isolate));
}
void JSPromise::JSPromiseVerify(Isolate* isolate) {
......
......@@ -29,7 +29,8 @@ class PromiseReactionJobTask : public Microtask {
DECL_ACCESSORS(argument, Object)
DECL_ACCESSORS(context, Context)
DECL_ACCESSORS(handler, HeapObject)
// [promise_or_capability]: Either a JSPromise or a PromiseCapability.
// [promise_or_capability]: Either a JSPromise (in case of native promises),
// a PromiseCapability (general case), or undefined (in case of await).
DECL_ACCESSORS(promise_or_capability, HeapObject)
static const int kArgumentOffset = Microtask::kHeaderSize;
......@@ -124,10 +125,8 @@ class PromiseCapability : public Struct {
//
// The PromiseReaction::promise_or_capability field can either hold a JSPromise
// instance (in the fast case of a native promise) or a PromiseCapability in
// case of a Promise subclass.
//
// We need to keep the context in the PromiseReaction so that we can run
// the default handlers (in case they are undefined) in the proper context.
// case of a Promise subclass. In case of await it can also be undefined if
// PromiseHooks are disabled (see https://github.com/tc39/ecma262/pull/1146).
//
// The PromiseReaction objects form a singly-linked list, terminated by
// Smi 0. On the JSPromise instance they are linked in reverse order,
......@@ -140,6 +139,8 @@ class PromiseReaction : public Struct {
DECL_ACCESSORS(next, Object)
DECL_ACCESSORS(reject_handler, HeapObject)
DECL_ACCESSORS(fulfill_handler, HeapObject)
// [promise_or_capability]: Either a JSPromise (in case of native promises),
// a PromiseCapability (general case), or undefined (in case of await).
DECL_ACCESSORS(promise_or_capability, HeapObject)
static const int kNextOffset = Struct::kHeaderSize;
......
......@@ -132,20 +132,50 @@ RUNTIME_FUNCTION(Runtime_PromiseHookInit) {
}
RUNTIME_FUNCTION(Runtime_AwaitPromisesInit) {
DCHECK_EQ(3, args.length());
DCHECK_EQ(5, args.length());
HandleScope scope(isolate);
CONVERT_ARG_HANDLE_CHECKED(JSPromise, wrapped_value, 0);
CONVERT_ARG_HANDLE_CHECKED(JSPromise, outer_promise, 1);
CONVERT_ARG_HANDLE_CHECKED(JSPromise, throwaway, 2);
isolate->RunPromiseHook(PromiseHookType::kInit, wrapped_value, outer_promise);
isolate->RunPromiseHook(PromiseHookType::kInit, throwaway, wrapped_value);
CONVERT_ARG_HANDLE_CHECKED(Object, value, 0);
CONVERT_ARG_HANDLE_CHECKED(JSPromise, promise, 1);
CONVERT_ARG_HANDLE_CHECKED(JSPromise, outer_promise, 2);
CONVERT_ARG_HANDLE_CHECKED(JSFunction, reject_handler, 3);
CONVERT_BOOLEAN_ARG_CHECKED(is_predicted_as_caught, 4);
// Allocate the throwaway promise and fire the appropriate PromiseHooks.
Handle<JSPromise> throwaway = isolate->factory()->NewJSPromiseWithoutHook();
isolate->RunPromiseHook(PromiseHookType::kInit, promise, outer_promise);
isolate->RunPromiseHook(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.
// To use captured stack trace later throwaway promise should have the same
// async_task_id as outer_promise since we generate WillHandle and DidHandle
// events using throwaway promise.
throwaway->set_async_task_id(outer_promise->async_task_id());
return ReadOnlyRoots(isolate).undefined_value();
// The Promise will be thrown away and not handled, but it
// shouldn't trigger unhandled reject events as its work is done
throwaway->set_has_handler(true);
// Enable proper debug support for promises.
if (isolate->debug()->is_active()) {
if (value->IsJSPromise()) {
Object::SetProperty(
isolate, reject_handler,
isolate->factory()->promise_forwarding_handler_symbol(),
isolate->factory()->true_value(), LanguageMode::kStrict)
.Check();
Handle<JSPromise>::cast(value)->set_handled_hint(is_predicted_as_caught);
}
// Mark the dependency to {outer_promise} in case the {throwaway}
// Promise is found on the Promise stack
Object::SetProperty(isolate, throwaway,
isolate->factory()->promise_handled_by_symbol(),
outer_promise, LanguageMode::kStrict)
.Check();
}
return *throwaway;
}
RUNTIME_FUNCTION(Runtime_PromiseHookBefore) {
......
......@@ -348,7 +348,7 @@ namespace internal {
F(PromiseHookAfter, 1, 1) \
F(PromiseHookBefore, 1, 1) \
F(PromiseHookInit, 2, 1) \
F(AwaitPromisesInit, 3, 1) \
F(AwaitPromisesInit, 5, 1) \
F(PromiseMarkAsHandled, 1, 1) \
F(PromiseRejectEventFromStack, 2, 1) \
F(PromiseResult, 1, 1) \
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment