Debug Architecture of Node.js

To understand and improve upon tools for debugging JavaScript one must first understand the architecture of its debugging system. Here we’ll discuss V8 and Node’s current arrangement, followed by a brief review of how three debugging tools work. The following diagram will help orient you; we’ll start with everything in the big red box marked “Node.”

Node Debug Architecture

V8

V8 includes a builtin debug system accessible via include/v8-debug.h and further described in src/debug. Originally V8 also included a relay agent to receive requests and send events and responses via TCP; but this was later removed.

Because Node is written mostly in C++ it’s able to access the builtin debugging service, and does so in src/debug-agent.cc. More on that later.

Of course you can access the debug service from your C++ code in the same way Node does; but there are also a couple ways to access it from JavaScript in Node. At the command line you can specify --expose-debug-as=<name> to bind the C++ debug service as a global var name.Debug; or within a script you can call require('vm').runInDebugContext('Debug').

For example, start a REPL with node --expose-debug-as=debug and run the following (thank you @pmuellr for inspiration):

var debug = debug.Debug
function test() {}

debug.setListener((event, execState, eventData, data) => {
    if (event != debug.DebugEvent.Break) return

    var script   = eventData.func().script().name()
    var line     = eventData.sourceLine()
    var col      = eventData.sourceColumn()

    var location = script + ":" + line + ":" + col

    var funcName = eventData.func().name()
    if (funcName != "") {
        location += " " + funcName + "()"
    }

    console.log(location)
})

debug.setBreakPoint(test, 0, 0)
test()
// *output*: 'repl:0:17 test()' or similar

If you prefer not to depend on the --expose-debug-as flag on the command line, you can also use the builtin vm module in Node:

let vm = require('vm')
let debug = vm.runInDebugContext('Debug')
// ... as above

So this is how V8’s own debugging system works and how you can access it. Next let’s see what Node adds.

Node

The debugger agent removed from V8 was added to Node by Fedor Indutny several months later and shortly thereafter moved to its current location at src/debug-agent.h.

The agent is started and enabled in a separate thread when node is started with one of the debug flags:

Flag Effect
–debug[=port#] Start debug agent listening on port 5858 or port#.
–debug-brk[=port#] Start debug agent listening on port 5858 or port#. Break immediately upon entering user script.
debug Start debug agent as well as debugger REPL client.


It can also be started later by sending SIGUSR1 to the running node process, such as through kill -s SIGUSR1 <pid_of_node_process>.

You may also notice the --debug-agent flag when browsing the source, such as in src/node.js. This flag is for internal use and notifies the agent thread to initialize itself with lib/_debug_agent.js. That script sets up the TCP server and relays requests, responses, and events between TCP clients and the main thread.

Debugging Protocols

V8 originally implemented its own protocol and this is still what it uses internally. But Chrome DevTools and Chrome now use the Chrome Remote Debugging Protocol. Tools which rely on Chrome DevTools to debug V8 and Node must translate from the Chrome Debugging Protocol to the V8 protocol.

Tools

We’ve learned how to connect to the V8 debug service and how to enable its relay agent in Node. Now let’s review some protocol clients and tools and how to use them. Another diagram follows; the yellow star represents the entry point to the tool.

Node Debugging Tools

Builtin Node Debugger

A basic console debugger comes with Node and can be used by running node debug. This starts Node with the lib/_debugger.js script, which spawns a child process with the script specified on the command line or connects to a running process. Only part of the debug protocol is supported currently; add more by updating _debugger.js.

node-inspector

The venerable node-inspector sets up 3 processes: the Inspector service, the Node process being debugged, and the browser-hosted DevTools pages. Most work takes place in the Inspector process which connects via websocket with the DevTools frontend and via TCP socket with the debuggee. It brokers commands and events between them and transforms from the DevTools protocol to the V8 protocol in flight. Notably, node-inspector uses the V8 protocol from nodejs/node/lib/_debugger.js.

To see messages between the frontend and Inspector and between Inspector and debuggee, start the Inspector server independently with DEBUG=node-inspector:protocol:* node-inspector, then in a new console start the debuggee with node --debug=5858 myScript.js, then go to the URL http://127.0.0.1:8080/?ws=127.0.0.1:8080&port=5858 to start communications. The node-inspector console window will log messages from both connections.

Jam3 DevTool

Matt DesLauriers recently released Jam3/devtool, another Node debug tool based on Chrome DevTools (henceforth CDT). Unlike node-inspector, devtool uses the CDT built in to Electron, and connects it to Electron’s internal V8 instance rather than proxying messages to an external debuggee. This means no translation between the Chrome Debugging Protocol and V8 Debugging Protocol is necessary either.

Devtool loads the specified module and a dummy HTML page (by default) in a hidden Electron window and then opens Electron’s builtin CDT connected to that window. Because Electron runs JavaScript modules in Node, all of Node’s modules are available to the module and within CDT. There are some quirks to Node as hosted by Electron which Matt has shimmed in the project.

Visual Studio Code

The Debugging API Docs provide great info on VSCode’s debugging architecture. To sum them up, a debugger adapter runs out of process and translates between VSCode’s debugging protocol and the interface of the target language and/or runtime.

Chrome DevTools were intended for debugging web pages local to Chromium, so using them to debug Node processes depends on a couple hacks and can be unreliable. In contrast, Visual Studio Code provides an interface designed for Node and can easily talk V8’s debugger protocol. This also gives it the flexibility to connect to any local or remote Node process and ensures the handles implied by the graphical interface match available functionality. On the other hand, CDT-based tools can provide functionality available through the Chrome Debugging Protocol but not V8’s, such as heap and CPU profiling.

To attach VSCode to a non-local instance of Node, add the address property to the attach config in .vscode/launch.json, and update the localRoot and remoteRoot properties to match your environments. For example:

{
    "name": "Attach",
    "type": "node",
    "request": "attach",
    "address": "joshgav-node-playground.westus.cloudapp.azure.com",
    "port": 5858,
    "sourceMaps": false,
    "outDir": null,
    "localRoot": "${workspaceRoot}",
    "remoteRoot": "/home/joshgav/demo-site"
}

Note that in reality I’d tunnel over SSH rather than opening port 5858 directly to the Internet!

Also of note, VSCode provides a public debugging protocol similar to V8’s. This makes it relatively straightforward to add new JSVM’s and innovative debugging features and decreases dependency on the non-standardized V8 protocol.

There are many opportunities to help developers be more productive through improved support for debugging Node applications. Reach out to me or any of my colleagues to let us know what’s important to you!