mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +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.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)
|
// 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;
|
_knownPhysIds.Add(phys);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 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;
|
continue;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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)
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user