18 除錯與效能分析
當事情不如預期時,你需要知道如何找出問題。
18.1 Shell 啟動時間分析
18.1.1 zsh 效能分析
18.1.1.1 問題背景
當你開啟新的終端視窗時,如果發現 shell 啟動緩慢(超過 1 秒),這會嚴重影響工作效率。問題通常來自:
- 載入過多的插件或設定
- 執行耗時的初始化腳本
- 補全系統載入太多檔案
- 不必要的同步網路操作
18.1.1.2 分析方法
使用 zsh 內建的 zprof 模組來分析啟動時各個部分的執行時間。這是一個專門為 zsh 設計的效能分析工具,可以精確測量每個函數和命令的執行時間。
核心概念:
zmodload zsh/zprof載入效能分析模組zprof輸出分析報告,顯示最耗時的函數和命令- 報告按照累積時間(cumulative time)排序
18.1.1.3 進階用法:環境變數控制
為避免每次啟動都輸出分析報告,可以用環境變數來控制是否啟用效能分析。
方法說明:
${ZSH_DEBUGRC+1}是 zsh 參數擴展語法,當變數已設定時回傳 1-n檢查字串是否非空- 只有在明確設定
ZSH_DEBUGRC時才執行zprof
18.1.1.4 實際使用
當需要分析時,設定環境變數並啟動 zsh:
延伸說明:
-i啟動互動式 shell(載入完整設定)-c exit載入完成後立即退出- 這樣可以看到完整的啟動分析報告
18.1.2 測量啟動時間
18.1.2.1 問題背景
zprof 提供詳細的函數層級分析,但有時你只想知道總體啟動時間是否符合預期。特別是在優化後,需要一個簡單的方式來驗證改善效果。
18.1.2.2 測量方法
使用系統的 time 命令測量 shell 完整啟動到退出的時間。多次測量可以得到更準確的平均值,避免單次測量的偶然誤差。
核心概念:
/usr/bin/time使用系統的 time(不是 shell 內建的)- 多次測量(這裡是 4 次)以獲得代表性數據
- 觀察 real time(實際經過的時間)
18.1.2.3 效能目標與優化方向
目標:控制在 200 毫秒以內
常見優化策略:
- 延遲載入:將不常用的功能延遲到實際使用時才載入
- 快取機制:使用 zsh-defer 或 compinit 的 dump 檔案
- 精簡插件:移除不必要的插件,只保留真正需要的
- 優化順序:將快速的初始化放前面,耗時的放後面
- 並行載入:某些操作可以背景執行(如更新檢查)
除錯流程:
- 執行
timezsh看總時間 - 如果超過 200 ms,執行
ZSH_DEBUGRC=1 zsh -i -c exit - 查看
zprof輸出,找出最耗時的部分 - 針對性優化(延遲載入、移除、或改用更快的替代方案)
- 重複測量直到達標
18.2 Neovim 效能分析
18.2.1 啟動時間分析
18.2.1.1 問題背景
Neovim 啟動緩慢通常是因為:
- 載入過多插件(特別是同步載入)
- 插件未正確延遲載入(lazy loading)
- 複雜的初始化腳本
- 過多的自動命令(autocmd)
- LSP 伺服器立即啟動
一個優化良好的 Neovim 配置應該在 100 ms 內啟動完成。
18.2.1.2 分析方法
Neovim 內建 --startuptime 選項,可以記錄啟動過程中每個步驟的時間消耗。這會產生一個詳細的時間軸,顯示:
- 每個檔案載入的時間
- 每個插件初始化的時間
- 自動命令執行的時間
- 累積時間
核心概念:
--startuptime指定輸出檔案-c exit啟動後立即退出tail -20查看最後 20 行(最耗時的部分)
18.2.1.3 實用別名
為了方便反覆測試,可以設定一個別名:
延伸用法:
- 查看完整報告:
cat startup.log - 搜尋特定插件:
grep "plugin_name" startup.log - 排序最耗時項目:
sort -k2 -n startup.log | tail -20
優化建議:
- 延遲載入:使用 lazy.nvim 的
lazy = true和事件觸發 - 精簡插件:移除不常用的插件
- 優化 autocmd:減少自動命令,使用
pattern限制範圍 - 延遲 LSP:LSP 可以在 BufEnter 時才啟動
18.2.2 插件載入分析
18.2.2.1 問題背景
即使使用了延遲載入,某些插件可能因為配置不當而在啟動時載入,或是載入後執行耗時的初始化操作。需要一個工具來監控插件的實際載入時間。
18.2.2.2 分析方法
如果你使用 lazy.nvim 插件管理器,它內建了效能分析工具。:Lazy profile 會顯示:
- 每個插件的載入時間
- 插件是否已載入
- 載入觸發條件
- 依賴關係
使用技巧:
- 按
<CR>查看插件詳細資訊 - 按
/搜尋特定插件 - 查看
loaded欄位確認延遲載入是否生效
常見問題排查:
- 插件在啟動時載入:檢查是否設定
lazy = false或缺少觸發事件 - 載入時間過長:可能是插件本身效能問題,考慮替代方案
- 依賴鏈問題:某個插件的依賴導致其他插件提前載入
優化流程:
- 執行
:Lazy profile查看已載入的插件 - 找出啟動時載入但不需要的插件
- 為這些插件添加適當的
event、cmd或ft觸發條件 - 重新啟動 Neovim 並驗證改善效果
18.3 程式效能分析
18.3.1 Python 效能分析
18.3.1.1 問題背景
當 Python 程式執行緩慢時,你需要找出瓶頸在哪裡:
- 哪些函數佔用最多 CPU 時間?
- 哪些程式碼行數執行最慢?
- 函數被呼叫了多少次?
- 時間花在計算還是 I/O 操作?
盲目優化往往事倍功半,正確的做法是先測量,找出真正的瓶頸,再針對性優化。
18.3.1.2 分析方法
Python 提供兩個主要的效能分析工具:
18.3.1.2.1 1. cProfile:函數層級分析
核心概念:
cProfile是 Python 標準庫,無需安裝-s cumtime按累積時間排序(包含子函數時間)- 輸出顯示:函數呼叫次數、總時間、平均時間
適用場景:
- 快速找出最耗時的函數
- 了解函數呼叫關係
- 系統層級的效能概覽
輸出解讀:
ncalls:呼叫次數tottime:函數自身時間(不含子函數)cumtime:累積時間(含子函數)percall:平均每次呼叫時間
18.3.1.2.2 2. line_profiler:程式碼行層級分析
核心概念:
- 需要安裝:
pip install line-profiler - 用
@profile裝飾器標記要分析的函數 -l啟用行層級分析,-v顯示詳細輸出
適用場景:
- 精確定位慢的程式碼行
- 優化迴圈和演算法
- 深入分析特定函數
使用範例:
延伸工具:
memory_profiler:記憶體使用分析py-spy:不需修改程式碼的取樣分析器scalene:CPU + 記憶體 + GPU 分析
18.3.2 Node.js 效能分析
18.3.2.1 問題背景
Node.js 應用程式的效能問題可能來自:
- 事件迴圈阻塞
- 記憶體洩漏
- 非同步操作未正確處理
- CPU 密集型運算
Node.js 內建效能分析工具,可以產生詳細的效能報告。
18.3.2.2 分析方法
使用 Node.js 內建的 V8 profiler 進行效能分析。這是一個兩步驟流程:
步驟 1:收集效能資料
這會產生 isolate-*.log 檔案,包含原始的效能資料。
步驟 2:處理並分析資料
核心概念:
--prof啟用 V8 profiler--prof-process將原始資料轉換為可讀報告- 報告包含:函數執行時間、呼叫堆疊、優化狀態
報告解讀:
[JavaScript]:JavaScript 程式碼執行時間[C++]:Node.js 內部和原生模組時間[Summary]:總體統計[Bottom up]:由下而上的呼叫樹(最耗時的葉節點)
延伸工具:
clinic.js:全面的 Node.js 診斷工具0x:火焰圖視覺化工具- Chrome DevTools:用於瀏覽器和 Node.js 除錯
最佳實務:
- 在接近生產環境的條件下測量
- 執行足夠長的時間以獲得代表性資料
- 多次測量以排除偶然因素
- 優化前後都要測量,驗證改善效果
18.4 系統監控
18.4.1 即時系統監控
18.4.1.1 問題背景
在開發和除錯過程中,你需要即時了解系統資源使用情況:
- 哪個程序佔用最多 CPU?
- 記憶體使用情況如何?
- 是否有程序造成系統負載過高?
- 磁碟 I/O 和網路活動狀態?
傳統的 top 指令功能有限且介面不友善,現代替代工具提供更好的視覺化和互動體驗。
18.4.1.2 監控工具
18.4.1.2.1 傳統工具
基本監控:
18.4.1.2.2 現代替代工具
推薦使用 zenith(Rust 開發):
zenith 優勢:
- 美觀的圖形化介面
- 網路和磁碟 I/O 視覺化
- 更低的資源消耗
- 更直覺的操作方式
使用技巧:
q退出?顯示說明- 方向鍵瀏覽程序列表
其他選項:
18.4.2 磁碟空間分析
18.4.2.1 問題背景
磁碟空間問題常見於:
- 專案依賴套件(node_modules、.venv)累積
- 日誌檔案未清理
- 快取和暫存檔案
- Docker 映像檔和容器
需要快速找出佔用空間的目錄和檔案。
18.4.2.2 分析工具
18.4.2.2.1 命令列工具
傳統指令:
df -h:顯示磁碟分割區使用情況(human-readable)du -sh *:顯示當前目錄下所有項目的大小
進階用法:
du -sh * | sort -h:按大小排序du -h --max-depth=1 | sort -h:只看一層目錄
18.4.2.2.2 互動式視覺化工具
推薦使用 diskonaut:
diskonaut 特色:
- 互動式樹狀圖視覺化
- 方向鍵導航目錄
d刪除檔案/目錄- 即時更新大小資訊
使用技巧:
- 在專案根目錄執行
diskonaut - 快速找出
node_modules、.git等大目錄 - 直接刪除不需要的檔案
其他選項:
18.4.3 網路連線監控
18.4.3.1 問題背景
開發時需要監控網路活動:
- 哪些埠號正在監聽?
- 哪個程序在使用網路?
- 即時網路流量如何?
- 是否有可疑的連線?
這對於除錯伺服器應用程式和檢查安全性特別重要。
18.4.3.2 監控方法
18.4.3.2.1 查看連線和監聽埠號
基本指令:
核心概念:
-a:顯示所有連線和監聽埠-n:用數字顯示位址和埠號grep LISTEN:只看監聽中的埠
macOS 替代指令:
lsof-i -P | grep LISTEN:更詳細的資訊lsof -i :8080:查看特定埠號的使用情況
18.4.3.2.2 即時流量監控
推薦使用 bandwhich:
bandwhich 功能:
- 即時顯示各程序的網路使用量
- 顯示遠端連線位址
- 區分上傳和下載流量
- 需要 root 權限(使用
sudo)
使用技巧:
- 按
Tab切換不同檢視 - 找出耗用頻寬的程序
- 監控背景更新和同步
延伸工具:
18.5 除錯技巧
18.5.1 Shell script 除錯
18.5.1.1 問題背景
Shell script 除錯困難的原因:
- 沒有內建除錯器
- 錯誤訊息常常不明確
- 變數作用域和引號問題
- 管線和重導向容易出錯
- 指令執行失敗但 script 繼續執行
需要一套系統性的方法來追蹤 script 執行和捕捉錯誤。
18.5.1.2 除錯方法
使用 set 內建命令啟用 shell 的除錯和安全模式。這些選項可以幫助你快速發現問題。
核心選項:
選項說明:
-x(xtrace):在執行前印出每個命令(會展開變數)- 每行前面會有
+符號 - 可以看到實際執行的命令
- 對除錯管線和變數展開特別有用
- 每行前面會有
-e(errexit):任何命令回傳非零值時立即退出- 避免錯誤累積
- 早期發現問題
- 注意:管線中只檢查最後一個命令
-u(nounset):使用未定義變數時報錯- 捕捉變數名稱拼寫錯誤
- 避免空字串造成的錯誤
- 提高 script 穩定性
組合使用:
除錯技巧:
部分啟用:只在需要的部分使用
set -x追蹤特定變數:
捕捉錯誤位置:
18.5.2 逐步執行除錯
18.5.2.1 問題背景
有時你需要逐行檢查 script 的執行狀態,特別是處理複雜邏輯或除錯難以重現的問題時。Shell 沒有像其他語言的互動式除錯器,但可以用 trap 來模擬。
18.5.2.2 實作方法
使用 trap 命令在每個命令執行後暫停,讓你可以檢查當下的狀態。
核心概念:
trap捕捉特殊訊號或事件DEBUG是特殊訊號,在每個命令前觸發read -p顯示提示並等待使用者輸入
進階用法:
實用技巧:
- 臨時啟用:在需要的部分插入 trap
- 檢查變數:在每步查看關鍵變數的值
- 驗證條件:確認 if/while 條件是否如預期
取消 trap:
18.5.3 Log 檔案分析
18.5.3.1 問題背景
應用程式執行時產生大量 log,需要有效的方法來:
- 即時追蹤新的 log 訊息
- 快速搜尋特定錯誤
- 統計錯誤類型和頻率
- 找出異常模式
手動查看 log 檔案效率太低,需要使用工具來自動化分析。
18.5.3.2 分析方法
18.5.3.2.1 即時追蹤 log
基本指令:
核心概念:
tail -f持續顯示檔案新增的內容(follow)- 適合監控正在執行的應用程式
Ctrl+C停止追蹤
進階用法:
tail -f -n 50 app.log:從最後 50 行開始tail -f *.log:同時追蹤多個檔案less +F app.log:可以暫停和搜尋(按Ctrl+C暫停,F繼續)
18.5.3.2.2 搜尋錯誤訊息
使用 ripgrep(推薦):
核心概念:
rg(ripgrep) 比 grep 更快-i忽略大小寫"error|fail"正則表達式,搜尋包含 error 或 fail 的行
進階搜尋:
rg -A 3 -B 3 "ERROR":顯示前後 3 行(context)rg -t python "exception":只搜尋 Python 檔案rg --stats "error":顯示統計資訊
18.5.3.2.3 統計錯誤類型
分析錯誤分布:
命令拆解:
grep "ERROR" app.log:找出所有錯誤行cut -d: -f2:用:分割,取第 2 個欄位(錯誤訊息)sort:排序相同的錯誤訊息uniq -c:計算每種錯誤的出現次數sort -rn:按次數反向排序(-r: reverse, -n: numeric)
實用變體:
Log 分析最佳實務:
- 結構化 log:使用 JSON 格式更易分析
- Log 層級:區分 DEBUG、INFO、WARNING、ERROR、CRITICAL
- Log 輪替:避免檔案過大,使用 logrotate
- 集中管理:考慮使用 Loki、ELK 等 log 聚合工具
18.6 效能優化檢查清單
18.6.1 開發環境效能標準
以下是建議的效能目標,用來確保流暢的開發體驗:
18.6.1.1 Shell 環境
-
- 測量方法:使用
timezsh函數 - 優化重點:延遲載入、快取、精簡插件
- 測量方法:使用
-
- 檢查
.zcompdump是否定期更新 - 確認 compinit 使用快取模式
- 檢查
-
- 網路請求應該背景執行或快取
- 避免啟動時檢查更新
18.6.1.2 編輯器效能
-
- 測量方法:
nvim --startuptime startup.log -c exit - 關鍵指標:total time in last line
- 測量方法:
-
- 使用
:Lazy profile檢查 - 確認非必要插件有觸發條件(event、cmd、ft)
- LSP 不應在啟動時立即載入
- 使用
-
- 輸入無延遲
- 檔案切換 < 100 ms
- 補全彈出 < 200 ms
18.6.1.3 系統資源
-
- 閒置時 < 5%
- 編輯時 < 30%
-
- Neovim < 200 MB(含 LSP)
- Terminal < 100 MB
-
- 定期清理 node_modules、快取
- 使用 diskonaut 找出大型目錄
18.6.2 效能優化流程
- 建立基準線:記錄當前效能數據
- 識別瓶頸:使用分析工具找出問題
- 單一變數優化:一次只改一個設定
- 測量改善:驗證優化效果
- 記錄結果:保留優化前後的數據
18.7 實作練習
以下練習幫助你建立效能分析和優化的實務經驗。
18.7.1 練習 1:測量 shell 啟動時間
目標:了解當前 shell 效能並找出瓶頸
步驟:
建立測量函數:
執行測量並記錄結果
如果超過 200 ms,啟用分析:
查看
zprof輸出,找出最耗時的 5 個項目針對這些項目進行優化(延遲載入、快取、移除)
預期成果:
- 知道啟動時間和主要瓶頸
- 至少完成一項優化
- 能夠測量優化前後的差異
18.7.2 練習 2:分析 Neovim 啟動瓶頸
目標:優化編輯器啟動速度
步驟:
測量啟動時間:
檢查插件載入狀態:
- 開啟 Neovim
- 執行
:Lazy profile - 找出啟動時載入的插件
為非必要插件設定延遲載入:
重新測量並比較
預期成果:
- 啟動時間降至 100 ms 以下
- 了解哪些插件應該延遲載入
- 掌握 lazy.nvim 的載入策略
18.7.3 練習 3:設定系統監控工具
目標:建立日常開發的監控環境
步驟:
安裝現代監控工具:
建立方便的別名:
實際使用場景:
- 用
ztop監控開發伺服器的資源使用 - 用
disk找出大型 node_modules 並清理 - 用
net檢查哪些程序在使用網路
- 用
預期成果:
- 能快速檢查系統狀態
- 發現並解決至少一個資源問題
- 熟悉各工具的操作方式
18.7.4 進階挑戰
- 完整效能審計:對整個開發環境進行全面分析和優化
- 建立監控儀表板:使用 tmux + zenith 建立即時監控面板
- 自動化效能測試:建立 CI 流程定期檢查啟動時間
先測量,再優化。
- 不要憑感覺優化,要用數據說話
- 優化前後都要測量,證明改善效果
- 關注使用者體驗,而非純粹的數字
- 80/20 法則:20% 的優化帶來 80% 的改善
過早優化是萬惡之源—Donald Knuth
先讓它能動,再讓它正確,最後才讓它快速。
- 過度優化:花太多時間在微小的改善
- 忽略測量:沒有數據支持的優化
- 破壞功能:為了速度犧牲正確性
- 不可重現:單次測量結果不可靠
- 錯誤歸因:誤判真正的瓶頸
記住:可工作的慢系統勝過不可工作的快系統。