Commit 9c0c8761 authored by Sathya Gunasekaran's avatar Sathya Gunasekaran Committed by Commit Bot

[promise] Lookup the resolve property only once

In the PerformPromise{All, Race, AllSettled} operations, the resolve
property of the constructor is looked up only once.

In the implementation, for the fast path, where the constructor's
resolve property is untainted, the resolve function is set to undefined.
Since undefined can't be a valid value for the resolve function,
we can switch on it (in CallResolve) to directly call the  PromiseResolve
builtin. If the resolve property is tainted, we do an observable property
lookup, save this value, and call this property later (in CallResolve).

I ran this CL against the test262 tests locally and they all pass:
https://github.com/tc39/test262/pull/2131

Spec:
- https://github.com/tc39/ecma262/pull/1506
- https://github.com/tc39/proposal-promise-allSettled/pull/40

Bug: v8:9152
Change-Id: Icb36a90b5a244a67a729611c7b3315d2c29de6e9
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1574705
Commit-Queue: Sathya Gunasekaran <gsathya@chromium.org>
Reviewed-by: 's avatarJakob Gruber <jgruber@chromium.org>
Cr-Commit-Position: refs/heads/master@{#60957}
parent d533da3a
......@@ -722,20 +722,18 @@ Node* PromiseBuiltinsAssembler::InvokeThen(Node* native_context, Node* receiver,
return var_result.value();
}
Node* PromiseBuiltinsAssembler::InvokeResolve(Node* native_context,
Node* constructor, Node* value,
Label* if_exception,
Variable* var_exception) {
Node* PromiseBuiltinsAssembler::CallResolve(Node* native_context,
Node* constructor, Node* resolve,
Node* value, Label* if_exception,
Variable* var_exception) {
CSA_ASSERT(this, IsNativeContext(native_context));
CSA_ASSERT(this, IsConstructor(constructor));
VARIABLE(var_result, MachineRepresentation::kTagged);
Label if_fast(this), if_slow(this, Label::kDeferred), done(this, &var_result);
// We can skip the "resolve" lookup on {constructor} if it's the
// Promise constructor and the Promise.resolve protector is intact,
// as that guards the lookup path for the "resolve" property on the
// Promise constructor.
BranchIfPromiseResolveLookupChainIntact(native_context, constructor, &if_fast,
&if_slow);
// Undefined can never be a valid value for the resolve function,
// instead it is used as a special marker for the fast path.
Branch(IsUndefined(resolve), &if_fast, &if_slow);
BIND(&if_fast);
{
......@@ -749,9 +747,7 @@ Node* PromiseBuiltinsAssembler::InvokeResolve(Node* native_context,
BIND(&if_slow);
{
Node* const resolve =
GetProperty(native_context, constructor, factory()->resolve_string());
GotoIfException(resolve, if_exception, var_exception);
CSA_ASSERT(this, IsCallable(resolve));
Node* const result = CallJS(
CodeFactory::Call(isolate(), ConvertReceiverMode::kNotNullOrUndefined),
......@@ -2047,8 +2043,32 @@ Node* PromiseBuiltinsAssembler::PerformPromiseAll(
TVARIABLE(Smi, var_index, SmiConstant(1));
Label loop(this, &var_index), done_loop(this),
too_many_elements(this, Label::kDeferred),
close_iterator(this, Label::kDeferred);
close_iterator(this, Label::kDeferred), if_slow(this, Label::kDeferred);
// We can skip the "resolve" lookup on {constructor} if it's the
// Promise constructor and the Promise.resolve protector is intact,
// as that guards the lookup path for the "resolve" property on the
// Promise constructor.
TVARIABLE(Object, var_promise_resolve_function, UndefinedConstant());
GotoIfNotPromiseResolveLookupChainIntact(native_context, constructor,
&if_slow);
Goto(&loop);
BIND(&if_slow);
{
// 5. Let _promiseResolve_ be ? Get(_constructor_, `"resolve"`).
TNode<Object> resolve =
GetProperty(native_context, constructor, factory()->resolve_string());
GotoIfException(resolve, if_exception, var_exception);
// 6. If IsCallable(_promiseResolve_) is *false*, throw a *TypeError*
// exception.
ThrowIfNotCallable(CAST(context), resolve, "resolve");
var_promise_resolve_function = resolve;
Goto(&loop);
}
BIND(&loop);
{
// Let next be IteratorStep(iteratorRecord.[[Iterator]]).
......@@ -2120,8 +2140,7 @@ Node* PromiseBuiltinsAssembler::PerformPromiseAll(
// the PromiseReaction (aka we can pass undefined to PerformPromiseThen),
// since this is only necessary for DevTools and PromiseHooks.
Label if_fast(this), if_slow(this);
GotoIfNotPromiseResolveLookupChainIntact(native_context, constructor,
&if_slow);
GotoIfNot(IsUndefined(var_promise_resolve_function.value()), &if_slow);
GotoIf(IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate(),
&if_slow);
GotoIf(IsPromiseSpeciesProtectorCellInvalid(), &if_slow);
......@@ -2142,10 +2161,11 @@ Node* PromiseBuiltinsAssembler::PerformPromiseAll(
BIND(&if_slow);
{
// Let nextPromise be ? Invoke(constructor, "resolve", « nextValue »).
Node* const next_promise =
InvokeResolve(native_context, constructor, next_value,
&close_iterator, var_exception);
// Let nextPromise be ? Call(constructor, _promiseResolve_, « nextValue
// »).
Node* const next_promise = CallResolve(
native_context, constructor, var_promise_resolve_function.value(),
next_value, &close_iterator, var_exception);
// Perform ? Invoke(nextPromise, "then", « resolveElement,
// resultCapability.[[Reject]] »).
......@@ -2587,11 +2607,34 @@ TF_BUILTIN(PromiseRace, PromiseBuiltinsAssembler) {
// Let result be PerformPromiseRace(iteratorRecord, C, promiseCapability).
{
Label loop(this), break_loop(this);
// We can skip the "resolve" lookup on {constructor} if it's the
// Promise constructor and the Promise.resolve protector is intact,
// as that guards the lookup path for the "resolve" property on the
// Promise constructor.
Label loop(this), break_loop(this), if_slow(this, Label::kDeferred);
Node* const native_context = LoadNativeContext(context);
TVARIABLE(Object, var_promise_resolve_function, UndefinedConstant());
GotoIfNotPromiseResolveLookupChainIntact(native_context, receiver,
&if_slow);
Goto(&loop);
BIND(&if_slow);
{
// 3. Let _promiseResolve_ be ? Get(_constructor_, `"resolve"`).
TNode<Object> resolve =
GetProperty(native_context, receiver, factory()->resolve_string());
GotoIfException(resolve, &reject_promise, &var_exception);
// 4. If IsCallable(_promiseResolve_) is *false*, throw a *TypeError*
// exception.
ThrowIfNotCallable(CAST(context), resolve, "resolve");
var_promise_resolve_function = resolve;
Goto(&loop);
}
BIND(&loop);
{
Node* const native_context = LoadNativeContext(context);
Node* const fast_iterator_result_map = LoadContextElement(
native_context, Context::ITERATOR_RESULT_MAP_INDEX);
......@@ -2610,10 +2653,11 @@ TF_BUILTIN(PromiseRace, PromiseBuiltinsAssembler) {
iter_assembler.IteratorValue(context, next, fast_iterator_result_map,
&reject_promise, &var_exception);
// Let nextPromise be ? Invoke(constructor, "resolve", « nextValue »).
Node* const next_promise =
InvokeResolve(native_context, receiver, next_value, &close_iterator,
&var_exception);
// Let nextPromise be ? Call(constructor, _promiseResolve_, « nextValue
// »).
Node* const next_promise = CallResolve(
native_context, receiver, var_promise_resolve_function.value(),
next_value, &close_iterator, &var_exception);
// Perform ? Invoke(nextPromise, "then", « resolveElement,
// resultCapability.[[Reject]] »).
......
......@@ -115,8 +115,10 @@ class V8_EXPORT_PRIVATE PromiseBuiltinsAssembler : public CodeStubAssembler {
Node* receiver_map, Label* if_fast,
Label* if_slow);
Node* InvokeResolve(Node* native_context, Node* constructor, Node* value,
Label* if_exception, Variable* var_exception);
// If resolve is Undefined, we use the builtin %PromiseResolve%
// intrinsic, otherwise we use the given resolve function.
Node* CallResolve(Node* native_context, Node* constructor, Node* resolve,
Node* value, Label* if_exception, Variable* var_exception);
template <typename... TArgs>
Node* InvokeThen(Node* native_context, Node* receiver, TArgs... args);
......
......@@ -5827,6 +5827,21 @@ Node* CodeStubAssembler::ThrowIfNotJSReceiver(Node* context, Node* value,
return var_value_map.value();
}
void CodeStubAssembler::ThrowIfNotCallable(TNode<Context> context,
TNode<Object> value,
const char* method_name) {
Label out(this), throw_exception(this, Label::kDeferred);
GotoIf(TaggedIsSmi(value), &throw_exception);
Branch(IsCallable(CAST(value)), &out, &throw_exception);
// The {value} is not a compatible receiver for this method.
BIND(&throw_exception);
ThrowTypeError(context, MessageTemplate::kCalledNonCallable, method_name);
BIND(&out);
}
void CodeStubAssembler::ThrowRangeError(Node* context, MessageTemplate message,
Node* arg0, Node* arg1, Node* arg2) {
Node* template_index = SmiConstant(static_cast<int>(message));
......
......@@ -2114,6 +2114,8 @@ class V8_EXPORT_PRIVATE CodeStubAssembler
Node* ThrowIfNotJSReceiver(Node* context, Node* value,
MessageTemplate msg_template,
const char* method_name = nullptr);
void ThrowIfNotCallable(TNode<Context> context, TNode<Object> value,
const char* method_name);
void ThrowRangeError(Node* context, MessageTemplate message,
Node* arg0 = nullptr, Node* arg1 = nullptr,
......
// Copyright 2019 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
let count = 0;
class MyPromise extends Promise {
static get resolve() {
count++;
return super.resolve;
}
}
MyPromise.all([1, 2, 3, 4, 5]);
assertEquals(1, count);
%PerformMicrotaskCheckpoint();
assertEquals(1, count);
count = 0;
MyPromise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.reject(3)
]);
assertEquals(1, count);
%PerformMicrotaskCheckpoint();
assertEquals(1, count);
// Copyright 2019 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 --harmony-promise-all-settled
let count = 0;
class MyPromise extends Promise {
static get resolve() {
count++;
return super.resolve;
}
}
MyPromise.allSettled([1, 2, 3, 4, 5]);
assertEquals(1, count);
%PerformMicrotaskCheckpoint();
assertEquals(1, count);
count = 0;
MyPromise.allSettled([
Promise.resolve(1),
Promise.resolve(2),
Promise.reject(3)
]);
assertEquals(1, count);
%PerformMicrotaskCheckpoint();
assertEquals(1, count);
// Copyright 2019 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
let count = 0;
class MyPromise extends Promise {
static get resolve() {
count++;
return super.resolve;
}
}
MyPromise.race([1, 2, 3, 4, 5]);
assertEquals(1, count);
%PerformMicrotaskCheckpoint();
assertEquals(1, count);
count = 0;
MyPromise.race([
Promise.resolve(1),
Promise.resolve(2),
Promise.reject(3)
]);
assertEquals(1, count);
%PerformMicrotaskCheckpoint();
assertEquals(1, count);
......@@ -531,6 +531,16 @@
# https://bugs.chromium.org/p/v8/issues/detail?id=9051
'language/statements/async-generator/yield-star-return-then-getter-ticks': [FAIL],
# https://bugs.chromium.org/p/v8/issues/detail?id=9152
'built-ins/Promise/all/capability-executor-called-twice': [FAIL],
'built-ins/Promise/all/capability-resolve-throws-no-close': [FAIL],
'built-ins/Promise/all/capability-resolve-throws-reject': [FAIL],
'built-ins/Promise/all/invoke-resolve-get-error-close': [FAIL],
'built-ins/Promise/all/species-get-error': [FAIL],
'built-ins/Promise/race/capability-executor-called-twice': [FAIL],
'built-ins/Promise/race/invoke-resolve-get-error-close': [FAIL],
'built-ins/Promise/race/species-get-error': [FAIL],
######################## NEEDS INVESTIGATION ###########################
# https://bugs.chromium.org/p/v8/issues/detail?id=7833
......
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