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)排序
# 在 .zshrc 開頭加入
zmodload zsh/zprof

# 在 .zshrc 結尾加入
zprof

18.1.1.3 進階用法:環境變數控制

為避免每次啟動都輸出分析報告,可以用環境變數來控制是否啟用效能分析。

方法說明:

  • ${ZSH_DEBUGRC+1} 是 zsh 參數擴展語法,當變數已設定時回傳 1
  • -n 檢查字串是否非空
  • 只有在明確設定 ZSH_DEBUGRC 時才執行 zprof
if [ -n "${ZSH_DEBUGRC+1}" ]; then
    zprof
fi

18.1.1.4 實際使用

當需要分析時,設定環境變數並啟動 zsh:

ZSH_DEBUGRC=1 zsh -i -c exit

延伸說明:

  • -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(實際經過的時間)
function timezsh() {
    for i in $(seq 1 4); do
        /usr/bin/time $SHELL -i -c exit
    done
}

18.1.2.3 效能目標與優化方向

目標:控制在 200 毫秒以內

常見優化策略:

  1. 延遲載入:將不常用的功能延遲到實際使用時才載入
  2. 快取機制:使用 zsh-defer 或 compinit 的 dump 檔案
  3. 精簡插件:移除不必要的插件,只保留真正需要的
  4. 優化順序:將快速的初始化放前面,耗時的放後面
  5. 並行載入:某些操作可以背景執行(如更新檢查)

除錯流程:

  1. 執行 timezsh 看總時間
  2. 如果超過 200 ms,執行 ZSH_DEBUGRC=1 zsh -i -c exit
  3. 查看 zprof 輸出,找出最耗時的部分
  4. 針對性優化(延遲載入、移除、或改用更快的替代方案)
  5. 重複測量直到達標

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 行(最耗時的部分)
nvim --startuptime startup.log -c exit
tail -20 startup.log

18.2.1.3 實用別名

為了方便反覆測試,可以設定一個別名:

alias nstart="nvim --startuptime startup.log -c exit && tail -100 startup.log"

延伸用法:

  • 查看完整報告:cat startup.log
  • 搜尋特定插件:grep "plugin_name" startup.log
  • 排序最耗時項目:sort -k2 -n startup.log | tail -20

優化建議:

  1. 延遲載入:使用 lazy.nvim 的 lazy = true 和事件觸發
  2. 精簡插件:移除不常用的插件
  3. 優化 autocmd:減少自動命令,使用 pattern 限制範圍
  4. 延遲 LSP:LSP 可以在 BufEnter 時才啟動

18.2.2 插件載入分析

18.2.2.1 問題背景

即使使用了延遲載入,某些插件可能因為配置不當而在啟動時載入,或是載入後執行耗時的初始化操作。需要一個工具來監控插件的實際載入時間。

18.2.2.2 分析方法

如果你使用 lazy.nvim 插件管理器,它內建了效能分析工具。:Lazy profile 會顯示:

  • 每個插件的載入時間
  • 插件是否已載入
  • 載入觸發條件
  • 依賴關係
:Lazy profile

使用技巧:

  • <CR> 查看插件詳細資訊
  • / 搜尋特定插件
  • 查看 loaded 欄位確認延遲載入是否生效

常見問題排查:

  1. 插件在啟動時載入:檢查是否設定 lazy = false 或缺少觸發事件
  2. 載入時間過長:可能是插件本身效能問題,考慮替代方案
  3. 依賴鏈問題:某個插件的依賴導致其他插件提前載入

優化流程:

  1. 執行 :Lazy profile 查看已載入的插件
  2. 找出啟動時載入但不需要的插件
  3. 為這些插件添加適當的 eventcmdft 觸發條件
  4. 重新啟動 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 按累積時間排序(包含子函數時間)
  • 輸出顯示:函數呼叫次數、總時間、平均時間

適用場景:

  • 快速找出最耗時的函數
  • 了解函數呼叫關係
  • 系統層級的效能概覽
# 使用 cProfile
python -m cProfile -s cumtime script.py

輸出解讀:

  • ncalls:呼叫次數
  • tottime:函數自身時間(不含子函數)
  • cumtime:累積時間(含子函數)
  • percall:平均每次呼叫時間
18.3.1.2.2 2. line_profiler:程式碼行層級分析

核心概念:

  • 需要安裝:pip install line-profiler
  • @profile 裝飾器標記要分析的函數
  • -l 啟用行層級分析,-v 顯示詳細輸出

適用場景:

  • 精確定位慢的程式碼行
  • 優化迴圈和演算法
  • 深入分析特定函數
# 使用 line_profiler(需要安裝)
kernprof -l -v script.py

使用範例:

# script.py
@profile
def slow_function():
    result = []
    for i in range(1000000):
        result.append(i * 2)
    return result

延伸工具:

18.3.2 Node.js 效能分析

18.3.2.1 問題背景

Node.js 應用程式的效能問題可能來自:

  • 事件迴圈阻塞
  • 記憶體洩漏
  • 非同步操作未正確處理
  • CPU 密集型運算

Node.js 內建效能分析工具,可以產生詳細的效能報告。

18.3.2.2 分析方法

使用 Node.js 內建的 V8 profiler 進行效能分析。這是一個兩步驟流程:

步驟 1:收集效能資料

# 使用內建 profiler
node --prof app.js

這會產生 isolate-*.log 檔案,包含原始的效能資料。

步驟 2:處理並分析資料

node --prof-process isolate-*.log > processed.txt

核心概念:

  • --prof 啟用 V8 profiler
  • --prof-process 將原始資料轉換為可讀報告
  • 報告包含:函數執行時間、呼叫堆疊、優化狀態

報告解讀:

  • [JavaScript]:JavaScript 程式碼執行時間
  • [C++]:Node.js 內部和原生模組時間
  • [Summary]:總體統計
  • [Bottom up]:由下而上的呼叫樹(最耗時的葉節點)

延伸工具:

最佳實務:

  1. 在接近生產環境的條件下測量
  2. 執行足夠長的時間以獲得代表性資料
  3. 多次測量以排除偶然因素
  4. 優化前後都要測量,驗證改善效果

18.4 系統監控

18.4.1 即時系統監控

18.4.1.1 問題背景

在開發和除錯過程中,你需要即時了解系統資源使用情況:

  • 哪個程序佔用最多 CPU?
  • 記憶體使用情況如何?
  • 是否有程序造成系統負載過高?
  • 磁碟 I/O 和網路活動狀態?

傳統的 top 指令功能有限且介面不友善,現代替代工具提供更好的視覺化和互動體驗。

18.4.1.2 監控工具

18.4.1.2.1 傳統工具

基本監控:

# 基本
top
htop
  • top:系統內建,但介面較陽春
  • htop:改良版,支援滑鼠操作、彩色顯示
18.4.1.2.2 現代替代工具

推薦使用 zenith(Rust 開發):

# 更現代的替代
brew install zenith  # Rust 寫的
alias ztop='zenith'

zenith 優勢:

  • 美觀的圖形化介面
  • 網路和磁碟 I/O 視覺化
  • 更低的資源消耗
  • 更直覺的操作方式

使用技巧:

  • q 退出
  • ? 顯示說明
  • 方向鍵瀏覽程序列表

其他選項:

  • btop:功能豐富,高度可自訂
  • bottom:簡潔高效,類似 htop
  • glances:跨平台,支援 Web 介面

18.4.2 磁碟空間分析

18.4.2.1 問題背景

磁碟空間問題常見於:

  • 專案依賴套件(node_modules、.venv)累積
  • 日誌檔案未清理
  • 快取和暫存檔案
  • Docker 映像檔和容器

需要快速找出佔用空間的目錄和檔案。

18.4.2.2 分析工具

18.4.2.2.1 命令列工具

傳統指令:

# 基本
df -h    # 查看檔案系統使用情況
du -sh * # 查看當前目錄下各項目大小
  • df -h:顯示磁碟分割區使用情況(human-readable)
  • du -sh *:顯示當前目錄下所有項目的大小

進階用法:

  • du -sh * | sort -h:按大小排序
  • du -h --max-depth=1 | sort -h:只看一層目錄
18.4.2.2.2 互動式視覺化工具

推薦使用 diskonaut

# 互動式
brew install diskonaut
diskonaut

diskonaut 特色:

  • 互動式樹狀圖視覺化
  • 方向鍵導航目錄
  • d 刪除檔案/目錄
  • 即時更新大小資訊

使用技巧:

  • 在專案根目錄執行 diskonaut
  • 快速找出 node_modules.git 等大目錄
  • 直接刪除不需要的檔案

其他選項:

  • ncdu:傳統的互動式磁碟分析工具
  • dust:Rust 開發,快速的 du 替代品
  • duf:現代化的 df 替代品

18.4.3 網路連線監控

18.4.3.1 問題背景

開發時需要監控網路活動:

  • 哪些埠號正在監聽?
  • 哪個程序在使用網路?
  • 即時網路流量如何?
  • 是否有可疑的連線?

這對於除錯伺服器應用程式和檢查安全性特別重要。

18.4.3.2 監控方法

18.4.3.2.1 查看連線和監聽埠號

基本指令:

# 查看連線
netstat -an | grep LISTEN

核心概念:

  • -a:顯示所有連線和監聽埠
  • -n:用數字顯示位址和埠號
  • grep LISTEN:只看監聽中的埠

macOS 替代指令:

  • lsof -i -P | grep LISTEN:更詳細的資訊
  • lsof -i :8080:查看特定埠號的使用情況
18.4.3.2.2 即時流量監控

推薦使用 bandwhich

# 監控流量
brew install bandwhich
sudo bandwhich

bandwhich 功能:

  • 即時顯示各程序的網路使用量
  • 顯示遠端連線位址
  • 區分上傳和下載流量
  • 需要 root 權限(使用 sudo

使用技巧:

  • Tab 切換不同檢視
  • 找出耗用頻寬的程序
  • 監控背景更新和同步

延伸工具:

  • iftop:即時網路流量監控
  • nettop:macOS 內建網路監控
  • Wireshark:完整的封包分析工具

18.5 除錯技巧

18.5.1 Shell script 除錯

18.5.1.1 問題背景

Shell script 除錯困難的原因:

  • 沒有內建除錯器
  • 錯誤訊息常常不明確
  • 變數作用域和引號問題
  • 管線和重導向容易出錯
  • 指令執行失敗但 script 繼續執行

需要一套系統性的方法來追蹤 script 執行和捕捉錯誤。

18.5.1.2 除錯方法

使用 set 內建命令啟用 shell 的除錯和安全模式。這些選項可以幫助你快速發現問題。

核心選項:

# 啟用除錯模式
set -x  # 顯示每個命令
set -e  # 錯誤時停止
set -u  # 未定義變數時報錯

# 或在 shebang 中
#!/bin/bash -xeu

選項說明:

  • -x (xtrace):在執行前印出每個命令(會展開變數)
    • 每行前面會有 + 符號
    • 可以看到實際執行的命令
    • 對除錯管線和變數展開特別有用
  • -e (errexit):任何命令回傳非零值時立即退出
    • 避免錯誤累積
    • 早期發現問題
    • 注意:管線中只檢查最後一個命令
  • -u (nounset):使用未定義變數時報錯
    • 捕捉變數名稱拼寫錯誤
    • 避免空字串造成的錯誤
    • 提高 script 穩定性

組合使用:

#!/bin/bash
set -euo pipefail  # 推薦的安全組合

# -o pipefail:管線中任一命令失敗即失敗

除錯技巧:

  1. 部分啟用:只在需要的部分使用 set -x

    set -x
    # 除錯這段
    set +x  # 關閉除錯
  2. 追蹤特定變數

    set -x
    echo "DEBUG: var=$var"
    set +x
  3. 捕捉錯誤位置

    trap 'echo "Error at line $LINENO"' ERR

18.5.2 逐步執行除錯

18.5.2.1 問題背景

有時你需要逐行檢查 script 的執行狀態,特別是處理複雜邏輯或除錯難以重現的問題時。Shell 沒有像其他語言的互動式除錯器,但可以用 trap 來模擬。

18.5.2.2 實作方法

使用 trap 命令在每個命令執行後暫停,讓你可以檢查當下的狀態。

# 使用 trap 在每個命令後暫停
trap 'read -p "Press enter to continue"' DEBUG

核心概念:

  • trap 捕捉特殊訊號或事件
  • DEBUG 是特殊訊號,在每個命令前觸發
  • read -p 顯示提示並等待使用者輸入

進階用法:

# 顯示更多資訊
trap 'read -p "Line $LINENO: Press enter"' DEBUG

# 條件式除錯
trap 'if [[ $DEBUG == 1 ]]; then read -p "Continue?"; fi' DEBUG

# 顯示變數值
trap 'echo "var=$var"; read' DEBUG

實用技巧:

  1. 臨時啟用:在需要的部分插入 trap
  2. 檢查變數:在每步查看關鍵變數的值
  3. 驗證條件:確認 if/while 條件是否如預期

取消 trap

trap - DEBUG  # 移除 DEBUG trap

18.5.3 Log 檔案分析

18.5.3.1 問題背景

應用程式執行時產生大量 log,需要有效的方法來:

  • 即時追蹤新的 log 訊息
  • 快速搜尋特定錯誤
  • 統計錯誤類型和頻率
  • 找出異常模式

手動查看 log 檔案效率太低,需要使用工具來自動化分析。

18.5.3.2 分析方法

18.5.3.2.1 即時追蹤 log

基本指令:

# 即時追蹤
tail -f /var/log/system.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 -i "error|fail" /var/log/*.log

核心概念:

  • 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 | sort | uniq -c | sort -rn

命令拆解:

  1. grep "ERROR" app.log:找出所有錯誤行
  2. cut -d: -f2:用 : 分割,取第 2 個欄位(錯誤訊息)
  3. sort:排序相同的錯誤訊息
  4. uniq -c:計算每種錯誤的出現次數
  5. sort -rn:按次數反向排序(-r: reverse, -n: numeric)

實用變體:

# 統計最常見的錯誤(前 10 個)
grep "ERROR" app.log | cut -d: -f2 | sort | uniq -c | sort -rn | head -10

# 統計每小時錯誤數
grep "ERROR" app.log | cut -d' ' -f1 | cut -d: -f1 | uniq -c

# 找出特定時間範圍的錯誤
rg "2024-01-15.*ERROR" app.log

Log 分析最佳實務:

  1. 結構化 log:使用 JSON 格式更易分析
  2. Log 層級:區分 DEBUG、INFO、WARNING、ERROR、CRITICAL
  3. Log 輪替:避免檔案過大,使用 logrotate
  4. 集中管理:考慮使用 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 效能優化流程

  1. 建立基準線:記錄當前效能數據
  2. 識別瓶頸:使用分析工具找出問題
  3. 單一變數優化:一次只改一個設定
  4. 測量改善:驗證優化效果
  5. 記錄結果:保留優化前後的數據

18.7 實作練習

以下練習幫助你建立效能分析和優化的實務經驗。

18.7.1 練習 1:測量 shell 啟動時間

目標:了解當前 shell 效能並找出瓶頸

步驟

  1. 建立測量函數:

    function timezsh() {
        for i in $(seq 1 4); do
            /usr/bin/time $SHELL -i -c exit
        done
    }
  2. 執行測量並記錄結果

  3. 如果超過 200 ms,啟用分析:

    ZSH_DEBUGRC=1 zsh -i -c exit
  4. 查看 zprof 輸出,找出最耗時的 5 個項目

  5. 針對這些項目進行優化(延遲載入、快取、移除)

預期成果

  • 知道啟動時間和主要瓶頸
  • 至少完成一項優化
  • 能夠測量優化前後的差異

18.7.2 練習 2:分析 Neovim 啟動瓶頸

目標:優化編輯器啟動速度

步驟

  1. 測量啟動時間:

    nvim --startuptime startup.log -c exit
    tail -20 startup.log
  2. 檢查插件載入狀態:

    • 開啟 Neovim
    • 執行 :Lazy profile
    • 找出啟動時載入的插件
  3. 為非必要插件設定延遲載入:

    {
        "plugin-name",
        event = "BufEnter",  -- 或 cmd, ft
    }
  4. 重新測量並比較

預期成果

  • 啟動時間降至 100 ms 以下
  • 了解哪些插件應該延遲載入
  • 掌握 lazy.nvim 的載入策略

18.7.3 練習 3:設定系統監控工具

目標:建立日常開發的監控環境

步驟

  1. 安裝現代監控工具:

    brew install zenith diskonaut bandwhich
  2. 建立方便的別名:

    alias ztop='zenith'
    alias disk='diskonaut'
    alias net='sudo bandwhich'
  3. 實際使用場景:

    • ztop 監控開發伺服器的資源使用
    • disk 找出大型 node_modules 並清理
    • net 檢查哪些程序在使用網路

預期成果

  • 能快速檢查系統狀態
  • 發現並解決至少一個資源問題
  • 熟悉各工具的操作方式

18.7.4 進階挑戰

  1. 完整效能審計:對整個開發環境進行全面分析和優化
  2. 建立監控儀表板:使用 tmux + zenith 建立即時監控面板
  3. 自動化效能測試:建立 CI 流程定期檢查啟動時間
Tip效能優化的黃金原則

先測量,再優化。

  • 不要憑感覺優化,要用數據說話
  • 優化前後都要測量,證明改善效果
  • 關注使用者體驗,而非純粹的數字
  • 80/20 法則:20% 的優化帶來 80% 的改善

過早優化是萬惡之源—Donald Knuth

先讓它能動,再讓它正確,最後才讓它快速。

Warning常見陷阱
  1. 過度優化:花太多時間在微小的改善
  2. 忽略測量:沒有數據支持的優化
  3. 破壞功能:為了速度犧牲正確性
  4. 不可重現:單次測量結果不可靠
  5. 錯誤歸因:誤判真正的瓶頸

記住:可工作的慢系統勝過不可工作的快系統。