Files
node/test/parallel/test-inspect-async-hook-setup-at-inspect.js
Christian Aurich d99d2abef8 test: deflake test-inspect-async-hook-setup-at-inspect
The test sends Debugger.setAsyncCallStackDepth in the same batch as
Runtime.enable and Debugger.enable, and treats the command response as
proof that async stack recording is on. It is not.

While the target runs JS the command is dispatched from a V8 interrupt,
where Agent::SyncAsyncHookState() cannot call into JS and defers
enabling the async hook to an immediate, answering the command anyway.
The `debugger` in the polling interval then pauses the target before
that immediate runs, and the nested message loop does not process
immediates, so setupTimeoutWithBreak() schedules the timer with the
hook still disabled and Debugger.paused arrives without asyncStackTrace.

Send the command after the first pause instead. Messages dispatched
while the target is paused come from the nested message loop rather
than an interrupt, so the hook is enabled inline and the response is
the barrier the test assumed it to be.

Deferring is deliberate: calling into JS from a V8 interrupt is not
safe. This corrects an assumption of the test, not the runtime.

Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65584
Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md
Refs: https://github.com/nodejs/node/pull/65028
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
2026-08-31 17:25:06 +00:00

79 lines
2.9 KiB
JavaScript

'use strict';
const common = require('../common');
common.skipIfInspectorDisabled();
common.skipIf32Bits();
const { NodeInstance } = require('../common/inspector-helper.js');
const assert = require('assert');
// Even with --inspect, the default async call stack depth is 0. We need a
// chance to call Debugger.setAsyncCallStackDepth *before* activating the timer
// for async stack traces to work.
const script = `
process._rawDebug('Waiting until the inspector is activated...');
const waiting = setInterval(() => { debugger; }, 50);
// This function is called by the inspector client (session)
function setupTimeoutWithBreak() {
clearInterval(waiting);
process._rawDebug('Debugger ready, setting up timeout with a break');
setTimeout(() => { debugger; }, 50);
}
`;
async function waitForInitialSetup(session) {
console.error('[test]', 'Waiting for initial setup');
await session.waitForBreakOnLine(2, '[eval]');
}
// Sent only once the target is paused. While the target runs JS the command
// is dispatched from a V8 interrupt, where Agent::SyncAsyncHookState() cannot
// call into JS and defers enabling the async hook to an immediate that the
// pause then blocks, answering the command anyway. Dispatching from the paused
// message loop enables the hook inline, so the response is a real barrier.
async function enableAsyncStackTraces(session) {
console.error('[test]', 'Enabling async stack traces');
await session.send({ 'method': 'Debugger.setAsyncCallStackDepth',
'params': { 'maxDepth': 10 } });
}
async function setupTimeoutForStackTrace(session) {
console.error('[test]', 'Setting up timeout for async stack trace');
await session.send([
{ 'method': 'Runtime.evaluate',
'params': { expression: 'setupTimeoutWithBreak()' } },
{ 'method': 'Debugger.resume' },
]);
}
async function checkAsyncStackTrace(session) {
console.error('[test]', 'Verify basic properties of asyncStackTrace');
const paused = await session.waitForBreakOnLine(8, '[eval]');
assert(paused.params.asyncStackTrace,
`${Object.keys(paused.params)} contains "asyncStackTrace" property`);
assert(paused.params.asyncStackTrace.description, 'Timeout');
assert(paused.params.asyncStackTrace.callFrames
.some((frame) => frame.functionName === 'setupTimeoutWithBreak'));
}
async function runTests() {
const instance = new NodeInstance(['--inspect=0'], script);
const session = await instance.connectInspectorSession();
await session.send([
{ 'method': 'Runtime.enable' },
{ 'method': 'Debugger.enable' },
{ 'method': 'Debugger.setBlackboxPatterns',
'params': { 'patterns': [] } },
]);
await waitForInitialSetup(session);
await enableAsyncStackTraces(session);
await setupTimeoutForStackTrace(session);
await checkAsyncStackTrace(session);
console.error('[test]', 'Stopping child instance');
session.disconnect();
instance.kill();
}
runTests();