Node.js除錯指南

小平果118發表於2019-02-16

現今 Node.js 愈發受歡迎,應用場景也越來越多,學會高效除錯 Node.js 會讓日常開發更高效。下面講下使用inspector除錯nodejs程式

Node6.3+ 的版本提供了兩個用於除錯的協議:v8 Debugger Protocolv8 Inspector Protocol 可以使用第三方的 Client/IDE 等監測和介入 Node(v8) 執行過程,進行除錯。

v8 Inspector Protocol 是新加入的除錯協議,通過 websocket (通常使用 9229 埠)與 Client/IDE 互動,同時基於 Chrome/Chromium 瀏覽器的 devtools 提供了圖形化的除錯介面。

1 開啟除錯

1.1 除錯伺服器程式碼

如果你的指令碼搭建http或者net伺服器,你可以直接使用--inspect

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

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

app.listen(3000, () => { 
  console.log('程式監聽了3000埠')
})
複製程式碼

使用 node --inspect=9229 app.js 啟動你的指令碼,9229 是指定的埠號

# 控制檯會輸出如下:
/usr/local/bin/node --inspect=9229 src/inspector/demo.js 
Debugger listening on ws://127.0.0.1:9229/c4f1e345-e811-47a2-b44a-65f68c0c2cc3
Debugger attached.
# 可以在瀏覽器裡開啟:http://127.0.0.1:9229/json 看到一些資訊, c4f1e345-e811-47a2-b44a-65f68c0c2cc3 為uuid,不同除錯皮膚的uuid來區分;
複製程式碼

--inspect 對於一般的程式都是一閃而過,斷點訊號還沒傳送出去,就執行完畢了。 斷點根本不起作用,可以--inspect-brk

1.2 除錯指令碼程式碼

如果你的指令碼執行完之後直接結束程式,那麼你需要使用--inspect-brk來啟動偵錯程式,這樣使得指令碼可以程式碼執行之前break,否則,整個程式碼直接執行到程式碼結尾,結束程式,根本無法進行除錯。

node --inspect-brk=9229 app.js

2 除錯工具接入

2.1 VS Code

Vs Code 內建了 Node debugger ,支援 v8 Debugger Protocolv8 Inspector Protocol 兩種協議。對於 v8 Inspector Protocol ,只需要在配置裡新增一條 Attach 型別配置

Debug 控制皮膚, 點選 settings 圖示,開啟 .vscode/launch.json. 點選 “Node.js” 進行初始配置即可.

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "program": "${workspaceFolder}/app.js"
    }
  ]
}
複製程式碼

2.2 Chrome DevTools

  • 方法1:在Chrome瀏覽器開啟 chrome://inspect點選Configure按鈕,確定host和埠在列表中。

Node.js除錯指南

Node.js除錯指南

  • 方法2:從上述host和埠/json/list複製 devtoolsFrontendUrl–-inspect 提示資訊,並複製到Chrome.

2.2.1 Console Panel

chrome 接入要除錯的 node 程式後,可以在 Console 中代理 Node 程式中所有的控制檯輸出,提供了靈活的 Filter 過濾功能,還可以在 Node 程式程式碼的上下文中直接執行程式碼。

Node.js除錯指南

2.2.2 Sources Panel

Sources 中可以檢視所有載入的指令碼,還包括第三方庫和Node 核心庫,選中檔案可以進行編輯,Ctrl + C 儲存可以直接修改執行中的指令碼。

Node.js除錯指南

2.2.3 Profile Panel

Profile 用於對執行中的指令碼進行效能監測,包括CPU和記憶體的使用,CPU profile,可以記錄時間線上 Javascript 函式執行時佔用的 CPU 時間.

profile 記錄時間段有兩種

  • 手動開始/停止:單擊 start 開始記錄,單擊 stop 停止記錄
  • 在程式碼中插入開始/停止的 API 呼叫 console.profile('tag') console.profileEnd('tag') ,可以在 Sources 皮膚中直接編輯儲存程式碼,然後 F5 重新整理一下。

profile有三種檢視

  • chart:俗稱火焰圖,以時間為橫軸顯示函式呼叫棧。下面簡單舉例分析

Node.js除錯指南
火焰圖的函式呼叫棧是倒置的,最上面為棧底,最下面為棧頂。一個棧是一個 tick ,一個 tick 一定是由 Node 底層開始呼叫的,在 Node 中使用 process.nextTick(fn)setTimeout(fn, deloy) 的系統回撥會產生新的 tick ,對應產生新的呼叫棧。

函式的呼叫順序是從棧底到棧頂。上圖中第一個棧 parserOnHeadersComplete 由底層呼叫,parserOnHeadersComplete 中呼叫了 parserOnIncomingparserOnIncoming 中呼叫了emit...依次類推。

呼叫棧的寬度是函式執行的時間。一個函式的執行時間包含了其內部呼叫其他函式的執行時間,所以相對靠近棧底的函式的呼叫時間一定比靠近棧頂的函式的呼叫時間長。除去內部呼叫其他函式的執行時間,就是當前函式的執行時間。

點選函式會跳轉到 Sources 皮膚中函式定義的位置。

Node.js除錯指南
將滑鼠懸停在函式上可顯示其名稱和資料:

Node.js除錯指南
下面解釋摘自 chrome-devtools 文件

  • Name:函式的名稱。

  • Self time:完成函式當前的呼叫所需的時間,僅包含函式本身的宣告,不包含函式呼叫的任何函式。

  • Total time: 完成此函式和其呼叫的任何函式當前的呼叫所需的時間。

  • URL:形式為 file.js:100 的函式定義的位置,其中 file.js 是定義函式的檔名稱,100 是定義的行號。

  • Aggregated self time:記錄中函式所有呼叫的總時間,不包含此函式呼叫的函式。

  • Aggregated total time: 函式所有呼叫的總時間,不包含此函式呼叫的函式。

  • Not optimized:如果分析器已檢測出函式存在潛在的優化,會在此處列出。

  • heavy(Bottom Up):統計資料,自底向上,底指的是火焰圖的底。

Node.js除錯指南

  • tree(Top Down):統計資料,自頂向下,頂指的是火焰圖的頂。

Node.js除錯指南

可以看到程式大部分時間是消耗在longCall這個函式的呼叫上;

2.2.4 Memory profile

堆分析器可以按頁面的 JavaScript 物件和相關 DOM 節點顯示記憶體分配(另請參閱物件保留樹)。使用分析器可以拍攝 JS 堆快照分析記憶體圖比較快照以及查詢記憶體洩漏.

Node.js除錯指南

3. Node Inspector 代理實現

通過 node inspector 來進行斷點除錯是一個很常用的 debug 方式。但是以前的除錯中有幾個問題會導致我們的除錯效率降低。

  • vscode 中除錯,在 inspector 埠變更或者 websocket id 變更後要重連。
  • devtools 中除錯,在inspector 埠變更或者 websocket id 變更後要重連。

那 node inspector是如何解決上述兩個問題呢?

Node.js除錯指南

對於第一個問題,在 vscode 上,它是會自己去呼叫 /json 介面獲取最新的 websocket id,然後使用新的 websocket id 連線到 node inspector 服務上。因此解決方法就是實現一個 tcp 代理功能做資料轉發即可。

對於第二個問題,由於 devtools 是不會自動去獲取新的 websocket id 的,所以我們需要做動態替換,所以解決方案就是代理服務去 /json 獲取 websocket id,然後在 websocket 握手的時候將 websocket id 進行動態替換到請求頭上。

畫了一張流程圖:

Node.js除錯指南

3.1 Tcp 代理

首先,先實現一個 tcp 代理的功能,其實很簡單,就是通過 nodenet 模組建立一個代理埠的 Tcp Server,然後當有連線過來的時候,再建立一個連線到目標埠即可,然後就可以進行資料的轉發了。

簡易的實現如下:

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);
  });
  // 如果真要應用到業務中,還得監聽一下錯誤/關閉事件,在連線關閉時即時銷燬建立的 socket。
}).listen(proxyPort);
複製程式碼

上面實現了比較簡單的一個代理服務,通過 pipe 方法將兩個服務的資料連通起來。client 有資料的時候會被轉發到 server 中,server 有資料的時候也會轉發到 client 中。

當完成這個 Tcp 代理功能之後,就已經可以實現 vscode 的除錯需求了,在 vscode 中專案下 launch.json 中指定埠為代理埠,在 configurations 中新增配置

{
  "type": "node",
  "request": "attach",
  "name": "Attach",
  "protocol": "inspector",
  "restart": true,
  "port": 9229
}
複製程式碼

那麼當應用重啟,或者更換 inspect 的埠,vscode 都能自動重新通過代理埠 attach 到你的應用。

3.2 獲取 websocketId

這一步開始,就是為了解決 devtools連結不變的情況下能夠重新 attach 的問題了,在啟動 node inspector server的時候,inspector 服務還提供了一個 /jsonhttp 介面用來獲取 websocket id

這個就相當簡單了,直接發個 http 請求到目標埠的 /json,就可以獲取到資料了:

[ { 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: 'ws://127.0.0.1:5858/e7ef6313-1ce0-4b07-b690-d3cf5274d8b0' } ]
複製程式碼

上面資料中的 id 欄位,就是我們需要的 websocket id 了。

3.3 Inspector 代理

拿到了 websocket id後,就可以在 tcp 代理中做 websocket id 的動態替換了,首先我們需要固定連結,因此先定一個代理連結,比如我的代理服務埠是 9229,那麼 chrome devtools 的代理連結就是:

chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/__ws_proxy__

上面除了最後面的 ws=127.0.0.1:9229/__ws_proxy__ 其他都是固定的,而最後這個也一眼就可以看出來是 websocket 的連結。其中 __ws_proxy__則是用來佔位的,用於在 chrome devtools 向這個代理連結發起 websocket 握手請求的時候,將 __ws_proxy__ 替換成 websocket id 然後轉發到 nodeinspector 服務上。

對上面的 tcp 代理中的 pipe 邏輯的程式碼做一些小修改即可。

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)
複製程式碼

通過 through2 建立一個 transform 流來對傳輸的資料進行一下更改。

簡單判斷一下 chunk 的頭三個位元組是否為GET,如果是 GET 說明這可能是個 http 請求,也就可能是 websocket 的協議升級請求。把請求頭列印出來就是這個樣子的:

GET /__ws_proxy__ HTTP/1.1
Host: 127.0.0.1:9229
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: chrome-devtools://devtools
Sec-WebSocket-Version: 13
複製程式碼

然後將其中的路徑/__ws_proxy替換成對應的 websocketId,然後轉發到 nodeinspector server 上,即可完成websocket 的握手,接下來的 websocket 通訊就不需要對資料做處理,直接轉發即可。

接下來就算各種重啟應用,或者更換 inspector 的埠,都不需要更換 debug 連結,只需要再 inspector server 重啟的時候,在下圖的彈窗中

Node.js除錯指南

點選一下 Reconnect DevTools 即可恢復 debug。

參考:Node Inspector 代理實現

相關文章