v8/test/mjsunit/promise-hooks.js

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

276 lines
7.2 KiB
JavaScript
Raw Normal View History

Reland "[api] JSFunction PromiseHook for v8::Context" This is a reland of d5457f5fb7ea05ca05a697599ffa50d35c1ae3c7 after a speculative revert. Additionally it fixes an issue with throwing promise hooks. Original change's description: > [api] JSFunction PromiseHook for v8::Context > > This will enable Node.js to get much better performance from async_hooks > as currently PromiseHook delegates to C++ for the hook function and then > Node.js delegates it right back to JavaScript, introducing several > unnecessary barrier hops in code that gets called very, very frequently > in modern, promise-heavy applications. > > This API mirrors the form of the original C++ function based PromiseHook > API, however it is intentionally separate to allow it to use JSFunctions > triggered within generated code to, as much as possible, avoid entering > runtime functions entirely. > > Because PromiseHook has internal use also, beyond just the Node.js use, > I have opted to leave the existing API intact and keep this separate to > avoid conflicting with any possible behaviour expectations of other API > users. > > The design ideas for this new API stemmed from discussion with some V8 > team members at a previous Node.js Diagnostics Summit hosted by Google > in Munich, and the relevant documentation of the discussion can be found > here: https://docs.google.com/document/d/1g8OrG5lMIUhRn1zbkutgY83MiTSMx-0NHDs8Bf-nXxM/edit#heading=h.w1bavzz80l1e > > A summary of the reasons for why this new design is important can be > found here: https://docs.google.com/document/d/1vtgoT4_kjgOr-Bl605HR2T6_SC-C8uWzYaOPDK5pmRo/edit?usp=sharing > > Bug: v8:11025 > Change-Id: I0b403b00c37d3020b5af07b654b860659d3a7697 > Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2759188 > Reviewed-by: Marja Hölttä <marja@chromium.org> > Reviewed-by: Camillo Bruni <cbruni@chromium.org> > Reviewed-by: Anton Bikineev <bikineev@chromium.org> > Reviewed-by: Igor Sheludko <ishell@chromium.org> > Commit-Queue: Camillo Bruni <cbruni@chromium.org> > Cr-Commit-Position: refs/heads/master@{#73858} Bug: v8:11025 Bug: chromium:1197475 Change-Id: I73a71e97d9c3dff89a2b092c3fe4adff81ede8ef Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2823917 Reviewed-by: Marja Hölttä <marja@chromium.org> Reviewed-by: Igor Sheludko <ishell@chromium.org> Reviewed-by: Anton Bikineev <bikineev@chromium.org> Reviewed-by: Camillo Bruni <cbruni@chromium.org> Commit-Queue: Camillo Bruni <cbruni@chromium.org> Cr-Commit-Position: refs/heads/master@{#74071}
2021-04-14 22:44:39 +00:00
// 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.
Reland "[api] JSFunction PromiseHook for v8::Context" This is a reland of d5457f5fb7ea05ca05a697599ffa50d35c1ae3c7 after a speculative revert. Additionally it fixes an issue with throwing promise hooks. Original change's description: > [api] JSFunction PromiseHook for v8::Context > > This will enable Node.js to get much better performance from async_hooks > as currently PromiseHook delegates to C++ for the hook function and then > Node.js delegates it right back to JavaScript, introducing several > unnecessary barrier hops in code that gets called very, very frequently > in modern, promise-heavy applications. > > This API mirrors the form of the original C++ function based PromiseHook > API, however it is intentionally separate to allow it to use JSFunctions > triggered within generated code to, as much as possible, avoid entering > runtime functions entirely. > > Because PromiseHook has internal use also, beyond just the Node.js use, > I have opted to leave the existing API intact and keep this separate to > avoid conflicting with any possible behaviour expectations of other API > users. > > The design ideas for this new API stemmed from discussion with some V8 > team members at a previous Node.js Diagnostics Summit hosted by Google > in Munich, and the relevant documentation of the discussion can be found > here: https://docs.google.com/document/d/1g8OrG5lMIUhRn1zbkutgY83MiTSMx-0NHDs8Bf-nXxM/edit#heading=h.w1bavzz80l1e > > A summary of the reasons for why this new design is important can be > found here: https://docs.google.com/document/d/1vtgoT4_kjgOr-Bl605HR2T6_SC-C8uWzYaOPDK5pmRo/edit?usp=sharing > > Bug: v8:11025 > Change-Id: I0b403b00c37d3020b5af07b654b860659d3a7697 > Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2759188 > Reviewed-by: Marja Hölttä <marja@chromium.org> > Reviewed-by: Camillo Bruni <cbruni@chromium.org> > Reviewed-by: Anton Bikineev <bikineev@chromium.org> > Reviewed-by: Igor Sheludko <ishell@chromium.org> > Commit-Queue: Camillo Bruni <cbruni@chromium.org> > Cr-Commit-Position: refs/heads/master@{#73858} Bug: v8:11025 Bug: chromium:1197475 Change-Id: I73a71e97d9c3dff89a2b092c3fe4adff81ede8ef Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2823917 Reviewed-by: Marja Hölttä <marja@chromium.org> Reviewed-by: Igor Sheludko <ishell@chromium.org> Reviewed-by: Anton Bikineev <bikineev@chromium.org> Reviewed-by: Camillo Bruni <cbruni@chromium.org> Commit-Queue: Camillo Bruni <cbruni@chromium.org> Cr-Commit-Position: refs/heads/master@{#74071}
2021-04-14 22:44:39 +00:00
// Flags: --allow-natives-syntax --opt --no-always-opt --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))
}
}
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();
}
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 { 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();
})();