---
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爆掉了

因為當天沒有更新版本,且該段程式碼運行很久一段時間了,且還只有單獨一台server發生問題,所以覺得應該不是程式本身的問題,懷疑是那台機器在那一段時間運行了甚麼需要大量調用wmi導致。
因為稍微研究了一下,除了wmi還可以透過Performance Counter(後面以PerfCounter來稱呼)取得Cpu Usage並且效率比wmi高,且可以避免下次wmi又崩潰導致所有的排程程式都受到影響,所以決定將原本的功能改透過PerfCounter來查詢。
## 目標
將Windows的PerfCounter Data取出特定process的CPU使用量,並且寫入該程式本身的性能Log
透過Windows的效能監視器,也可以查詢到單一程式當下的CPU使用量

透過下列的視窗加入特定程式的計數器,如畫面所示,正要加入的是"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。 |
透過效能監視器加入計數器的示範圖例

最後目標可以透過程式定時產生對應的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

##### Instance Name Format V2:
關閉一個程式完全不影到其他程式的Instance Name

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的高階使用介面,所以性能會再差一些

**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的問題。

經過了解,原來是因為 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)