Commit 1f8dcc5d authored by Benedikt Meurer's avatar Benedikt Meurer Committed by Commit Bot

[builtins] Optimize PromiseResolveThenableJob for the common case.

The idea here is that in case the `thenable` is a JSPromise and `then`
is the initial `Promise.prototype.then` method, and the @@species lookup
chain is intact, we can skip creating the temporary promise and the
closures (with the shared context), and instead directly call into our
PerformPromiseThen. This is sound since - given above mentioned
conditions - our short-cut

  PerformPromiseThen(thenable, undefined, undefined, promise_to_resolve)

is not observably different from the actual

  resolve, reject = CreateResolvingFunctions(promise_to_resolve)
  result_capability = NewPromiseCapability(%Promise%)
  PerformPromiseThen(thenable, resolve, reject, result_capability)

except through PromiseHooks (and potentially via the async stack
traces). So we disable the fast-path if either promise hooks are enabled
or the debugger is active for now.

This improves the performance on the wikipedia benchmark by 20-25% and
the bluebird-doxbee benchmark by around 20%.

Bug: v8:7253
Change-Id: I23c92ad365c2b71d65057573f2d8febe2afe00b0
Reviewed-on: https://chromium-review.googlesource.com/911800
Commit-Queue: Benedikt Meurer <bmeurer@chromium.org>
Reviewed-by: 's avatarSathya Gunasekaran <gsathya@chromium.org>
Cr-Commit-Position: refs/heads/master@{#51261}
parent c0412961
......@@ -956,7 +956,7 @@ TF_BUILTIN(RunMicrotasks, InternalBuiltinsAssembler) {
microtask, PromiseResolveThenableJobTask::kThenableOffset);
Node* const result =
CallBuiltin(Builtins::kPromiseResolveThenableJob, microtask_context,
CallBuiltin(Builtins::kPromiseResolveThenableJob, native_context,
promise_to_resolve, thenable, then);
GotoIfException(result, &if_exception, &var_exception);
LeaveMicrotaskContext();
......
......@@ -896,35 +896,90 @@ TF_BUILTIN(PromisePrototypeCatch, PromiseBuiltinsAssembler) {
// ES #sec-promiseresolvethenablejob
TF_BUILTIN(PromiseResolveThenableJob, PromiseBuiltinsAssembler) {
Node* context = Parameter(Descriptor::kContext);
Node* promise_to_resolve = Parameter(Descriptor::kPromiseToResolve);
Node* thenable = Parameter(Descriptor::kThenable);
Node* then = Parameter(Descriptor::kThen);
Node* native_context = LoadNativeContext(context);
Node* const native_context = Parameter(Descriptor::kContext);
Node* const promise_to_resolve = Parameter(Descriptor::kPromiseToResolve);
Node* const thenable = Parameter(Descriptor::kThenable);
Node* const then = Parameter(Descriptor::kThen);
CSA_ASSERT(this, TaggedIsNotSmi(thenable));
CSA_ASSERT(this, IsJSReceiver(thenable));
CSA_ASSERT(this, IsJSPromise(promise_to_resolve));
CSA_ASSERT(this, IsNativeContext(native_context));
// TODO(bmeurer): Add a fast-path for the case that {thenable} is a
// JSPromise and {then} is the original Promise.prototype.then method.
Node* resolve = nullptr;
Node* reject = nullptr;
std::tie(resolve, reject) = CreatePromiseResolvingFunctions(
promise_to_resolve, FalseConstant(), native_context);
// We can use a simple optimization here if we know that {then} is the initial
// Promise.prototype.then method, and {thenable} is a JSPromise whose
// @@species lookup chain is intact: We can connect {thenable} and
// {promise_to_resolve} directly in that case and avoid the allocation of a
// temporary JSPromise and the closures plus context.
//
// We take the generic (slow-)path if a PromiseHook is enabled or the debugger
// is active, to make sure we expose spec compliant behavior.
Label if_fast(this), if_slow(this, Label::kDeferred);
Node* const promise_then =
LoadContextElement(native_context, Context::PROMISE_THEN_INDEX);
GotoIfNot(WordEqual(then, promise_then), &if_slow);
Node* const thenable_map = LoadMap(thenable);
GotoIfNot(IsJSPromiseMap(thenable_map), &if_slow);
GotoIf(IsPromiseHookEnabledOrDebugIsActive(), &if_slow);
BranchIfPromiseSpeciesLookupChainIntact(native_context, thenable_map,
&if_fast, &if_slow);
Label if_exception(this, Label::kDeferred);
VARIABLE(var_exception, MachineRepresentation::kTagged, TheHoleConstant());
Node* result = CallJS(
CodeFactory::Call(isolate(), ConvertReceiverMode::kNotNullOrUndefined),
native_context, then, thenable, resolve, reject);
GotoIfException(result, &if_exception, &var_exception);
Return(result);
BIND(&if_fast);
{
// We know that the {thenable} is a JSPromise, which doesn't require
// any special treatment and that {then} corresponds to the initial
// Promise.prototype.then method. So instead of allocating a temporary
// JSPromise to connect the {thenable} with the {promise_to_resolve},
// we can directly schedule the {promise_to_resolve} with default
// handlers onto the {thenable} promise. This does not only save the
// JSPromise allocation, but also avoids the allocation of the two
// resolving closures and the shared context.
//
// What happens normally in this case is
//
// resolve, reject = CreateResolvingFunctions(promise_to_resolve)
// result_capability = NewPromiseCapability(%Promise%)
// PerformPromiseThen(thenable, resolve, reject, result_capability)
//
// which means that PerformPromiseThen will either schedule a new
// PromiseReaction with resolve and reject or a PromiseReactionJob
// with resolve or reject based on the state of {thenable}. And
// resolve or reject will just invoke the default [[Resolve]] or
// [[Reject]] functions on the {promise_to_resolve}.
//
// This is the same as just doing
//
// PerformPromiseThen(thenable, undefined, undefined, promise_to_resolve)
//
// which performs exactly the same (observable) steps.
TailCallBuiltin(Builtins::kPerformPromiseThen, native_context, thenable,
UndefinedConstant(), UndefinedConstant(),
promise_to_resolve);
}
BIND(&if_exception);
BIND(&if_slow);
{
// We need to reject the thenable.
Node* result = CallJS(
CodeFactory::Call(isolate(), ConvertReceiverMode::kNullOrUndefined),
native_context, UndefinedConstant(), var_exception.value());
Node* resolve = nullptr;
Node* reject = nullptr;
std::tie(resolve, reject) = CreatePromiseResolvingFunctions(
promise_to_resolve, FalseConstant(), native_context);
Label if_exception(this, Label::kDeferred);
VARIABLE(var_exception, MachineRepresentation::kTagged, TheHoleConstant());
Node* const result = CallJS(
CodeFactory::Call(isolate(), ConvertReceiverMode::kNotNullOrUndefined),
native_context, then, thenable, resolve, reject);
GotoIfException(result, &if_exception, &var_exception);
Return(result);
BIND(&if_exception);
{
// We need to reject the {thenable}.
Node* const result = CallJS(
CodeFactory::Call(isolate(), ConvertReceiverMode::kNullOrUndefined),
native_context, UndefinedConstant(), var_exception.value());
Return(result);
}
}
}
......
......@@ -18022,6 +18022,18 @@ TEST(PromiseHook) {
"var x = X.resolve().then(() => {});\n";
CompileRun(source);
promise_hook_data->Reset();
source =
"var resolve, value = '';\n"
"var p = new Promise(r => resolve = r);\n";
CompileRun(source);
CHECK_EQ(v8::Promise::kPending, GetPromise("p")->State());
CompileRun("resolve(Promise.resolve(value));\n");
CHECK_EQ(v8::Promise::kFulfilled, GetPromise("p")->State());
CHECK_EQ(9, promise_hook_data->promise_hook_count);
delete promise_hook_data;
isolate->SetPromiseHook(nullptr);
}
......
// Copyright 2018 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.
(function() {
let resolve;
let onFulfilledValue;
const p = new Promise(r => resolve = r);
resolve(Promise.resolve(1));
p.then(
v => {
onFulfilledValue = v;
},
e => {
assertUnreachable();
});
setTimeout(_ => assertEquals(1, onFulfilledValue));
})();
(function() {
let resolve;
let onRejectedReason;
const p = new Promise(r => resolve = r);
resolve(Promise.reject(1));
p.then(
v => {
assertUnreachable();
},
e => {
onRejectedReason = e;
});
setTimeout(_ => assertEquals(1, onRejectedReason));
})();
(function() {
let onFulfilledValue;
(async () => Promise.resolve(1))().then(
v => {
onFulfilledValue = v;
},
e => {
assertUnreachable();
});
setTimeout(_ => assertEquals(1, onFulfilledValue));
})();
(function() {
let onRejectedReason;
(async () => Promise.reject(1))().then(
v => {
assertUnreachable();
},
e => {
onRejectedReason = e;
});
setTimeout(_ => assertEquals(1, onRejectedReason));
})();
(function() {
let resolve;
let onFulfilledValue;
const p = new Promise(r => resolve = r);
resolve({
then(onFulfilled, onRejected) {
onFulfilled(1);
}
});
p.then(
v => {
onFulfilledValue = v;
},
e => {
assertUnreachable();
});
setTimeout(_ => assertEquals(1, onFulfilledValue));
})();
(function() {
let resolve;
let onRejectedReason;
const p = new Promise(r => resolve = r);
resolve({
then(onFulfilled, onRejected) {
onRejected(1);
}
});
p.then(
v => {
assertUnreachable();
},
e => {
onRejectedReason = e;
});
setTimeout(_ => assertEquals(1, onRejectedReason));
})();
(function() {
let onFulfilledValue;
(async () => ({
then(onFulfilled, onRejected) {
onFulfilled(1);
}
}))().then(
v => {
onFulfilledValue = v;
},
e => {
assertUnreachable();
});
setTimeout(_ => assertEquals(1, onFulfilledValue));
})();
(function() {
let onRejectedReason;
(async () => ({
then(onFulfilled, onRejected) {
onRejected(1);
}
}))().then(
v => {
assertUnreachable();
},
e => {
onRejectedReason = e;
});
setTimeout(_ => assertEquals(1, onRejectedReason));
})();
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