As Node.js becomes more popular and used in more and more scenarios, learning to debug Node.js effectively will make daily development more efficient. Let’s debug nodeJS using the Inspector

The version of Node6.3+ provides two debugging protocols: the V8 Debugger Protocol and the V8 Inspector Protocol, which can be used by third-party clients/ides to monitor and intervene in the running process of Node(V8).

V8 Inspector Protocol is a new debugging Protocol that interacts with Client/IDE via WebSocket (usually using port 9229). Meanwhile, DevTools based on Chrome/Chromium browser provides a graphical debugging interface.

1 Enabling debugging

1.1 Debugging server code

If your script is set up for HTTP or NET servers, you can use –inspect directly

const Koa = require('koa')
const app = new Koa()

app.use(async ctx => {
  
  let a = 0
  const longCall = (a)= > { 
    while (a < 10e8) { 
      a++
    }
  }
  longCall()
  ctx.body = `Hello ${a}`
})

app.listen(3000, () = > {console.log('Program listening on port 3000')})Copy the code

Launch your script using node –inspect=9229 app.js, which is the specified port number

The console output is as follows:
/usr/local/ bin/node - inspect = 9229 SRC/inspector/demo js Debugger listening on the ws: / / 127.0.0.1:9229 / c4f1e345-e811-47a2-b44a-65f68c0c2cc3
Debugger attached.
# can open in your browser: http://127.0.0.1:9229/json to see some information, c4f1e345 - a2 - b44a e811-47-65 f68c0c2cc3 uuid, uuid to distinguish different debug panel;
Copy the code

–inspect for ordinary programs is a flash, the breakpoint signal has not been sent, the execution is completed. The break point doesn’t work at all, so –inspect-brk;

1.2 Debugging script Code

If your script runs and terminates the process directly, you need to start the debugger with — inspec-brk. This enables the script to break before the code is executed. Otherwise, the entire code runs to the end of the code, terminating the process, and no debugging at all.

node --inspect-brk=9229 app.js

2 Add debugging tools

2.1 VS Code

Vs Code has built-in Node Debugger and supports both the V8 Debugger Protocol and v8 Inspector Protocol. For the V8 Inspector Protocol, just add a Attach type configuration to the configuration

In the Debug control panel, click the Settings icon to open.vscode/launch.json. Click Node.js to perform initial configuration.

{
  "version": "0.2.0"."configurations": [{"type": "node"."request": "launch"."name": "Launch Program"."program": "${workspaceFolder}/app.js"}}]Copy the code

2.2 the Chrome DevTools

  • Method 1: Open it in Chromechrome://inspectClick on theConfigureButton to determine whether the host and port are in the list.

  • Method 2: From the above host and port/json/listcopydevtoolsFrontendUrl- inspectPrompt message and copy to Chrome.

2.2.1 the Console Panel

When Chrome is connected to the node process to be debugger, it can proxy all Console output from the node process in the Console, providing a flexible Filter function, and can execute code directly in the context of the Node process code.

2.2.2 Sources Panel

In Sources, you can view all the loaded scripts, including third-party libraries and Node core libraries. Select the files to edit them, and Ctrl + C to save the files to directly modify the scripts in operation.

2.2.3 Profile Panel

Profiles are used to monitor the performance of running scripts, including CPU and memory usage. A CPU Profile records the CPU time spent executing Javascript functions on a timeline.

Profile There are two types of recording periods

  • Manually start/stop: ClickstartTo start recording, clickstopStop recording
  • Insert start/stop API calls into your codeconsole.profile('tag') console.profileEnd('tag'), can be inSourcesPanel directly edit save code, and then F5 refresh.

Profiles have three views

  • Chart: Commonly known as flame chart, shows the stack of function calls on the horizontal axis of time. Here is a simple example

tick
tick
Node
process.nextTick(fn)
setTimeout(fn, deloy)
tick

Functions are called from the bottom of the stack to the top. ParserOnHeadersComplete is called by parserOnHeadersComplete, parserOnIncoming is called by parserOnIncoming, parserOnIncoming is called by Emit… And so on.

The width of the call stack is the time the function takes to execute. The execution time of a function includes the execution time of its internal calls to other functions, so functions relatively near the bottom of the stack must take longer to call than those near the top. This is the execution time of the current function, excluding the execution time of internal calls to other functions.

Clicking on a function will jump to the location of the function definition in the Sources panel.

Chrome – devtools document

  • Name: Indicates the Name of the function.

  • Self time: the time required to complete the current call to the function, including only the declaration of the function itself, not any of the functions called by the function.

  • Total Time: The time required to complete the current call to this function and any functions it calls.

  • URL: The location of a function definition of the form file.js:100, where file.js is the name of the file that defines the function and 100 is the line number that is defined.

  • Aggregated self time: Records the total time of all calls to functions in the function, excluding functions called by this function.

  • Aggregated total time: Total time of all calls to a function, excluding functions called by this function.

  • Not Optimized: If the analyzer has detected potential optimizations for the function, they are listed here.

  • Heavy (Bottom Up) : Statistics, from Bottom Up, Bottom refers to the Bottom of the flame graph.

  • Tree (Top Down) : Statistics, from Top Down, with the Top being the Top of the flame graph.

You can see that most of the program’s time is spent calling the longCall function;

2.2.4 the Memory profile

The heap profiler can display memory allocations by the page’s JavaScript objects and associated DOM nodes (see also object retention trees). The analyzer allows you to take JS heap snapshots, analyze memory maps, compare snapshots, and find memory leaks.

3. Node Inspector agent implementation

Using the Node Inspector to debug breakpoints is a common debug method. However, there were several problems in previous debugging that made our debugging less efficient.

  • invscodeDebug ininspectorPort change orwebsocket idReconnect after the change.
  • indevtoolsDebug ininspectorPort change orwebsocket idReconnect after the change.

How does Node Inspector solve these two problems?

For the first problem, in VSCode, it will call the/JSON interface to get the latest WebSocket ID, and then connect to the Node Inspector service using the new WebSocket ID. Therefore, the solution is to implement a TCP proxy function to do data forwarding.

For the second problem, since DevTools does not automatically get the new WebSocket ID, we need to do dynamic replacement, so the solution is to proxy the service to get the WebSocket ID from/JSON. The WebSocket ID is then dynamically replaced to the request header during the WebSocket handshake.

Drew a flow chart:

3.1 the Tcp proxy

First, implement a TCP proxy function. In fact, it is very simple to create a TCP Server with a proxy port through the Node net module, and then create a TCP Server to connect to the target port when there is a connection. Then, data can be forwarded.

A simple implementation is as follows:

const net = require('net');
const proxyPort = 9229;
const forwardPort = 5858;

net.createServer(client= > {
  const server = net.connect({
    host: '127.0.0.1'.port: forwardPort,
  }, () => {
    client.pipe(server).pipe(client);
  });
  // If you really want to apply it to business, you should also listen for error/close events and immediately destroy the created socket when the connection is closed.
}).listen(proxyPort);
Copy the code

Above, we implement a simple proxy service that connects the data of the two services using the PIPE method. When the client has data, it will be forwarded to the server, and when the server has data, it will also be forwarded to the client.

Configurations when you complete the Tcp proxy function, you can implement the debugging requirements of vscode by specifying the port of the proxy in the launch.json of the project in vscode and adding the configuration in the configurations

{
  "type": "node"."request": "attach"."name": "Attach"."protocol": "inspector"."restart": true."port": 9229
}
Copy the code

When the application is restarted or inspect’s port is changed, VSCode will automatically reattach to your application via the proxy port.

3.2 get websocketId

This step starts with the problem of reattaching the devTools link without changing it. When you start the Node Inspector Server, The Inspector service also provides a/JSON HTTP interface to get the WebSocket ID.

Send an HTTP request to/JSON on the target port and get the data:

[ { description: 'node.js instance', devtoolsFrontendUrl: '...', faviconUrl: 'https://nodejs.org/static/favicon.ico', id: 'e7ef6313-1ce0-4b07-b690-d3cf5274d8b0', title: '/Users/wanghx/Workspace/larva-team/vscode-log/index.js', type: 'node', url: 'file:///Users/wanghx/Workspace/larva-team/vscode-log/index.js', webSocketDebuggerUrl: 'the ws: / / 127.0.0.1:5858 / b07 language-learning e7ef6313-1-4 - b690 - d3cf5274d8b0'}]Copy the code

The id field in the data above is the webSocket ID we need.

3.3 Inspector agent

For example, if my proxy service port is 9229, then chrome Devtools’s proxy link will be:

chrome-devtools://devtools/bundled/inspector.html? Experiments = 8 only true&v = true&ws = 127.0.0.1:9229 / __ws_proxy__

All of the above are fixed except for the last ws=127.0.0.1:9229/__ws_proxy__, and the last one is clearly a Websocket link. __ws_proxy__ is used as a placeholder when Chrome DevTools sends a WebSocket handshake request to the proxy link. Replace __ws_proxy__ with websocket ID and forward to node inspector.

Make a few minor changes to the pipe logic in the TCP proxy code above.

const through = require('through2')

client
    .pipe(through.obj((chunk, enc, done) = > {
        if (chunk[0= = =0x47 && chunk[1= = =0x45 && chunk[2= = =0x54) {
          const content = chunk.toString();
          if (content.includes('__ws_proxy__')) {
            return done(null, Buffer.from(content.replace('__ws_proxy__', websocketId)));
          }
        }
        done(null, chunk);
      }))
    .pipe(server)
    .pipe(client)
Copy the code

Create a transform stream through through2 to make changes to the transmitted data.

Simply check whether the first three bytes of chunk are GET. If they are GET, it could be an HTTP request, or a WebSocket protocol upgrade request. This is what the request header looks like when printed:

GET /__ws_proxy__ HTTP/1.1
Host: 127.0. 01.:9229
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: chrome-devtools://devtools
Sec-WebSocket-Version: 13
Copy the code

Then replace the path /__ws_proxy with the corresponding websocketId, and forward it to the Node inspector Server. The webSocket handshake is completed, and the webSocket communication does not need to process the data. Just forward it directly.

If you restart the app or change the inspector port, you don’t need to change the debug link. You just need to restart the Inspector server in the popover shown in the following image

Click Reconnect DevTools to restore debug.

Reference: Node Inspector agent implementation