Skip to content

Commit 50c0bc1

Browse files
committed
inspector: track async stacks when necessary
With this change, we do async stack tracking only when explicitly requested by the inspector client. This avoids unnecessary overhead for clients that might not be interested in async stack traces. Fixes: #16180
1 parent ff747e3 commit 50c0bc1

File tree

6 files changed

+198
-66
lines changed

6 files changed

+198
-66
lines changed

lib/internal/inspector_async_hook.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,4 @@ function disable() {
5555

5656
exports.setup = function() {
5757
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-
}
6458
};

src/env.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class ModuleWrap;
101101
V(address_string, "address") \
102102
V(args_string, "args") \
103103
V(async, "async") \
104+
V(async_stack_depth_string, "Debugger.setAsyncCallStackDepth") \
104105
V(buffer_string, "buffer") \
105106
V(bytes_string, "bytes") \
106107
V(bytes_parsed_string, "bytesParsed") \
@@ -185,7 +186,9 @@ class ModuleWrap;
185186
V(length_string, "length") \
186187
V(mac_string, "mac") \
187188
V(max_buffer_string, "maxBuffer") \
189+
V(max_depth_string, "maxDepth") \
188190
V(message_string, "message") \
191+
V(method_string, "method") \
189192
V(minttl_string, "minttl") \
190193
V(model_string, "model") \
191194
V(modulus_string, "modulus") \
@@ -228,6 +231,7 @@ class ModuleWrap;
228231
V(output_string, "output") \
229232
V(order_string, "order") \
230233
V(owner_string, "owner") \
234+
V(params_string, "params") \
231235
V(parse_error_string, "Parse Error") \
232236
V(path_string, "path") \
233237
V(pbkdf2_error_string, "PBKDF2 Error") \

src/inspector_agent.cc

Lines changed: 133 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ using v8::Function;
2929
using v8::HandleScope;
3030
using v8::Isolate;
3131
using v8::Local;
32+
using v8::Maybe;
33+
using v8::MaybeLocal;
3234
using v8::Object;
35+
using v8::String;
3336
using v8::Value;
3437

3538
using v8_inspector::StringBuffer;
@@ -449,7 +452,9 @@ Agent::Agent(Environment* env) : parent_env_(env),
449452
client_(nullptr),
450453
platform_(nullptr),
451454
enabled_(false),
452-
next_context_number_(1) {}
455+
next_context_number_(1),
456+
pending_enable_async_hook_(false),
457+
pending_disable_async_hook_(false) {}
453458

454459
// Destructor needs to be defined here in implementation file as the header
455460
// does not have full definition of some classes.
@@ -497,18 +502,6 @@ bool Agent::StartIoThread(bool wait_for_connect) {
497502
v8::Isolate* isolate = parent_env_->isolate();
498503
HandleScope handle_scope(isolate);
499504

500-
// Enable tracking of async stack traces
501-
if (!enable_async_hook_function_.IsEmpty()) {
502-
Local<Function> enable_fn = enable_async_hook_function_.Get(isolate);
503-
auto context = parent_env_->context();
504-
auto result = enable_fn->Call(context, Undefined(isolate), 0, nullptr);
505-
if (result.IsEmpty()) {
506-
FatalError(
507-
"node::InspectorAgent::StartIoThread",
508-
"Cannot enable Inspector's AsyncHook, please report this.");
509-
}
510-
}
511-
512505
// Send message to enable debug in workers
513506
Local<Object> process_object = parent_env_->process_object();
514507
Local<Value> emit_fn =
@@ -554,20 +547,6 @@ void Agent::Stop() {
554547
}
555548

556549
void Agent::Connect(InspectorSessionDelegate* delegate) {
557-
if (!enabled_) {
558-
// Enable tracking of async stack traces
559-
v8::Isolate* isolate = parent_env_->isolate();
560-
HandleScope handle_scope(isolate);
561-
auto context = parent_env_->context();
562-
Local<Function> enable_fn = enable_async_hook_function_.Get(isolate);
563-
auto result = enable_fn->Call(context, Undefined(isolate), 0, nullptr);
564-
if (result.IsEmpty()) {
565-
FatalError(
566-
"node::InspectorAgent::Connect",
567-
"Cannot enable Inspector's AsyncHook, please report this.");
568-
}
569-
}
570-
571550
enabled_ = true;
572551
client_->connectFrontend(delegate);
573552
}
@@ -593,6 +572,7 @@ void Agent::FatalException(Local<Value> error, Local<v8::Message> message) {
593572

594573
void Agent::Dispatch(const StringView& message) {
595574
CHECK_NE(client_, nullptr);
575+
InterceptAsyncStackDepthMessage(message);
596576
client_->dispatchMessageFromFrontend(message);
597577
}
598578

@@ -625,6 +605,37 @@ void Agent::RegisterAsyncHook(Isolate* isolate,
625605
v8::Local<v8::Function> disable_function) {
626606
enable_async_hook_function_.Reset(isolate, enable_function);
627607
disable_async_hook_function_.Reset(isolate, disable_function);
608+
if (pending_enable_async_hook_) {
609+
pending_enable_async_hook_ = false;
610+
EnableAsyncHook(isolate);
611+
} else if (pending_disable_async_hook_) {
612+
pending_disable_async_hook_ = false;
613+
DisableAsyncHook(isolate);
614+
}
615+
}
616+
617+
void Agent::EnableAsyncHook(Isolate* isolate) {
618+
CHECK(!enable_async_hook_function_.IsEmpty());
619+
Local<Function> enable_fn = enable_async_hook_function_.Get(isolate);
620+
auto context = parent_env_->context();
621+
auto result = enable_fn->Call(context, Undefined(isolate), 0, nullptr);
622+
if (result.IsEmpty()) {
623+
FatalError(
624+
"node::inspector::Agent::EnableAsyncHook",
625+
"Cannot enable Inspector's AsyncHook, please report this.");
626+
}
627+
}
628+
629+
void Agent::DisableAsyncHook(Isolate* isolate) {
630+
CHECK(!disable_async_hook_function_.IsEmpty());
631+
Local<Function> disable_fn = disable_async_hook_function_.Get(isolate);
632+
auto context = parent_env_->context();
633+
auto result = disable_fn->Call(context, Undefined(isolate), 0, nullptr);
634+
if (result.IsEmpty()) {
635+
FatalError(
636+
"node::inspector::Agent::DisableAsyncHook",
637+
"Cannot disable Inspector's AsyncHook, please report this.");
638+
}
628639
}
629640

630641
void Agent::AsyncTaskScheduled(const StringView& task_name, void* task,
@@ -648,6 +659,101 @@ void Agent::AllAsyncTasksCanceled() {
648659
client_->AllAsyncTasksCanceled();
649660
}
650661

662+
void Agent::InterceptAsyncStackDepthMessage(const StringView& message) {
663+
// This logic would be better implemented in JavaScript, but when using
664+
// --inspect-brk, the debugger must necessarily attach before much JavaScript
665+
// can execute. The Debugger.setAsyncCallStackDepth message arrives too early
666+
// and we must intercept this in C++.
667+
668+
v8::Isolate* isolate = parent_env_->isolate();
669+
Local<Context> context = parent_env_->context();
670+
671+
MaybeLocal<String> string =
672+
String::NewFromTwoByte(isolate, message.characters16(),
673+
v8::NewStringType::kNormal, message.length());
674+
if (string.IsEmpty()) {
675+
return;
676+
}
677+
678+
// Basically, the logic we want to implement is:
679+
// let parsed; try {
680+
// parsed = JSON.parse(string);
681+
// } catch (e) { return; }
682+
// if (parsed && parsed.method && parsed.method === 'Debugger.setAsync..' &&
683+
// parsed.params && parsed.params.maxDepth &&
684+
// typeof parsed.params.maxDepth === 'number') {
685+
// // Enable or Disable, depending on maxDepth.
686+
// }
687+
//
688+
// We ignore (return early) on malformed messages and let v8-inspector deal
689+
// with them.
690+
691+
MaybeLocal<Value> maybe_parsed =
692+
v8::JSON::Parse(context, string.ToLocalChecked());
693+
if (maybe_parsed.IsEmpty()) {
694+
return;
695+
}
696+
697+
Local<Value> parsed = maybe_parsed.ToLocalChecked();
698+
if (!parsed->IsObject()) {
699+
return;
700+
}
701+
702+
Local<Object> object = parsed.As<Object>();
703+
704+
Local<Value> method;
705+
if (!object->Get(context, parent_env_->method_string()).ToLocal(&method) ||
706+
!method->IsString() ||
707+
!method->StrictEquals(parent_env_->async_stack_depth_string())) {
708+
return;
709+
}
710+
711+
Local<Value> params;
712+
if (!object->Get(context, parent_env_->params_string()).ToLocal(&params) ||
713+
!params->IsObject()) {
714+
return;
715+
}
716+
717+
Local<Value> depth_value;
718+
if (!params.As<Object>()->Get(context, parent_env_->max_depth_string())
719+
.ToLocal(&depth_value) ||
720+
!depth_value->IsNumber()) {
721+
return;
722+
}
723+
724+
Maybe<double> maybe_depth = depth_value->NumberValue(context);
725+
if (maybe_depth.IsNothing()) {
726+
return;
727+
}
728+
729+
double depth = maybe_depth.FromJust();
730+
if (depth == 0) {
731+
// Disable.
732+
if (!disable_async_hook_function_.IsEmpty()) {
733+
DisableAsyncHook(isolate);
734+
} else {
735+
if (pending_enable_async_hook_) {
736+
CHECK(!pending_disable_async_hook_);
737+
pending_enable_async_hook_ = false;
738+
} else {
739+
pending_disable_async_hook_ = true;
740+
}
741+
}
742+
} else {
743+
// Enable.
744+
if (!enable_async_hook_function_.IsEmpty()) {
745+
EnableAsyncHook(isolate);
746+
} else {
747+
if (pending_disable_async_hook_) {
748+
CHECK(!pending_enable_async_hook_);
749+
pending_disable_async_hook_ = false;
750+
} else {
751+
pending_enable_async_hook_ = true;
752+
}
753+
}
754+
}
755+
}
756+
651757
void Agent::RequestIoThreadStart() {
652758
// We need to attempt to interrupt V8 flow (in case Node is running
653759
// continuous JS code) and to wake up libuv thread (in case Node is waiting

src/inspector_agent.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class Agent {
6262
void AsyncTaskStarted(void* task);
6363
void AsyncTaskFinished(void* task);
6464
void AllAsyncTasksCanceled();
65+
void InterceptAsyncStackDepthMessage(const v8_inspector::StringView& message);
6566

6667
void RegisterAsyncHook(v8::Isolate* isolate,
6768
v8::Local<v8::Function> enable_function,
@@ -93,6 +94,9 @@ class Agent {
9394
void ContextCreated(v8::Local<v8::Context> context);
9495

9596
private:
97+
void EnableAsyncHook(v8::Isolate* isolate);
98+
void DisableAsyncHook(v8::Isolate* isolate);
99+
96100
node::Environment* parent_env_;
97101
std::unique_ptr<NodeInspectorClient> client_;
98102
std::unique_ptr<InspectorIo> io_;
@@ -102,6 +106,8 @@ class Agent {
102106
DebugOptions debug_options_;
103107
int next_context_number_;
104108

109+
bool pending_enable_async_hook_;
110+
bool pending_disable_async_hook_;
105111
v8::Persistent<v8::Function> enable_async_hook_function_;
106112
v8::Persistent<v8::Function> disable_async_hook_function_;
107113
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
const common = require('../common');
3+
common.skipIfInspectorDisabled();
4+
common.skipIf32Bits();
5+
6+
const assert = require('assert');
7+
const async_hooks = require('async_hooks');
8+
const inspector = require('inspector');
9+
10+
// Verify that inspector-async-hook is not registered,
11+
// thus emitInit() ignores invalid arguments
12+
// See test/async-hooks/test-emit-init.js
13+
function verifyAsyncHookDisabled(message) {
14+
assert.doesNotThrow(() => async_hooks.emitInit(), message);
15+
}
16+
17+
// Verify that inspector-async-hook is registered
18+
// by checking that emitInit with invalid arguments
19+
// throw an error.
20+
// See test/async-hooks/test-emit-init.js
21+
function verifyAsyncHookEnabled(message) {
22+
assert.throws(() => async_hooks.emitInit(), message);
23+
}
24+
25+
// By default inspector async hooks should not have been installed.
26+
verifyAsyncHookDisabled('inspector async hook should be disabled at startup');
27+
28+
const session = new inspector.Session();
29+
verifyAsyncHookDisabled('creating a session should not enable async hooks');
30+
31+
session.connect();
32+
verifyAsyncHookDisabled('creating a session should not enable async hooks');
33+
34+
session.post('Debugger.setAsyncCallStackDepth', { invalid: 'message' }, () => {
35+
verifyAsyncHookDisabled('invalid message should not enable async hooks');
36+
37+
session.post('Debugger.setAsyncCallStackDepth', { maxDepth: 'five' }, () => {
38+
verifyAsyncHookDisabled('invalid maxDepth (sting) should not enable async' +
39+
' hooks');
40+
41+
session.post('Debugger.setAsyncCallStackDepth', { maxDepth: NaN }, () => {
42+
verifyAsyncHookDisabled('invalid maxDepth (NaN) should not enable async' +
43+
' hooks');
44+
45+
session.post('Debugger.setAsyncCallStackDepth', { maxDepth: 10 }, () => {
46+
verifyAsyncHookEnabled('valid message should enable async hook');
47+
48+
session.post('Debugger.setAsyncCallStackDepth', { maxDepth: 0 }, () => {
49+
verifyAsyncHookDisabled('Setting maxDepth to 0 should disable ' +
50+
'async hooks');
51+
});
52+
});
53+
});
54+
});
55+
});

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

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)