Issue #2089 - Optimize Promise.then and Promise.all/race with default resolving functions

Based-on: m-c 1401508,1412200,1473970/{1-4}
This commit is contained in:
Martok 2023-01-22 15:14:08 +01:00 committed by roytam1
commit 9744b0c2bc
17 changed files with 906 additions and 142 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,46 @@
// Promise.race(...) may add a dummy PromiseReaction which is only used for the
// debugger.
//
// See BlockOnPromise when called from PerformPromiseRace for when this dummy
// reaction is created.
var g = newGlobal();
var dbg = new Debugger();
var gw = dbg.addDebuggee(g);
function test(withFastPath) {
g.eval(`
function newPromiseCapability() {
var resolve, reject, promise = new Promise(function(r1, r2) {
resolve = r1;
reject = r2;
});
return {promise, resolve, reject};
}
var {promise: alwaysPending} = newPromiseCapability();
if (!${withFastPath}) {
// Disable the BlockOnPromise fast path by giving |alwaysPending| a
// non-default "then" function property. This will ensure the dummy
// reaction is created.
alwaysPending.then = function() {};
}
var result = Promise.race([alwaysPending]);
`);
var alwaysPending = gw.makeDebuggeeValue(g.alwaysPending);
var result = gw.makeDebuggeeValue(g.result);
assertEq(alwaysPending.promiseDependentPromises.length, 1);
assertEq(alwaysPending.promiseDependentPromises[0], result);
assertEq(result.promiseDependentPromises.length, 0);
}
// No dummy reaction created when the fast path is taken.
test(true);
// Dummy reaction is created when we can't take the fast path.
test(false);

View file

@ -0,0 +1,71 @@
// Promise.race(...) may add a dummy PromiseReaction which is only used for the
// debugger. Ensure that this dummy reaction can't influence the normal Promise
// resolution behaviour.
//
// See BlockOnPromise when called from PerformPromiseRace for when this dummy
// reaction is created.
function newPromiseCapability() {
var resolve, reject, promise = new Promise(function(r1, r2) {
resolve = r1;
reject = r2;
});
return {promise, resolve, reject};
}
function neverCalled() {
// Quit with non-zero exit code to ensure a test suite error is shown,
// even when this function is called within promise handlers which normally
// swallow any exceptions.
quit(1);
}
var c = 0;
var g_resolve;
var resolvedValues = [];
function resolveCapability(v) {
resolvedValues.push(v);
}
class P extends Promise {
constructor(executor) {
// Only the very first object created through this constructor gets
// special treatment, all other invocations create built-in Promise
// objects.
if (c++ > 1) {
return new Promise(executor);
}
executor(resolveCapability, neverCalled);
var {promise, resolve} = newPromiseCapability();
g_resolve = resolve;
// Use an async function to create a Promise without resolving functions.
var p = async function(){ await promise; return 456; }();
// Ensure the species constructor is not the built-in Promise constructor
// to avoid falling into the fast path.
p.constructor = {
[Symbol.species]: P
};
return p;
}
}
var {promise: alwaysPending} = newPromiseCapability();
// The promise returned from race() should never be resolved.
P.race([alwaysPending]).then(neverCalled, neverCalled);
g_resolve(123);
drainJobQueue();
// Check |resolvedValues| to ensure resolving functions were properly called.
assertEq(resolvedValues.length, 2);
assertEq(resolvedValues[0], alwaysPending);
assertEq(resolvedValues[1], 456);

View file

@ -0,0 +1,15 @@
function newPromiseCapability() {
var resolve, reject, promise = new Promise(function(r1, r2) {
resolve = r1;
reject = r2;
});
return {promise, resolve, reject};
}
var {promise, resolve} = newPromiseCapability();
resolve(Promise.resolve(0));
// Don't assert when the Promise was already resolved.
resolvePromise(promise, 123);

View file

@ -0,0 +1,21 @@
// Test we don't assert when the promise is settled after enqueuing a PromiseReactionJob.
function newPromiseCapability() {
var resolve, reject, promise = new Promise(function(r1, r2) {
resolve = r1;
reject = r2;
});
return {promise, resolve, reject};
}
var {promise, resolve} = newPromiseCapability();
var p = Promise.resolve(0);
// Enqueue a PromiseResolveThenableJob followed by a PromiseReactionJob.
resolve(p);
// The PromiseReactionJob expects a pending promise, but this settlePromiseNow
// call will already have settled the promise.
settlePromiseNow(promise);

View file

@ -0,0 +1,18 @@
// Don't assert when the promise in the resolving functions is wrapped in a CCW.
function newPromiseCapability(newTarget) {
var resolve, reject, promise = Reflect.construct(Promise, [function(r1, r2) {
resolve = r1;
reject = r2;
}], newTarget);
return {promise, resolve, reject};
}
var g = newGlobal();
var {promise, resolve} = newPromiseCapability(g.Promise);
g.settlePromiseNow(promise);
// Don't assert when resolving the promise.
resolve(0);

View file

@ -0,0 +1,18 @@
// Don't assert when the promise in the resolving functions is wrapped in a CCW.
function newPromiseCapability(newTarget) {
var resolve, reject, promise = Reflect.construct(Promise, [function(r1, r2) {
resolve = r1;
reject = r2;
}], newTarget);
return {promise, resolve, reject};
}
var g = newGlobal();
var {promise, reject} = newPromiseCapability(g.Promise);
g.settlePromiseNow(promise);
// Don't assert when rejecting the promise.
reject(0);

View file

@ -0,0 +1,27 @@
// Test we don't assert when the promise is settled and the SpeciesConstructor
// call in Promise.prototype.then throws an exception.
function newPromiseCapability() {
var resolve, reject, promise = new Promise(function(r1, r2) {
resolve = r1;
reject = r2;
});
return {promise, resolve, reject};
}
var {promise, resolve} = newPromiseCapability();
var p = Promise.resolve(0);
p.constructor = {
[Symbol.species]: function() {
throw new Error();
}
};
// Enqueue a PromiseResolveThenableJob.
resolve(p);
// Settle the promise after the resolve call.
settlePromiseNow(promise);

View file

@ -0,0 +1,27 @@
// Test we don't assert when the promise is settled and the SpeciesConstructor
// call in Promise.prototype.then throws an exception.
function newPromiseCapability() {
var resolve, reject, promise = new Promise(function(r1, r2) {
resolve = r1;
reject = r2;
});
return {promise, resolve, reject};
}
var {promise, resolve} = newPromiseCapability();
var p = Promise.resolve(0);
p.constructor = {
[Symbol.species]: function() {
// Settle the promise in the SpeciesConstructor call.
settlePromiseNow(promise);
throw new Error();
}
};
// Enqueue a PromiseResolveThenableJob.
resolve(p);

View file

@ -0,0 +1,18 @@
// Test we don't assert when the promise is settled and we then try to call the
// resolving function.
function newPromiseCapability() {
var resolve, reject, promise = new Promise(function(r1, r2) {
resolve = r1;
reject = r2;
});
return {promise, resolve, reject};
}
var {promise, resolve} = newPromiseCapability();
settlePromiseNow(promise);
// Don't assert when the promise is already settled.
resolve(0);

View file

@ -0,0 +1,18 @@
// Test we don't assert when the promise is settled and we then try to call the
// rejecting function.
function newPromiseCapability() {
var resolve, reject, promise = new Promise(function(r1, r2) {
resolve = r1;
reject = r2;
});
return {promise, resolve, reject};
}
var {promise, reject} = newPromiseCapability();
settlePromiseNow(promise);
// Don't assert when the promise is already settled.
reject(0);

View file

@ -0,0 +1,20 @@
// Don't assert when a side-effect when getting the "then" property settled the promise.
function newPromiseCapability() {
var resolve, reject, promise = new Promise(function(r1, r2) {
resolve = r1;
reject = r2;
});
return {promise, resolve, reject};
}
var {promise, resolve} = newPromiseCapability();
var thenable = {
get then() {
settlePromiseNow(promise);
}
};
resolve(thenable);

View file

@ -0,0 +1,23 @@
// Don't assert when a side-effect when getting the "then" property settled the promise.
function newPromiseCapability() {
var resolve, reject, promise = new Promise(function(r1, r2) {
resolve = r1;
reject = r2;
});
return {promise, resolve, reject};
}
var {promise, resolve} = newPromiseCapability();
var thenable = {
get then() {
settlePromiseNow(promise);
// Throw an error to reject the promise.
throw new Error();
}
};
resolve(thenable);

View file

@ -0,0 +1,7 @@
// Don't assert when settlePromiseNow() is called on an async-function promise.
var promise = async function(){ await 0; }();
try {
settlePromiseNow(promise);
} catch {}

View file

@ -0,0 +1,15 @@
function newPromiseCapability() {
var resolve, reject, promise = new Promise(function(r1, r2) {
resolve = r1;
reject = r2;
});
return {promise, resolve, reject};
}
var {promise, resolve, reject} = newPromiseCapability();
settlePromiseNow(promise);
assertEq(resolve(0), undefined);
assertEq(reject(0), undefined);

View file

@ -0,0 +1,30 @@
load(libdir + "asserts.js");
const g = newGlobal({sameCompartmentAs: this});
let resolve, reject;
let promise = new Promise((resolveFn, rejectFn) => {
resolve = resolveFn;
reject = rejectFn;
});
// Set to a built-in Promise.prototype.then function, but from a different realm.
promise.then = g.Promise.prototype.then;
// Make SpeciesConstructor throw a TypeError exception.
promise.constructor = {
[Symbol.species]: "not a constructor"
};
async function f(p) {
await p;
}
let error;
f(promise).catch(e => { error = e; });
resolve(promise);
drainJobQueue();
assertEq(error.constructor === g.TypeError, true);

View file

@ -721,8 +721,8 @@ JSRuntime::enqueuePromiseJob(JSContext* cx, HandleFunction job, HandleObject pro
if (promise) {
RootedObject unwrappedPromise(cx, promise);
// While the job object is guaranteed to be unwrapped, the promise
// might be wrapped. See the comments in
// intrinsic_EnqueuePromiseReactionJob for details.
// might be wrapped. See the comments in EnqueuePromiseReactionJob in
// builtin/Promise.cpp for details.
if (IsWrapper(promise))
unwrappedPromise = UncheckedUnwrap(promise);
if (unwrappedPromise->is<PromiseObject>())