// 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 --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)) } } 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(); } if (has_promise_hooks) { 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(); })(); (function overflow(){ d8.promise.setHooks(() => { new Promise(()=>{}) }); // Trigger overflow from JS code: Promise.all([Promise.resolve(1)]); %PerformMicrotaskCheckpoint(); d8.promise.setHooks(); }); }