Skip to content

Commit 5337718

Browse files
deepak1556christianaurichzm
authored andcommitted
inspector: defer JS execution from V8 interrupts
Inspector messages for a main thread connection are dispatched from a V8 interrupt handler. Defer frontend messages in this case using a microtask. This preserves the existing timing contract for main thread inspector connections, where notifications are observable after a microtask checkpoint. Starting the inspector I/O thread and toggling network tracking can also call into JS from an interrupt. Signed-off-by: deepak1556 <hop2deep@gmail.com> Signed-off-by: Christian Aurich Zanettini Martins <christian.aurichzm@gmail.com>
1 parent 594c3a7 commit 5337718

3 files changed

Lines changed: 145 additions & 102 deletions

File tree

‎src/inspector_agent.cc‎

Lines changed: 99 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,52 @@ static Mutex start_io_thread_agents_mutex;
8080
static std::vector<Agent*> start_io_thread_agents;
8181
static bool debug_signal_handler_started = false;
8282

83+
template <typename Defer, typename OnFailure>
84+
void SyncJavaScriptHookState(Environment* env,
85+
JavaScriptHookState* state,
86+
Local<Function> enable_function,
87+
Local<Function> disable_function,
88+
bool require_bootstrapping,
89+
Defer defer,
90+
OnFailure on_failure) {
91+
// The debugger can request an interrupt from within a hook's JS toggle
92+
// function. A nested sync only records the new requested state; the
93+
// outermost sync sees it when re-checking the loop after each toggle.
94+
if (state->syncing) return;
95+
state->syncing = true;
96+
auto on_exit = OnScopeLeave([state]() { state->syncing = false; });
97+
98+
Isolate* isolate = env->isolate();
99+
while (state->wanted != state->enabled) {
100+
// No hook events will be emitted during cleanup, and calling into JS is no
101+
// longer possible.
102+
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
103+
if (!env->can_call_into_js()) return;
104+
105+
bool enable = state->wanted;
106+
Local<Function> function = enable ? enable_function : disable_function;
107+
if (function.IsEmpty()) return;
108+
109+
if (env->is_processing_v8_interrupt()) {
110+
defer();
111+
return;
112+
}
113+
114+
if (require_bootstrapping) CHECK(env->has_run_bootstrapping_code());
115+
Local<Context> context = env->context();
116+
v8::TryCatch try_catch(isolate);
117+
USE(function->Call(context, Undefined(isolate), 0, nullptr));
118+
if (try_catch.HasCaught()) {
119+
// Termination may abort the toggle invocation. Do not record a state that
120+
// may not have taken effect, so a later sync can retry it.
121+
if (try_catch.HasTerminated()) return;
122+
PrintCaughtException(isolate, context, try_catch);
123+
on_failure();
124+
}
125+
state->enabled = enable;
126+
}
127+
}
128+
83129
static void RequestIoThreadStartOnAgents() {
84130
Mutex::ScopedLock lock(start_io_thread_agents_mutex);
85131
for (Agent* agent : start_io_thread_agents) agent->RequestIoThreadStart();
@@ -483,6 +529,16 @@ class SameThreadInspectorSession : public InspectorSession {
483529

484530
void NotifyClusterWorkersDebugEnabled(Environment* env) {
485531
Isolate* isolate = env->isolate();
532+
533+
// Starting the inspector from an interrupt is safe, but emitting the cluster
534+
// notification calls into JS.
535+
if (env->is_processing_v8_interrupt()) {
536+
env->SetImmediate(
537+
[](Environment* env) { NotifyClusterWorkersDebugEnabled(env); },
538+
CallbackFlags::kUnrefed);
539+
return;
540+
}
541+
486542
HandleScope handle_scope(isolate);
487543

488544
// Send message to enable debug in cluster workers
@@ -1000,54 +1056,40 @@ void Agent::SetupNetworkTracking(Local<Function> enable_function,
10001056
Local<Function> disable_function) {
10011057
parent_env_->set_inspector_enable_network_tracking(enable_function);
10021058
parent_env_->set_inspector_disable_network_tracking(disable_function);
1003-
if (pending_enable_network_tracking) {
1004-
pending_enable_network_tracking = false;
1005-
EnableNetworkTracking();
1006-
} else if (pending_disable_network_tracking) {
1007-
pending_disable_network_tracking = false;
1008-
DisableNetworkTracking();
1009-
}
1059+
SyncNetworkTrackingState();
10101060
}
10111061

10121062
void Agent::EnableNetworkTracking() {
1013-
if (network_tracking_enabled_) {
1014-
return;
1015-
}
1016-
HandleScope scope(parent_env_->isolate());
1017-
Local<Function> enable = parent_env_->inspector_enable_network_tracking();
1018-
if (enable.IsEmpty()) {
1019-
pending_enable_network_tracking = true;
1020-
} else {
1021-
ToggleNetworkTracking(parent_env_->isolate(), enable);
1022-
network_tracking_enabled_ = true;
1023-
}
1063+
network_tracking_state_.wanted = true;
1064+
SyncNetworkTrackingState();
10241065
}
10251066

10261067
void Agent::DisableNetworkTracking() {
1027-
if (!network_tracking_enabled_) {
1028-
return;
1029-
}
1030-
HandleScope scope(parent_env_->isolate());
1031-
Local<Function> disable = parent_env_->inspector_disable_network_tracking();
1032-
if (disable.IsEmpty()) {
1033-
pending_disable_network_tracking = true;
1034-
} else if (!client_->hasConnectedSessions()) {
1035-
ToggleNetworkTracking(parent_env_->isolate(), disable);
1036-
network_tracking_enabled_ = false;
1037-
}
1068+
if (client_->hasConnectedSessions()) return;
1069+
network_tracking_state_.wanted = false;
1070+
SyncNetworkTrackingState();
10381071
}
10391072

1040-
void Agent::ToggleNetworkTracking(Isolate* isolate, Local<Function> fn) {
1041-
if (!parent_env_->can_call_into_js()) return;
1042-
auto context = parent_env_->context();
1073+
void Agent::SyncNetworkTrackingState() {
1074+
Isolate* isolate = parent_env_->isolate();
10431075
HandleScope scope(isolate);
1044-
CHECK(!fn.IsEmpty());
1045-
v8::TryCatch try_catch(isolate);
1046-
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1047-
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1048-
PrintCaughtException(isolate, context, try_catch);
1049-
UNREACHABLE("Cannot toggle network tracking, please report this.");
1050-
}
1076+
SyncJavaScriptHookState(
1077+
parent_env_,
1078+
&network_tracking_state_,
1079+
parent_env_->inspector_enable_network_tracking(),
1080+
parent_env_->inspector_disable_network_tracking(),
1081+
false,
1082+
[this]() {
1083+
parent_env_->SetImmediate(
1084+
[](Environment* env) {
1085+
Agent* agent = env->inspector_agent();
1086+
if (agent != nullptr) agent->SyncNetworkTrackingState();
1087+
},
1088+
CallbackFlags::kUnrefed);
1089+
},
1090+
[]() {
1091+
UNREACHABLE("Cannot toggle network tracking, please report this.");
1092+
});
10511093
}
10521094

10531095
void Agent::WaitForDisconnect() {
@@ -1098,7 +1140,7 @@ void Agent::RegisterAsyncHook(Isolate* isolate,
10981140
}
10991141

11001142
void Agent::SetAsyncHookTrackingEnabled(bool enabled) {
1101-
async_hook_wanted_ = enabled;
1143+
async_hook_state_.wanted = enabled;
11021144
SyncAsyncHookState();
11031145
}
11041146

@@ -1112,52 +1154,25 @@ void Agent::SetAsyncHookTrackingEnabled(bool enabled) {
11121154
// When it's not safe to call into JS, this is a no-op and we'll try again in
11131155
// RegisterAsyncHook() (for 1) or from a scheduled immediate (for 2).
11141156
void Agent::SyncAsyncHookState() {
1115-
// The debugger can request an interrupt within the toggle JS function itself,
1116-
// A nested call only records the new requested state, the outermost call sees
1117-
// it when re-checking the loop condition after each toggle.
1118-
if (syncing_async_hook_state_) return;
1119-
syncing_async_hook_state_ = true;
1120-
auto on_exit = OnScopeLeave([this]() { syncing_async_hook_state_ = false; });
1121-
11221157
Isolate* isolate = parent_env_->isolate();
11231158
HandleScope scope(isolate);
1124-
while (async_hook_wanted_ != async_hook_enabled_) {
1125-
// Guard against running this during cleanup -- no async events will be
1126-
// emitted anyway at that point anymore, and calling into JS is not
1127-
// possible. This should probably not be something we're attempting in the
1128-
// first place,
1129-
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1130-
if (!parent_env_->can_call_into_js()) return;
1131-
1132-
bool enable = async_hook_wanted_;
1133-
Local<Function> fn = enable ? parent_env_->inspector_enable_async_hooks()
1134-
: parent_env_->inspector_disable_async_hooks();
1135-
if (fn.IsEmpty()) return;
1136-
1137-
if (parent_env_->is_processing_v8_interrupt()) {
1138-
parent_env_->SetImmediate(
1139-
[](Environment* env) {
1140-
Agent* agent = env->inspector_agent();
1141-
if (agent != nullptr) agent->SyncAsyncHookState();
1142-
},
1143-
CallbackFlags::kUnrefed);
1144-
return;
1145-
}
1146-
1147-
CHECK(parent_env_->has_run_bootstrapping_code());
1148-
Local<Context> context = parent_env_->context();
1149-
v8::TryCatch try_catch(isolate);
1150-
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1151-
if (try_catch.HasCaught()) {
1152-
// Termination may abort the toggle invocation, retrying now would just
1153-
// be terminated again. Instead of recording the toggle that may not have
1154-
// taken effect, leave the states as-is so that a later sync retries.
1155-
if (try_catch.HasTerminated()) return;
1156-
PrintCaughtException(isolate, context, try_catch);
1157-
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1158-
}
1159-
async_hook_enabled_ = enable;
1160-
}
1159+
SyncJavaScriptHookState(
1160+
parent_env_,
1161+
&async_hook_state_,
1162+
parent_env_->inspector_enable_async_hooks(),
1163+
parent_env_->inspector_disable_async_hooks(),
1164+
true,
1165+
[this]() {
1166+
parent_env_->SetImmediate(
1167+
[](Environment* env) {
1168+
Agent* agent = env->inspector_agent();
1169+
if (agent != nullptr) agent->SyncAsyncHookState();
1170+
},
1171+
CallbackFlags::kUnrefed);
1172+
},
1173+
[]() {
1174+
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1175+
});
11611176
}
11621177

11631178
void Agent::AsyncTaskScheduled(const StringView& task_name, void* task,

‎src/inspector_agent.h‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ class ParentInspectorHandle;
2929
class NodeInspectorClient;
3030
class WorkerManager;
3131

32+
struct JavaScriptHookState {
33+
bool wanted = false;
34+
bool enabled = false;
35+
bool syncing = false;
36+
};
37+
3238
class InspectorSession {
3339
public:
3440
virtual ~InspectorSession() = default;
@@ -134,7 +140,7 @@ class Agent {
134140

135141
private:
136142
void SyncAsyncHookState();
137-
void ToggleNetworkTracking(v8::Isolate* isolate, v8::Local<v8::Function> fn);
143+
void SyncNetworkTrackingState();
138144

139145
node::Environment* parent_env_;
140146
// Encapsulates majority of the Inspector functionality
@@ -154,19 +160,17 @@ class Agent {
154160
// The state of the async hook used for async stack traces that the protocol
155161
// last requested, and the state JS currently has. SyncAsyncHookState()
156162
// reconciles the two when it is possible and safe to call into JS.
157-
bool async_hook_wanted_ = false;
158-
bool async_hook_enabled_ = false;
159-
bool syncing_async_hook_state_ = false;
163+
JavaScriptHookState async_hook_state_;
160164

161165
// Woken by the SIGUSR1 watchdog; closed by the cleanup hook or ~Agent(),
162166
// whichever runs first, and freed by its close callback.
163167
uv_async_t* start_io_thread_async_ = nullptr;
164168
void StopAcceptingIoThreadStarts();
165169
static void StopAcceptingIoThreadStartsHook(void* agent);
166170

167-
bool network_tracking_enabled_ = false;
168-
bool pending_enable_network_tracking = false;
169-
bool pending_disable_network_tracking = false;
171+
// Network tracking uses JS hooks. Reconcile the protocol requested and
172+
// applied states after leaving a V8 interrupt.
173+
JavaScriptHookState network_tracking_state_;
170174
std::shared_ptr<NetworkResourceManager> network_resource_manager_;
171175
};
172176

‎src/inspector_js_api.cc‎

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,38 @@ struct MainThreadConnection {
6262
template <typename ConnectionType>
6363
class JSBindingsConnection : public BaseObject {
6464
public:
65+
struct PendingMicrotask {
66+
explicit PendingMicrotask(JSBindingsConnection* connection)
67+
: env(connection->env()), connection(connection) {
68+
env->AddCleanupHook(Delete, this);
69+
}
70+
71+
static void Delete(void* data) {
72+
delete static_cast<PendingMicrotask*>(data);
73+
}
74+
75+
static void Run(void* data) {
76+
std::unique_ptr<PendingMicrotask> pending(
77+
static_cast<PendingMicrotask*>(data));
78+
pending->env->RemoveCleanupHook(Delete, pending.get());
79+
pending->connection->FlushPendingMessages();
80+
}
81+
82+
Environment* env;
83+
BaseObjectPtr<JSBindingsConnection> connection;
84+
};
85+
6586
class JSBindingsSessionDelegate : public InspectorSessionDelegate {
6687
public:
67-
JSBindingsSessionDelegate(Environment* env,
68-
JSBindingsConnection* connection)
69-
: env_(env),
70-
connection_(connection) {
71-
}
88+
explicit JSBindingsSessionDelegate(JSBindingsConnection* connection)
89+
: connection_(connection) {}
7290

7391
void SendMessageToFrontend(const v8_inspector::StringView& message)
7492
override {
7593
connection_->SendMessageToFrontend(message);
7694
}
7795

7896
private:
79-
Environment* env_;
8097
BaseObjectPtr<JSBindingsConnection> connection_;
8198
};
8299

@@ -86,10 +103,10 @@ class JSBindingsConnection : public BaseObject {
86103
: BaseObject(env, wrap), callback_(env->isolate(), callback) {
87104
Agent* inspector = env->inspector_agent();
88105
session_ = ConnectionType::Connect(
89-
inspector, std::make_unique<JSBindingsSessionDelegate>(env, this));
90-
// Inspector responses may be produced from a GC weak callback, where
91-
// invoking the JavaScript session callback is forbidden. Defer delivery
92-
// until the GC has completed.
106+
inspector, std::make_unique<JSBindingsSessionDelegate>(this));
107+
// Inspector responses may be produced from a GC weak callback or a V8
108+
// interrupt, where invoking the JavaScript session callback is forbidden.
109+
// Defer delivery until it is safe to call into JavaScript.
93110
env->isolate()->AddGCPrologueCallback(GCPrologueCallback, this);
94111
env->isolate()->AddGCEpilogueCallback(GCEpilogueCallback, this);
95112
}
@@ -100,7 +117,8 @@ class JSBindingsConnection : public BaseObject {
100117
}
101118

102119
void SendMessageToFrontend(const v8_inspector::StringView& message) {
103-
if (in_gc_ || delivering_ || !pending_messages_.empty()) {
120+
if (in_gc_ || env()->is_processing_v8_interrupt() || delivering_ ||
121+
!pending_messages_.empty()) {
104122
pending_messages_.emplace_back(
105123
message.is8Bit()
106124
? std::u16string(message.characters8(),
@@ -128,6 +146,12 @@ class JSBindingsConnection : public BaseObject {
128146
void ScheduleFlush() {
129147
if (flush_scheduled_ || delivering_) return;
130148
flush_scheduled_ = true;
149+
if (!in_gc_ && env()->is_processing_v8_interrupt()) {
150+
auto* pending = new PendingMicrotask(this);
151+
env()->context()->GetMicrotaskQueue()->EnqueueMicrotask(
152+
env()->isolate(), PendingMicrotask::Run, pending);
153+
return;
154+
}
131155
BaseObjectPtr<JSBindingsConnection> strong_ref{this};
132156
env()->SetImmediate(
133157
[strong_ref](Environment*) { strong_ref->FlushPendingMessages(); },

0 commit comments

Comments
 (0)