用 PerfView 與 dotnet-trace 找出「變慢」的原因 ── .NET 效能調查實務入門
· 小村 豪 · PerfView, dotnet-trace, ETW, 效能調查, .NET, CSharp, 缺陷調查, Windows 開發, 技術諮詢
前一篇〈Windows 事件記錄・ETW 入門〉談到了 ETW 是什麼,以及用 EventSource 定義自訂事件的做法,並把用 PerfView 分析的部分列為「範圍之外」。這篇文章就是那篇的續集。主題是當商用應用程式「變慢」「CPU 卡住不動」「偶爾當掉」時,如何用 PerfView 與 dotnet-trace 找到原因所在的函式。
補充說明,本文的主題是 CPU 與回應時間的調查。記憶體持續增加的問題判斷方式,請見〈在 .NET 中區分 GC 等待與記憶體洩漏〉;當機的問題則整理在〈用 WinDbg + SOS 讀取當機傾印檔〉。
1. 先講結論
- 「變慢」有兩種。CPU 被用到滿而變慢(CPU bound),以及 CPU 明明很閒卻被拖住而變慢(阻塞)。 前者需要 CPU 取樣,後者需要 ThreadTime(內容切換)收集,光看預設的 CPU 收集是看不到後者的原因的。1
- 不確定該用哪個時,先從 dotnet-trace 開始。 它是以 EventPipe 為基礎的跨平台工具,只要是啟動目標處理程序的同一個使用者,就能不需要系統管理員權限進行收集。2 收集到的
.nettrace可以用 PerfView 或 Visual Studio 開啟。3 - 需要用到 PerfView 的時機,是想看整台機器、原生程式碼、甚至阻塞時間的時候。 因為是以 ETW 為基礎,所以也能處理核心事件與原生程式碼的堆疊。相對地,啟動 ETW 工作階段需要系統管理員權限。3
- CPU 取樣預設每 1 毫秒(依處理器)收集一次。 把 1 個樣本讀成大約 1ms 的 CPU 時間,並以總樣本數達到 1,000 個以上(最好接近 5,000 個)作為判斷的參考基準。樣本數太少時,排名靠前的函式可能只是偶然。1
- 解讀數字時,一切都從 inclusive(自己+所有被呼叫的部分)與 exclusive(自己本身)的區分開始。 exclusive 較大的函式是「實際上在消耗 CPU 的地方」,inclusive 較大的函式則表示「其下某處很吃重」。
- 測量之前,先把「什麼變慢了」固定成單一操作。 用「整體感覺很慢」的狀態去收集,是讀不出結果的。要先把操作與時間固定下來,例如「這份報表輸出要花 40 秒」,然後才開始收集。改善前後比較測量的思路,也可以參考〈在 Windows 上正確比較不同版本程式速度的方法〉。
依症狀整理該從哪裡下手的判斷表如下。
| 症狀 | 先用的工具 | 要看的東西 |
|---|---|---|
| CPU 一直卡在高位 | dotnet-trace collect / PerfView 的 CPU Stacks | exclusive 較大的函式 |
| CPU 很低但處理很慢、會卡住 | PerfView(ThreadTime 收集) | 哪個執行緒在等什麼而被阻塞 |
| 變慢了,但先想知道大致趨勢 | dotnet-counters | CPU 使用率、GC 頻率、ThreadPool 佇列堵塞 |
| 懷疑 GC 太多 | dotnet-counters → dotnet-trace(GC 事件) | GC 次數・停頓時間、配置較多的地方 |
| 想知道特定業務處理中哪裡慢 | EventSource 自訂事件+以上方法 | 自訂事件之間的經過時間,以及該區間的堆疊 |
2. 整理工具之間的關係 ── ETW 與 EventPipe
登場的工具看起來很多,但基礎其實只有兩種。
- ETW(Event Tracing for Windows):貫穿整個作業系統的追蹤基礎架構。可以用同一條時間軸從核心一路記錄到應用程式,但工作階段的啟動與停止需要系統管理員權限,且是 Windows 專屬。3
- EventPipe:內建於 .NET 執行環境中的追蹤機制。不需要系統管理員權限,在所有作業系統上行為一致,但代價是能看到的範圍僅限於受控程式碼與執行環境,無法取得核心事件或原生堆疊。2
| dotnet-trace(EventPipe) | PerfView(ETW) | |
|---|---|---|
| 系統管理員權限 | 不需要(針對同一使用者的處理程序) | 需要 |
| 對象 | 指定的單一 .NET 處理程序 | 整台機器(所有處理程序+核心) |
| 原生堆疊 | 無法取得 | 可以取得 |
| 阻塞時間(內容切換) | 無法取得 | 可透過 ThreadTime 收集取得 |
| 支援作業系統 | Windows / Linux / macOS | 僅 Windows |
只要把這張對照表記在腦中,「先用 dotnet-trace 輕量收集,不夠的話再用 PerfView 連 ETW 一起收集」這種取捨自然就會浮現。
3. 收集之前 ── 先用 dotnet-counters 看 10 分鐘的趨勢
比起立刻收集追蹤資料,先用 dotnet-counters 瀏覽執行環境的主要指標,往往更快。4
dotnet tool install --global dotnet-counters
dotnet-counters ps
dotnet-counters monitor --process-id <PID> --counters System.Runtime
這裡要看的是 CPU 使用率、GC 堆積大小與 GC 次數、ThreadPool 的佇列長度與執行緒數、例外次數。如果這個階段就看出「GC 每秒跑好幾次」「ThreadPool 佇列持續變長」之類的趨勢,就能縮小接下來該收集哪種追蹤資料的範圍(是配置問題,還是阻塞問題)。關於記憶體相關趨勢的讀法,詳見〈區分 GC 等待與記憶體洩漏的方法〉一文。
4. 用 dotnet-trace 收集追蹤資料
dotnet-trace 是以 EventPipe 為基礎的收集工具。5 安裝與基本收集方式如下。
dotnet tool install --global dotnet-trace
dotnet-trace ps
dotnet-trace collect --process-id <PID> --duration 00:00:00:30
如果沒有指定任何選項,就會用包含執行環境主要事件與執行緒取樣的預設設定檔進行收集。過去存在的 cpu-sampling 這個設定檔已經廢除,現在的預設是 dotnet-common 與 dotnet-sampled-thread-time 的組合。5 依用途也可以選擇 --profile gc-verbose(GC 與物件配置取樣)或 --profile gc-collect(只以低負擔記錄 GC 的發生)。
如果要一併收集上一篇文章中建立的 EventSource 自訂提供者,加上 --providers 即可。
dotnet-trace collect --process-id <PID> --providers KomuraSoft-OrderService
收集結果會是 .nettrace 檔案。開啟方式有三種:用 Visual Studio 或 PerfView 開啟,或是轉換成 speedscope 格式在瀏覽器中查看。5
dotnet-trace convert trace.nettrace --format Speedscope
speedscope 格式是可以在 speedscope.app 開啟的輕量火焰圖,適合直覺地瀏覽「時間都花在哪個函式的下層」。若要精確到函式層級的細緻分析,PerfView 的堆疊檢視器更為強大,接下來就進入下一章。
5. 用 PerfView 調查 CPU ── 堆疊檢視器的讀法
PerfView 是 Microsoft 免費公開的效能調查工具,只要從 GitHub 發行頁面下載一個 PerfView.exe 就能執行(不需安裝)。6
5.1 收集資料
在 GUI 中,基本流程是用「Collect > Collect」開啟對話框,Start Collection → 重現目標操作 → Stop Collection。若用命令列,形式如下(因為要啟動 ETW 工作階段,需以系統管理員身分執行 3)。
PerfView collect /nogui /acceptEULA /maxCollectSec:30
預設收集包含所有處理器上每 1 毫秒一次的 CPU 樣本(含呼叫堆疊),以及 CLR 的主要事件。1 收集期間的額外負擔,在預設設定下大致是百分之幾的程度。7
5.2 讀取 CPU Stacks
開啟收集結果(.etl.zip),在「CPU Stacks」選擇目標處理程序,就會開啟堆疊檢視器。最先要看的是 By Name 分頁中的兩個欄位。
- Exc(exclusive):該函式本身正在執行時的樣本數。是實際在消耗 CPU 的地方。
- Inc(inclusive):該函式與其呼叫的所有內容加總的樣本數。表示其下某處很吃重。
基本讀法是從 Exc 排名最高的開始,判斷「這是自己的程式碼,還是執行環境・函式庫」。如果自己的程式碼出現在 Exc 排名前面,那個函式的演算法或迴圈就是直接的目標。如果是 System.String 系列或 JSON 序列化器排在 Exc 前面,就在 Callers 分頁用 Inc 往回追,找出自己的程式碼中是從哪裡大量呼叫它的。
另一個重要的檢查點是樣本數本身。CPU 樣本是每 1ms 間隔的統計資料,樣本數太少就容易受偶然影響。參考標準是總數要達到 1,000 個以上,最好接近 5,000 個再判斷,如果不夠就反覆執行目標操作以延長收集時間。1
另外,如果自家模組的函式名稱顯示成位址,代表符號(PDB)沒有被解析出來。手邊準備好建置產物的 PDB,這件事本身就決定了能否進行調查。這個話題在〈什麼是 PDB(程式資料庫)〉中有詳細說明。
6. 「CPU 明明很閒卻很慢」 ── 用 ThreadTime 看阻塞時間
實務上一半以上的「變慢」,原因不在 CPU,而是等待。等鎖、等資料庫或 HTTP 回應、檔案 I/O、因 Task.Result 而近乎死結的等待。這類問題在 CPU 樣本中,只會顯示成「本來就沒在用 CPU 的時間」,因此預設收集看不出原因。
在 PerfView 中,收集時開啟 Thread Time 選項,就會額外記錄內容切換事件,讓每個執行緒「使用 CPU 的時間」和「被阻塞的時間」可以一起追蹤。1
PerfView collect /nogui /acceptEULA /threadTime /maxCollectSec:30
分析時開啟「Thread Time Stacks」檢視。它和 CPU Stacks 是同一個堆疊檢視器,差別在於樣本除了 CPU 時間之外,還包含 BLOCKED_TIME(被阻塞的時間)。把範圍縮小到慢速操作的時間區段後,觀察負責處理的執行緒的 BLOCKED_TIME 累積在哪個堆疊(哪種等待)上,就能得出像「總共 40 秒裡有 35 秒是在等這個 HTTP 呼叫」這樣的細項。
UI 卡住的症狀(操作後數秒沒有回應)的本質,幾乎都是 UI 執行緒被阻塞。用 ThreadTime 找出 UI 執行緒卡在哪裡之後,長久的對策就是把同步等待改為非同步設計。這方面的設計整理,請參考〈用一張表整理 WPF/WinForms 的 async 與 UI 執行緒〉。
要注意的是,ThreadTime 收集會針對每一次內容切換記錄事件,資料量會明顯比預設收集更大。收集時間要切短一點,正式環境中應避免長時間收集。
7. 與 EventSource 組合使用 ── 用業務語言切出「哪個區段慢」
CPU Stacks 和 Thread Time 都是整個處理程序的彙總結果。如果想以業務處理的單位切分,像是「一筆訂單處理中哪裡慢」,上一篇文章提到的 EventSource 的 Start/Stop 事件就能發揮作用。
- 在應用程式端計測像
OrderProcessingStart/OrderProcessingStop這樣的事件(實作方式請參考上一篇文章) - 在 PerfView 的「Events」檢視中確認該事件發生的時間,並把堆疊檢視器的時間範圍(Start/End 篩選器)收窄到那個區段
- 在收窄後的範圍內讀取 CPU/阻塞時間的細項
這樣就能只針對「那一次變慢」進行分析,不會被平常的雜訊淹沒。自訂事件也能用 dotnet-trace 以同樣的方式收集,因此只要加入一次計測,開發機和正式環境都能用同樣的調查方法。效能調查的難易度,實務上的體感是取決於「應用程式端有沒有觀測點」,而不是操作工具的熟練度。
總結
- 出發點是把「變慢」分成 CPU bound 和阻塞兩種。前者只有 CPU 取樣(1 個樣本≒1ms)能看到,後者只有 ThreadTime 收集能看到。
- 基本的分工是:先用不需要系統管理員權限的 dotnet-trace,需要整台機器・原生程式碼・阻塞時間時再用 PerfView。收集完的
.nettrace再用 PerfView 讀取,這種併用方式也很常見。 - 堆疊檢視器的核心讀法在於區分 exclusive 與 inclusive,並要隨時確認樣本數是否足夠。
- 事先用 EventSource 自訂事件切出業務處理的區段,能讓調查的精確度再提升一個層級。
合同會社小村軟體(KomuraSoft)處理「變慢」「當掉」「CPU 卡住」等 Windows 商用應用程式的效能問題調查,以及為了方便測量而進行的計測・設計相關諮詢。如果已經有重現步驟與追蹤檔案,也可以只委託後續的分析工作。
相關文章
- Windows 事件記錄・ETW 入門 ── 把商用應用程式的記錄搭上作業系統標準機制
- 在 .NET 中區分 GC 等待與記憶體洩漏 ── 觀測、比較、證明持續增加的記憶體的實務程序
- 用 WinDbg + SOS 讀取當機傾印檔 ── 收集完之後的實務分析入門
- Windows 當機傾印檔收集入門 - WER/ProcDump/WinDbg
- 什麼是 PDB(程式資料庫) ── 理解偵錯資訊、符號與 Source Link
- 在 Windows 上正確比較不同版本程式速度的方法
- 用一張表整理 WPF/WinForms 的 async 與 UI 執行緒
相關諮詢領域
參考連結
-
GitHub, PerfView User’s Guide. 說明預設收集是每個處理器每 1 毫秒間隔的 CPU 樣本(含呼叫堆疊)、判斷時建議收集 1,000~5,000 個左右的樣本、以及 Thread Time 選項可以收集內容切換以分析阻塞時間(也可從 PerfView 本身的 Help 中參考)。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, EventPipe Overview. 說明 EventPipe 是不依賴系統管理員權限等高權限元件即可追蹤 .NET 應用程式的機制,以及對象僅限於受控程式碼與執行環境,無法取得核心事件或原生堆疊。 ↩ ↩2
-
Microsoft Learn, Collect and View EventSource Traces. 說明 ETW 追蹤收集永遠需要系統管理員權限,以及 PerfView 與 Visual Studio 都能開啟 .nettrace 檔案。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, dotnet-counters diagnostic tool. 說明透過 System.Runtime 計數器監控執行中處理程序的 CPU、GC、ThreadPool 等指標。 ↩
-
Microsoft Learn, dotnet-trace diagnostic tool. 說明 collect 指令的用法、預設啟用的設定檔(dotnet-common / dotnet-sampled-thread-time)與 cpu-sampling 設定檔的廢除、gc-verbose / gc-collect 等設定檔,以及轉換成 Speedscope/Chromium 格式。 ↩ ↩2 ↩3
-
GitHub, microsoft/perfview. 說明 PerfView 是用於調查 CPU、記憶體相關效能問題的免費效能分析工具,以及可以直接從發行頁面取得執行檔。 ↩
-
GitHub, microsoft/perfview Issue #598. 說明 PerfView 維護者對於預設收集時額外負擔大致在 3% 左右的解釋,以及可以用 /CpuSampleMSec 調整取樣間隔以降低負擔。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
Windows 事件記錄・ETW 入門 ── 讓業務應用程式的日誌搭上 OS 標準機制
Windows 業務應用程式的日誌,是不是只靠檔案日誌解決?事件記錄與 ETW,是維運人員與 OS 標準工具能看到的另一個層級的記錄。本文說明三種手段的分工、.NET 的寫入方式、透過 EventSource 進行 ETW 儀器化、收集與調查的實務,以及來源未註冊、日誌肥大...
不只是 appsettings.json ── Windows 商用應用程式的組態管理實務(環境別設定・機密資訊・寫入位置)
本文從實務角度整理 Windows 商用應用程式的組態管理,涵蓋 appsettings.json 的分層、IConfiguration 與 IOptions/IOptionsSnapshot/IOptionsMonitor 的選用方式、環境別設定、可寫入設定的存放位置、機...
Windows 業務應用程式的列印與 PDF 輸出 ── System.Drawing.Printing / WPF / 報表函式庫的取捨
本文以要件別判斷表整理 PrintDocument 的 WinForms 列印、WPF 的 FlowDocument/FixedDocument 列印,以及 PDF 輸出的各種選項。並從實務角度說明分頁控制、DPI 落差等實作陷阱,以及從 Windows 服務列印、依賴預設...
用 WinDbg + SOS 解讀當機傾印檔 ── 收集之後的實務分析入門
本文說明如何用 WinDbg 與 SOS 擴充功能實際讀取已收集的 Windows 當機傾印檔。內容涵蓋符號路徑設定、以 !clrstack・!pe・!dumpheap -stat・!gcroot 追蹤例外與記憶體洩漏的方法、原生當機的 !analyze -v,以及與 do...
防止 Windows 應用程式重複啟動 ── 具名 Mutex 與重複執行時的視窗前置
整理業務用 Windows 應用程式的常見需求「不讓同一個應用程式啟動兩次」,如何用具名 Mutex 實作。涵蓋 Global\ 與 Local\ 命名空間差異在 RDP 環境中的陷阱、擁有執行緒限制與 AbandonedMutexException、考量 SetForeg...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
技術諮詢 & 設計審查
協助釐清設計方向、架構邊界、生命週期責任,以及既有 Windows 資產的處理方式。
常見問題
整理諮詢這個主題時常見的問題。
- PerfView 和 dotnet-trace,該用哪一個?
- 建議先從 dotnet-trace 開始嘗試。它不需要系統管理員權限就能使用,可以只針對目標處理程序收集,操作也只要一行指令就能完成。另一方面,如果想看到原生程式碼的堆疊、想看整台機器的狀況(其他處理程序或核心的影響),或是想以內容切換為單位追蹤阻塞時間,就需要以 ETW 為基礎的 PerfView。把收集到的 .nettrace 檔案用 PerfView 開啟來分析,這種併用方式也很常見。
- 和 Visual Studio 的分析器是什麼關係?
- 如果問題能在開發機上重現,Visual Studio 的 CPU 使用率工具或記憶體工具會更方便,先用那些通常就足夠了。PerfView 或 dotnet-trace 能發揮作用的場合,是在無法安裝 Visual Studio 的驗證機、正式機上收集資料時,想把收集與分析分到不同機器上時,或是想連 ETW 事件(自訂 EventSource 或核心事件)都一起看的時候。順帶一提,Visual Studio 也能開啟 dotnet-trace 輸出的 .nettrace 檔案。
- 在正式環境的伺服器上執行也沒問題嗎?
- 這兩個工具都是以正式環境中的短時間收集為前提設計的,但並非毫無條件。請依照以下步驟:把收集時間限制在數十秒到數分鐘之間,先在驗證環境用相同指令測試以確認額外負擔與檔案大小,並確認磁碟有足夠的可用空間。特別是 ThreadTime(內容切換)收集或配置追蹤,事件量很大,檔案會迅速變大。此外,追蹤內容中會包含命令列引數等資訊,因此對取得的檔案也要謹慎處理。
- WPR/WPA 該如何區分使用?
- WPR(Windows Performance Recorder)和 WPA(Windows Performance Analyzer)是以同樣的 ETW 為基礎、更偏向作業系統層面的調查工具。在磁碟 I/O、電源、開機時間等作業系統全面的詳細分析上,WPR/WPA 更強大;而在 .NET 應用程式的 CPU、GC、阻塞時間調查上,針對受控程式碼顯示最佳化的 PerfView 更容易閱讀。如果目的是調查商用應用程式,PerfView 與 dotnet-trace 大部分情況都已經足夠。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。