現今 Node.js
愈發受歡迎,應用場景也越來越多,學會高效除錯 Node.js
會讓日常開發更高效。下面講下使用inspector
除錯nodejs
程式
Node6.3+
的版本提供了兩個用於除錯的協議:v8 Debugger Protocol
和 v8 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 Protocol
和 v8 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和埠在列表中。
- 方法2:從上述host和埠
/json/list
複製devtoolsFrontendUrl
或–-inspect
提示資訊,並複製到Chrome.
2.2.1 Console Panel
chrome
接入要除錯的 node
程式後,可以在 Console
中代理 Node
程式中所有的控制檯輸出,提供了靈活的 Filter 過濾功能,還可以在 Node 程式程式碼的上下文中直接執行程式碼。
2.2.2 Sources Panel
Sources
中可以檢視所有載入的指令碼,還包括第三方庫和Node
核心庫,選中檔案可以進行編輯,Ctrl + C
儲存可以直接修改執行中的指令碼。
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:俗稱火焰圖,以時間為橫軸顯示函式呼叫棧。下面簡單舉例分析
tick
,一個 tick
一定是由 Node
底層開始呼叫的,在 Node 中使用 process.nextTick(fn)
和setTimeout(fn, deloy)
的系統回撥會產生新的 tick
,對應產生新的呼叫棧。
函式的呼叫順序是從棧底到棧頂。上圖中第一個棧 parserOnHeadersComplete
由底層呼叫,parserOnHeadersComplete
中呼叫了 parserOnIncoming
, parserOnIncoming
中呼叫了emit
...依次類推。
呼叫棧的寬度是函式執行的時間。一個函式的執行時間包含了其內部呼叫其他函式的執行時間,所以相對靠近棧底的函式的呼叫時間一定比靠近棧頂的函式的呼叫時間長。除去內部呼叫其他函式的執行時間,就是當前函式的執行時間。
點選函式會跳轉到 Sources
皮膚中函式定義的位置。
-
Name
:函式的名稱。 -
Self time
:完成函式當前的呼叫所需的時間,僅包含函式本身的宣告,不包含函式呼叫的任何函式。 -
Total time
: 完成此函式和其呼叫的任何函式當前的呼叫所需的時間。 -
URL
:形式為 file.js:100 的函式定義的位置,其中 file.js 是定義函式的檔名稱,100 是定義的行號。 -
Aggregated self time
:記錄中函式所有呼叫的總時間,不包含此函式呼叫的函式。 -
Aggregated total time
: 函式所有呼叫的總時間,不包含此函式呼叫的函式。 -
Not optimized
:如果分析器已檢測出函式存在潛在的優化,會在此處列出。 -
heavy(Bottom Up):統計資料,自底向上,底指的是火焰圖的底。
- tree(Top Down):統計資料,自頂向下,頂指的是火焰圖的頂。
可以看到程式大部分時間是消耗在longCall
這個函式的呼叫上;
2.2.4 Memory profile
堆分析器可以按頁面的 JavaScript 物件和相關 DOM 節點顯示記憶體分配(另請參閱物件保留樹)。使用分析器可以拍攝 JS 堆快照
、分析記憶體圖
、比較快照
以及查詢記憶體洩漏
.
3. Node Inspector 代理實現
通過 node inspector
來進行斷點除錯是一個很常用的 debug 方式。但是以前的除錯中有幾個問題會導致我們的除錯效率降低。
- 在
vscode
中除錯,在inspector
埠變更或者websocket id
變更後要重連。 - 在
devtools
中除錯,在inspector
埠變更或者websocket id
變更後要重連。
那 node inspector是如何解決上述兩個問題呢?
對於第一個問題,在 vscode
上,它是會自己去呼叫 /json
介面獲取最新的 websocket id
,然後使用新的 websocket id
連線到 node inspector
服務上。因此解決方法就是實現一個 tcp
代理功能做資料轉發即可。
對於第二個問題,由於 devtools
是不會自動去獲取新的 websocket id
的,所以我們需要做動態替換,所以解決方案就是代理服務去 /json
獲取 websocket id
,然後在 websocket
握手的時候將 websocket id
進行動態替換到請求頭上。
畫了一張流程圖:
3.1 Tcp 代理
首先,先實現一個 tcp
代理的功能,其實很簡單,就是通過 node
的 net
模組建立一個代理埠的 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
服務還提供了一個 /json
的 http
介面用來獲取 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
然後轉發到 node
的 inspector
服務上。
對上面的 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
,然後轉發到 node
的 inspector server
上,即可完成websocket
的握手,接下來的 websocket
通訊就不需要對資料做處理,直接轉發即可。
接下來就算各種重啟應用,或者更換 inspector
的埠,都不需要更換 debug
連結,只需要再 inspector server
重啟的時候,在下圖的彈窗中
點選一下 Reconnect DevTools 即可恢復 debug。