v8/test/mjsunit/promise-hooks.js
Stephen Belanger ff8d67c884 Reland "Fix Context PromiseHook behaviour with debugger enabled"
This is a reland of commit 872b7faa32

Original change's description:
> Fix Context PromiseHook behaviour with debugger enabled
>
> This is a solution for https://github.com/nodejs/node/issues/43148.
>
> Due to differences in behaviour between code with and without the debugger enabled, some promise lifecycle events were being missed and some extra ones were being added. This change resolves this and verifies the event sequence is consistent between code with and without the debugger.
>
> Change-Id: I3dabf1dceb14233226b1752083d659f1c2f97966
> Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3779922
> Reviewed-by: Victor Gomes <victorgomes@chromium.org>
> Commit-Queue: Camillo Bruni <cbruni@chromium.org>
> Reviewed-by: Camillo Bruni <cbruni@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#82132}

Change-Id: Ifdd407261c793887fbd012d5a04ba36b3744c349
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3805979
Reviewed-by: Camillo Bruni <cbruni@chromium.org>
Reviewed-by: Clemens Backes <clemensb@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Reviewed-by: Victor Gomes <victorgomes@chromium.org>
Cr-Commit-Position: refs/heads/main@{#82575}
2022-08-18 20:36:37 +00:00

329 lines
8.6 KiB
JavaScript

// 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 --turbofan --no-always-turbofan --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))
}
}
let has_promise_hooks = false;
try {
d8.promise.setHooks();
has_promise_hooks = true;
} catch {
has_promise_hooks = false;
}
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();
}
function doTest () {
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 Promise.reject();
}, () => {
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();
});
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();
(function regress1126309() {
function __f_16(test) {
test();
d8.promise.setHooks(undefined, () => {});
%PerformMicrotaskCheckpoint();
d8.promise.setHooks();
}
__f_16(async () => { await Promise.resolve()});
})();
(function boundFunction() {
function hook() {};
const bound = hook.bind(this);
d8.promise.setHooks(bound, bound, bound, bound);
Promise.resolve();
Promise.reject();
%PerformMicrotaskCheckpoint();
d8.promise.setHooks();
})();
(function promiseAll() {
let initCount = 0;
d8.promise.setHooks(() => { initCount++});
Promise.all([Promise.resolve(1)]);
%PerformMicrotaskCheckpoint();
assertEquals(initCount, 3);
d8.promise.setHooks();
})();
(function overflow(){
d8.promise.setHooks(() => { new Promise(()=>{}) });
// Trigger overflow from JS code:
Promise.all([Promise.resolve(1)]);
%PerformMicrotaskCheckpoint();
d8.promise.setHooks();
});
}
if (has_promise_hooks) {
doTest();
d8.debugger.enable();
doTest();
}