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.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Text;
namespace CoreWidgetProvider.Helpers; namespace CoreWidgetProvider.Helpers;
internal sealed partial class GPUStats : IDisposable internal sealed partial class GPUStats : IDisposable
{ {
// GPU counters // Performance counter category & counter names
private readonly Dictionary<int, List<PerformanceCounter>> _gpuCounters = new(); 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 public sealed class Data
{ {
@@ -27,7 +52,7 @@ internal sealed partial class GPUStats : IDisposable
public float Temperature { get; set; } public float Temperature { get; set; }
public List<float> GpuChartValues { get; set; } = new(); public List<float> GpuChartValues { get; set; } = [];
} }
public GPUStats() 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 // 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 // numbers as the total utilization, and then have them broken out in
// the card template and in the details metadata. // the card template and in the details metadata.
_gpuCounters.Clear(); _knownPhysIds.Clear();
var perfCounterCategory = new PerformanceCounterCategory("GPU Engine"); var instanceNames = _gpuEngineCategory.GetInstanceNames();
var instanceNames = perfCounterCategory.GetInstanceNames();
foreach (var instanceName in instanceNames) foreach (var instanceName in instanceNames)
{ {
if (!instanceName.EndsWith("3D", StringComparison.InvariantCulture)) if (!instanceName.EndsWith(EngineType3D, StringComparison.InvariantCulture))
{ {
continue; continue;
} }
var utilizationCounters = perfCounterCategory.GetCounters(instanceName) var counterKey = instanceName;
.Where(x => x.CounterName.StartsWith("Utilization Percentage", StringComparison.InvariantCulture));
foreach (var counter in utilizationCounters)
{
var counterKey = counter.InstanceName;
// skip these values // skip these values
GetKeyValueFromCounterKey("pid", ref counterKey); GetKeyValueFromCounterKey(KeyPid, ref counterKey);
GetKeyValueFromCounterKey("luid", ref counterKey); GetKeyValueFromCounterKey(KeyLuid, ref counterKey);
int phys; if (int.TryParse(GetKeyValueFromCounterKey(KeyPhys, ref counterKey), out var phys))
var success = int.TryParse(GetKeyValueFromCounterKey("phys", ref counterKey), out phys);
if (success)
{ {
GetKeyValueFromCounterKey("eng", ref counterKey); _knownPhysIds.Add(phys);
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);
}
} }
} }
} }
@@ -108,70 +111,87 @@ internal sealed partial class GPUStats : IDisposable
// //
// For now, we'll just use the indices as the GPU names. // For now, we'll just use the indices as the GPU names.
_stats.Clear(); _stats.Clear();
foreach (var (k, v) in _gpuCounters) foreach (var id in _knownPhysIds)
{ {
var id = k; _stats.Add(new Data() { PhysId = id, Name = GpuNamePrefix + id });
var counters = v;
_stats.Add(new Data() { PhysId = id, Name = "GPU " + id });
} }
} }
public void GetData() public void GetData()
{ {
foreach (var gpu in _stats) try
{ {
List<PerformanceCounter>? counters; // Single batch read - one kernel transition for ALL GPU Engine instances
var success = _gpuCounters.TryGetValue(gpu.PhysId, out counters); 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. return;
try
{
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)
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)
{ {
// We can't modify the list during the loop, so save it to remove at the end. var instanceName = instance.InstanceName;
// _log.Information(ex, "Failed to get next value, remove"); if (!instanceName.EndsWith(EngineType3D, StringComparison.InvariantCulture))
countersToRemove.Add(counter); {
continue;
}
var counterKey = instanceName;
GetKeyValueFromCounterKey(KeyPid, ref counterKey);
GetKeyValueFromCounterKey(KeyLuid, ref counterKey);
if (!int.TryParse(GetKeyValueFromCounterKey(KeyPhys, ref counterKey), out var phys))
{
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) catch (Exception)
{ {
// _log.Error(ex, "Error going through process counters."); // Skip this instance on calculation error.
}
} }
} }
foreach (var counter in countersToRemove) // Swap samples - stale entries are automatically cleaned up
_previousSamples = currentSamples;
// Update stats
foreach (var gpu in _stats)
{ {
counters.Remove(counter); var sum = gpuUsage.TryGetValue(gpu.PhysId, out var usage) ? usage : 0f;
counter.Dispose();
}
gpu.Usage = sum / 100; gpu.Usage = sum / 100;
lock (gpu.GpuChartValues) lock (gpu.GpuChartValues)
{ {
ChartHelper.AddNextChartValue(sum, gpu.GpuChartValues); ChartHelper.AddNextChartValue(sum, gpu.GpuChartValues);
} }
} }
}
catch (Exception) catch (Exception)
{ {
// _log.Error(ex, "Error summing process counters."); // Ignore errors from ReadCategory (e.g., category not available).
}
}
} }
} }
internal string CreateGPUImageUrl(int gpuChartIndex) 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) internal string GetGPUName(int gpuActiveIndex)
@@ -234,16 +254,16 @@ internal sealed partial class GPUStats : IDisposable
// removed. // removed.
if (_stats.Count <= gpuActiveIndex) if (_stats.Count <= gpuActiveIndex)
{ {
return "--"; return TemperatureUnavailable;
} }
var temperature = _stats[gpuActiveIndex].Temperature; var temperature = _stats[gpuActiveIndex].Temperature;
if (temperature == 0) 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) private string GetKeyValueFromCounterKey(string key, ref string counterKey)
@@ -254,13 +274,13 @@ internal sealed partial class GPUStats : IDisposable
} }
counterKey = counterKey.Substring(key.Length + 1); counterKey = counterKey.Substring(key.Length + 1);
if (key.Equals("engtype", StringComparison.Ordinal)) if (key.Equals(KeyEngineType, StringComparison.Ordinal))
{ {
return counterKey; return counterKey;
} }
var pos = counterKey.IndexOf('_'); var pos = counterKey.IndexOf('_');
if (key.Equals("luid", StringComparison.Ordinal)) if (key.Equals(KeyLuid, StringComparison.Ordinal))
{ {
pos = counterKey.IndexOf('_', pos + 1); pos = counterKey.IndexOf('_', pos + 1);
} }
@@ -272,12 +292,6 @@ internal sealed partial class GPUStats : IDisposable
public void Dispose() public void Dispose()
{ {
foreach (var counterPair in _gpuCounters) _previousSamples.Clear();
{
foreach (var counter in counterPair.Value)
{
counter.Dispose();
}
}
} }
} }