Skip to content

feat(inspector): serve multiple DevTools clients at once - #526

Draft
edusperoni wants to merge 1 commit into
mainfrom
feat/inspector-multi-session
Draft

edusperoni wants to merge 1 commit into
mainfrom
feat/inspector-multi-session

Conversation

@edusperoni

Copy link
Copy Markdown
Collaborator

What is the current behavior?

The inspector serves one client at a time (#516). A second connection closes the first, and the single V8 session is reset or rebound for the newcomer. You cannot, for example, keep Chrome DevTools open while a CDP script or a second DevTools window attaches.

What is the new behavior?

Any number of clients can connect at once, as with Chrome and Node. Each gets its own V8 session and sees only replies to its own commands.

Sessions

  • New InspectorSessions keeps one V8InspectorSession per connection on an isolate. Each session's channel writes only to its own connection. It is used for the main isolate and for every worker.
  • Connect, disconnect and messages go through one per-isolate queue, so a client's session exists before its first message and outlives its last.
  • A client's session is destroyed when it disconnects. Its destructor disables the client's agents, so a pause ends only if no other client still has the debugger enabled. This is the same rule V8 applies in Chrome.
  • A session can't be destroyed while it is dispatching. The pause loop can sit under that client's own Runtime.evaluate. In that case it gets Debugger.disable right away, so the pause is released if nobody else wants it, and it is destroyed when that dispatch returns.
  • This replaces fix(inspector): release the frontend's resources when DevTools disconnects #516's sync sentinel, session rebinding, frontend generations, deferred resets and lifecycle mutex.

Workers

  • Each worker holds a session per client, created by the client's first message to that worker and dropped when the client disconnects. Output goes to the owning connection.
  • Target.setAutoAttach and the set of announced workers are tracked per client. attachedToTarget and detachedFromTarget go only to the clients that asked.

Shared state

  • Pauses are shared. Every client with the debugger enabled sees Debugger.paused, and any of them can resume.
  • Debugger.pause schedules the pause in the requesting client's session. A request still pending when a pause starts is dropped, so it can't pause again after the resume.
  • Tracing is process-global. One client owns it from Tracing.start until its Tracing.end or disconnect, and other clients get "Tracing is already started".
  • Network.loadNetworkResource streams belong to the client that opened them. IO.read/IO.close from another client get "Invalid stream handle", and the streams are dropped when their client disconnects.
  • The JS domains from @nativescript/core (DOM, CSS, Network) are global singletons that enable themselves:
    • Their events go to every client.
    • Command replies go only to the requesting client.
    • <Domain>.disable reaches the handler only once no other client has that domain enabled.
  • Heap object tracking and sampling, and the pause-on-exceptions setting, are isolate-wide in V8. One client's HeapProfiler.disable (or its disconnect) stops tracking for everyone. This is a V8 limitation that Chrome shares, documented on the class.

Server

  • InspectorServer no longer closes the previous client.
  • Per connection, onClientConnected precedes its messages and onClientDisconnected follows them. A message read just before a write error closed the connection is dropped.
  • A client more than 256 MB behind on writes is closed. Every client receives broadcast events, so a half-open socket would otherwise buffer forever.
  • Connection reset and broken pipe are ordinary disconnects and are no longer logged as errors.

Also fixed: a pause triggered by a message the drain dispatched (e.g. evaluating debugger in the console) re-entered dispatch_sync on the queue the drain held and trapped in libdispatch. The drain now checks the pause-loop flag under that queue but dispatches outside it. The pause loop schedules a drain when it exits.

Known issues / follow-ups

  • Debugger.pause on a worker never pauses it, while debugger; statements do. This also fails on main before this change, verified with the fix(inspector): release the frontend's resources when DevTools disconnects #516 build, so it is left for a separate fix.
  • Disconnecting the last client doesn't disable core's JS domains. They enable themselves at startup, because they can miss the enable event, so they keep emitting into the void. Unchanged from before.
  • Android parity comes later.

Testing

  • Full suite (./run_tests.sh --keep-sim): 1835 specs, 0 failures.
  • End-to-end on a simulator: a small CDP client drives TestRunner, which runs a keep-alive entry script.
    • Concurrent: two clients send the same message id and each gets its own reply. Both see console output. B keeps working after A leaves.
    • Shared pause: A pauses, both see Debugger.paused, B resumes, and both see Debugger.resumed.
    • Leave while paused: alone, the app resumes. With B debugging, it stays paused until B resumes.
    • Leave while paused inside A's own Runtime.evaluate (debugger;): alone, the app resumes. With B debugging, it stays paused, and A's evaluate reply never reaches B. In both cases a later client can pause and resume again. Before the drain fix, this path crashed in libdispatch.
    • Workers: both clients get attachedToTarget, and both evaluate in the worker with the same id, each getting its own reply. The worker pauses on debugger; and both see it. A leaves while paused, the worker stays paused for B, B resumes, and B's worker session keeps working.
    • Tracing: B is refused while A traces and can't end A's trace. After A leaves, B traces to completion.
    • fix(inspector): release the frontend's resources when DevTools disconnects #516's single-client scenarios (idle and paused disconnect, in-flight replies on the main isolate and a worker) still pass.
    • After all scenarios the app holds only its listening socket.

Each connection now gets its own V8InspectorSession on the main isolate
and on every worker, and sees only replies to its own commands, as with
Chrome and Node. InspectorSessions keeps the per-connection sessions for
one isolate; connect, disconnect and messages share one queue, so a
client's session exists before its first message and outlives its last.

A departing client's session is destroyed, which disables its agents:
a pause ends only if no other client still has the debugger enabled.
A session dispatching when its client leaves (the pause loop can run
under its own Runtime.evaluate) gets Debugger.disable at once and is
destroyed when that dispatch returns.

Per-client state: Target auto-attach and announced workers, IO streams,
and tracing (one owner at a time). @nativescript/core's JS domains are
global, so their events go to every client and a <Domain>.disable only
reaches the handler once no other client has the domain enabled.

The server no longer closes the previous client. It drops a message
read just before a write error closed its connection, closes a client
more than 256 MB behind on writes, and no longer logs connection resets
and broken pipes.

Also fix a trap in libdispatch when a message dispatched by the drain
pauses (e.g. evaluating `debugger` in the console): the drain held
messageLoopQueue_ while dispatching and the pause loop dispatch_sync'd
onto it. The drain now only reads the pause-loop flag under that queue.

This replaces the single-client sync sentinel, session rebinding and
frontend generations.
@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant