mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 17:56:44 +02:00
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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user