mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
CmdPal: Harden performance monitor and enable crash recovery (#46541)
<!-- 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 has two parts: 1. Hardens the managed paths in the Performance Monitor extension to catch everything we can. 1. Adds crash recovery for cases where something fails in a way we cannot handle. ## Pictures? Pictures! <img width="1060" height="591" alt="image" src="https://github.com/user-attachments/assets/ee91c610-32eb-4117-b9b8-6bbc40b9b426" /> <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #46522 <!-- - [ ] 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 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -9,13 +9,17 @@ using System.Linq;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class CPUStats : IDisposable
|
||||
internal sealed partial class CPUStats : PerformanceCounterSourceBase, IDisposable
|
||||
{
|
||||
// CPU counters
|
||||
private readonly PerformanceCounter _procPerf = new("Processor Information", "% Processor Utility", "_Total");
|
||||
private readonly PerformanceCounter _procPerformance = new("Processor Information", "% Processor Performance", "_Total");
|
||||
private readonly PerformanceCounter _procFrequency = new("Processor Information", "Processor Frequency", "_Total");
|
||||
private readonly PerformanceCounter? _procPerf;
|
||||
private readonly PerformanceCounter? _procPerformance;
|
||||
private readonly PerformanceCounter? _procFrequency;
|
||||
private readonly Dictionary<Process, PerformanceCounter> _cpuCounters = new();
|
||||
private bool _processCountersInitialized;
|
||||
private bool _cpuCounterReadFailureLogged;
|
||||
private bool _processCounterEnumerationFailureLogged;
|
||||
private bool _processCounterReadFailureLogged;
|
||||
|
||||
internal sealed class ProcessStats
|
||||
{
|
||||
@@ -42,69 +46,121 @@ internal sealed partial class CPUStats : IDisposable
|
||||
new ProcessStats()
|
||||
];
|
||||
|
||||
InitCPUPerfCounters();
|
||||
_procPerf = CreatePerformanceCounter("Processor Information", "% Processor Utility", "_Total");
|
||||
_procPerformance = CreatePerformanceCounter("Processor Information", "% Processor Performance", "_Total");
|
||||
_procFrequency = CreatePerformanceCounter("Processor Information", "Processor Frequency", "_Total");
|
||||
}
|
||||
|
||||
private void InitCPUPerfCounters()
|
||||
private void EnsureCPUProcessCountersInitialized()
|
||||
{
|
||||
var allProcesses = Process.GetProcesses().Where(p => (long)p.MainWindowHandle != 0);
|
||||
|
||||
foreach (var process in allProcesses)
|
||||
if (_processCountersInitialized)
|
||||
{
|
||||
_cpuCounters.Add(process, new PerformanceCounter("Process", "% Processor Time", process.ProcessName, true));
|
||||
return;
|
||||
}
|
||||
|
||||
_processCountersInitialized = true;
|
||||
|
||||
try
|
||||
{
|
||||
var allProcesses = Process.GetProcesses().Where(p => (long)p.MainWindowHandle != 0);
|
||||
|
||||
foreach (var process in allProcesses)
|
||||
{
|
||||
try
|
||||
{
|
||||
var counter = CreatePerformanceCounter("Process", "% Processor Time", process.ProcessName, logFailure: false);
|
||||
if (counter is not null)
|
||||
{
|
||||
_cpuCounters.Add(process, counter);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Skip processes whose counters cannot be created.
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogFailureOnce(ref _processCounterEnumerationFailureLogged, "Failed to initialize CPU process performance counters.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void GetData(bool includeTopProcesses)
|
||||
{
|
||||
var timer = Stopwatch.StartNew();
|
||||
CpuUsage = _procPerf.NextValue() / 100;
|
||||
var usageMs = timer.ElapsedMilliseconds;
|
||||
CpuSpeed = _procFrequency.NextValue() * (_procPerformance.NextValue() / 100);
|
||||
var speedMs = timer.ElapsedMilliseconds - usageMs;
|
||||
lock (CpuChartValues)
|
||||
try
|
||||
{
|
||||
ChartHelper.AddNextChartValue(CpuUsage * 100, CpuChartValues);
|
||||
}
|
||||
|
||||
var chartMs = timer.ElapsedMilliseconds - speedMs;
|
||||
|
||||
var processCPUUsages = new Dictionary<Process, float>();
|
||||
|
||||
if (includeTopProcesses)
|
||||
{
|
||||
foreach (var processCounter in _cpuCounters)
|
||||
var timer = Stopwatch.StartNew();
|
||||
if (_procPerf is not null)
|
||||
{
|
||||
try
|
||||
CpuUsage = _procPerf.NextValue() / 100;
|
||||
}
|
||||
|
||||
var usageMs = timer.ElapsedMilliseconds;
|
||||
if (_procFrequency is not null && _procPerformance is not null)
|
||||
{
|
||||
CpuSpeed = _procFrequency.NextValue() * (_procPerformance.NextValue() / 100);
|
||||
}
|
||||
|
||||
var speedMs = timer.ElapsedMilliseconds - usageMs;
|
||||
lock (CpuChartValues)
|
||||
{
|
||||
ChartHelper.AddNextChartValue(CpuUsage * 100, CpuChartValues);
|
||||
}
|
||||
|
||||
var chartMs = timer.ElapsedMilliseconds - speedMs;
|
||||
|
||||
var processCPUUsages = new Dictionary<Process, float>();
|
||||
|
||||
if (includeTopProcesses)
|
||||
{
|
||||
EnsureCPUProcessCountersInitialized();
|
||||
|
||||
var countersToRemove = new List<Process>();
|
||||
foreach (var processCounter in _cpuCounters.ToArray())
|
||||
{
|
||||
// process might be terminated
|
||||
processCPUUsages.Add(processCounter.Key, processCounter.Value.NextValue() / Environment.ProcessorCount);
|
||||
try
|
||||
{
|
||||
// process might be terminated
|
||||
processCPUUsages.Add(processCounter.Key, processCounter.Value.NextValue() / Environment.ProcessorCount);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
countersToRemove.Add(processCounter.Key);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogFailureOnce(ref _processCounterReadFailureLogged, "Failed while reading CPU process performance counters.", ex);
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
|
||||
foreach (var process in countersToRemove)
|
||||
{
|
||||
// _log.Information($"ProcessCounter Key {processCounter.Key} no longer exists, removing from _cpuCounters.");
|
||||
_cpuCounters.Remove(processCounter.Key);
|
||||
if (_cpuCounters.Remove(process, out var counter))
|
||||
{
|
||||
counter.Dispose();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
|
||||
var cpuIndex = 0;
|
||||
foreach (var processCPUValue in processCPUUsages.OrderByDescending(x => x.Value).Take(3))
|
||||
{
|
||||
// _log.Error(ex, "Error going through process counters.");
|
||||
ProcessCPUStats[cpuIndex].Process = processCPUValue.Key;
|
||||
ProcessCPUStats[cpuIndex].CpuUsage = processCPUValue.Value;
|
||||
cpuIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
var cpuIndex = 0;
|
||||
foreach (var processCPUValue in processCPUUsages.OrderByDescending(x => x.Value).Take(3))
|
||||
{
|
||||
ProcessCPUStats[cpuIndex].Process = processCPUValue.Key;
|
||||
ProcessCPUStats[cpuIndex].CpuUsage = processCPUValue.Value;
|
||||
cpuIndex++;
|
||||
}
|
||||
timer.Stop();
|
||||
var total = timer.ElapsedMilliseconds;
|
||||
var processesMs = total - chartMs;
|
||||
|
||||
// CoreLogger.LogDebug($"[{usageMs}]+[{speedMs}]+[{chartMs}]+[{processesMs}]=[{total}]");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogFailureOnce(ref _cpuCounterReadFailureLogged, "Failed while reading CPU performance counters.", ex);
|
||||
}
|
||||
|
||||
timer.Stop();
|
||||
var total = timer.ElapsedMilliseconds;
|
||||
var processesMs = total - chartMs;
|
||||
|
||||
// CoreLogger.LogDebug($"[{usageMs}]+[{speedMs}]+[{chartMs}]+[{processesMs}]=[{total}]");
|
||||
}
|
||||
|
||||
internal string CreateCPUImageUrl()
|
||||
@@ -134,9 +190,9 @@ internal sealed partial class CPUStats : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_procPerf.Dispose();
|
||||
_procPerformance.Dispose();
|
||||
_procFrequency.Dispose();
|
||||
_procPerf?.Dispose();
|
||||
_procPerformance?.Dispose();
|
||||
_procFrequency?.Dispose();
|
||||
|
||||
foreach (var counter in _cpuCounters.Values)
|
||||
{
|
||||
|
||||
@@ -3,22 +3,23 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.CmdPal.Ext.PerformanceMonitor;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class DataManager : IDisposable
|
||||
{
|
||||
private readonly SystemData _systemData;
|
||||
private readonly SystemData _systemData = SystemData.Shared;
|
||||
private readonly DataType _dataType;
|
||||
private readonly Timer _updateTimer;
|
||||
private readonly Action _updateAction;
|
||||
private bool _updateFailureLogged;
|
||||
|
||||
private const int OneSecondInMilliseconds = 1000;
|
||||
|
||||
public DataManager(DataType type, Action updateWidget)
|
||||
{
|
||||
_systemData = new SystemData();
|
||||
_updateAction = updateWidget;
|
||||
_dataType = type;
|
||||
|
||||
@@ -30,102 +31,140 @@ internal sealed partial class DataManager : IDisposable
|
||||
|
||||
private void GetMemoryData()
|
||||
{
|
||||
lock (SystemData.MemStats)
|
||||
lock (_systemData.MemoryStats)
|
||||
{
|
||||
SystemData.MemStats.GetData();
|
||||
_systemData.MemoryStats.GetData();
|
||||
}
|
||||
}
|
||||
|
||||
private void GetNetworkData()
|
||||
{
|
||||
lock (SystemData.NetStats)
|
||||
lock (_systemData.NetworkStats)
|
||||
{
|
||||
SystemData.NetStats.GetData();
|
||||
_systemData.NetworkStats.GetData();
|
||||
}
|
||||
}
|
||||
|
||||
private void GetGPUData()
|
||||
{
|
||||
lock (SystemData.GPUStats)
|
||||
lock (_systemData.GPUStats)
|
||||
{
|
||||
SystemData.GPUStats.GetData();
|
||||
_systemData.GPUStats.GetData();
|
||||
}
|
||||
}
|
||||
|
||||
private void GetCPUData(bool includeTopProcesses)
|
||||
{
|
||||
lock (SystemData.CpuStats)
|
||||
lock (_systemData.CpuStats)
|
||||
{
|
||||
SystemData.CpuStats.GetData(includeTopProcesses);
|
||||
_systemData.CpuStats.GetData(includeTopProcesses);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
switch (_dataType)
|
||||
var firstUpdateBlockSuffix = GetFirstUpdateBlockSuffix();
|
||||
var isTracked = firstUpdateBlockSuffix is not null && PerformanceMonitorCommandsProvider.CrashSentinel.BeginBlock(firstUpdateBlockSuffix);
|
||||
|
||||
try
|
||||
{
|
||||
case DataType.CPU:
|
||||
case DataType.CpuWithTopProcesses:
|
||||
{
|
||||
// CPU
|
||||
GetCPUData(_dataType == DataType.CpuWithTopProcesses);
|
||||
break;
|
||||
}
|
||||
switch (_dataType)
|
||||
{
|
||||
case DataType.CPU:
|
||||
case DataType.CpuWithTopProcesses:
|
||||
{
|
||||
// CPU
|
||||
GetCPUData(_dataType == DataType.CpuWithTopProcesses);
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.GPU:
|
||||
{
|
||||
// gpu
|
||||
GetGPUData();
|
||||
break;
|
||||
}
|
||||
case DataType.GPU:
|
||||
{
|
||||
// gpu
|
||||
GetGPUData();
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Memory:
|
||||
{
|
||||
// memory
|
||||
GetMemoryData();
|
||||
break;
|
||||
}
|
||||
case DataType.Memory:
|
||||
{
|
||||
// memory
|
||||
GetMemoryData();
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Network:
|
||||
{
|
||||
// network
|
||||
GetNetworkData();
|
||||
break;
|
||||
}
|
||||
case DataType.Network:
|
||||
{
|
||||
// network
|
||||
GetNetworkData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isTracked)
|
||||
{
|
||||
PerformanceMonitorCommandsProvider.CrashSentinel.CompleteBlock(firstUpdateBlockSuffix!);
|
||||
}
|
||||
|
||||
_updateAction?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (isTracked)
|
||||
{
|
||||
PerformanceMonitorCommandsProvider.CrashSentinel.CancelBlock(firstUpdateBlockSuffix!);
|
||||
}
|
||||
|
||||
_updateAction?.Invoke();
|
||||
_updateTimer.Stop();
|
||||
if (!_updateFailureLogged)
|
||||
{
|
||||
_updateFailureLogged = true;
|
||||
Microsoft.CmdPal.Common.CoreLogger.LogError($"Unexpected exception while updating performance monitor data for {_dataType}. Timer stopped.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetFirstUpdateBlockSuffix()
|
||||
{
|
||||
return _dataType switch
|
||||
{
|
||||
DataType.CPU => "CPU.FirstUpdate",
|
||||
DataType.CpuWithTopProcesses => "CPU.FirstUpdate",
|
||||
DataType.GPU => "GPU.FirstUpdate",
|
||||
DataType.Memory => "Memory.FirstUpdate",
|
||||
DataType.Network => "Network.FirstUpdate",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
internal MemoryStats GetMemoryStats()
|
||||
{
|
||||
lock (SystemData.MemStats)
|
||||
lock (_systemData.MemoryStats)
|
||||
{
|
||||
return SystemData.MemStats;
|
||||
return _systemData.MemoryStats;
|
||||
}
|
||||
}
|
||||
|
||||
internal NetworkStats GetNetworkStats()
|
||||
{
|
||||
lock (SystemData.NetStats)
|
||||
lock (_systemData.NetworkStats)
|
||||
{
|
||||
return SystemData.NetStats;
|
||||
return _systemData.NetworkStats;
|
||||
}
|
||||
}
|
||||
|
||||
internal GPUStats GetGPUStats()
|
||||
{
|
||||
lock (SystemData.GPUStats)
|
||||
lock (_systemData.GPUStats)
|
||||
{
|
||||
return SystemData.GPUStats;
|
||||
return _systemData.GPUStats;
|
||||
}
|
||||
}
|
||||
|
||||
internal CPUStats GetCPUStats()
|
||||
{
|
||||
lock (SystemData.CpuStats)
|
||||
lock (_systemData.CpuStats)
|
||||
{
|
||||
return SystemData.CpuStats;
|
||||
return _systemData.CpuStats;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +180,6 @@ internal sealed partial class DataManager : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_systemData.Dispose();
|
||||
_updateTimer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ using System.Text;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class GPUStats : IDisposable
|
||||
internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposable
|
||||
{
|
||||
// Performance counter category & counter names
|
||||
private const string GpuEngineCategoryName = "GPU Engine";
|
||||
@@ -32,7 +32,7 @@ internal sealed partial class GPUStats : IDisposable
|
||||
private const string TemperatureUnavailable = "--";
|
||||
|
||||
// Batch read via category - single kernel transition per tick
|
||||
private readonly PerformanceCounterCategory _gpuEngineCategory = new(GpuEngineCategoryName);
|
||||
private readonly PerformanceCounterCategory? _gpuEngineCategory;
|
||||
|
||||
// Discovered physical GPU IDs
|
||||
private readonly HashSet<int> _knownPhysIds = [];
|
||||
@@ -41,6 +41,8 @@ internal sealed partial class GPUStats : IDisposable
|
||||
|
||||
// Previous raw samples for computing cooked (delta-based) values
|
||||
private Dictionary<string, CounterSample> _previousSamples = [];
|
||||
private bool _gpuEnumerationFailureLogged;
|
||||
private bool _gpuReadFailureLogged;
|
||||
|
||||
public sealed class Data
|
||||
{
|
||||
@@ -57,47 +59,61 @@ internal sealed partial class GPUStats : IDisposable
|
||||
|
||||
public GPUStats()
|
||||
{
|
||||
_gpuEngineCategory = CreatePerformanceCounterCategory(GpuEngineCategoryName);
|
||||
|
||||
GetGPUPerfCounters();
|
||||
LoadGPUsFromCounters();
|
||||
}
|
||||
|
||||
public void GetGPUPerfCounters()
|
||||
{
|
||||
// There are really 4 different things we should be tracking the usage
|
||||
// of. Similar to how the instance name ends with `3D`, the following
|
||||
// suffixes are important.
|
||||
//
|
||||
// * `3D`
|
||||
// * `VideoEncode`
|
||||
// * `VideoDecode`
|
||||
// * `VideoProcessing`
|
||||
//
|
||||
// We could totally put each of those sets of counters into their own
|
||||
// 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.
|
||||
_knownPhysIds.Clear();
|
||||
|
||||
var instanceNames = _gpuEngineCategory.GetInstanceNames();
|
||||
|
||||
foreach (var instanceName in instanceNames)
|
||||
if (_gpuEngineCategory is null)
|
||||
{
|
||||
if (!instanceName.EndsWith(EngineType3D, StringComparison.InvariantCulture))
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// There are really 4 different things we should be tracking the usage
|
||||
// of. Similar to how the instance name ends with `3D`, the following
|
||||
// suffixes are important.
|
||||
//
|
||||
// * `3D`
|
||||
// * `VideoEncode`
|
||||
// * `VideoDecode`
|
||||
// * `VideoProcessing`
|
||||
//
|
||||
// We could totally put each of those sets of counters into their own
|
||||
// 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.
|
||||
_knownPhysIds.Clear();
|
||||
|
||||
var instanceNames = _gpuEngineCategory.GetInstanceNames();
|
||||
|
||||
foreach (var instanceName in instanceNames)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var counterKey = instanceName;
|
||||
|
||||
// skip these values
|
||||
GetKeyValueFromCounterKey(KeyPid, ref counterKey);
|
||||
GetKeyValueFromCounterKey(KeyLuid, ref counterKey);
|
||||
|
||||
if (int.TryParse(GetKeyValueFromCounterKey(KeyPhys, ref counterKey), out var phys))
|
||||
{
|
||||
_knownPhysIds.Add(phys);
|
||||
if (!instanceName.EndsWith(EngineType3D, StringComparison.InvariantCulture))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var counterKey = instanceName;
|
||||
|
||||
// skip these values
|
||||
GetKeyValueFromCounterKey(KeyPid, ref counterKey);
|
||||
GetKeyValueFromCounterKey(KeyLuid, ref counterKey);
|
||||
|
||||
if (int.TryParse(GetKeyValueFromCounterKey(KeyPhys, ref counterKey), out var phys))
|
||||
{
|
||||
_knownPhysIds.Add(phys);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogFailureOnce(ref _gpuEnumerationFailureLogged, "Failed while enumerating GPU performance counters.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadGPUsFromCounters()
|
||||
@@ -119,6 +135,11 @@ internal sealed partial class GPUStats : IDisposable
|
||||
|
||||
public void GetData()
|
||||
{
|
||||
if (_gpuEngineCategory is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Single batch read - one kernel transition for ALL GPU Engine instances
|
||||
@@ -183,9 +204,9 @@ internal sealed partial class GPUStats : IDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Ignore errors from ReadCategory (e.g., category not available).
|
||||
LogFailureOnce(ref _gpuReadFailureLogged, "Failed while reading GPU performance counters.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,14 @@ using Windows.Win32;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class MemoryStats : IDisposable
|
||||
internal sealed partial class MemoryStats : PerformanceCounterSourceBase, IDisposable
|
||||
{
|
||||
private readonly PerformanceCounter _memCommitted = new("Memory", "Committed Bytes", string.Empty);
|
||||
private readonly PerformanceCounter _memCached = new("Memory", "Cache Bytes", string.Empty);
|
||||
private readonly PerformanceCounter _memCommittedLimit = new("Memory", "Commit Limit", string.Empty);
|
||||
private readonly PerformanceCounter _memPoolPaged = new("Memory", "Pool Paged Bytes", string.Empty);
|
||||
private readonly PerformanceCounter _memPoolNonPaged = new("Memory", "Pool Nonpaged Bytes", string.Empty);
|
||||
private readonly PerformanceCounter? _memCommitted;
|
||||
private readonly PerformanceCounter? _memCached;
|
||||
private readonly PerformanceCounter? _memCommittedLimit;
|
||||
private readonly PerformanceCounter? _memPoolPaged;
|
||||
private readonly PerformanceCounter? _memPoolNonPaged;
|
||||
private bool _memoryCounterReadFailureLogged;
|
||||
|
||||
public float MemUsage
|
||||
{
|
||||
@@ -60,6 +61,15 @@ internal sealed partial class MemoryStats : IDisposable
|
||||
|
||||
public List<float> MemChartValues { get; set; } = new();
|
||||
|
||||
public MemoryStats()
|
||||
{
|
||||
_memCommitted = CreatePerformanceCounter("Memory", "Committed Bytes");
|
||||
_memCached = CreatePerformanceCounter("Memory", "Cache Bytes");
|
||||
_memCommittedLimit = CreatePerformanceCounter("Memory", "Commit Limit");
|
||||
_memPoolPaged = CreatePerformanceCounter("Memory", "Pool Paged Bytes");
|
||||
_memPoolNonPaged = CreatePerformanceCounter("Memory", "Pool Nonpaged Bytes");
|
||||
}
|
||||
|
||||
public void GetData()
|
||||
{
|
||||
Windows.Win32.System.SystemInformation.MEMORYSTATUSEX memStatus = default;
|
||||
@@ -77,11 +87,18 @@ internal sealed partial class MemoryStats : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
MemCached = (ulong)_memCached.NextValue();
|
||||
MemCommitted = (ulong)_memCommitted.NextValue();
|
||||
MemCommitLimit = (ulong)_memCommittedLimit.NextValue();
|
||||
MemPagedPool = (ulong)_memPoolPaged.NextValue();
|
||||
MemNonPagedPool = (ulong)_memPoolNonPaged.NextValue();
|
||||
try
|
||||
{
|
||||
MemCached = (ulong)(_memCached?.NextValue() ?? 0);
|
||||
MemCommitted = (ulong)(_memCommitted?.NextValue() ?? 0);
|
||||
MemCommitLimit = (ulong)(_memCommittedLimit?.NextValue() ?? 0);
|
||||
MemPagedPool = (ulong)(_memPoolPaged?.NextValue() ?? 0);
|
||||
MemNonPagedPool = (ulong)(_memPoolNonPaged?.NextValue() ?? 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogFailureOnce(ref _memoryCounterReadFailureLogged, "Failed while reading memory performance counters.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public string CreateMemImageUrl()
|
||||
@@ -91,10 +108,10 @@ internal sealed partial class MemoryStats : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_memCommitted.Dispose();
|
||||
_memCached.Dispose();
|
||||
_memCommittedLimit.Dispose();
|
||||
_memPoolPaged.Dispose();
|
||||
_memPoolNonPaged.Dispose();
|
||||
_memCommitted?.Dispose();
|
||||
_memCached?.Dispose();
|
||||
_memCommittedLimit?.Dispose();
|
||||
_memPoolPaged?.Dispose();
|
||||
_memPoolNonPaged?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Common;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class NetworkStats : IDisposable
|
||||
internal sealed partial class NetworkStats : PerformanceCounterSourceBase, IDisposable
|
||||
{
|
||||
private readonly Dictionary<string, List<PerformanceCounter>> _networkCounters = new();
|
||||
private bool _networkCounterReadFailureLogged;
|
||||
|
||||
private Dictionary<string, Data> NetworkUsages { get; set; } = new();
|
||||
|
||||
@@ -42,17 +44,44 @@ internal sealed partial class NetworkStats : IDisposable
|
||||
|
||||
private void InitNetworkPerfCounters()
|
||||
{
|
||||
var perfCounterCategory = new PerformanceCounterCategory("Network Interface");
|
||||
var instanceNames = perfCounterCategory.GetInstanceNames();
|
||||
foreach (var instanceName in instanceNames)
|
||||
try
|
||||
{
|
||||
var instanceCounters = new List<PerformanceCounter>();
|
||||
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Sent/sec", instanceName));
|
||||
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Received/sec", instanceName));
|
||||
instanceCounters.Add(new PerformanceCounter("Network Interface", "Current Bandwidth", instanceName));
|
||||
_networkCounters.Add(instanceName, instanceCounters);
|
||||
NetChartValues.Add(instanceName, new List<float>());
|
||||
NetworkUsages.Add(instanceName, new Data());
|
||||
var perfCounterCategory = CreatePerformanceCounterCategory("Network Interface");
|
||||
if (perfCounterCategory is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var instanceNames = perfCounterCategory.GetInstanceNames();
|
||||
foreach (var instanceName in instanceNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytesSent = CreatePerformanceCounter("Network Interface", "Bytes Sent/sec", instanceName, logFailure: false);
|
||||
var bytesReceived = CreatePerformanceCounter("Network Interface", "Bytes Received/sec", instanceName, logFailure: false);
|
||||
var currentBandwidth = CreatePerformanceCounter("Network Interface", "Current Bandwidth", instanceName, logFailure: false);
|
||||
if (bytesSent is null || bytesReceived is null || currentBandwidth is null)
|
||||
{
|
||||
bytesSent?.Dispose();
|
||||
bytesReceived?.Dispose();
|
||||
currentBandwidth?.Dispose();
|
||||
continue;
|
||||
}
|
||||
|
||||
var instanceCounters = new List<PerformanceCounter> { bytesSent, bytesReceived, currentBandwidth };
|
||||
_networkCounters.Add(instanceName, instanceCounters);
|
||||
NetChartValues.Add(instanceName, new List<float>());
|
||||
NetworkUsages.Add(instanceName, new Data());
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Skip interfaces whose counters cannot be initialized.
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError("Failed to initialize network performance counters.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,9 +117,9 @@ internal sealed partial class NetworkStats : IDisposable
|
||||
maxUsage = usage;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log.Error(ex, "Error getting network data.");
|
||||
LogFailureOnce(ref _networkCounterReadFailureLogged, "Failed while reading network performance counters.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.CmdPal.Common;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal abstract class PerformanceCounterSourceBase
|
||||
{
|
||||
protected PerformanceCounter? CreatePerformanceCounter(string categoryName, string counterName, string instanceName = "", bool readOnly = true, bool logFailure = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new PerformanceCounter(categoryName, counterName, instanceName, readOnly);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (logFailure)
|
||||
{
|
||||
var suffix = string.IsNullOrEmpty(instanceName) ? string.Empty : $@"\{instanceName}";
|
||||
CoreLogger.LogError($@"Failed to initialize performance counter '{categoryName}\{counterName}{suffix}'.", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected PerformanceCounterCategory? CreatePerformanceCounterCategory(string categoryName, bool logFailure = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!PerformanceCounterCategory.Exists(categoryName))
|
||||
{
|
||||
if (logFailure)
|
||||
{
|
||||
CoreLogger.LogError($@"Performance counter category '{categoryName}' does not exist on this system.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PerformanceCounterCategory(categoryName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (logFailure)
|
||||
{
|
||||
CoreLogger.LogError($@"Failed to initialize performance counter category '{categoryName}'.", ex);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void LogFailureOnce(ref bool hasLoggedFailure, string message, Exception ex)
|
||||
{
|
||||
if (!hasLoggedFailure)
|
||||
{
|
||||
hasLoggedFailure = true;
|
||||
CoreLogger.LogError(message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,24 +3,53 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.CmdPal.Ext.PerformanceMonitor;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class SystemData : IDisposable
|
||||
internal sealed partial class SystemData
|
||||
{
|
||||
public static MemoryStats MemStats { get; set; } = new MemoryStats();
|
||||
public static SystemData Shared { get; } = new();
|
||||
|
||||
public static NetworkStats NetStats { get; set; } = new NetworkStats();
|
||||
private readonly Lazy<MemoryStats> _memoryStats = new(() => CreateGuarded("Memory.Initialize", static () => new MemoryStats()));
|
||||
private readonly Lazy<NetworkStats> _networkStats = new(() => CreateGuarded("Network.Initialize", static () => new NetworkStats()));
|
||||
private readonly Lazy<GPUStats> _gpuStats = new(() => CreateGuarded("GPU.Initialize", static () => new GPUStats()));
|
||||
private readonly Lazy<CPUStats> _cpuStats = new(() => CreateGuarded("CPU.Initialize", static () => new CPUStats()));
|
||||
|
||||
public static GPUStats GPUStats { get; set; } = new GPUStats();
|
||||
public MemoryStats MemoryStats => _memoryStats.Value;
|
||||
|
||||
public static CPUStats CpuStats { get; set; } = new CPUStats();
|
||||
public NetworkStats NetworkStats => _networkStats.Value;
|
||||
|
||||
public SystemData()
|
||||
public GPUStats GPUStats => _gpuStats.Value;
|
||||
|
||||
public CPUStats CpuStats => _cpuStats.Value;
|
||||
|
||||
private SystemData()
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
private static T CreateGuarded<T>(string blockSuffix, Func<T> factory)
|
||||
{
|
||||
var isTracked = PerformanceMonitorCommandsProvider.CrashSentinel.BeginBlock(blockSuffix);
|
||||
|
||||
try
|
||||
{
|
||||
var value = factory();
|
||||
if (isTracked)
|
||||
{
|
||||
PerformanceMonitorCommandsProvider.CrashSentinel.CompleteBlock(blockSuffix);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (isTracked)
|
||||
{
|
||||
PerformanceMonitorCommandsProvider.CrashSentinel.CancelBlock(blockSuffix);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using CoreWidgetProvider.Helpers;
|
||||
using Microsoft.CmdPal.Common;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -10,30 +14,129 @@ namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
|
||||
|
||||
public partial class PerformanceMonitorCommandsProvider : CommandProvider
|
||||
{
|
||||
private readonly ICommandItem[] _commands;
|
||||
private readonly ICommandItem _band;
|
||||
public const string ProviderIdValue = "PerformanceMonitor";
|
||||
public const string ProviderLoadGuardBlockId = ProviderIdValue + ".ProviderLoad";
|
||||
public const string PageIdValue = "com.microsoft.cmdpal.performanceWidget";
|
||||
|
||||
public PerformanceMonitorCommandsProvider()
|
||||
internal static ProviderCrashSentinel CrashSentinel { get; } = new(ProviderIdValue);
|
||||
|
||||
private readonly Lock _stateLock = new();
|
||||
private ICommandItem[] _commands = [];
|
||||
private ICommandItem _band = new CommandItem();
|
||||
private PerformanceWidgetsPage? _mainPage;
|
||||
private PerformanceWidgetsPage? _bandPage;
|
||||
private bool _softDisabled;
|
||||
|
||||
public PerformanceMonitorCommandsProvider(bool softDisabled = false)
|
||||
{
|
||||
DisplayName = Resources.GetResource("Performance_Monitor_Title");
|
||||
Id = "PerformanceMonitor";
|
||||
Id = ProviderIdValue;
|
||||
Icon = Icons.PerformanceMonitorIcon;
|
||||
|
||||
var page = new PerformanceWidgetsPage(false);
|
||||
var band = new PerformanceWidgetsPage(true);
|
||||
_band = new CommandItem(band) { Title = DisplayName };
|
||||
_commands = [
|
||||
new CommandItem(page) { Title = DisplayName },
|
||||
];
|
||||
if (softDisabled)
|
||||
{
|
||||
SetDisabledState();
|
||||
}
|
||||
else
|
||||
{
|
||||
SetEnabledState();
|
||||
}
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands()
|
||||
{
|
||||
return _commands;
|
||||
lock (_stateLock)
|
||||
{
|
||||
return _commands;
|
||||
}
|
||||
}
|
||||
|
||||
public override ICommandItem[]? GetDockBands()
|
||||
{
|
||||
return new ICommandItem[] { _band };
|
||||
lock (_stateLock)
|
||||
{
|
||||
return [_band];
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryReactivateImmediately()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (!_softDisabled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CrashSentinel.ClearProviderState();
|
||||
SetEnabledState();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError("Failed to reactivate Performance Monitor in the current session. Keeping placeholder pages loaded.", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
RaiseItemsChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
DisposeActivePages();
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
private void SetDisabledState()
|
||||
{
|
||||
DisposeActivePages();
|
||||
|
||||
var page = new PerformanceMonitorDisabledPage(this);
|
||||
var band = new PerformanceMonitorDisabledPage(this);
|
||||
_band = new CommandItem(band)
|
||||
{
|
||||
Title = Resources.GetResource("Performance_Monitor_Disabled_Band_Title"),
|
||||
Subtitle = DisplayName,
|
||||
};
|
||||
_commands =
|
||||
[
|
||||
new CommandItem(page)
|
||||
{
|
||||
Title = DisplayName,
|
||||
Subtitle = Resources.GetResource("Performance_Monitor_Disabled_Subtitle"),
|
||||
},
|
||||
];
|
||||
_softDisabled = true;
|
||||
}
|
||||
|
||||
private void SetEnabledState()
|
||||
{
|
||||
DisposeActivePages();
|
||||
|
||||
_mainPage = new PerformanceWidgetsPage(false);
|
||||
_bandPage = new PerformanceWidgetsPage(true);
|
||||
_band = new CommandItem(_bandPage) { Title = DisplayName };
|
||||
_commands =
|
||||
[
|
||||
new CommandItem(_mainPage) { Title = DisplayName },
|
||||
];
|
||||
_softDisabled = false;
|
||||
}
|
||||
|
||||
private void DisposeActivePages()
|
||||
{
|
||||
_mainPage?.Dispose();
|
||||
_mainPage = null;
|
||||
|
||||
_bandPage?.Dispose();
|
||||
_bandPage = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CoreWidgetProvider.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
|
||||
|
||||
internal sealed partial class PerformanceMonitorDisabledPage : ContentPage
|
||||
{
|
||||
private readonly MarkdownContent _content;
|
||||
|
||||
public PerformanceMonitorDisabledPage(PerformanceMonitorCommandsProvider provider)
|
||||
{
|
||||
Id = PerformanceMonitorCommandsProvider.PageIdValue;
|
||||
Name = Resources.GetResource("Performance_Monitor_Disabled_Title");
|
||||
Title = Resources.GetResource("Performance_Monitor_Title");
|
||||
Icon = Icons.PerformanceMonitorIcon;
|
||||
|
||||
_content = new MarkdownContent(Resources.GetResource("Performance_Monitor_Disabled_Body"));
|
||||
Commands =
|
||||
[
|
||||
new CommandContextItem(new ReactivatePerformanceMonitorCommand(provider)),
|
||||
];
|
||||
}
|
||||
|
||||
public override IContent[] GetContent()
|
||||
{
|
||||
return [_content];
|
||||
}
|
||||
|
||||
private sealed partial class ReactivatePerformanceMonitorCommand(PerformanceMonitorCommandsProvider provider) : InvokableCommand
|
||||
{
|
||||
private readonly PerformanceMonitorCommandsProvider _provider = provider;
|
||||
|
||||
public override string Id => "com.microsoft.cmdpal.performanceWidget.reactivate";
|
||||
|
||||
public override IconInfo Icon => Icons.NavigateForwardIcon;
|
||||
|
||||
public override string Name => Resources.GetResource("Performance_Monitor_Reenable_Title");
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
if (_provider.TryReactivateImmediately())
|
||||
{
|
||||
return CommandResult.ShowToast(new ToastArgs
|
||||
{
|
||||
Message = Resources.GetResource("Performance_Monitor_Reenable_Success"),
|
||||
Result = CommandResult.GoHome(),
|
||||
});
|
||||
}
|
||||
|
||||
return CommandResult.ShowToast(Resources.GetResource("Performance_Monitor_Reenable_Failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,6 +189,32 @@
|
||||
<data name="Performance_Monitor_Title" xml:space="preserve">
|
||||
<value>Performance monitor</value>
|
||||
</data>
|
||||
<data name="Performance_Monitor_Disabled_Title" xml:space="preserve">
|
||||
<value>Performance monitor is temporarily disabled</value>
|
||||
</data>
|
||||
<data name="Performance_Monitor_Disabled_Subtitle" xml:space="preserve">
|
||||
<value>Temporarily disabled after repeated startup crashes</value>
|
||||
</data>
|
||||
<data name="Performance_Monitor_Disabled_Band_Title" xml:space="preserve">
|
||||
<value>Perf disabled</value>
|
||||
</data>
|
||||
<data name="Performance_Monitor_Disabled_Body" xml:space="preserve">
|
||||
<value>Performance monitor was temporarily disabled after repeated crashes while initializing or reading performance counters.
|
||||
|
||||
Select **Re-enable now** to clear the crash guard and restore the Performance Monitor command and dock band in the current session.</value>
|
||||
</data>
|
||||
<data name="Performance_Monitor_Reenable_Title" xml:space="preserve">
|
||||
<value>Re-enable now</value>
|
||||
</data>
|
||||
<data name="Performance_Monitor_Reenable_Subtitle" xml:space="preserve">
|
||||
<value>Clear the crash guard and restore Performance Monitor in this session</value>
|
||||
</data>
|
||||
<data name="Performance_Monitor_Reenable_Success" xml:space="preserve">
|
||||
<value>Performance monitor has been restored for this session.</value>
|
||||
</data>
|
||||
<data name="Performance_Monitor_Reenable_Failed" xml:space="preserve">
|
||||
<value>Performance monitor could not be restored right now. It will stay disabled for this session.</value>
|
||||
</data>
|
||||
<data name="CPU_Usage_Title" xml:space="preserve">
|
||||
<value>CPU Usage</value>
|
||||
</data>
|
||||
|
||||
Reference in New Issue
Block a user