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:
Jiří Polášek
2026-03-28 00:39:26 +01:00
committed by GitHub
parent f686155d9b
commit 943c2a1ff5
15 changed files with 1338 additions and 184 deletions

View File

@@ -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)
{

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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"));
}
}
}

View File

@@ -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>