diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/GPUStats.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/GPUStats.cs index 36805cdf83..30cbb05de1 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/GPUStats.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/GPUStats.cs @@ -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> _gpuCounters = new(); + // Performance counter category & counter names + private const string GpuEngineCategoryName = "GPU Engine"; + private const string UtilizationPercentageCounter = "Utilization Percentage"; - private readonly List _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 _knownPhysIds = []; + + private readonly List _stats = []; + + // Previous raw samples for computing cooked (delta-based) values + private Dictionary _previousSamples = []; public sealed class Data { @@ -27,7 +52,7 @@ internal sealed partial class GPUStats : IDisposable public float Temperature { get; set; } - public List GpuChartValues { get; set; } = new(); + public List 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? 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(); + var currentSamples = new Dictionary(); + + foreach (InstanceData instance in utilizationData.Values) + { + var instanceName = instance.InstanceName; + if (!instanceName.EndsWith(EngineType3D, StringComparison.InvariantCulture)) { - var sum = 0.0f; - var countersToRemove = new List(); - 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(); } }