Skip to content

Commit b0f3439

Browse files
committed
feat(inspector): attach Chrome DevTools to Web Worker isolates
Workers were invisible to the debugger: the inspector only served the main isolate and even dropped worker console output on the floor. DevTools discovers extra isolates through the Target domain with flat-session multiplexing, so implement that surface in the runtime: - Add WorkerInspectorClient: a per-worker V8Inspector + session that lives entirely on the worker's thread. Incoming CDP messages queue through a dedicated CFRunLoopSource on the worker's runloop; while paused at a breakpoint a nested loop on the worker thread pumps the same queue without re-entering the runloop, so postMessage deliveries stay queued during a pause (matching Chrome's semantics). - Turn JsV8InspectorClient into the root session + router: handle Target.setAutoAttach natively (announce workers via Target.attachedToTarget, detach on death), route messages carrying a top-level sessionId to the worker's thread directly from the socket thread, and make the frontend sender thread-safe. Routing off the socket thread means a worker stays debuggable while the main isolate is paused, and vice versa. - Debugger.pause for a busy worker uses RequestInterrupt keyed by workerId, so a late-firing interrupt after worker death is a no-op. This also fixes the main-session fast path, which used to interrupt the main isolate for a pause aimed at any session. - Serve Network.loadNetworkResource and IO.read/IO.close on the socket thread for any session (sessionId echoed), so worker source maps load too. - Route worker console.* to the worker's own inspector and deliver through V8ConsoleMessageStorage::addMessage instead of the live-only runtime agent path: messages logged before the frontend attaches (or before Runtime.enable reaches a session) are stored and replayed as console history. Applies to the main isolate as well. - Name execution contexts ("main" / worker script url) so the DevTools console context selector rows are labeled and selectable. - Worker lifecycle: inspector is created on the worker thread before RunModule (debug builds only), terminate() kicks a paused worker out of its nested pause loop, teardown unregisters the target before the isolate is disposed, and frontend reconnects reset worker sessions. waitForDebuggerOnStart (pause new workers before their first line) is left as future work; workers currently announce waitingForDebugger: false.
1 parent 91ce499 commit b0f3439

8 files changed

Lines changed: 866 additions & 80 deletions

File tree

NativeScript/inspector/JsV8InspectorClient.h

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include <functional>
77
#include <map>
8+
#include <mutex>
89
#include <string>
910
#include <vector>
1011

@@ -15,6 +16,8 @@
1516

1617
namespace v8_inspector {
1718

19+
class WorkerInspectorClient;
20+
1821
class JsV8InspectorClient : V8InspectorClient, V8Inspector::Channel {
1922
public:
2023
JsV8InspectorClient(tns::Runtime* runtime);
@@ -23,6 +26,20 @@ class JsV8InspectorClient : V8InspectorClient, V8Inspector::Channel {
2326
void disconnect();
2427
void dispatchMessage(const std::string& message);
2528

29+
// The single instance debugging the main isolate (created when IsDebug);
30+
// also acts as the router for worker sessions. Null in release builds.
31+
static JsV8InspectorClient* GetInstance();
32+
33+
// Thread-safe write to the connected frontend (no-op when disconnected).
34+
void SendToFrontend(const std::string& message);
35+
36+
// Worker targets (Chrome DevTools Target domain, flat-session mode).
37+
// Register/Unregister are called on the worker's own thread.
38+
void RegisterWorkerTarget(int workerId, WorkerInspectorClient* client);
39+
void UnregisterWorkerTarget(int workerId);
40+
// Called on a worker thread by the Debugger.pause interrupt.
41+
void SchedulePauseInWorker(int workerId);
42+
2643
// Overrides of V8Inspector::Channel
2744
void sendResponse(int callId, std::unique_ptr<StringBuffer> message) override;
2845
void sendNotification(std::unique_ptr<StringBuffer> message) override;
@@ -63,13 +80,39 @@ class JsV8InspectorClient : V8InspectorClient, V8Inspector::Channel {
6380

6481
// Streams backing Network.loadNetworkResource responses, read by the
6582
// frontend through IO.read/IO.close (how Chrome DevTools fetches source
66-
// maps from the target). Only touched from dispatchMessage (main thread).
83+
// maps from the target). Served on the socket thread for any session;
84+
// guarded by resourceStreamsMutex_.
6785
struct ResourceStream {
6886
std::string data;
6987
size_t offset = 0;
7088
};
7189
std::map<std::string, ResourceStream> resourceStreams_;
7290
int lastStreamId_ = 0;
91+
std::mutex resourceStreamsMutex_;
92+
93+
// Worker targets announced to the frontend via Target.attachedToTarget,
94+
// keyed by their flat-protocol sessionId. Guarded by workerTargetsMutex_;
95+
// a registered client pointer stays valid until UnregisterWorkerTarget
96+
// (which runs on the worker's own thread, before the client is deleted).
97+
struct WorkerTarget {
98+
int workerId;
99+
WorkerInspectorClient* client;
100+
bool announced = false;
101+
};
102+
std::map<std::string, WorkerTarget> workerTargets_;
103+
std::mutex workerTargetsMutex_;
104+
bool autoAttach_ = false; // guarded by workerTargetsMutex_
105+
106+
static JsV8InspectorClient* instance_;
107+
108+
std::mutex senderMutex_;
109+
110+
// Routes a frontend message carrying a sessionId to its worker session
111+
// (socket thread). msgId is -1 when the message has no id.
112+
void RouteToWorker(const std::string& sessionId, const std::string& method,
113+
long long msgId, const std::string& message);
114+
// Announces all not-yet-announced workers (after Target.setAutoAttach).
115+
void AnnounceWorkerTargets();
73116

74117
// Override of V8InspectorClient
75118
v8::Local<v8::Context> ensureDefaultContextInGroup(
@@ -90,10 +133,15 @@ class JsV8InspectorClient : V8InspectorClient, V8Inspector::Channel {
90133
const v8::FunctionCallbackInfo<v8::Value>& args);
91134

92135
// Source map delivery to Chrome DevTools (Network.loadNetworkResource + IO
93-
// domain). V8's inspector doesn't implement these embedder domains.
94-
void HandleLoadNetworkResource(int msgId, const std::string& url);
95-
void HandleIORead(int msgId, const std::string& handle, int size);
96-
void HandleIOClose(int msgId, const std::string& handle);
136+
// domain). V8's inspector doesn't implement these embedder domains. The
137+
// handlers are filesystem-only and thread-safe; they serve any session
138+
// (sessionId is echoed in the reply when non-empty).
139+
void HandleLoadNetworkResource(int msgId, const std::string& url,
140+
const std::string& sessionId);
141+
void HandleIORead(int msgId, const std::string& handle, int size,
142+
const std::string& sessionId);
143+
void HandleIOClose(int msgId, const std::string& handle,
144+
const std::string& sessionId);
97145

98146
// {N} specific helpers
99147
bool CallDomainHandlerFunction(v8::Local<v8::Context> context,

0 commit comments

Comments
 (0)