Skip to content

Commit a0895ed

Browse files
bajtosTimothyGu
authored andcommitted
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: #11370 PR-URL: #13870 Reviewed-by: Timothy Gu <[email protected]> Reviewed-by: Anna Henningsen <[email protected]>
1 parent b616e87 commit a0895ed

17 files changed

+656
-17
lines changed

lib/internal/bootstrap_node.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
if (global.__coverage__)
4646
NativeModule.require('internal/process/write-coverage').setup();
4747

48+
NativeModule.require('internal/inspector_async_hook').setup();
49+
4850
// Do not initialize channel in debugger agent, it deletes env variable
4951
// and the main thread won't see it.
5052
if (process.argv[1] !== '--debug-agent')

lib/internal/inspector_async_hook.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict';
2+
3+
const { createHook } = require('async_hooks');
4+
const inspector = process.binding('inspector');
5+
const config = process.binding('config');
6+
7+
if (!inspector || !inspector.asyncTaskScheduled) {
8+
exports.setup = function() {};
9+
return;
10+
}
11+
12+
const hook = createHook({
13+
init(asyncId, type, triggerAsyncId, resource) {
14+
// It's difficult to tell which tasks will be recurring and which won't,
15+
// therefore we mark all tasks as recurring. Based on the discussion
16+
// in https://github.com/nodejs/node/pull/13870#discussion_r124515293,
17+
// this should be fine as long as we call asyncTaskCanceled() too.
18+
const recurring = true;
19+
inspector.asyncTaskScheduled(type, asyncId, recurring);
20+
},
21+
22+
before(asyncId) {
23+
inspector.asyncTaskStarted(asyncId);
24+
},
25+
26+
after(asyncId) {
27+
inspector.asyncTaskFinished(asyncId);
28+
},
29+
30+
destroy(asyncId) {
31+
inspector.asyncTaskCanceled(asyncId);
32+
},
33+
});
34+
35+
function enable() {
36+
if (config.bits < 64) {
37+
// V8 Inspector stores task ids as (void*) pointers.
38+
// async_hooks store ids as 64bit numbers.
39+
// As a result, we cannot reliably translate async_hook ids to V8 async_task
40+
// ids on 32bit platforms.
41+
process.emitWarning(
42+
'Warning: Async stack traces in debugger are not available ' +
43+
`on ${config.bits}bit platforms. The feature is disabled.`,
44+
{
45+
code: 'INSPECTOR_ASYNC_STACK_TRACES_NOT_AVAILABLE',
46+
});
47+
} else {
48+
hook.enable();
49+
}
50+
}
51+
52+
function disable() {
53+
hook.disable();
54+
}
55+
56+
exports.setup = function() {
57+
inspector.registerAsyncHook(enable, disable);
58+
59+
if (inspector.isEnabled()) {
60+
// If the inspector was already enabled via --inspect or --inspect-brk,
61+
// the we need to enable the async hook immediately at startup.
62+
enable();
63+
}
64+
};

node.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
'lib/internal/freelist.js',
8989
'lib/internal/fs.js',
9090
'lib/internal/http.js',
91+
'lib/internal/inspector_async_hook.js',
9192
'lib/internal/linkedlist.js',
9293
'lib/internal/net.js',
9394
'lib/internal/module.js',

src/inspector_agent.cc

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,27 @@
2323
namespace node {
2424
namespace inspector {
2525
namespace {
26+
27+
using node::FatalError;
28+
2629
using v8::Array;
30+
using v8::Boolean;
2731
using v8::Context;
2832
using v8::External;
2933
using v8::Function;
3034
using v8::FunctionCallbackInfo;
3135
using v8::HandleScope;
36+
using v8::Integer;
3237
using v8::Isolate;
3338
using v8::Local;
3439
using v8::Maybe;
3540
using v8::MaybeLocal;
41+
using v8::Name;
3642
using v8::NewStringType;
3743
using v8::Object;
3844
using v8::Persistent;
3945
using v8::String;
46+
using v8::Undefined;
4047
using v8::Value;
4148

4249
using v8_inspector::StringBuffer;
@@ -616,6 +623,28 @@ class NodeInspectorClient : public V8InspectorClient {
616623
timers_.erase(data);
617624
}
618625

626+
// Async stack traces instrumentation.
627+
void AsyncTaskScheduled(const StringView& task_name, void* task,
628+
bool recurring) {
629+
client_->asyncTaskScheduled(task_name, task, recurring);
630+
}
631+
632+
void AsyncTaskCanceled(void* task) {
633+
client_->asyncTaskCanceled(task);
634+
}
635+
636+
void AsyncTaskStarted(void* task) {
637+
client_->asyncTaskStarted(task);
638+
}
639+
640+
void AsyncTaskFinished(void* task) {
641+
client_->asyncTaskFinished(task);
642+
}
643+
644+
void AllAsyncTasksCanceled() {
645+
client_->allAsyncTasksCanceled();
646+
}
647+
619648
private:
620649
node::Environment* env_;
621650
v8::Platform* platform_;
@@ -676,9 +705,21 @@ bool Agent::StartIoThread(bool wait_for_connect) {
676705
}
677706

678707
v8::Isolate* isolate = parent_env_->isolate();
708+
HandleScope handle_scope(isolate);
709+
710+
// Enable tracking of async stack traces
711+
if (!enable_async_hook_function_.IsEmpty()) {
712+
Local<Function> enable_fn = enable_async_hook_function_.Get(isolate);
713+
auto context = parent_env_->context();
714+
auto result = enable_fn->Call(context, Undefined(isolate), 0, nullptr);
715+
if (result.IsEmpty()) {
716+
FatalError(
717+
"node::InspectorAgent::StartIoThread",
718+
"Cannot enable Inspector's AsyncHook, please report this.");
719+
}
720+
}
679721

680722
// Send message to enable debug in workers
681-
HandleScope handle_scope(isolate);
682723
Local<Object> process_object = parent_env_->process_object();
683724
Local<Value> emit_fn =
684725
process_object->Get(FIXED_ONE_BYTE_STRING(isolate, "emit"));
@@ -717,10 +758,40 @@ void Agent::Stop() {
717758
if (io_ != nullptr) {
718759
io_->Stop();
719760
io_.reset();
761+
enabled_ = false;
762+
}
763+
764+
v8::Isolate* isolate = parent_env_->isolate();
765+
HandleScope handle_scope(isolate);
766+
767+
// Disable tracking of async stack traces
768+
if (!disable_async_hook_function_.IsEmpty()) {
769+
Local<Function> disable_fn = disable_async_hook_function_.Get(isolate);
770+
auto result = disable_fn->Call(parent_env_->context(),
771+
Undefined(parent_env_->isolate()), 0, nullptr);
772+
if (result.IsEmpty()) {
773+
FatalError(
774+
"node::InspectorAgent::Stop",
775+
"Cannot disable Inspector's AsyncHook, please report this.");
776+
}
720777
}
721778
}
722779

723780
void Agent::Connect(InspectorSessionDelegate* delegate) {
781+
if (!enabled_) {
782+
// Enable tracking of async stack traces
783+
v8::Isolate* isolate = parent_env_->isolate();
784+
HandleScope handle_scope(isolate);
785+
auto context = parent_env_->context();
786+
Local<Function> enable_fn = enable_async_hook_function_.Get(isolate);
787+
auto result = enable_fn->Call(context, Undefined(isolate), 0, nullptr);
788+
if (result.IsEmpty()) {
789+
FatalError(
790+
"node::InspectorAgent::Connect",
791+
"Cannot enable Inspector's AsyncHook, please report this.");
792+
}
793+
}
794+
724795
enabled_ = true;
725796
client_->connectFrontend(delegate);
726797
}
@@ -773,6 +844,34 @@ void Agent::PauseOnNextJavascriptStatement(const std::string& reason) {
773844
channel->schedulePauseOnNextStatement(reason);
774845
}
775846

847+
void Agent::RegisterAsyncHook(Isolate* isolate,
848+
v8::Local<v8::Function> enable_function,
849+
v8::Local<v8::Function> disable_function) {
850+
enable_async_hook_function_.Reset(isolate, enable_function);
851+
disable_async_hook_function_.Reset(isolate, disable_function);
852+
}
853+
854+
void Agent::AsyncTaskScheduled(const StringView& task_name, void* task,
855+
bool recurring) {
856+
client_->AsyncTaskScheduled(task_name, task, recurring);
857+
}
858+
859+
void Agent::AsyncTaskCanceled(void* task) {
860+
client_->AsyncTaskCanceled(task);
861+
}
862+
863+
void Agent::AsyncTaskStarted(void* task) {
864+
client_->AsyncTaskStarted(task);
865+
}
866+
867+
void Agent::AsyncTaskFinished(void* task) {
868+
client_->AsyncTaskFinished(task);
869+
}
870+
871+
void Agent::AllAsyncTasksCanceled() {
872+
client_->AllAsyncTasksCanceled();
873+
}
874+
776875
void Open(const FunctionCallbackInfo<Value>& args) {
777876
Environment* env = Environment::GetCurrent(args);
778877
inspector::Agent* agent = env->inspector_agent();
@@ -810,6 +909,59 @@ void Url(const FunctionCallbackInfo<Value>& args) {
810909
args.GetReturnValue().Set(OneByteString(env->isolate(), url.c_str()));
811910
}
812911

912+
static void* GetAsyncTask(int64_t asyncId) {
913+
// The inspector assumes that when other clients use its asyncTask* API,
914+
// they use real pointers, or at least something aligned like real pointer.
915+
// In general it means that our task_id should always be even.
916+
//
917+
// On 32bit platforms, the 64bit asyncId would get truncated when converted
918+
// to a 32bit pointer. However, the javascript part will never enable
919+
// the async_hook on 32bit platforms, therefore the truncation will never
920+
// happen in practice.
921+
return reinterpret_cast<void*>(asyncId << 1);
922+
}
923+
924+
template<void (Agent::*asyncTaskFn)(void*)>
925+
static void InvokeAsyncTaskFnWithId(const FunctionCallbackInfo<Value>& args) {
926+
Environment* env = Environment::GetCurrent(args);
927+
CHECK(args[0]->IsNumber());
928+
int64_t task_id = args[0]->IntegerValue(env->context()).FromJust();
929+
(env->inspector_agent()->*asyncTaskFn)(GetAsyncTask(task_id));
930+
}
931+
932+
static void AsyncTaskScheduledWrapper(const FunctionCallbackInfo<Value>& args) {
933+
Environment* env = Environment::GetCurrent(args);
934+
935+
CHECK(args[0]->IsString());
936+
Local<String> task_name = args[0].As<String>();
937+
String::Value task_name_value(task_name);
938+
StringView task_name_view(*task_name_value, task_name_value.length());
939+
940+
CHECK(args[1]->IsNumber());
941+
int64_t task_id = args[1]->IntegerValue(env->context()).FromJust();
942+
void* task = GetAsyncTask(task_id);
943+
944+
CHECK(args[2]->IsBoolean());
945+
bool recurring = args[2]->BooleanValue(env->context()).FromJust();
946+
947+
env->inspector_agent()->AsyncTaskScheduled(task_name_view, task, recurring);
948+
}
949+
950+
static void RegisterAsyncHookWrapper(const FunctionCallbackInfo<Value>& args) {
951+
Environment* env = Environment::GetCurrent(args);
952+
953+
CHECK(args[0]->IsFunction());
954+
v8::Local<v8::Function> enable_function = args[0].As<Function>();
955+
CHECK(args[1]->IsFunction());
956+
v8::Local<v8::Function> disable_function = args[1].As<Function>();
957+
env->inspector_agent()->RegisterAsyncHook(env->isolate(),
958+
enable_function, disable_function);
959+
}
960+
961+
static void IsEnabled(const FunctionCallbackInfo<Value>& args) {
962+
Environment* env = Environment::GetCurrent(args);
963+
args.GetReturnValue().Set(env->inspector_agent()->enabled());
964+
}
813965

814966
// static
815967
void Agent::InitInspector(Local<Object> target, Local<Value> unused,
@@ -830,6 +982,17 @@ void Agent::InitInspector(Local<Object> target, Local<Value> unused,
830982
env->SetMethod(target, "connect", ConnectJSBindingsSession);
831983
env->SetMethod(target, "open", Open);
832984
env->SetMethod(target, "url", Url);
985+
986+
env->SetMethod(target, "asyncTaskScheduled", AsyncTaskScheduledWrapper);
987+
env->SetMethod(target, "asyncTaskCanceled",
988+
InvokeAsyncTaskFnWithId<&Agent::AsyncTaskCanceled>);
989+
env->SetMethod(target, "asyncTaskStarted",
990+
InvokeAsyncTaskFnWithId<&Agent::AsyncTaskStarted>);
991+
env->SetMethod(target, "asyncTaskFinished",
992+
InvokeAsyncTaskFnWithId<&Agent::AsyncTaskFinished>);
993+
994+
env->SetMethod(target, "registerAsyncHook", RegisterAsyncHookWrapper);
995+
env->SetMethod(target, "isEnabled", IsEnabled);
833996
}
834997

835998
void Agent::RequestIoThreadStart() {

src/inspector_agent.h

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,7 @@ namespace node {
1616
class Environment;
1717
} // namespace node
1818

19-
namespace v8 {
20-
class Context;
21-
template <typename V>
22-
class FunctionCallbackInfo;
23-
template<typename T>
24-
class Local;
25-
class Message;
26-
class Object;
27-
class Platform;
28-
class Value;
29-
} // namespace v8
19+
#include "v8.h"
3020

3121
namespace v8_inspector {
3222
class StringView;
@@ -67,6 +57,18 @@ class Agent {
6757
void FatalException(v8::Local<v8::Value> error,
6858
v8::Local<v8::Message> message);
6959

60+
// Async stack traces instrumentation.
61+
void AsyncTaskScheduled(const v8_inspector::StringView& taskName, void* task,
62+
bool recurring);
63+
void AsyncTaskCanceled(void* task);
64+
void AsyncTaskStarted(void* task);
65+
void AsyncTaskFinished(void* task);
66+
void AllAsyncTasksCanceled();
67+
68+
void RegisterAsyncHook(v8::Isolate* isolate,
69+
v8::Local<v8::Function> enable_function,
70+
v8::Local<v8::Function> disable_function);
71+
7072
// These methods are called by the WS protocol and JS binding to create
7173
// inspector sessions. The inspector responds by using the delegate to send
7274
// messages back.
@@ -107,6 +109,9 @@ class Agent {
107109
std::string path_;
108110
DebugOptions debug_options_;
109111
int next_context_number_;
112+
113+
v8::Persistent<v8::Function> enable_async_hook_function_;
114+
v8::Persistent<v8::Function> disable_async_hook_function_;
110115
};
111116

112117
} // namespace inspector

src/node_config.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ using v8::Boolean;
1313
using v8::Context;
1414
using v8::Integer;
1515
using v8::Local;
16+
using v8::Number;
1617
using v8::Object;
1718
using v8::ReadOnly;
1819
using v8::String;
@@ -30,6 +31,15 @@ using v8::Value;
3031
True(env->isolate()), ReadOnly).FromJust(); \
3132
} while (0)
3233

34+
#define READONLY_PROPERTY(obj, name, value) \
35+
do { \
36+
obj->DefineOwnProperty(env->context(), \
37+
OneByteString(env->isolate(), name), \
38+
value, \
39+
ReadOnly).FromJust(); \
40+
} while (0)
41+
42+
3343
static void InitConfig(Local<Object> target,
3444
Local<Value> unused,
3545
Local<Context> context) {
@@ -91,6 +101,10 @@ static void InitConfig(Local<Object> target,
91101

92102
if (config_expose_http2)
93103
READONLY_BOOLEAN_PROPERTY("exposeHTTP2");
104+
105+
READONLY_PROPERTY(target,
106+
"bits",
107+
Number::New(env->isolate(), 8 * sizeof(intptr_t)));
94108
} // InitConfig
95109

96110
} // namespace node

0 commit comments

Comments
 (0)