diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/ExtensionLoadState.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/ExtensionLoadState.cs
new file mode 100644
index 0000000000..2a6f39f84b
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/ExtensionLoadState.cs
@@ -0,0 +1,45 @@
+// 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.
+
+namespace Microsoft.CmdPal.Common;
+
+///
+/// Shared constants for the extension load sentinel file used by
+/// ProviderLoadGuard and provider-specific crash sentinels to
+/// coordinate crash detection across process lifetimes.
+///
+public static class ExtensionLoadState
+{
+ ///
+ /// Name of the sentinel JSON file written to the config directory.
+ /// Both the app-level guard and individual extension sentinels must
+ /// read and write the same file for crash detection to work.
+ ///
+ public const string SentinelFileName = "extensionLoadState.json";
+
+ ///
+ /// JSON property name storing the owning provider id for a guarded block.
+ ///
+ public const string ProviderIdKey = "providerId";
+
+ ///
+ /// JSON property name indicating a guarded block was active when the
+ /// process exited.
+ ///
+ public const string LoadingKey = "loading";
+
+ ///
+ /// JSON property name storing the consecutive crash count for a guarded
+ /// block.
+ ///
+ public const string CrashCountKey = "crashCount";
+
+ ///
+ /// Shared lock that must be held around every read-modify-write cycle
+ /// on the sentinel file. Both ProviderLoadGuard and
+ /// provider-specific crash sentinels run in the same process and would
+ /// otherwise race on the file, silently dropping entries.
+ ///
+ public static readonly object SentinelFileLock = new();
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ProviderCrashSentinel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ProviderCrashSentinel.cs
new file mode 100644
index 0000000000..6903306bef
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ProviderCrashSentinel.cs
@@ -0,0 +1,241 @@
+// 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.Text.Json;
+using System.Text.Json.Nodes;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Common.Helpers;
+
+///
+/// Tracks guarded provider blocks in the shared extension-load sentinel file so
+/// callers can fail closed after repeated native crashes that bypass managed
+/// exception handling.
+///
+public sealed class ProviderCrashSentinel
+{
+ private readonly string _providerId;
+ private readonly Lock _sentinelLock = new();
+ private readonly HashSet _completedBlocks = [];
+ private readonly HashSet _activeBlocks = [];
+
+ public ProviderCrashSentinel(string providerId)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(providerId);
+ _providerId = providerId;
+ }
+
+ public bool BeginBlock(string blockSuffix)
+ {
+ var blockId = CreateBlockId(blockSuffix);
+
+ lock (_sentinelLock)
+ {
+ if (_completedBlocks.Contains(blockId) || !_activeBlocks.Add(blockId))
+ {
+ return false;
+ }
+
+ UpdateState(
+ state =>
+ {
+ var entry = GetOrCreateEntry(state, blockId);
+ entry[ExtensionLoadState.LoadingKey] = true;
+ });
+ return true;
+ }
+ }
+
+ public void CompleteBlock(string blockSuffix)
+ {
+ var blockId = CreateBlockId(blockSuffix);
+
+ lock (_sentinelLock)
+ {
+ if (!_activeBlocks.Remove(blockId))
+ {
+ return;
+ }
+
+ _completedBlocks.Add(blockId);
+ UpdateState(state => state.Remove(blockId));
+ }
+ }
+
+ public void CancelBlock(string blockSuffix)
+ {
+ var blockId = CreateBlockId(blockSuffix);
+
+ lock (_sentinelLock)
+ {
+ if (!_activeBlocks.Remove(blockId))
+ {
+ return;
+ }
+
+ UpdateState(state => state.Remove(blockId));
+ }
+ }
+
+ public void ClearProviderState()
+ {
+ lock (_sentinelLock)
+ {
+ _completedBlocks.RemoveWhere(blockId => blockId.StartsWith(_providerId + ".", StringComparison.Ordinal));
+ _activeBlocks.RemoveWhere(blockId => blockId.StartsWith(_providerId + ".", StringComparison.Ordinal));
+
+ UpdateState(
+ state =>
+ {
+ var keysToRemove = state
+ .Where(kvp => IsProviderEntry(kvp.Key, kvp.Value as JsonObject))
+ .Select(kvp => kvp.Key)
+ .ToArray();
+
+ foreach (var key in keysToRemove)
+ {
+ state.Remove(key);
+ }
+ });
+ }
+ }
+
+ private void UpdateState(Action update)
+ {
+ try
+ {
+ lock (ExtensionLoadState.SentinelFileLock)
+ {
+ var sentinelPath = GetSentinelPath();
+ var state = LoadState(sentinelPath);
+ update(state);
+ SaveState(sentinelPath, state);
+ }
+ }
+ catch (Exception ex)
+ {
+ CoreLogger.LogError($"Failed to update crash sentinel state for provider '{_providerId}'.", ex);
+ }
+ }
+
+ private static JsonObject LoadState(string sentinelPath)
+ {
+ if (!File.Exists(sentinelPath))
+ {
+ return [];
+ }
+
+ try
+ {
+ var json = File.ReadAllText(sentinelPath);
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ CoreLogger.LogWarning($"Crash sentinel state file '{sentinelPath}' was empty. Treating as empty state.");
+ DeleteInvalidStateFile(sentinelPath);
+ return [];
+ }
+
+ if (JsonNode.Parse(json) is JsonObject state)
+ {
+ return state;
+ }
+
+ CoreLogger.LogError($"Crash sentinel state file '{sentinelPath}' did not contain a JSON object. Treating as empty state.");
+ DeleteInvalidStateFile(sentinelPath);
+ }
+ catch (JsonException ex)
+ {
+ CoreLogger.LogError($"Failed to parse crash sentinel state from '{sentinelPath}'. Treating as empty state.", ex);
+ DeleteInvalidStateFile(sentinelPath);
+ }
+ catch (IOException ex)
+ {
+ CoreLogger.LogError($"Failed to read crash sentinel state from '{sentinelPath}'. Treating as empty state.", ex);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ CoreLogger.LogError($"Access denied reading crash sentinel state from '{sentinelPath}'. Treating as empty state.", ex);
+ }
+
+ return [];
+ }
+
+ private static void DeleteInvalidStateFile(string sentinelPath)
+ {
+ try
+ {
+ File.Delete(sentinelPath);
+ }
+ catch (IOException ex)
+ {
+ CoreLogger.LogError($"Failed to delete invalid crash sentinel state file '{sentinelPath}'.", ex);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ CoreLogger.LogError($"Access denied deleting invalid crash sentinel state file '{sentinelPath}'.", ex);
+ }
+ }
+
+ private static void SaveState(string sentinelPath, JsonObject state)
+ {
+ if (state.Count == 0)
+ {
+ if (File.Exists(sentinelPath))
+ {
+ File.Delete(sentinelPath);
+ }
+
+ return;
+ }
+
+ var directory = Path.GetDirectoryName(sentinelPath);
+ if (!string.IsNullOrEmpty(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ var tempPath = sentinelPath + ".tmp";
+ File.WriteAllText(tempPath, state.ToJsonString());
+ File.Move(tempPath, sentinelPath, overwrite: true);
+ }
+
+ private JsonObject GetOrCreateEntry(JsonObject state, string blockId)
+ {
+ if (state[blockId] is JsonObject existing)
+ {
+ existing[ExtensionLoadState.ProviderIdKey] = _providerId;
+ return existing;
+ }
+
+ var entry = new JsonObject
+ {
+ [ExtensionLoadState.ProviderIdKey] = _providerId,
+ [ExtensionLoadState.LoadingKey] = false,
+ [ExtensionLoadState.CrashCountKey] = 0,
+ };
+ state[blockId] = entry;
+ return entry;
+ }
+
+ private bool IsProviderEntry(string blockId, JsonObject? entry)
+ {
+ var providerId = entry?[ExtensionLoadState.ProviderIdKey]?.GetValue();
+ if (string.Equals(providerId, _providerId, StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ return blockId.StartsWith(_providerId + ".", StringComparison.Ordinal);
+ }
+
+ private string CreateBlockId(string blockSuffix)
+ {
+ return $"{_providerId}.{blockSuffix}";
+ }
+
+ private static string GetSentinelPath()
+ {
+ return Path.Combine(Utilities.BaseSettingsPath("Microsoft.CmdPal"), ExtensionLoadState.SentinelFileName);
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ProviderLoadGuard.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ProviderLoadGuard.cs
new file mode 100644
index 0000000000..2da90693df
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ProviderLoadGuard.cs
@@ -0,0 +1,231 @@
+// 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.Text.Json.Nodes;
+
+namespace Microsoft.CmdPal.Common.Helpers;
+
+///
+/// Reads the shared provider crash sentinel file at startup, increments crash
+/// counts for blocks left marked as active, and determines which providers
+/// should be soft-disabled for the current session.
+///
+public sealed class ProviderLoadGuard
+{
+ private const int MaxConsecutiveCrashes = 2;
+
+ private readonly string _sentinelPath;
+ private readonly HashSet _disabledProviders = [];
+
+ public ProviderLoadGuard(string configDirectory)
+ {
+ _sentinelPath = Path.Combine(configDirectory, ExtensionLoadState.SentinelFileName);
+ DetectCrashes();
+ }
+
+ ///
+ /// Returns true if the provider has been disabled due to repeated crashes
+ /// in one of its tracked guarded blocks.
+ ///
+ public bool IsProviderDisabled(string providerId) => _disabledProviders.Contains(providerId);
+
+ ///
+ /// Call immediately before attempting a guarded operation.
+ /// Marks the block as "loading" in the sentinel file so that a
+ /// subsequent native crash leaves evidence on disk.
+ ///
+ public void Enter(string blockId, string providerId)
+ {
+ UpdateState(state =>
+ {
+ var entry = GetOrCreateEntry(state, blockId, providerId);
+ entry[ExtensionLoadState.LoadingKey] = true;
+ });
+ }
+
+ ///
+ /// Call after a guarded operation succeeds or fails gracefully via managed
+ /// exception. Clears the loading flag and removes the block entry.
+ ///
+ public void Exit(string blockId)
+ {
+ UpdateState(state => state.Remove(blockId));
+ }
+
+ ///
+ /// Removes any persisted crash state for a provider so it can be retried
+ /// on the next launch.
+ ///
+ public void ClearProvider(string providerId)
+ {
+ _disabledProviders.Remove(providerId);
+ UpdateState(state =>
+ {
+ var keysToRemove = state
+ .Where(kvp => TryGetProviderId(kvp.Key, kvp.Value as JsonObject) == providerId)
+ .Select(kvp => kvp.Key)
+ .ToArray();
+
+ foreach (var key in keysToRemove)
+ {
+ state.Remove(key);
+ }
+ });
+ }
+
+ private void DetectCrashes()
+ {
+ // Read the sentinel file once at startup to detect providers that
+ // crashed on the previous launch, then write back the updated state.
+ lock (ExtensionLoadState.SentinelFileLock)
+ {
+ var state = ReadState();
+
+ var keysToCheck = state.Select(kvp => kvp.Key).ToArray();
+
+ foreach (var key in keysToCheck)
+ {
+ if (state[key] is not JsonObject entry)
+ {
+ continue;
+ }
+
+ var providerId = TryGetProviderId(key, entry);
+ var wasLoading = entry[ExtensionLoadState.LoadingKey]?.GetValue() ?? false;
+
+ if (wasLoading)
+ {
+ // The guarded block was active when the process died.
+ var crashCount = (entry[ExtensionLoadState.CrashCountKey]?.GetValue() ?? 0) + 1;
+ entry[ExtensionLoadState.CrashCountKey] = crashCount;
+ entry[ExtensionLoadState.LoadingKey] = false;
+
+ if (crashCount >= MaxConsecutiveCrashes)
+ {
+ _disabledProviders.Add(providerId);
+ CoreLogger.LogError($"Provider '{providerId}' disabled after {crashCount} consecutive crash(es) in guarded block '{key}'.");
+ }
+ else
+ {
+ CoreLogger.LogWarning($"Guarded block '{key}' for provider '{providerId}' crashed on previous launch (crash {crashCount}/{MaxConsecutiveCrashes}). Will retry.");
+ }
+ }
+
+ var currentCrashCount = entry[ExtensionLoadState.CrashCountKey]?.GetValue() ?? 0;
+ if (currentCrashCount >= MaxConsecutiveCrashes)
+ {
+ // Persist disabled state from a previous session.
+ _disabledProviders.Add(providerId);
+ }
+
+ if (!(entry[ExtensionLoadState.LoadingKey]?.GetValue() ?? false) && currentCrashCount == 0)
+ {
+ state.Remove(key);
+ }
+ }
+
+ WriteState(state);
+ }
+ }
+
+ ///
+ /// Reads the sentinel file, applies a mutation, and writes it back
+ /// under to prevent
+ /// concurrent writers from clobbering each other's entries.
+ ///
+ private void UpdateState(Action mutate)
+ {
+ try
+ {
+ lock (ExtensionLoadState.SentinelFileLock)
+ {
+ var state = ReadState();
+ mutate(state);
+ WriteState(state);
+ }
+ }
+ catch (Exception ex)
+ {
+ CoreLogger.LogError("Failed to update extension load sentinel file.", ex);
+ }
+ }
+
+ private static JsonObject GetOrCreateEntry(JsonObject state, string blockId, string providerId)
+ {
+ if (state[blockId] is JsonObject existing)
+ {
+ existing[ExtensionLoadState.ProviderIdKey] = providerId;
+ return existing;
+ }
+
+ var entry = new JsonObject
+ {
+ [ExtensionLoadState.ProviderIdKey] = providerId,
+ [ExtensionLoadState.LoadingKey] = false,
+ [ExtensionLoadState.CrashCountKey] = 0,
+ };
+ state[blockId] = entry;
+ return entry;
+ }
+
+ private static string TryGetProviderId(string blockId, JsonObject? entry)
+ {
+ var providerId = entry?[ExtensionLoadState.ProviderIdKey]?.GetValue();
+ if (!string.IsNullOrWhiteSpace(providerId))
+ {
+ return providerId;
+ }
+
+ var separatorIndex = blockId.IndexOf('.');
+ return separatorIndex > 0 ? blockId[..separatorIndex] : blockId;
+ }
+
+ private JsonObject ReadState()
+ {
+ try
+ {
+ if (File.Exists(_sentinelPath))
+ {
+ var json = File.ReadAllText(_sentinelPath);
+ return JsonNode.Parse(json)?.AsObject() ?? [];
+ }
+ }
+ catch (Exception ex)
+ {
+ CoreLogger.LogError("Failed to read extension load sentinel file.", ex);
+ }
+
+ return [];
+ }
+
+ private void WriteState(JsonObject state)
+ {
+ try
+ {
+ if (state.Count == 0)
+ {
+ if (File.Exists(_sentinelPath))
+ {
+ File.Delete(_sentinelPath);
+ }
+
+ return;
+ }
+
+ var directory = Path.GetDirectoryName(_sentinelPath);
+ if (directory != null)
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ var tempPath = _sentinelPath + ".tmp";
+ File.WriteAllText(tempPath, state.ToJsonString());
+ File.Move(tempPath, _sentinelPath, overwrite: true);
+ }
+ catch (Exception ex)
+ {
+ CoreLogger.LogError("Failed to write extension load sentinel file.", ex);
+ }
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
index 81d5892664..7ad32a2213 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
@@ -125,7 +125,7 @@ public partial class App : Application, IDisposable
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
- AddBuiltInCommands(services);
+ AddBuiltInCommands(services, appInfoService.ConfigDirectory);
AddCoreServices(services, appInfoService);
@@ -134,8 +134,10 @@ public partial class App : Application, IDisposable
return services.BuildServiceProvider();
}
- private static void AddBuiltInCommands(ServiceCollection services)
+ private static void AddBuiltInCommands(ServiceCollection services, string configDirectory)
{
+ var providerLoadGuard = new ProviderLoadGuard(configDirectory);
+
// Built-in Commands. Order matters - this is the order they'll be presented by default.
var allApps = new AllAppsCommandProvider();
var files = new IndexerCommandsProvider();
@@ -166,8 +168,7 @@ public partial class App : Application, IDisposable
}
catch (Exception ex)
{
- Logger.LogError("Couldn't load winget");
- Logger.LogError(ex.ToString());
+ Logger.LogError("Couldn't load winget", ex);
}
services.AddSingleton();
@@ -178,7 +179,37 @@ public partial class App : Application, IDisposable
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
+
+ var performanceMonitorSoftDisabled = providerLoadGuard.IsProviderDisabled(PerformanceMonitorCommandsProvider.ProviderIdValue);
+ if (performanceMonitorSoftDisabled)
+ {
+ Logger.LogWarning("Performance monitor is temporarily disabled after repeated crashes. Loading placeholder pages instead of activating performance counters.");
+ }
+
+ if (!performanceMonitorSoftDisabled)
+ {
+ providerLoadGuard.Enter(PerformanceMonitorCommandsProvider.ProviderLoadGuardBlockId, PerformanceMonitorCommandsProvider.ProviderIdValue);
+ }
+
+ try
+ {
+ var performanceMonitor = new PerformanceMonitorCommandsProvider(performanceMonitorSoftDisabled);
+ services.AddSingleton(performanceMonitor);
+
+ if (!performanceMonitorSoftDisabled)
+ {
+ providerLoadGuard.Exit(PerformanceMonitorCommandsProvider.ProviderLoadGuardBlockId);
+ }
+ }
+ catch (Exception ex)
+ {
+ if (!performanceMonitorSoftDisabled)
+ {
+ providerLoadGuard.Exit(PerformanceMonitorCommandsProvider.ProviderLoadGuardBlockId);
+ }
+
+ Logger.LogError("Couldn't load performance monitor", ex);
+ }
}
private static void AddUIServices(ServiceCollection services, DispatcherQueue dispatcherQueue)
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Common.UnitTests/Helpers/ProviderLoadGuardTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Common.UnitTests/Helpers/ProviderLoadGuardTests.cs
new file mode 100644
index 0000000000..2c12f3db3d
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Common.UnitTests/Helpers/ProviderLoadGuardTests.cs
@@ -0,0 +1,163 @@
+// 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.IO;
+using System.Text.Json.Nodes;
+using Microsoft.CmdPal.Common.Helpers;
+
+namespace Microsoft.CmdPal.Common.UnitTests.Helpers;
+
+[TestClass]
+public class ProviderLoadGuardTests
+{
+ private readonly List _temporaryDirectories = [];
+
+ [TestMethod]
+ public void EnterAndExit_PersistAndClearGuardedBlock()
+ {
+ var configDirectory = CreateTempDirectory();
+ var sentinelPath = GetSentinelPath(configDirectory);
+ var guard = new ProviderLoadGuard(configDirectory);
+
+ guard.Enter("Provider.Block", "Provider");
+
+ var state = ReadState(sentinelPath);
+ var entry = state["Provider.Block"] as JsonObject;
+
+ Assert.IsNotNull(entry);
+ Assert.AreEqual("Provider", entry[ExtensionLoadState.ProviderIdKey]?.GetValue());
+ Assert.AreEqual(true, entry[ExtensionLoadState.LoadingKey]?.GetValue());
+ Assert.AreEqual(0, entry[ExtensionLoadState.CrashCountKey]?.GetValue());
+
+ guard.Exit("Provider.Block");
+
+ Assert.IsFalse(File.Exists(sentinelPath));
+ }
+
+ [TestMethod]
+ public void Constructor_DisablesProviderAfterSecondConsecutiveCrash()
+ {
+ var configDirectory = CreateTempDirectory();
+ var sentinelPath = GetSentinelPath(configDirectory);
+
+ WriteState(
+ sentinelPath,
+ new JsonObject
+ {
+ ["Provider.Block"] = CreateEntry("Provider", loading: true, crashCount: 1),
+ });
+
+ var guard = new ProviderLoadGuard(configDirectory);
+
+ Assert.IsTrue(guard.IsProviderDisabled("Provider"));
+
+ var state = ReadState(sentinelPath);
+ var entry = state["Provider.Block"] as JsonObject;
+
+ Assert.IsNotNull(entry);
+ Assert.AreEqual(false, entry[ExtensionLoadState.LoadingKey]?.GetValue());
+ Assert.AreEqual(2, entry[ExtensionLoadState.CrashCountKey]?.GetValue());
+ }
+
+ [TestMethod]
+ public void ClearProvider_RemovesDisabledStateAndOnlyMatchingEntries()
+ {
+ var configDirectory = CreateTempDirectory();
+ var sentinelPath = GetSentinelPath(configDirectory);
+
+ WriteState(
+ sentinelPath,
+ new JsonObject
+ {
+ ["CustomBlock"] = CreateEntry("Provider", loading: false, crashCount: 2),
+ ["OtherProvider.Block"] = CreateEntry("OtherProvider", loading: false, crashCount: 2),
+ });
+
+ var guard = new ProviderLoadGuard(configDirectory);
+
+ Assert.IsTrue(guard.IsProviderDisabled("Provider"));
+ Assert.IsTrue(guard.IsProviderDisabled("OtherProvider"));
+
+ guard.ClearProvider("Provider");
+
+ Assert.IsFalse(guard.IsProviderDisabled("Provider"));
+ Assert.IsTrue(guard.IsProviderDisabled("OtherProvider"));
+
+ var state = ReadState(sentinelPath);
+ Assert.IsFalse(state.ContainsKey("CustomBlock"));
+ Assert.IsTrue(state.ContainsKey("OtherProvider.Block"));
+ }
+
+ [DataTestMethod]
+ [DataRow("")]
+ [DataRow("{")]
+ [DataRow("[]")]
+ public void Constructor_RecoversFromInvalidSentinelContents(string invalidSentinelContents)
+ {
+ var configDirectory = CreateTempDirectory();
+ var sentinelPath = GetSentinelPath(configDirectory);
+ File.WriteAllText(sentinelPath, invalidSentinelContents);
+
+ var guard = new ProviderLoadGuard(configDirectory);
+
+ Assert.IsFalse(guard.IsProviderDisabled("Provider"));
+ Assert.IsFalse(File.Exists(sentinelPath));
+
+ guard.Enter("Provider.Block", "Provider");
+
+ var state = ReadState(sentinelPath);
+ Assert.IsTrue(state.ContainsKey("Provider.Block"));
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ foreach (var directory in _temporaryDirectories)
+ {
+ if (Directory.Exists(directory))
+ {
+ Directory.Delete(directory, recursive: true);
+ }
+ }
+ }
+
+ private string CreateTempDirectory()
+ {
+ var directory = Path.Combine(Path.GetTempPath(), "CmdPal.ProviderLoadGuardTests", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(directory);
+ _temporaryDirectories.Add(directory);
+ return directory;
+ }
+
+ private static JsonObject CreateEntry(string providerId, bool loading, int crashCount)
+ {
+ return new JsonObject
+ {
+ [ExtensionLoadState.ProviderIdKey] = providerId,
+ [ExtensionLoadState.LoadingKey] = loading,
+ [ExtensionLoadState.CrashCountKey] = crashCount,
+ };
+ }
+
+ private static string GetSentinelPath(string configDirectory)
+ {
+ return Path.Combine(configDirectory, ExtensionLoadState.SentinelFileName);
+ }
+
+ private static JsonObject ReadState(string sentinelPath)
+ {
+ if (JsonNode.Parse(File.ReadAllText(sentinelPath)) is JsonObject state)
+ {
+ return state;
+ }
+
+ throw new AssertFailedException($"Sentinel state at '{sentinelPath}' was not a JSON object.");
+ }
+
+ private static void WriteState(string sentinelPath, JsonObject state)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(sentinelPath)!);
+ File.WriteAllText(sentinelPath, state.ToJsonString());
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/CPUStats.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/CPUStats.cs
index 99a02376ea..ab9caec88a 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/CPUStats.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/CPUStats.cs
@@ -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 _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();
-
- 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();
+
+ if (includeTopProcesses)
+ {
+ EnsureCPUProcessCountersInitialized();
+
+ var countersToRemove = new List();
+ 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)
{
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/DataManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/DataManager.cs
index 940411a6b7..35bace54f9 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/DataManager.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/DataManager.cs
@@ -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();
}
}
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 30cbb05de1..9c4aa4b57e 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
@@ -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 _knownPhysIds = [];
@@ -41,6 +41,8 @@ internal sealed partial class GPUStats : IDisposable
// Previous raw samples for computing cooked (delta-based) values
private Dictionary _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);
}
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/MemoryStats.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/MemoryStats.cs
index bb371353f0..934c8afc19 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/MemoryStats.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/MemoryStats.cs
@@ -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 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();
}
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/NetworkStats.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/NetworkStats.cs
index d5dc3ac15f..00e4b1e529 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/NetworkStats.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/NetworkStats.cs
@@ -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> _networkCounters = new();
+ private bool _networkCounterReadFailureLogged;
private Dictionary 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();
- 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());
- 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 { bytesSent, bytesReceived, currentBandwidth };
+ _networkCounters.Add(instanceName, instanceCounters);
+ NetChartValues.Add(instanceName, new List());
+ 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);
}
}
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/PerformanceCounterSourceBase.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/PerformanceCounterSourceBase.cs
new file mode 100644
index 0000000000..e370ad1f81
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/PerformanceCounterSourceBase.cs
@@ -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);
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/SystemData.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/SystemData.cs
index 52d0b2c536..c83293a074 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/SystemData.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/SystemData.cs
@@ -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 = new(() => CreateGuarded("Memory.Initialize", static () => new MemoryStats()));
+ private readonly Lazy _networkStats = new(() => CreateGuarded("Network.Initialize", static () => new NetworkStats()));
+ private readonly Lazy _gpuStats = new(() => CreateGuarded("GPU.Initialize", static () => new GPUStats()));
+ private readonly Lazy _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(string blockSuffix, Func 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;
+ }
}
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceMonitorCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceMonitorCommandsProvider.cs
index 6e7c66cdc8..3f00a99395 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceMonitorCommandsProvider.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceMonitorCommandsProvider.cs
@@ -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;
}
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceMonitorDisabledPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceMonitorDisabledPage.cs
new file mode 100644
index 0000000000..f2ae115297
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceMonitorDisabledPage.cs
@@ -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"));
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Strings/en-US/Resources.resw b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Strings/en-US/Resources.resw
index bcbbbfc001..1d49265baf 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Strings/en-US/Resources.resw
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Strings/en-US/Resources.resw
@@ -189,6 +189,32 @@
Performance monitor
+
+ Performance monitor is temporarily disabled
+
+
+ Temporarily disabled after repeated startup crashes
+
+
+ Perf disabled
+
+
+ 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.
+
+
+ Re-enable now
+
+
+ Clear the crash guard and restore Performance Monitor in this session
+
+
+ Performance monitor has been restored for this session.
+
+
+ Performance monitor could not be restored right now. It will stay disabled for this session.
+
CPU Usage