CmdPal: Refactor PerformanceMonitor extension GPU stats to use batch counter reads (#45835)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR reduces overall CPU usage caused by GPU statistics in
Performance Monitor extension.

Replaces per-instance PerformanceCounter objects with batch reads via
PerformanceCounterCategory.ReadCategory, reducing kernel transitions and
improving efficiency.


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #45823
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
Jiří Polášek
2026-03-05 21:34:52 +01:00
committed by GitHub
parent f0134e4448
commit bcbca0d5dd

View File

@@ -6,16 +6,41 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class GPUStats : IDisposable
{
// GPU counters
private readonly Dictionary<int, List<PerformanceCounter>> _gpuCounters = new();
// Performance counter category & counter names
private const string GpuEngineCategoryName = "GPU Engine";
private const string UtilizationPercentageCounter = "Utilization Percentage";
private readonly List<Data> _stats = new();
private static readonly CompositeFormat TemperatureFormat = CompositeFormat.Parse("{0:0.} \u00B0C");
// Instance-name key tokens
private const string KeyPid = "pid";
private const string KeyLuid = "luid";
private const string KeyPhys = "phys";
private const string KeyEngineType = "engtype";
// Engine type filter
private const string EngineType3D = "3D";
// Display strings
private const string GpuNamePrefix = "GPU ";
private const string TemperatureUnavailable = "--";
// Batch read via category - single kernel transition per tick
private readonly PerformanceCounterCategory _gpuEngineCategory = new(GpuEngineCategoryName);
// Discovered physical GPU IDs
private readonly HashSet<int> _knownPhysIds = [];
private readonly List<Data> _stats = [];
// Previous raw samples for computing cooked (delta-based) values
private Dictionary<string, CounterSample> _previousSamples = [];
public sealed class Data
{
@@ -27,7 +52,7 @@ internal sealed partial class GPUStats : IDisposable
public float Temperature { get; set; }
public List<float> GpuChartValues { get; set; } = new();
public List<float> GpuChartValues { get; set; } = [];
}
public GPUStats()
@@ -51,48 +76,26 @@ internal sealed partial class GPUStats : IDisposable
// set. That's what we should do, so that we can report the sum of those
// numbers as the total utilization, and then have them broken out in
// the card template and in the details metadata.
_gpuCounters.Clear();
_knownPhysIds.Clear();
var perfCounterCategory = new PerformanceCounterCategory("GPU Engine");
var instanceNames = perfCounterCategory.GetInstanceNames();
var instanceNames = _gpuEngineCategory.GetInstanceNames();
foreach (var instanceName in instanceNames)
{
if (!instanceName.EndsWith("3D", StringComparison.InvariantCulture))
if (!instanceName.EndsWith(EngineType3D, StringComparison.InvariantCulture))
{
continue;
}
var utilizationCounters = perfCounterCategory.GetCounters(instanceName)
.Where(x => x.CounterName.StartsWith("Utilization Percentage", StringComparison.InvariantCulture));
var counterKey = instanceName;
foreach (var counter in utilizationCounters)
// skip these values
GetKeyValueFromCounterKey(KeyPid, ref counterKey);
GetKeyValueFromCounterKey(KeyLuid, ref counterKey);
if (int.TryParse(GetKeyValueFromCounterKey(KeyPhys, ref counterKey), out var phys))
{
var counterKey = counter.InstanceName;
// skip these values
GetKeyValueFromCounterKey("pid", ref counterKey);
GetKeyValueFromCounterKey("luid", ref counterKey);
int phys;
var success = int.TryParse(GetKeyValueFromCounterKey("phys", ref counterKey), out phys);
if (success)
{
GetKeyValueFromCounterKey("eng", ref counterKey);
var engtype = GetKeyValueFromCounterKey("engtype", ref counterKey);
if (engtype != "3D")
{
continue;
}
if (!_gpuCounters.TryGetValue(phys, out var value))
{
value = new();
_gpuCounters.Add(phys, value);
}
value.Add(counter);
}
_knownPhysIds.Add(phys);
}
}
}
@@ -108,70 +111,87 @@ internal sealed partial class GPUStats : IDisposable
//
// For now, we'll just use the indices as the GPU names.
_stats.Clear();
foreach (var (k, v) in _gpuCounters)
foreach (var id in _knownPhysIds)
{
var id = k;
var counters = v;
_stats.Add(new Data() { PhysId = id, Name = "GPU " + id });
_stats.Add(new Data() { PhysId = id, Name = GpuNamePrefix + id });
}
}
public void GetData()
{
foreach (var gpu in _stats)
try
{
List<PerformanceCounter>? counters;
var success = _gpuCounters.TryGetValue(gpu.PhysId, out counters);
// Single batch read - one kernel transition for ALL GPU Engine instances
var categoryData = _gpuEngineCategory.ReadCategory();
if (success && counters != null)
if (!categoryData.Contains(UtilizationPercentageCounter))
{
// TODO: This outer try/catch should be replaced with more secure locking around shared resources.
try
return;
}
var utilizationData = categoryData[UtilizationPercentageCounter];
// Accumulate usage per physical GPU
var gpuUsage = new Dictionary<int, float>();
var currentSamples = new Dictionary<string, CounterSample>();
foreach (InstanceData instance in utilizationData.Values)
{
var instanceName = instance.InstanceName;
if (!instanceName.EndsWith(EngineType3D, StringComparison.InvariantCulture))
{
var sum = 0.0f;
var countersToRemove = new List<PerformanceCounter>();
foreach (var counter in counters)
{
try
{
// NextValue() can throw an InvalidOperationException if the counter is no longer there.
sum += counter.NextValue();
}
catch (InvalidOperationException)
{
// We can't modify the list during the loop, so save it to remove at the end.
// _log.Information(ex, "Failed to get next value, remove");
countersToRemove.Add(counter);
}
catch (Exception)
{
// _log.Error(ex, "Error going through process counters.");
}
}
foreach (var counter in countersToRemove)
{
counters.Remove(counter);
counter.Dispose();
}
gpu.Usage = sum / 100;
lock (gpu.GpuChartValues)
{
ChartHelper.AddNextChartValue(sum, gpu.GpuChartValues);
}
continue;
}
catch (Exception)
var counterKey = instanceName;
GetKeyValueFromCounterKey(KeyPid, ref counterKey);
GetKeyValueFromCounterKey(KeyLuid, ref counterKey);
if (!int.TryParse(GetKeyValueFromCounterKey(KeyPhys, ref counterKey), out var phys))
{
// _log.Error(ex, "Error summing process counters.");
continue;
}
var sample = instance.Sample;
currentSamples[instanceName] = sample;
if (_previousSamples.TryGetValue(instanceName, out var prevSample))
{
try
{
var cookedValue = CounterSampleCalculator.ComputeCounterValue(prevSample, sample);
gpuUsage[phys] = gpuUsage.GetValueOrDefault(phys) + cookedValue;
}
catch (Exception)
{
// Skip this instance on calculation error.
}
}
}
// Swap samples - stale entries are automatically cleaned up
_previousSamples = currentSamples;
// Update stats
foreach (var gpu in _stats)
{
var sum = gpuUsage.TryGetValue(gpu.PhysId, out var usage) ? usage : 0f;
gpu.Usage = sum / 100;
lock (gpu.GpuChartValues)
{
ChartHelper.AddNextChartValue(sum, gpu.GpuChartValues);
}
}
}
catch (Exception)
{
// Ignore errors from ReadCategory (e.g., category not available).
}
}
internal string CreateGPUImageUrl(int gpuChartIndex)
{
return ChartHelper.CreateImageUrl(_stats.ElementAt(gpuChartIndex).GpuChartValues, ChartHelper.ChartType.GPU);
return ChartHelper.CreateImageUrl(_stats[gpuChartIndex].GpuChartValues, ChartHelper.ChartType.GPU);
}
internal string GetGPUName(int gpuActiveIndex)
@@ -234,16 +254,16 @@ internal sealed partial class GPUStats : IDisposable
// removed.
if (_stats.Count <= gpuActiveIndex)
{
return "--";
return TemperatureUnavailable;
}
var temperature = _stats[gpuActiveIndex].Temperature;
if (temperature == 0)
{
return "--";
return TemperatureUnavailable;
}
return temperature.ToString("0.", CultureInfo.InvariantCulture) + " \x00B0C";
return string.Format(CultureInfo.InvariantCulture, TemperatureFormat.Format, temperature);
}
private string GetKeyValueFromCounterKey(string key, ref string counterKey)
@@ -254,13 +274,13 @@ internal sealed partial class GPUStats : IDisposable
}
counterKey = counterKey.Substring(key.Length + 1);
if (key.Equals("engtype", StringComparison.Ordinal))
if (key.Equals(KeyEngineType, StringComparison.Ordinal))
{
return counterKey;
}
var pos = counterKey.IndexOf('_');
if (key.Equals("luid", StringComparison.Ordinal))
if (key.Equals(KeyLuid, StringComparison.Ordinal))
{
pos = counterKey.IndexOf('_', pos + 1);
}
@@ -272,12 +292,6 @@ internal sealed partial class GPUStats : IDisposable
public void Dispose()
{
foreach (var counterPair in _gpuCounters)
{
foreach (var counter in counterPair.Value)
{
counter.Dispose();
}
}
_previousSamples.Clear();
}
}