---
# System prepended metadata

title: 如何在.Net使用Performance Counter撈取CPU使用量
tags: [PerformanceCounter, DotNet, blog]

---

---
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)
