Browse Source

inspector: enable async stack traces

Implement a special async_hooks listener that forwards information
about async tasks to V8Inspector asyncTask* API, thus enabling
DevTools feature "async stack traces".

The feature is enabled only on 64bit platforms due to a technical
limitation of V8 Inspector: inspector uses a pointer as a task id,
while async_hooks use 64bit numbers as ids.

To avoid performance penalty of async_hooks when not debugging,
the new listener is enabled only when the process enters a debug mode:

 - When the process is started with `--inspect` or `--inspect-brk`,
   the listener is enabled immediately and async stack traces
   lead all the way to the first tick of the event loop.

 - When the debug mode is enabled via SIGUSR1 or `_debugProcess()`,
   the listener is enabled together with the debugger. As a result,
   only async operations started after the signal was received
   will be correctly observed and reported to V8 Inspector. For example,
   a `setInterval()` called in the first tick of the event will not be
   shown in the async stack trace when the callback is invoked. This
   behaviour is consistent with Chrome DevTools.

Last but not least, this commit fixes handling of InspectorAgent's
internal property `enabled_` to ensure it's set back to `false`
after the debugger is deactivated (typically via `process._debugEnd()`).

Fixes: https://github.com/nodejs/node/issues/11370
PR-URL: https://github.com/nodejs/node/pull/13870
Reviewed-by: Timothy Gu <timothygu99@gmail.com>
Reviewed-by: Anna Henningsen <anna@addaleax.net>
canary-base
Miroslav Bajtoš 8 years ago
committed by Timothy Gu
parent
commit
a0895ed6c4
No known key found for this signature in database GPG Key ID: 7FE6B095B582B0D4
  1. 2
      lib/internal/bootstrap_node.js
  2. 64
      lib/internal/inspector_async_hook.js
  3. 1
      node.gyp
  4. 165
      src/inspector_agent.cc
  5. 27
      src/inspector_agent.h
  6. 14
      src/node_config.cc
  7. 10
      test/common/README.md
  8. 6
      test/common/index.js
  9. 16
      test/inspector/inspector-helper.js
  10. 45
      test/inspector/test-async-hook-setup-at-inspect-brk.js
  11. 70
      test/inspector/test-async-hook-setup-at-inspect.js
  12. 81
      test/inspector/test-async-hook-setup-at-signal.js
  13. 33
      test/inspector/test-async-hook-teardown-at-debug-end.js
  14. 69
      test/inspector/test-async-stack-traces-promise-then.js
  15. 41
      test/inspector/test-async-stack-traces-set-interval.js
  16. 26
      test/inspector/test-inspector-enabled.js
  17. 3
      test/inspector/test-inspector.js

2
lib/internal/bootstrap_node.js

@ -45,6 +45,8 @@
if (global.__coverage__) if (global.__coverage__)
NativeModule.require('internal/process/write-coverage').setup(); NativeModule.require('internal/process/write-coverage').setup();
NativeModule.require('internal/inspector_async_hook').setup();
// Do not initialize channel in debugger agent, it deletes env variable // Do not initialize channel in debugger agent, it deletes env variable
// and the main thread won't see it. // and the main thread won't see it.
if (process.argv[1] !== '--debug-agent') if (process.argv[1] !== '--debug-agent')

64
lib/internal/inspector_async_hook.js

@ -0,0 +1,64 @@
'use strict';
const { createHook } = require('async_hooks');
const inspector = process.binding('inspector');
const config = process.binding('config');
if (!inspector || !inspector.asyncTaskScheduled) {
exports.setup = function() {};
return;
}
const hook = createHook({
init(asyncId, type, triggerAsyncId, resource) {
// It's difficult to tell which tasks will be recurring and which won't,
// therefore we mark all tasks as recurring. Based on the discussion
// in https://github.com/nodejs/node/pull/13870#discussion_r124515293,
// this should be fine as long as we call asyncTaskCanceled() too.
const recurring = true;
inspector.asyncTaskScheduled(type, asyncId, recurring);
},
before(asyncId) {
inspector.asyncTaskStarted(asyncId);
},
after(asyncId) {
inspector.asyncTaskFinished(asyncId);
},
destroy(asyncId) {
inspector.asyncTaskCanceled(asyncId);
},
});
function enable() {
if (config.bits < 64) {
// V8 Inspector stores task ids as (void*) pointers.
// async_hooks store ids as 64bit numbers.
// As a result, we cannot reliably translate async_hook ids to V8 async_task
// ids on 32bit platforms.
process.emitWarning(
'Warning: Async stack traces in debugger are not available ' +
`on ${config.bits}bit platforms. The feature is disabled.`,
{
code: 'INSPECTOR_ASYNC_STACK_TRACES_NOT_AVAILABLE',
});
} else {
hook.enable();
}
}
function disable() {
hook.disable();
}
exports.setup = function() {
inspector.registerAsyncHook(enable, disable);
if (inspector.isEnabled()) {
// If the inspector was already enabled via --inspect or --inspect-brk,
// the we need to enable the async hook immediately at startup.
enable();
}
};

1
node.gyp

@ -88,6 +88,7 @@
'lib/internal/freelist.js', 'lib/internal/freelist.js',
'lib/internal/fs.js', 'lib/internal/fs.js',
'lib/internal/http.js', 'lib/internal/http.js',
'lib/internal/inspector_async_hook.js',
'lib/internal/linkedlist.js', 'lib/internal/linkedlist.js',
'lib/internal/net.js', 'lib/internal/net.js',
'lib/internal/module.js', 'lib/internal/module.js',

165
src/inspector_agent.cc

@ -23,20 +23,27 @@
namespace node { namespace node {
namespace inspector { namespace inspector {
namespace { namespace {
using node::FatalError;
using v8::Array; using v8::Array;
using v8::Boolean;
using v8::Context; using v8::Context;
using v8::External; using v8::External;
using v8::Function; using v8::Function;
using v8::FunctionCallbackInfo; using v8::FunctionCallbackInfo;
using v8::HandleScope; using v8::HandleScope;
using v8::Integer;
using v8::Isolate; using v8::Isolate;
using v8::Local; using v8::Local;
using v8::Maybe; using v8::Maybe;
using v8::MaybeLocal; using v8::MaybeLocal;
using v8::Name;
using v8::NewStringType; using v8::NewStringType;
using v8::Object; using v8::Object;
using v8::Persistent; using v8::Persistent;
using v8::String; using v8::String;
using v8::Undefined;
using v8::Value; using v8::Value;
using v8_inspector::StringBuffer; using v8_inspector::StringBuffer;
@ -616,6 +623,28 @@ class NodeInspectorClient : public V8InspectorClient {
timers_.erase(data); timers_.erase(data);
} }
// Async stack traces instrumentation.
void AsyncTaskScheduled(const StringView& task_name, void* task,
bool recurring) {
client_->asyncTaskScheduled(task_name, task, recurring);
}
void AsyncTaskCanceled(void* task) {
client_->asyncTaskCanceled(task);
}
void AsyncTaskStarted(void* task) {
client_->asyncTaskStarted(task);
}
void AsyncTaskFinished(void* task) {
client_->asyncTaskFinished(task);
}
void AllAsyncTasksCanceled() {
client_->allAsyncTasksCanceled();
}
private: private:
node::Environment* env_; node::Environment* env_;
v8::Platform* platform_; v8::Platform* platform_;
@ -676,9 +705,21 @@ bool Agent::StartIoThread(bool wait_for_connect) {
} }
v8::Isolate* isolate = parent_env_->isolate(); v8::Isolate* isolate = parent_env_->isolate();
HandleScope handle_scope(isolate);
// Enable tracking of async stack traces
if (!enable_async_hook_function_.IsEmpty()) {
Local<Function> enable_fn = enable_async_hook_function_.Get(isolate);
auto context = parent_env_->context();
auto result = enable_fn->Call(context, Undefined(isolate), 0, nullptr);
if (result.IsEmpty()) {
FatalError(
"node::InspectorAgent::StartIoThread",
"Cannot enable Inspector's AsyncHook, please report this.");
}
}
// Send message to enable debug in workers // Send message to enable debug in workers
HandleScope handle_scope(isolate);
Local<Object> process_object = parent_env_->process_object(); Local<Object> process_object = parent_env_->process_object();
Local<Value> emit_fn = Local<Value> emit_fn =
process_object->Get(FIXED_ONE_BYTE_STRING(isolate, "emit")); process_object->Get(FIXED_ONE_BYTE_STRING(isolate, "emit"));
@ -717,10 +758,40 @@ void Agent::Stop() {
if (io_ != nullptr) { if (io_ != nullptr) {
io_->Stop(); io_->Stop();
io_.reset(); io_.reset();
enabled_ = false;
}
v8::Isolate* isolate = parent_env_->isolate();
HandleScope handle_scope(isolate);
// Disable tracking of async stack traces
if (!disable_async_hook_function_.IsEmpty()) {
Local<Function> disable_fn = disable_async_hook_function_.Get(isolate);
auto result = disable_fn->Call(parent_env_->context(),
Undefined(parent_env_->isolate()), 0, nullptr);
if (result.IsEmpty()) {
FatalError(
"node::InspectorAgent::Stop",
"Cannot disable Inspector's AsyncHook, please report this.");
}
} }
} }
void Agent::Connect(InspectorSessionDelegate* delegate) { void Agent::Connect(InspectorSessionDelegate* delegate) {
if (!enabled_) {
// Enable tracking of async stack traces
v8::Isolate* isolate = parent_env_->isolate();
HandleScope handle_scope(isolate);
auto context = parent_env_->context();
Local<Function> enable_fn = enable_async_hook_function_.Get(isolate);
auto result = enable_fn->Call(context, Undefined(isolate), 0, nullptr);
if (result.IsEmpty()) {
FatalError(
"node::InspectorAgent::Connect",
"Cannot enable Inspector's AsyncHook, please report this.");
}
}
enabled_ = true; enabled_ = true;
client_->connectFrontend(delegate); client_->connectFrontend(delegate);
} }
@ -773,6 +844,34 @@ void Agent::PauseOnNextJavascriptStatement(const std::string& reason) {
channel->schedulePauseOnNextStatement(reason); channel->schedulePauseOnNextStatement(reason);
} }
void Agent::RegisterAsyncHook(Isolate* isolate,
v8::Local<v8::Function> enable_function,
v8::Local<v8::Function> disable_function) {
enable_async_hook_function_.Reset(isolate, enable_function);
disable_async_hook_function_.Reset(isolate, disable_function);
}
void Agent::AsyncTaskScheduled(const StringView& task_name, void* task,
bool recurring) {
client_->AsyncTaskScheduled(task_name, task, recurring);
}
void Agent::AsyncTaskCanceled(void* task) {
client_->AsyncTaskCanceled(task);
}
void Agent::AsyncTaskStarted(void* task) {
client_->AsyncTaskStarted(task);
}
void Agent::AsyncTaskFinished(void* task) {
client_->AsyncTaskFinished(task);
}
void Agent::AllAsyncTasksCanceled() {
client_->AllAsyncTasksCanceled();
}
void Open(const FunctionCallbackInfo<Value>& args) { void Open(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args); Environment* env = Environment::GetCurrent(args);
inspector::Agent* agent = env->inspector_agent(); inspector::Agent* agent = env->inspector_agent();
@ -810,6 +909,59 @@ void Url(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(OneByteString(env->isolate(), url.c_str())); args.GetReturnValue().Set(OneByteString(env->isolate(), url.c_str()));
} }
static void* GetAsyncTask(int64_t asyncId) {
// The inspector assumes that when other clients use its asyncTask* API,
// they use real pointers, or at least something aligned like real pointer.
// In general it means that our task_id should always be even.
//
// On 32bit platforms, the 64bit asyncId would get truncated when converted
// to a 32bit pointer. However, the javascript part will never enable
// the async_hook on 32bit platforms, therefore the truncation will never
// happen in practice.
return reinterpret_cast<void*>(asyncId << 1);
}
template<void (Agent::*asyncTaskFn)(void*)>
static void InvokeAsyncTaskFnWithId(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsNumber());
int64_t task_id = args[0]->IntegerValue(env->context()).FromJust();
(env->inspector_agent()->*asyncTaskFn)(GetAsyncTask(task_id));
}
static void AsyncTaskScheduledWrapper(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsString());
Local<String> task_name = args[0].As<String>();
String::Value task_name_value(task_name);
StringView task_name_view(*task_name_value, task_name_value.length());
CHECK(args[1]->IsNumber());
int64_t task_id = args[1]->IntegerValue(env->context()).FromJust();
void* task = GetAsyncTask(task_id);
CHECK(args[2]->IsBoolean());
bool recurring = args[2]->BooleanValue(env->context()).FromJust();
env->inspector_agent()->AsyncTaskScheduled(task_name_view, task, recurring);
}
static void RegisterAsyncHookWrapper(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsFunction());
v8::Local<v8::Function> enable_function = args[0].As<Function>();
CHECK(args[1]->IsFunction());
v8::Local<v8::Function> disable_function = args[1].As<Function>();
env->inspector_agent()->RegisterAsyncHook(env->isolate(),
enable_function, disable_function);
}
static void IsEnabled(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
args.GetReturnValue().Set(env->inspector_agent()->enabled());
}
// static // static
void Agent::InitInspector(Local<Object> target, Local<Value> unused, void Agent::InitInspector(Local<Object> target, Local<Value> unused,
@ -830,6 +982,17 @@ void Agent::InitInspector(Local<Object> target, Local<Value> unused,
env->SetMethod(target, "connect", ConnectJSBindingsSession); env->SetMethod(target, "connect", ConnectJSBindingsSession);
env->SetMethod(target, "open", Open); env->SetMethod(target, "open", Open);
env->SetMethod(target, "url", Url); env->SetMethod(target, "url", Url);
env->SetMethod(target, "asyncTaskScheduled", AsyncTaskScheduledWrapper);
env->SetMethod(target, "asyncTaskCanceled",
InvokeAsyncTaskFnWithId<&Agent::AsyncTaskCanceled>);
env->SetMethod(target, "asyncTaskStarted",
InvokeAsyncTaskFnWithId<&Agent::AsyncTaskStarted>);
env->SetMethod(target, "asyncTaskFinished",
InvokeAsyncTaskFnWithId<&Agent::AsyncTaskFinished>);
env->SetMethod(target, "registerAsyncHook", RegisterAsyncHookWrapper);
env->SetMethod(target, "isEnabled", IsEnabled);
} }
void Agent::RequestIoThreadStart() { void Agent::RequestIoThreadStart() {

27
src/inspector_agent.h

@ -16,17 +16,7 @@ namespace node {
class Environment; class Environment;
} // namespace node } // namespace node
namespace v8 { #include "v8.h"
class Context;
template <typename V>
class FunctionCallbackInfo;
template<typename T>
class Local;
class Message;
class Object;
class Platform;
class Value;
} // namespace v8
namespace v8_inspector { namespace v8_inspector {
class StringView; class StringView;
@ -67,6 +57,18 @@ class Agent {
void FatalException(v8::Local<v8::Value> error, void FatalException(v8::Local<v8::Value> error,
v8::Local<v8::Message> message); v8::Local<v8::Message> message);
// Async stack traces instrumentation.
void AsyncTaskScheduled(const v8_inspector::StringView& taskName, void* task,
bool recurring);
void AsyncTaskCanceled(void* task);
void AsyncTaskStarted(void* task);
void AsyncTaskFinished(void* task);
void AllAsyncTasksCanceled();
void RegisterAsyncHook(v8::Isolate* isolate,
v8::Local<v8::Function> enable_function,
v8::Local<v8::Function> disable_function);
// These methods are called by the WS protocol and JS binding to create // These methods are called by the WS protocol and JS binding to create
// inspector sessions. The inspector responds by using the delegate to send // inspector sessions. The inspector responds by using the delegate to send
// messages back. // messages back.
@ -107,6 +109,9 @@ class Agent {
std::string path_; std::string path_;
DebugOptions debug_options_; DebugOptions debug_options_;
int next_context_number_; int next_context_number_;
v8::Persistent<v8::Function> enable_async_hook_function_;
v8::Persistent<v8::Function> disable_async_hook_function_;
}; };
} // namespace inspector } // namespace inspector

14
src/node_config.cc

@ -13,6 +13,7 @@ using v8::Boolean;
using v8::Context; using v8::Context;
using v8::Integer; using v8::Integer;
using v8::Local; using v8::Local;
using v8::Number;
using v8::Object; using v8::Object;
using v8::ReadOnly; using v8::ReadOnly;
using v8::String; using v8::String;
@ -30,6 +31,15 @@ using v8::Value;
True(env->isolate()), ReadOnly).FromJust(); \ True(env->isolate()), ReadOnly).FromJust(); \
} while (0) } while (0)
#define READONLY_PROPERTY(obj, name, value) \
do { \
obj->DefineOwnProperty(env->context(), \
OneByteString(env->isolate(), name), \
value, \
ReadOnly).FromJust(); \
} while (0)
static void InitConfig(Local<Object> target, static void InitConfig(Local<Object> target,
Local<Value> unused, Local<Value> unused,
Local<Context> context) { Local<Context> context) {
@ -91,6 +101,10 @@ static void InitConfig(Local<Object> target,
if (config_expose_http2) if (config_expose_http2)
READONLY_BOOLEAN_PROPERTY("exposeHTTP2"); READONLY_BOOLEAN_PROPERTY("exposeHTTP2");
READONLY_PROPERTY(target,
"bits",
Number::New(env->isolate(), 8 * sizeof(intptr_t)));
} // InitConfig } // InitConfig
} // namespace node } // namespace node

10
test/common/README.md

@ -325,6 +325,16 @@ Path to the 'root' directory. either `/` or `c:\\` (windows)
Logs '1..0 # Skipped: ' + `msg` and exits with exit code `0`. Logs '1..0 # Skipped: ' + `msg` and exits with exit code `0`.
### skipIfInspectorDisabled()
Skip the rest of the tests in the current file when the Inspector
was disabled at compile time.
### skipIf32Bits()
Skip the rest of the tests in the current file when the Node.js executable
was compiled with a pointer size smaller than 64 bits.
### spawnPwd(options) ### spawnPwd(options)
* `options` [&lt;Object>] * `options` [&lt;Object>]
* return [&lt;Object>] * return [&lt;Object>]

6
test/common/index.js

@ -746,6 +746,12 @@ exports.skipIfInspectorDisabled = function skipIfInspectorDisabled() {
} }
}; };
exports.skipIf32Bits = function skipIf32Bits() {
if (process.binding('config').bits < 64) {
exports.skip('The tested feature is not available in 32bit builds');
}
};
const arrayBufferViews = [ const arrayBufferViews = [
Int8Array, Int8Array,
Uint8Array, Uint8Array,

16
test/inspector/inspector-helper.js

@ -9,7 +9,7 @@ const url = require('url');
const _MAINSCRIPT = path.join(common.fixturesDir, 'loop.js'); const _MAINSCRIPT = path.join(common.fixturesDir, 'loop.js');
const DEBUG = false; const DEBUG = false;
const TIMEOUT = 15 * 1000; const TIMEOUT = common.platformTimeout(15 * 1000);
function spawnChildProcess(inspectorFlags, scriptContents, scriptFile) { function spawnChildProcess(inspectorFlags, scriptContents, scriptFile) {
const args = [].concat(inspectorFlags); const args = [].concat(inspectorFlags);
@ -253,9 +253,7 @@ class InspectorSession {
.waitForNotification( .waitForNotification(
(notification) => (notification) =>
this._isBreakOnLineNotification(notification, line, url), this._isBreakOnLineNotification(notification, line, url),
`break on ${url}:${line}`) `break on ${url}:${line}`);
.then((notification) =>
notification.params.callFrames[0].scopeChain[0].object.objectId);
} }
_matchesConsoleOutputNotification(notification, type, values) { _matchesConsoleOutputNotification(notification, type, values) {
@ -321,6 +319,16 @@ class NodeInstance {
}); });
} }
static async startViaSignal(scriptContents) {
const instance = new NodeInstance(
[], `${scriptContents}\nprocess._rawDebug('started');`, undefined);
const msg = 'Timed out waiting for process to start';
while (await common.fires(instance.nextStderrString(), msg, TIMEOUT) !==
'started') {}
process._debugProcess(instance._process.pid);
return instance;
}
onStderrLine(line) { onStderrLine(line) {
console.log('[err]', line); console.log('[err]', line);
if (this._portCallback) { if (this._portCallback) {

45
test/inspector/test-async-hook-setup-at-inspect-brk.js

@ -0,0 +1,45 @@
'use strict';
const common = require('../common');
common.skipIfInspectorDisabled();
common.skipIf32Bits();
common.crashOnUnhandledRejection();
const { NodeInstance } = require('./inspector-helper.js');
const assert = require('assert');
const script = `
setTimeout(() => {
debugger;
process.exitCode = 55;
}, 50);
`;
async function checkAsyncStackTrace(session) {
console.error('[test]', 'Verify basic properties of asyncStackTrace');
const paused = await session.waitForBreakOnLine(2, '[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 === 'Module._compile'));
}
async function runTests() {
const instance = new NodeInstance(undefined, script);
const session = await instance.connectInspectorSession();
await session.send([
{ 'method': 'Runtime.enable' },
{ 'method': 'Debugger.enable' },
{ 'method': 'Debugger.setAsyncCallStackDepth',
'params': { 'maxDepth': 10 } },
{ 'method': 'Debugger.setBlackboxPatterns',
'params': { 'patterns': [] } },
{ 'method': 'Runtime.runIfWaitingForDebugger' }
]);
await checkAsyncStackTrace(session);
await session.runToCompletion();
assert.strictEqual(55, (await instance.expectShutdown()).exitCode);
}
runTests();

70
test/inspector/test-async-hook-setup-at-inspect.js

@ -0,0 +1,70 @@
'use strict';
const common = require('../common');
common.skipIfInspectorDisabled();
common.skipIf32Bits();
common.crashOnUnhandledRejection();
const { NodeInstance } = require('../inspector/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]');
}
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.setAsyncCallStackDepth',
'params': { 'maxDepth': 10 } },
{ 'method': 'Debugger.setBlackboxPatterns',
'params': { 'patterns': [] } },
{ 'method': 'Runtime.runIfWaitingForDebugger' }
]);
await waitForInitialSetup(session);
await setupTimeoutForStackTrace(session);
await checkAsyncStackTrace(session);
console.error('[test]', 'Stopping child instance');
session.disconnect();
instance.kill();
}
runTests();

81
test/inspector/test-async-hook-setup-at-signal.js

@ -0,0 +1,81 @@
'use strict';
const common = require('../common');
common.skipIfInspectorDisabled();
common.skipIf32Bits();
common.crashOnUnhandledRejection();
const { NodeInstance } = require('../inspector/inspector-helper.js');
const assert = require('assert');
const script = `
process._rawDebug('Waiting until a signal enables the inspector...');
let waiting = setInterval(waitUntilDebugged, 50);
function waitUntilDebugged() {
if (!process.binding('inspector').isEnabled()) return;
clearInterval(waiting);
// At this point, even though the Inspector is enabled, the default async
// call stack depth is 0. We need a chance to call
// Debugger.setAsyncCallStackDepth *before* activating the actual timer for
// async stack traces to work. Directly using a debugger statement would be
// too brittle, and using a longer timeout would unnecesarily slow down the
// test on most machines. Triggering a debugger break through an interval is
// a faster and more reliable way.
process._rawDebug('Signal received, waiting for debugger setup');
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(15, '[eval]');
}
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(22, '[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 = await NodeInstance.startViaSignal(script);
const session = await instance.connectInspectorSession();
await session.send([
{ 'method': 'Runtime.enable' },
{ 'method': 'Debugger.enable' },
{ 'method': 'Debugger.setAsyncCallStackDepth',
'params': { 'maxDepth': 10 } },
{ 'method': 'Debugger.setBlackboxPatterns',
'params': { 'patterns': [] } },
{ 'method': 'Runtime.runIfWaitingForDebugger' }
]);
await waitForInitialSetup(session);
await setupTimeoutForStackTrace(session);
await checkAsyncStackTrace(session);
console.error('[test]', 'Stopping child instance');
session.disconnect();
instance.kill();
}
runTests();

33
test/inspector/test-async-hook-teardown-at-debug-end.js

@ -0,0 +1,33 @@
'use strict';
const common = require('../common');
common.skipIfInspectorDisabled();
common.skipIf32Bits();
const spawn = require('child_process').spawn;
const script = `
const assert = require('assert');
// Verify that inspector-async-hook is registered
// by checking that emitInit with invalid arguments
// throw an error.
// See test/async-hooks/test-emit-init.js
assert.throws(
() => async_hooks.emitInit(),
'inspector async hook should have been enabled initially');
process._debugEnd();
// Verify that inspector-async-hook is no longer registered,
// thus emitInit() ignores invalid arguments
// See test/async-hooks/test-emit-init.js
assert.doesNotThrow(
() => async_hooks.emitInit(),
'inspector async hook should have beend disabled by _debugEnd()');
`;
const args = ['--inspect', '-e', script];
const child = spawn(process.execPath, args, { stdio: 'inherit' });
child.on('exit', (code, signal) => {
process.exit(code || signal);
});

69
test/inspector/test-async-stack-traces-promise-then.js

@ -0,0 +1,69 @@
'use strict';
const common = require('../common');
common.skipIfInspectorDisabled();
common.skipIf32Bits();
common.crashOnUnhandledRejection();
const { NodeInstance } = require('./inspector-helper');
const assert = require('assert');
const script = `runTest();
function runTest() {
const p = Promise.resolve();
p.then(function break1() { // lineNumber 3
debugger;
});
p.then(function break2() { // lineNumber 6
debugger;
});
}
`;
async function runTests() {
const instance = new NodeInstance(undefined, script);
const session = await instance.connectInspectorSession();
await session.send([
{ 'method': 'Runtime.enable' },
{ 'method': 'Debugger.enable' },
{ 'method': 'Debugger.setAsyncCallStackDepth',
'params': { 'maxDepth': 10 } },
{ 'method': 'Debugger.setBlackboxPatterns',
'params': { 'patterns': [] } },
{ 'method': 'Runtime.runIfWaitingForDebugger' }
]);
console.error('[test] Waiting for break1');
debuggerPausedAt(await session.waitForBreakOnLine(4, '[eval]'),
'break1', 'runTest:3');
await session.send({ 'method': 'Debugger.resume' });
console.error('[test] Waiting for break2');
debuggerPausedAt(await session.waitForBreakOnLine(7, '[eval]'),
'break2', 'runTest:6');
await session.runToCompletion();
assert.strictEqual(0, (await instance.expectShutdown()).exitCode);
}
function debuggerPausedAt(msg, functionName, previousTickLocation) {
assert(
!!msg.params.asyncStackTrace,
`${Object.keys(msg.params)} contains "asyncStackTrace" property`);
assert.strictEqual(msg.params.callFrames[0].functionName, functionName);
assert.strictEqual(msg.params.asyncStackTrace.description, 'PROMISE');
const frameLocations = msg.params.asyncStackTrace.callFrames.map(
(frame) => `${frame.functionName}:${frame.lineNumber}`);
assertArrayIncludes(frameLocations, previousTickLocation);
}
function assertArrayIncludes(actual, expected) {
const expectedString = JSON.stringify(expected);
const actualString = JSON.stringify(actual);
assert(
actual.includes(expected),
`Expected ${actualString} to contain ${expectedString}.`);
}
runTests();

41
test/inspector/test-async-stack-traces-set-interval.js

@ -0,0 +1,41 @@
'use strict';
const common = require('../common');
common.skipIfInspectorDisabled();
common.skipIf32Bits();
common.crashOnUnhandledRejection();
const { NodeInstance } = require('./inspector-helper');
const assert = require('assert');
const script = 'setInterval(() => { debugger; }, 50);';
async function checkAsyncStackTrace(session) {
console.error('[test]', 'Verify basic properties of asyncStackTrace');
const paused = await session.waitForBreakOnLine(0, '[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 === 'Module._compile'));
}
async function runTests() {
const instance = new NodeInstance(undefined, script);
const session = await instance.connectInspectorSession();
await session.send([
{ 'method': 'Runtime.enable' },
{ 'method': 'Debugger.enable' },
{ 'method': 'Debugger.setAsyncCallStackDepth',
'params': { 'maxDepth': 10 } },
{ 'method': 'Debugger.setBlackboxPatterns',
'params': { 'patterns': [] } },
{ 'method': 'Runtime.runIfWaitingForDebugger' }
]);
await checkAsyncStackTrace(session);
console.error('[test]', 'Stopping child instance');
session.disconnect();
instance.kill();
}
runTests();

26
test/inspector/test-inspector-enabled.js

@ -0,0 +1,26 @@
'use strict';
const common = require('../common');
common.skipIfInspectorDisabled();
const spawn = require('child_process').spawn;
const script = `
const assert = require('assert');
const inspector = process.binding('inspector');
assert(
!!inspector.isEnabled(),
'inspector.isEnabled() should be true when run with --inspect');
process._debugEnd();
assert(
!inspector.isEnabled(),
'inspector.isEnabled() should be false after _debugEnd()');
`;
const args = ['--inspect', '-e', script];
const child = spawn(process.execPath, args, { stdio: 'inherit' });
child.on('exit', (code, signal) => {
process.exit(code || signal);
});

3
test/inspector/test-inspector.js

@ -98,7 +98,8 @@ async function testBreakpoint(session) {
`Script source is wrong: ${scriptSource}`); `Script source is wrong: ${scriptSource}`);
await session.waitForConsoleOutput('log', ['A message', 5]); await session.waitForConsoleOutput('log', ['A message', 5]);
const scopeId = await session.waitForBreakOnLine(5, mainScriptPath); const paused = await session.waitForBreakOnLine(5, mainScriptPath);
const scopeId = paused.params.callFrames[0].scopeChain[0].object.objectId;
console.log('[test]', 'Verify we can read current application state'); console.log('[test]', 'Verify we can read current application state');
const response = await session.send({ const response = await session.send({

Loading…
Cancel
Save