--- title: 如何在.Net使用Performance Counter撈取CPU使用量 tags: [DotNet,PerformanceCounter,blog] --- {%hackmd @siugar/blog-article-base-layout %} :arrow_left: [回到部落格首頁](/@siugar/blog-tw) :arrow_right: <a target="_blank" href="/@siugar/PerfCounter">本文單篇連結</a> # 如何在.Net使用Performance Counter撈取CPU使用量 [toc] ## 前言 因為我參與專案的排程程式有一個功能會定期將該程式本身的Cpu Usage寫入log,然後某一天特定一台機器突然大量的程式爆出Exception,顯示使用WMI撈取CPU Usage的程式爆出各類Exception ``` [Case 1] Exception: System.Management.ManagementException: 記憶體不足 於 System.Management.ManagementException.ThrowWithExtendedInfo(ManagementStatus errorCode) 於 System.Management.ManagementObjectCollection.ManagementObjectEnumerator.MoveNext() [Case 2] Exception: System.Runtime.InteropServices.COMException (0x800700A4) 於 System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo) 於 System.Management.ManagementObjectCollection.ManagementObjectEnumerator.MoveNext() [Case 3] Exception: System.Runtime.InteropServices.COMException (0x80004005) 於 System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo) [Case 4] Exception: System.InvalidOperationException: 由於該物件目前的狀態,導致作業無效。 於 System.Management.ManagementObjectCollection.ManagementObjectEnumerator.get_Current() [Case 5] Exception: System.Management.ManagementException: 正在關機 於 System.Management.ManagementException.ThrowWithExtendedInfo(ManagementStatus errorCode) 於 System.Management.ManagementObjectCollection.ManagementObjectEnumerator.MoveNext() ``` 後來運營人員查到那一台機器的wmi爆掉了 ![image](https://hackmd.io/_uploads/ry4FgBI1xg.png) 因為當天沒有更新版本,且該段程式碼運行很久一段時間了,且還只有單獨一台server發生問題,所以覺得應該不是程式本身的問題,懷疑是那台機器在那一段時間運行了甚麼需要大量調用wmi導致。 因為稍微研究了一下,除了wmi還可以透過Performance Counter(後面以PerfCounter來稱呼)取得Cpu Usage並且效率比wmi高,且可以避免下次wmi又崩潰導致所有的排程程式都受到影響,所以決定將原本的功能改透過PerfCounter來查詢。 ## 目標 將Windows的PerfCounter Data取出特定process的CPU使用量,並且寫入該程式本身的性能Log 透過Windows的效能監視器,也可以查詢到單一程式當下的CPU使用量 ![效能監視器Graph](https://hackmd.io/_uploads/rkD7FUDkxl.png) 透過下列的視窗加入特定程式的計數器,如畫面所示,正要加入的是"Proces V2"的"% Processor Time",而instance name為ms-teams:16280 **名詞說明** | 項目 | 值 |說明| |------------------|--------------------------|-| | Category | Process V2 |`效能資料的分類名稱`,例如 "Processor" 或 "Process",用來區分不同類型的效能計數器群組。| | | Counter Name | % Processor Time|在特定 Category 下的具體效能指標名稱 | Instance Name | ms-teams:16280 |`ms-teams` 為程式名稱(此例為 Microsoft Teams),冒號後 `16280` 為 Process Id。 | 透過效能監視器加入計數器的示範圖例 ![效能監視器_加入計數器](https://hackmd.io/_uploads/ry22jUvyxx.png) 最後目標可以透過程式定時產生對應的Log: ``` 2025-04-24 16:35:41,INFO,"PerfCounter Process: MyProgram_18991 TestCpuUsage, SettingsName:CPU, CPU使用率: 23.29%, ProcessorCount:4, Timer: 2025/4/24 下午 04:35:41", ``` ## 本文 ### 1. 基礎知識 a. Windows Server 2022與Windows 11,有支援Process V2,其他較舊的作業系統只有支援Process b. Process的Instance Name Format的版本1(預設)對我們取用資料很不利,所以需要改機碼,調整成版本2 ```ini Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\PerfProc\Performance] "ProcessNameFormat"=dword:00000002 ``` 下面為我整理的表格,說明各種作業系統對應支援PerfCounter Process版本的情況以及是否需要修改機碼。 |OS環境|實測版本|要否修改機碼(Y/N)|PerfCounter Category|Instance Name Format| |-|-|-|-|-| |Windows Server 2022|Window Server 2022 Datacenter:Azure Edition -x64 第2代|N|Process V2|Name:Pid| |Windows 11|Windows 11 家用版|N|Process V2|Name:Pid| |Windows Server 2019|Window Server 2019 Datacenter:Azure Edition -x64 第2代|Y|Process|Name_Pid| |Windows Server 2016|Window Server 2016 Datacenter:Azure Edition -x64 第2代|Y|Process|Name_Pid| ||Windows Server 2016 Standard|同上|同上|同上| |Windows 10|Windows 10 專業版|Y|Process|Name_Pid| #### 補充資料 **1. Process的Instance Name Format V1與V2有啥差異?** 假設我有一個程式名稱為abc.exe,同時開啟了3個,三個對應的Process Id分別為1234, 1235, 1236,當程式Pid 1234關閉後,Instance Name會有啥影響,如下面所示, ##### Instance Name Format V1: 關閉一個程式會影響到剩下的兩個程式的Instance Name ![image](https://hackmd.io/_uploads/r1yywvw1xe.png) ##### Instance Name Format V2: 關閉一個程式完全不影到其他程式的Instance Name ![image](https://hackmd.io/_uploads/B19MOvDkxx.png) MSDN有一篇文章 Handling Duplicate Instance Names[^DuplicateInstanceName]就有提到我介紹的部份。 **2. PerfCounter的Process 與 Process V2的差異?** * PerfCounter V2是新的效能計數器平台設計。 - 比V1性能好,安全,容易擴充 * PerfCounter V2 建立在ETW這個管道之上的現代性能計數器系統。 - 利用 ETW 的事件發布-訂閱機制來獲取性能數據 - ETW (Event Tracing for Windows) 是一個由微軟在 Windows 作業系統中提供的高效能、核心級的追蹤和記錄機制 `Process V2是建立在PerfCounter V2上的一個provider實作,所以有上述的優點` PerfCounter V1, V2的比較表 | 特性 | V1 架構 | V2 架構 | |------|---------|------------------------| | 出現時間 | Windows NT~Windows XP | Windows Vista 以後[^PerfCounterV2] | | 定義方式 | 透過 ini + h 檔案 | 使用 XML-based manifest | | 註冊方式 | lodctr 載入 ini 到 Registry | 透過 wevtutil 或 mc 工具產生 manifest | | 計數器來源 | 靠應用程式主動填入共享記憶體 | 使用 ETW 發送事件,系統收集 | | 擴充性 | 不易擴充,只能靜態定義 | 易於擴充,可動態註冊/釋放 | | 應用程式需啟動時才可被監控 | 是 | 否(ETW 可獨立收集事件) | | 跨 Session(如多用戶登入)支援 | 差 | 佳 | | 安全性與沙箱支援 | 有風險(可共用全系統資源) | 良好(可限制在特定 session) | | 現代支持情況 | 支援但漸被淘汰 | 強烈建議新開發採用 | **3. PerfCouter與WMI的關係說明** WMI是封裝了PerfCounter的高階使用介面,所以性能會再差一些 ![image](https://hackmd.io/_uploads/r1tvmXixxe.png) **4. 採用PerfCounter V2架構的新的Provider都會叫XXX 2嗎?** 不是,像Processor的V2版本是叫Processor Information。 **5. 為何Process的Instance Name Format和我之前接觸過的Oracle的PerfCounter"ODP.NET, Managed Driver"不一樣?** 過去我在程式內支援透過PerfCounter抓取Oracle資料庫的連線狀態,包含連線數量與Pool數量等等,但沒有遇到Instance Name Format的問題。 ![image](https://hackmd.io/_uploads/ByUEARuklg.png) 經過了解,原來是因為 ODP.NET, Managed Driver [^OraclePerfCounter]這個category是由Oracle提供的,當初也是得自己額外註冊,所以名稱的格式由第三方自定義了,格式如下: 假設我有一個程式名稱為abc.exe,ProcessId為1234 |格式|範例| |-|-| |program.exe [Pid, 1]|abc.exe [1234, 1]| 所以如果需要抓取PerfCounter的資料,需要注意一下是屬於微軟自己的格式還是第三方的格式,處理方式會不一樣。 ### 2. 程式碼介紹 應用環境: .Net Framework 4.6.2 支援OS: Windows 10, Windows 11, Windows Server 2016, 2019, 2022 使用PerfCounter來查詢,理論上不會很困難,只需要3個步驟 1. 建立PerfCounter 2. 使用PerfCounter取值 3. 運算Cpu Usage的結果 #### 1. 建立PerfCounter class PerformanceCounter的參數如下: ``` PerformanceCounter(string categoryName, string counterName, string instanceName, bool readOnly) ``` 因為不同OS版本對應的category與instance name format不同,所以需要動態決定。 ##### 核心概念 先用Process V2方式查找程式是否存在,如果不存在,再用Process方式查找(Instance Name Format:2) 如下面程式範例function ResolveCategoryAndInstance所示。 ##### 程式範例 ```csharp private void CreatePerfCounter(int pid) { string category = null; string instanceName = null; Process process = Process.GetProcessById(pid); if(ResolveCategoryAndInstance(process.ProcessName, pid, out category, out instanceName) is false) { return; } _cpuCounter = PerformanceCounter(category, "% Processor Time", instanceName, true) } private static bool ResolveCategoryAndInstance(string processName, int pid, out string category, out string instanceName) { var categories = new[] { "Process V2", "Process" }; foreach (var cat in categories) { if (!PerformanceCounterCategory.Exists(cat)) { continue; } string formattedName = cat == "Process V2" ? $"{processName}:{pid}" : $"{processName}_{pid}"; var instances = new PerformanceCounterCategory(cat).GetInstanceNames(); foreach (var name in instances) { if (name.Equals(formattedName, StringComparison.OrdinalIgnoreCase)) { category = cat; instanceName = name; return true; } } } category = null; instanceName = null; return false; } ``` #### 2.使用PerfCounter取值 取得PerfCounter的值很簡單,只需要定期去呼叫NextValue就可以了,提醒一下這邊只是範例,自己還是要記得try/catch處理一下 ##### 程式範例 ``` float cpuUsageOriginalValue = _cpuCounter.NextValue(); ``` #### 3. 運算Cpu Usage的結果 從Counter取到的是該程式所有CPU使用量的加總,如果是4核心系統最大CPU使用量是400%,但我希望可以看到的是平均的數值,所以需要再除以邏輯處理器的數量 舉例 cpuUsageOriginalValue = 200% 且 ProcessorCount = 4,則: ``` cpuUsage = 200 / 4 = 50% ``` 按照下面範例算出結果,再自行寫到log就完成了 ##### 程式範例 ``` float cpuUsage = cpuUsageOriginalValue / Environment.ProcessorCount; string cpuUsageString = $"{cpuUsage:F2}%"; ``` ### 3. PerfCounter與WMI性能比較 因為網路上沒找到具體的性能比較數據的文章,有多次詢問chatgpt提供,雖然有給數據與參考連結,但都是無效的參考連結,所以只好自己簡單測試一下。 #### 測試方法 查詢整台電腦的CPU Usage 100次,統計實際查詢區間花費時間 #### 程式運行結果 | Query Type | Total Query Time (ms) | Count | Average Time (ms) | |--------------|------------------------|--------|-------------------------------| | PerfCounter | 253.45 | 100 | <span style="color:red">2.53</span> | | WMI | 107916.42 | 100 | <span style="color:red">1079.16</span> | #### 性能分析探討 單次查詢花費時間比較,PerfCounter是<span style="color:red">426倍</span>的效率之於Wmi。 * PerfCounter: 2.53 ms * Wmi: 1079.16 ms ##### 原理說明 **PerfCounter** 1. 查詢幾乎可以立刻返回 2. 如果過於相近的時間連續查找(豪秒級),他會針對這次與上一次的數值去做計算,容易出現: - 資料為0 - 過於極端的突波值 所以在性能測試程式,我會在取用前先Sleep 1秒,確保資料取得為合理的值(實際負責專案是2秒取用一次) ###### 公式 ``` ΔIdleTime = IdleTime_這次 - IdleTime_上次 % Processor Time = 1 - (ΔIdleTime / ΔTotalTime) ``` **Wmi** 1. 查詢時間久的原因:**需等待資料產生才會返回** 2. 資料約每秒才產生一筆,因此花費的時間主要是「等待資料生成」而非實際 CPU 計算。 3. 雖然看似慢,但實際上並沒有真正消耗太多系統資源。 **綜合分析** **Wmi取用性能確實比PerfCounter差**,但在本專案中並無明顯影響,原因如下: - 查詢間隔固定為 2 秒。 - 查詢邏輯透過 `System.Timers.Timer` 非同步觸發,所以不影響主 Thread 的運作流程。 如果是寫一個監控程式需要取得比較多的數值時,性能上應該就會有較大的感受。 #### PerfCounter測試程式範例 ``` csharp= static void Main() { var counter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); counter.NextValue(); // 初始 call(必須等待1秒取得準確值) Thread.Sleep(1000); int i = 0; long totalTime = 0; // 用於累計 NextValue 的執行時間 for (i = 0; i < 100; i++) { Thread.Sleep(1000); //加入適當的延遲(如 1 秒)可以讓數據更準確,反映實際的 CPU 使用率 var stopwatch = new Stopwatch(); stopwatch.Start(); float value = counter.NextValue(); stopwatch.Stop(); totalTime += stopwatch.ElapsedTicks; Console.WriteLine($"{i} Value: {value}, Time: {stopwatch.ElapsedTicks} ticks"); } // 計算平均執行時間 double totalMilliseconds = totalTime * 1000.0 / Stopwatch.Frequency; double averageMilliseconds = totalMilliseconds / i; Console.WriteLine($"PerfCounter Total query time: {totalMilliseconds} ms, Count:{i}, Average time: {averageMilliseconds} ms"); } ``` #### WMI測試程式範例 ```csharp= static void Main(string[] args) { const int count = 100; var searcher = new ManagementObjectSearcher("SELECT LoadPercentage FROM Win32_Processor"); var swTotalTime = new Stopwatch(); swTotalTime.Start(); long totalTime = 0; int i = 0; for (i = 0; i < count; i++) { var stopwatch = new Stopwatch(); stopwatch.Start(); using (var results = searcher.Get()) // 釋放 COM 資源 { foreach (ManagementObject obj in results) { var value = obj["LoadPercentage"]; // 可略過實際使用 //Console.WriteLine($"Value: {value}"); } stopwatch.Stop(); totalTime += stopwatch.ElapsedTicks; } } swTotalTime.Stop(); double totalMilliseconds = totalTime * 1000.0 / Stopwatch.Frequency; double averageMilliseconds = totalMilliseconds / i; Console.WriteLine($"WMI Total query time: {totalMilliseconds} ms, Count:{i}, Average time: {averageMilliseconds} ms"); } ``` ## 結論 在本篇文章中,我們透過實務案例與實測數據,驗證了使用 PerfCounter 相較於 WMI 在查詢 CPU 使用率上的效能優勢,並且避開了WMI容易因系統負載或錯誤導致失敗的問題。 不過使用PerfCounter 需要注意 Instance Name 的名稱規則、系統版本支援狀況等細節也在前面介紹了處理方法。 綜合來說,如果你在 .NET 中需要撈取 CPU 使用率並寫入 Log,考量效能與穩定性,選用 PerfCounter 將會是更穩定且高效的選擇。 ## 參考資料 [^DuplicateInstanceName]: [MSDN文章: Handling Duplicate Instance Names](https://learn.microsoft.com/en-us/windows/win32/perfctrs/handling-duplicate-instance-names) [^PerfCounterV2]: [MSDN文章: Providing Counter Data Using Version 2.0](https://learn.microsoft.com/en-us/windows/win32/perfctrs/providing-counter-data-using-version-2-0) [^OraclePerfCounter]: [Oracle抓取性能資料的官方文件: Features of Oracle Data Provider for .NET](https://docs.oracle.com/en/database/oracle/oracle-database/19/odpnt/featConnecting.html#GUID-770B12CB-EA02-49C4-A145-5286F0FEA5F3)