feat(inspector): expose the V8 inspector over the Chrome DevTools Protocol - #416
Open
danielgatis wants to merge 1 commit into
Open
feat(inspector): expose the V8 inspector over the Chrome DevTools Protocol#416danielgatis wants to merge 1 commit into
danielgatis wants to merge 1 commit into
Conversation
V8 ships the inspector; what an embedder has to supply is a transport
and somewhere for execution to stop. `Switch.inspector.start({ port })`
supplies both, and chrome://inspect, the Node DevTools window and editor
debuggers attach to it: breakpoints, stepping, call frames and scopes,
Runtime.evaluate in the console, and the profilers.
The transport is native rather than built on Switch.listen(). When a
breakpoint hits, V8 calls runMessageLoopOnPause() and expects the
embedder to block inside it, servicing the debugger, until the protocol
resumes — and JavaScript is stopped for that whole time, so a transport
written in JavaScript could not read the message that would release it.
Node uses a dedicated thread for this; that is not needed here, because
nx.js already speaks to sockets as raw file descriptors, so the pause
loop polls its own fd directly. The socket is kept off the libuv loop
for the same reason: uv_run() is not running while paused.
Three things this got wrong first, each found by attaching a real
debugger rather than by reading:
The inspector is created during init, before the app's script is
compiled. Built on demand instead, it could evaluate expressions but
could not see — or breakpoint — a single line the app had written,
because V8 only tracks scripts parsed while an inspector exists.
/json advertises devtoolsFrontendUrl. Without it chrome://inspect lists
the target and the inspect button does nothing, and webSocketDebuggerUrl
echoes the request's Host header rather than the address the console
bound to, which no client can dial.
A client that closes without completing the WebSocket handshake is not a
detaching debugger. Treating it as one meant the target list polling
/json released every app that was waiting for a debugger.
Verified on device (Eden, aarch64 build of this branch):
Debugger.paused / evaluate while paused / Debugger.resumed
setBreakpointByUrl on romfs:/main.js -> hit in work() at line 34,
local read from the paused frame, resumed
wait: true -> app blocked before running, released on attach,
paused with reason "Break on start"
Contributor
|
@danielgatis is attempting to deploy a commit to the TooTallNate's Team Team on Vercel. A member of the Team first needs to authorize it. |
🦋 Changeset detectedLatest commit: 1cb7e87 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Author
|
@TooTallNate Any chance we can move forward with this PR? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes nothing yet — opening this alongside the idea rather than after it, so
the shape can be argued with before it settles.
What it does
…and
chrome://inspectattaches to the console. Breakpoints, stepping, callframes and scopes,
Runtime.evaluatein the console, the CPU and heapprofilers — the DevTools window, against a Switch.
{ wait: true }blocks startup until a debugger attaches and then breaks, theequivalent of
--inspect-brk, for anything that happens too early to catchotherwise.
Why the transport is native
This is the part worth reviewing, because the obvious alternative looks better
than it is.
Switch.listen()already exists, so the WebSocket server could have beenwritten in TypeScript with a thin
$.inspector*binding underneath. It cannot,and the reason is one line of the V8 contract: when a breakpoint hits, V8 calls
runMessageLoopOnPause()and expects the embedder to block inside it,servicing the debugger, until the protocol resumes. JavaScript is stopped for
that whole time — including the socket handler that would have read
Debugger.resume. The app would stop at the first breakpoint and never moveagain.
Node solves this with a dedicated thread. That turned out not to be necessary
here:
tcp.ccdeliberately speaks to sockets as raw file descriptors, so thepause loop can
poll()its own fd directly. The inspector socket is kept offthe libuv loop for the same reason —
uv_run()is driven by the main loop,which is not running while paused.
Three things this got wrong first
Each of these produced a debugger that connected and looked broken rather than
one that failed loudly, and each was found by attaching a real debugger.
The inspector is created during init, before the app's script is compiled.
Created on demand in
start()instead, it could evaluate expressions but couldnot see — or breakpoint — a single line the app had written: V8 only tracks
scripts parsed while an inspector exists. Sources was empty and nothing
explained why.
/jsonhas to advertisedevtoolsFrontendUrl. Without itchrome://inspectlists the target and the inspect button does nothing at all. And
webSocketDebuggerUrlechoes the request'sHostheader rather than theaddress the console bound to, which no client can dial.
A client that closes without completing the handshake is not a detaching
debugger. Treating it as one meant the target list, which polls
/json,released every app that was waiting for a debugger.
Verification
Built for
aarch64with devkitPro and driven on device (Eden) with a CDPclient, plus Chrome DevTools by hand.
Debugger.pause→Debugger.pausedRuntime.evaluatewhile paused42Debugger.resume→Debugger.resumedsetBreakpointByUrlonromfs:/main.jswork()at line 34Debugger.evaluateOnCallFramewait: trueBreak on startchrome://inspectromfs:/main.js; breakpoints workThat resume arriving while the isolate is stopped is the whole design in one
line: it can only have been delivered by a transport that is not JavaScript.
Open questions
Cost when unused. The
V8Inspectoris constructed during init so scriptsare tracked from the start; nothing listens until
start(). If that is toomuch for applet mode, it could hang off a config flag — happy to do that.
wait: truecannot break before the app starts. The pause is requestedfrom inside
start(), so everything above that call has already run. What itdoes break on is the next statement V8 reaches afterwards — in practice the
first function call in user code:
Useful, but not
--inspect-brk. Breaking before the app's script is compiledneeds a hook further up in the runtime — happy to add one if you would rather
this matched Node.
One debugger at a time. A second connection is accepted and closed.
A 503 with a reason would be friendlier if that matters.
Port and
waitare arguments today. If these belong innxjs.ininext tothe other runtime knobs instead, that is an easy change.