diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt
index 3e7341d5c3..fee0314208 100644
--- a/.github/actions/spell-check/allow/code.txt
+++ b/.github/actions/spell-check/allow/code.txt
@@ -38,6 +38,7 @@ Gbps
gcode
Heatshrink
Mbits
+Kbits
MBs
mkv
msix
@@ -97,6 +98,7 @@ Yubico
Perplexity
Groq
svgl
+devhome
# KEYS
@@ -322,6 +324,7 @@ REGSTR
# Misc Win32 APIs and PInvokes
INVOKEIDLIST
+MEMORYSTATUSEX
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
DDDD
@@ -342,3 +345,7 @@ reportbug
#ffmpeg
crf
nostdin
+
+# Performance counter keys
+engtype
+Nonpaged
diff --git a/.github/actions/spell-check/allow/names.txt b/.github/actions/spell-check/allow/names.txt
index ab2446f8ae..4df6c5c3e1 100644
--- a/.github/actions/spell-check/allow/names.txt
+++ b/.github/actions/spell-check/allow/names.txt
@@ -192,6 +192,7 @@ ycv
yeelam
Yuniardi
yuyoyuppe
+zadjii
Zeol
Zhao
Zhaopeng
@@ -228,6 +229,7 @@ regedit
roslyn
Skia
Spotify
+taskmgr
tldr
Vanara
wangyi
@@ -243,4 +245,3 @@ xamlstyler
Xavalon
Xbox
Youdao
-zadjii
diff --git a/PowerToys.slnx b/PowerToys.slnx
index 506545a754..a6bfd3a935 100644
--- a/PowerToys.slnx
+++ b/PowerToys.slnx
@@ -219,6 +219,10 @@
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
index 789f1aaa03..5101891e6f 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
@@ -13,6 +13,7 @@ using Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.CmdPal.Ext.Calc;
using Microsoft.CmdPal.Ext.ClipboardHistory;
using Microsoft.CmdPal.Ext.Indexer;
+using Microsoft.CmdPal.Ext.PerformanceMonitor;
using Microsoft.CmdPal.Ext.Registry;
using Microsoft.CmdPal.Ext.RemoteDesktop;
using Microsoft.CmdPal.Ext.Shell;
@@ -177,6 +178,7 @@ public partial class App : Application, IDisposable
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
}
private static void AddUIServices(ServiceCollection services, DispatcherQueue dispatcherQueue)
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
index 0ec7518188..a80b174cec 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
@@ -141,6 +141,7 @@
+
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Enums/WidgetDataState.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Enums/WidgetDataState.cs
new file mode 100644
index 0000000000..544c6aaf2f
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Enums/WidgetDataState.cs
@@ -0,0 +1,13 @@
+// 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 CoreWidgetProvider.Widgets.Enums;
+
+public enum WidgetDataState
+{
+ Unknown,
+ Requested, // Request is out, waiting on a response. Current data is stale.
+ Okay, // Received and updated data, stable state.
+ Failed, // Failed retrieving data.
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Enums/WidgetPageState.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Enums/WidgetPageState.cs
new file mode 100644
index 0000000000..b832e1ce30
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Enums/WidgetPageState.cs
@@ -0,0 +1,13 @@
+// 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 CoreWidgetProvider.Widgets.Enums;
+
+public enum WidgetPageState
+{
+ Unknown,
+ Configure,
+ Loading,
+ Content,
+}
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
new file mode 100644
index 0000000000..99a02376ea
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/CPUStats.cs
@@ -0,0 +1,146 @@
+// 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.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace CoreWidgetProvider.Helpers;
+
+internal sealed partial class CPUStats : 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 Dictionary _cpuCounters = new();
+
+ internal sealed class ProcessStats
+ {
+ public Process? Process { get; set; }
+
+ public float CpuUsage { get; set; }
+ }
+
+ public float CpuUsage { get; set; }
+
+ public float CpuSpeed { get; set; }
+
+ public ProcessStats[] ProcessCPUStats { get; set; }
+
+ public List CpuChartValues { get; set; } = new();
+
+ public CPUStats()
+ {
+ CpuUsage = 0;
+ ProcessCPUStats =
+ [
+ new ProcessStats(),
+ new ProcessStats(),
+ new ProcessStats()
+ ];
+
+ InitCPUPerfCounters();
+ }
+
+ private void InitCPUPerfCounters()
+ {
+ var allProcesses = Process.GetProcesses().Where(p => (long)p.MainWindowHandle != 0);
+
+ foreach (var process in allProcesses)
+ {
+ _cpuCounters.Add(process, new PerformanceCounter("Process", "% Processor Time", process.ProcessName, true));
+ }
+ }
+
+ 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)
+ {
+ ChartHelper.AddNextChartValue(CpuUsage * 100, CpuChartValues);
+ }
+
+ var chartMs = timer.ElapsedMilliseconds - speedMs;
+
+ var processCPUUsages = new Dictionary();
+
+ if (includeTopProcesses)
+ {
+ foreach (var processCounter in _cpuCounters)
+ {
+ try
+ {
+ // process might be terminated
+ processCPUUsages.Add(processCounter.Key, processCounter.Value.NextValue() / Environment.ProcessorCount);
+ }
+ catch (InvalidOperationException)
+ {
+ // _log.Information($"ProcessCounter Key {processCounter.Key} no longer exists, removing from _cpuCounters.");
+ _cpuCounters.Remove(processCounter.Key);
+ }
+ catch (Exception)
+ {
+ // _log.Error(ex, "Error going through process counters.");
+ }
+ }
+
+ 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}]");
+ }
+
+ internal string CreateCPUImageUrl()
+ {
+ return ChartHelper.CreateImageUrl(CpuChartValues, ChartHelper.ChartType.CPU);
+ }
+
+ internal string GetCpuProcessText(int cpuProcessIndex)
+ {
+ if (cpuProcessIndex >= ProcessCPUStats.Length)
+ {
+ return "no data";
+ }
+
+ return $"{ProcessCPUStats[cpuProcessIndex].Process?.ProcessName} ({ProcessCPUStats[cpuProcessIndex].CpuUsage / 100:p})";
+ }
+
+ internal void KillTopProcess(int cpuProcessIndex)
+ {
+ if (cpuProcessIndex >= ProcessCPUStats.Length)
+ {
+ return;
+ }
+
+ ProcessCPUStats[cpuProcessIndex].Process?.Kill();
+ }
+
+ public void Dispose()
+ {
+ _procPerf.Dispose();
+ _procPerformance.Dispose();
+ _procFrequency.Dispose();
+
+ foreach (var counter in _cpuCounters.Values)
+ {
+ counter.Dispose();
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/ChartHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/ChartHelper.cs
new file mode 100644
index 0000000000..bec3398c8b
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/ChartHelper.cs
@@ -0,0 +1,289 @@
+// 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.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using System.Xml.Linq;
+
+namespace CoreWidgetProvider.Helpers;
+
+internal sealed class ChartHelper
+{
+ public enum ChartType
+ {
+ CPU,
+ GPU,
+ Mem,
+ Net,
+ }
+
+ public const int ChartHeight = 86;
+ public const int ChartWidth = 268;
+
+ private const string LightGrayBoxStyle = "fill:none;stroke:lightgrey;stroke-width:1";
+
+ private const string CPULineStyle = "fill:none;stroke:rgb(57,184,227);stroke-width:1";
+ private const string GPULineStyle = "fill:none;stroke:rgb(222,104,242);stroke-width:1";
+ private const string MemLineStyle = "fill:none;stroke:rgb(92,158,250);stroke-width:1";
+ private const string NetLineStyle = "fill:none;stroke:rgb(245,98,142);stroke-width:1";
+
+ private const string FillStyle = "fill:url(#gradientId);stroke:transparent";
+
+ private const string CPUBrushStop1Style = "stop-color:rgb(57,184,227);stop-opacity:0.4";
+ private const string CPUBrushStop2Style = "stop-color:rgb(0,86,110);stop-opacity:0.25";
+
+ private const string GPUBrushStop1Style = "stop-color:rgb(222,104,242);stop-opacity:0.4";
+ private const string GPUBrushStop2Style = "stop-color:rgb(125,0,138);stop-opacity:0.25";
+
+ private const string MemBrushStop1Style = "stop-color:rgb(92,158,250);stop-opacity:0.4";
+ private const string MemBrushStop2Style = "stop-color:rgb(0,34,92);stop-opacity:0.25";
+
+ private const string NetBrushStop1Style = "stop-color:rgb(245,98,142);stop-opacity:0.4";
+ private const string NetBrushStop2Style = "stop-color:rgb(130,0,47);stop-opacity:0.25";
+
+ private const string SvgElement = "svg";
+ private const string RectElement = "rect";
+ private const string PolylineElement = "polyline";
+ private const string DefsElement = "defs";
+ private const string LinearGradientElement = "linearGradient";
+ private const string StopElement = "stop";
+
+ private const string HeightAttr = "height";
+ private const string WidthAttr = "width";
+ private const string StyleAttr = "style";
+ private const string PointsAttr = "points";
+ private const string OffsetAttr = "offset";
+ private const string X1Attr = "x1";
+ private const string X2Attr = "x2";
+ private const string Y1Attr = "y1";
+ private const string Y2Attr = "y2";
+ private const string IdAttr = "id";
+
+ private const int MaxChartValues = 34;
+
+ public static string CreateImageUrl(List chartValues, ChartType type)
+ {
+ var chartStr = CreateChart(chartValues, type);
+ return "data:image/svg+xml;utf8," + chartStr;
+ }
+
+ ///
+ /// Creates an SVG image for the chart.
+ ///
+ /// The values to plot on the chart
+ /// The type of chart. Each chart type uses different colors.
+ ///
+ /// The SVG is made of three shapes:
+ /// 1. A colored line, plotting the points on the graph
+ /// 2. A transparent line, outlining the gradient under the graph
+ /// 3. A grey box, outlining the entire image
+ /// The SVG also contains a definition for the fill gradient.
+ ///
+ /// A string representing the chart as an SVG image.
+ public static string CreateChart(List chartValues, ChartType type)
+ {
+ // The SVG created by this method will look similar to this:
+ /*
+
+ */
+
+ // The following code can be uncommented for testing when a static image is desired.
+ /* chartValues.Clear();
+ chartValues = new List
+ {
+ 10, 30, 20, 40, 30, 50, 40, 60, 50, 100,
+ 10, 30, 20, 40, 30, 50, 40, 60, 50, 70,
+ 0, 30, 20, 40, 30, 50, 40, 60, 50, 70,
+ };*/
+
+ var chartDoc = new XDocument();
+
+ lock (chartValues)
+ {
+ var svgElement = CreateBlankSvg(ChartHeight, ChartWidth);
+
+ // Create the line that will show the points on the graph.
+ var lineElement = new XElement(PolylineElement);
+ var points = TransformPointsToLine(chartValues, out var startX, out var finalX);
+ lineElement.SetAttributeValue(PointsAttr, points.ToString());
+ lineElement.SetAttributeValue(StyleAttr, GetLineStyle(type));
+
+ // Create the line that will contain the gradient fill.
+ TransformPointsToLoop(points, startX, finalX);
+ var fillElement = new XElement(PolylineElement);
+ fillElement.SetAttributeValue(PointsAttr, points.ToString());
+ fillElement.SetAttributeValue(StyleAttr, FillStyle);
+
+ // Add the gradient definition and the three shapes to the svg.
+ svgElement.Add(CreateGradientDefinition(type));
+ svgElement.Add(fillElement);
+ svgElement.Add(lineElement);
+ svgElement.Add(CreateBorderBox(ChartHeight, ChartWidth));
+
+ chartDoc.Add(svgElement);
+ }
+
+ return chartDoc.ToString();
+ }
+
+ private static XElement CreateBlankSvg(int height, int width)
+ {
+ var svgElement = new XElement(SvgElement);
+ svgElement.SetAttributeValue(HeightAttr, height);
+ svgElement.SetAttributeValue(WidthAttr, width);
+ return svgElement;
+ }
+
+ private static XElement CreateGradientDefinition(ChartType type)
+ {
+ var defsElement = new XElement(DefsElement);
+ var gradientElement = new XElement(LinearGradientElement);
+
+ // Vertical gradients are created when x1 and x2 are equal and y1 and y2 differ.
+ gradientElement.SetAttributeValue(X1Attr, "0%");
+ gradientElement.SetAttributeValue(X2Attr, "0%");
+ gradientElement.SetAttributeValue(Y1Attr, "0%");
+ gradientElement.SetAttributeValue(Y2Attr, "100%");
+ gradientElement.SetAttributeValue(IdAttr, "gradientId");
+
+ string stop1Style;
+ string stop2Style;
+ switch (type)
+ {
+ case ChartType.GPU:
+ stop1Style = GPUBrushStop1Style;
+ stop2Style = GPUBrushStop2Style;
+ break;
+ case ChartType.Mem:
+ stop1Style = MemBrushStop1Style;
+ stop2Style = MemBrushStop2Style;
+ break;
+ case ChartType.Net:
+ stop1Style = NetBrushStop1Style;
+ stop2Style = NetBrushStop2Style;
+ break;
+ case ChartType.CPU:
+ default:
+ stop1Style = CPUBrushStop1Style;
+ stop2Style = CPUBrushStop2Style;
+ break;
+ }
+
+ var stop1 = new XElement(StopElement);
+ stop1.SetAttributeValue(OffsetAttr, "0%");
+ stop1.SetAttributeValue(StyleAttr, stop1Style);
+
+ var stop2 = new XElement(StopElement);
+ stop2.SetAttributeValue(OffsetAttr, "95%");
+ stop2.SetAttributeValue(StyleAttr, stop2Style);
+
+ gradientElement.Add(stop1);
+ gradientElement.Add(stop2);
+ defsElement.Add(gradientElement);
+
+ return defsElement;
+ }
+
+ private static XElement CreateBorderBox(int height, int width)
+ {
+ var boxElement = new XElement(RectElement);
+ boxElement.SetAttributeValue(HeightAttr, height);
+ boxElement.SetAttributeValue(WidthAttr, width);
+ boxElement.SetAttributeValue(StyleAttr, LightGrayBoxStyle);
+ return boxElement;
+ }
+
+ private static string GetLineStyle(ChartType type)
+ {
+ var lineStyle = type switch
+ {
+ ChartType.CPU => CPULineStyle,
+ ChartType.GPU => GPULineStyle,
+ ChartType.Mem => MemLineStyle,
+ ChartType.Net => NetLineStyle,
+ _ => CPULineStyle,
+ };
+
+ return lineStyle;
+ }
+
+ private static StringBuilder TransformPointsToLine(List chartValues, out int startX, out int finalX)
+ {
+ var points = new StringBuilder();
+
+ // The X value where the graph starts must be adjusted so that the graph is right-aligned.
+ // The max available width of the widget is 268. Since there is a 1 px border around the chart, the width of the chart's line must be <=266.
+ // To create a chart of exactly the right size, we'll have 34 points with 8 pixels in between:
+ // 1 px left border + 1 px for first point + 33 segments * 8 px per segment + 1 px right border = 267 pixels total in width.
+ const int pxBetweenPoints = 8;
+
+ // When the chart doesn't have all points yet, move the chart over to the right by increasing the starting X coordinate.
+ // For a chart with only 1 point, the svg will not render a polyline.
+ // For a chart with 2 points, starting X coordinate == 2 + (34 - 2) * 8 == 1 + 32 * 8 == 1 + 256 == 257
+ // For a chart with 30 points, starting X coordinate == 2 + (34 - 34) * 8 == 1 + 0 * 8 == 1 + 0 == 2
+ startX = 2 + ((MaxChartValues - chartValues.Count) * pxBetweenPoints);
+ finalX = startX;
+
+ // Extend graph by one pixel to cover gap on the left when the chart is otherwise full.
+ if (startX == 2)
+ {
+ var invertedHeight = 100 - chartValues[0];
+ var finalY = (invertedHeight * (ChartHeight / 100.0)) - 1;
+ points.Append(CultureInfo.InvariantCulture, $"1,{finalY} ");
+ }
+
+ foreach (var origY in chartValues)
+ {
+ // We receive the height as a number up from the X axis (bottom of the chart), but we have to invert it
+ // since the Y coordinate is relative to the top of the chart.
+ var invertedHeight = 100 - origY;
+
+ // Scale the final Y to whatever the chart height is.
+ var finalY = (invertedHeight * (ChartHeight / 100.0)) - 1;
+
+ points.Append(CultureInfo.InvariantCulture, $"{finalX},{finalY} ");
+ finalX += pxBetweenPoints;
+ }
+
+ // Remove the trailing space.
+ if (points.Length > 0)
+ {
+ points.Remove(points.Length - 1, 1);
+ finalX -= pxBetweenPoints;
+ }
+
+ return points;
+ }
+
+ private static void TransformPointsToLoop(StringBuilder points, int startX, int finalX)
+ {
+ // Close the loop.
+ // Add a point at the most recent X value that corresponds with y = 0
+ points.Append(CultureInfo.InvariantCulture, $" {finalX},{ChartHeight - 1}");
+
+ // Add a point at the start of the chart that corresponds with y = 0
+ points.Append(CultureInfo.InvariantCulture, $" {startX},{ChartHeight - 1}");
+ }
+
+ public static void AddNextChartValue(float value, List chartValues)
+ {
+ if (chartValues.Count >= MaxChartValues)
+ {
+ chartValues.RemoveAt(0);
+ }
+
+ chartValues.Add(value);
+ }
+}
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
new file mode 100644
index 0000000000..940411a6b7
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/DataManager.cs
@@ -0,0 +1,147 @@
+// 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 Timer = System.Timers.Timer;
+
+namespace CoreWidgetProvider.Helpers;
+
+internal sealed partial class DataManager : IDisposable
+{
+ private readonly SystemData _systemData;
+ private readonly DataType _dataType;
+ private readonly Timer _updateTimer;
+ private readonly Action _updateAction;
+
+ private const int OneSecondInMilliseconds = 1000;
+
+ public DataManager(DataType type, Action updateWidget)
+ {
+ _systemData = new SystemData();
+ _updateAction = updateWidget;
+ _dataType = type;
+
+ _updateTimer = new Timer(OneSecondInMilliseconds);
+ _updateTimer.Elapsed += UpdateTimer_Elapsed;
+ _updateTimer.AutoReset = true;
+ _updateTimer.Enabled = false;
+ }
+
+ private void GetMemoryData()
+ {
+ lock (SystemData.MemStats)
+ {
+ SystemData.MemStats.GetData();
+ }
+ }
+
+ private void GetNetworkData()
+ {
+ lock (SystemData.NetStats)
+ {
+ SystemData.NetStats.GetData();
+ }
+ }
+
+ private void GetGPUData()
+ {
+ lock (SystemData.GPUStats)
+ {
+ SystemData.GPUStats.GetData();
+ }
+ }
+
+ private void GetCPUData(bool includeTopProcesses)
+ {
+ lock (SystemData.CpuStats)
+ {
+ SystemData.CpuStats.GetData(includeTopProcesses);
+ }
+ }
+
+ private void UpdateTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
+ {
+ switch (_dataType)
+ {
+ case DataType.CPU:
+ case DataType.CpuWithTopProcesses:
+ {
+ // CPU
+ GetCPUData(_dataType == DataType.CpuWithTopProcesses);
+ break;
+ }
+
+ case DataType.GPU:
+ {
+ // gpu
+ GetGPUData();
+ break;
+ }
+
+ case DataType.Memory:
+ {
+ // memory
+ GetMemoryData();
+ break;
+ }
+
+ case DataType.Network:
+ {
+ // network
+ GetNetworkData();
+ break;
+ }
+ }
+
+ _updateAction?.Invoke();
+ }
+
+ internal MemoryStats GetMemoryStats()
+ {
+ lock (SystemData.MemStats)
+ {
+ return SystemData.MemStats;
+ }
+ }
+
+ internal NetworkStats GetNetworkStats()
+ {
+ lock (SystemData.NetStats)
+ {
+ return SystemData.NetStats;
+ }
+ }
+
+ internal GPUStats GetGPUStats()
+ {
+ lock (SystemData.GPUStats)
+ {
+ return SystemData.GPUStats;
+ }
+ }
+
+ internal CPUStats GetCPUStats()
+ {
+ lock (SystemData.CpuStats)
+ {
+ return SystemData.CpuStats;
+ }
+ }
+
+ public void Start()
+ {
+ _updateTimer.Start();
+ }
+
+ public void Stop()
+ {
+ _updateTimer.Stop();
+ }
+
+ public void Dispose()
+ {
+ _systemData.Dispose();
+ _updateTimer.Dispose();
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/DataType.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/DataType.cs
new file mode 100644
index 0000000000..27462da6fa
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/DataType.cs
@@ -0,0 +1,35 @@
+// 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 CoreWidgetProvider.Helpers;
+
+public enum DataType
+{
+ ///
+ /// CPU related data.
+ ///
+ CPU,
+
+ ///
+ /// CPU related data, including the top processes.
+ /// Calculating the top processes takes a lot longer,
+ /// so by default we don't.
+ ///
+ CpuWithTopProcesses,
+
+ ///
+ /// Memory related data.
+ ///
+ Memory,
+
+ ///
+ /// GPU related data.
+ ///
+ GPU,
+
+ ///
+ /// Network related data.
+ ///
+ Network,
+}
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
new file mode 100644
index 0000000000..36805cdf83
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/GPUStats.cs
@@ -0,0 +1,283 @@
+// 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.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+
+namespace CoreWidgetProvider.Helpers;
+
+internal sealed partial class GPUStats : IDisposable
+{
+ // GPU counters
+ private readonly Dictionary> _gpuCounters = new();
+
+ private readonly List _stats = new();
+
+ public sealed class Data
+ {
+ public string? Name { get; set; }
+
+ public int PhysId { get; set; }
+
+ public float Usage { get; set; }
+
+ public float Temperature { get; set; }
+
+ public List GpuChartValues { get; set; } = new();
+ }
+
+ public GPUStats()
+ {
+ 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.
+ _gpuCounters.Clear();
+
+ var perfCounterCategory = new PerformanceCounterCategory("GPU Engine");
+ var instanceNames = perfCounterCategory.GetInstanceNames();
+
+ foreach (var instanceName in instanceNames)
+ {
+ if (!instanceName.EndsWith("3D", StringComparison.InvariantCulture))
+ {
+ continue;
+ }
+
+ var utilizationCounters = perfCounterCategory.GetCounters(instanceName)
+ .Where(x => x.CounterName.StartsWith("Utilization Percentage", StringComparison.InvariantCulture));
+
+ foreach (var counter in utilizationCounters)
+ {
+ var counterKey = counter.InstanceName;
+
+ // skip these values
+ GetKeyValueFromCounterKey("pid", ref counterKey);
+ GetKeyValueFromCounterKey("luid", ref counterKey);
+
+ int phys;
+ var success = int.TryParse(GetKeyValueFromCounterKey("phys", ref counterKey), out phys);
+ if (success)
+ {
+ GetKeyValueFromCounterKey("eng", ref counterKey);
+ var engtype = GetKeyValueFromCounterKey("engtype", ref counterKey);
+ if (engtype != "3D")
+ {
+ continue;
+ }
+
+ if (!_gpuCounters.TryGetValue(phys, out var value))
+ {
+ value = new();
+ _gpuCounters.Add(phys, value);
+ }
+
+ value.Add(counter);
+ }
+ }
+ }
+ }
+
+ public void LoadGPUsFromCounters()
+ {
+ // The old dev home code tracked GPU stats by querying WMI for the list
+ // of GPUs, and then matching them up with the performance counter IDs.
+ //
+ // We can't use WMI here, because it drags in a dependency on
+ // Microsoft.Management.Infrastructure, which is not compatible with
+ // AOT.
+ //
+ // For now, we'll just use the indices as the GPU names.
+ _stats.Clear();
+ foreach (var (k, v) in _gpuCounters)
+ {
+ var id = k;
+ var counters = v;
+ _stats.Add(new Data() { PhysId = id, Name = "GPU " + id });
+ }
+ }
+
+ public void GetData()
+ {
+ foreach (var gpu in _stats)
+ {
+ List? counters;
+ var success = _gpuCounters.TryGetValue(gpu.PhysId, out counters);
+
+ if (success && counters != null)
+ {
+ // TODO: This outer try/catch should be replaced with more secure locking around shared resources.
+ try
+ {
+ var sum = 0.0f;
+ var countersToRemove = new List();
+ foreach (var counter in counters)
+ {
+ try
+ {
+ // NextValue() can throw an InvalidOperationException if the counter is no longer there.
+ sum += counter.NextValue();
+ }
+ catch (InvalidOperationException)
+ {
+ // We can't modify the list during the loop, so save it to remove at the end.
+ // _log.Information(ex, "Failed to get next value, remove");
+ countersToRemove.Add(counter);
+ }
+ catch (Exception)
+ {
+ // _log.Error(ex, "Error going through process counters.");
+ }
+ }
+
+ foreach (var counter in countersToRemove)
+ {
+ counters.Remove(counter);
+ counter.Dispose();
+ }
+
+ gpu.Usage = sum / 100;
+ lock (gpu.GpuChartValues)
+ {
+ ChartHelper.AddNextChartValue(sum, gpu.GpuChartValues);
+ }
+ }
+ catch (Exception)
+ {
+ // _log.Error(ex, "Error summing process counters.");
+ }
+ }
+ }
+ }
+
+ internal string CreateGPUImageUrl(int gpuChartIndex)
+ {
+ return ChartHelper.CreateImageUrl(_stats.ElementAt(gpuChartIndex).GpuChartValues, ChartHelper.ChartType.GPU);
+ }
+
+ internal string GetGPUName(int gpuActiveIndex)
+ {
+ if (_stats.Count <= gpuActiveIndex)
+ {
+ return string.Empty;
+ }
+
+ return _stats[gpuActiveIndex].Name ?? string.Empty;
+ }
+
+ internal int GetPrevGPUIndex(int gpuActiveIndex)
+ {
+ if (_stats.Count == 0)
+ {
+ return 0;
+ }
+
+ if (gpuActiveIndex == 0)
+ {
+ return _stats.Count - 1;
+ }
+
+ return gpuActiveIndex - 1;
+ }
+
+ internal int GetNextGPUIndex(int gpuActiveIndex)
+ {
+ if (_stats.Count == 0)
+ {
+ return 0;
+ }
+
+ if (gpuActiveIndex == _stats.Count - 1)
+ {
+ return 0;
+ }
+
+ return gpuActiveIndex + 1;
+ }
+
+ internal float GetGPUUsage(int gpuActiveIndex, string gpuActiveEngType)
+ {
+ if (_stats.Count <= gpuActiveIndex)
+ {
+ return 0;
+ }
+
+ return _stats[gpuActiveIndex].Usage;
+ }
+
+ internal string GetGPUTemperature(int gpuActiveIndex)
+ {
+ // MG Jan 2026: This code was lifted from the old Dev Home codebase.
+ // However, the performance counters for GPU temperature are not being
+ // collected. So this function always returns "--" for now.
+ //
+ // I have not done the code archeology to figure out why they were
+ // removed.
+ if (_stats.Count <= gpuActiveIndex)
+ {
+ return "--";
+ }
+
+ var temperature = _stats[gpuActiveIndex].Temperature;
+ if (temperature == 0)
+ {
+ return "--";
+ }
+
+ return temperature.ToString("0.", CultureInfo.InvariantCulture) + " \x00B0C";
+ }
+
+ private string GetKeyValueFromCounterKey(string key, ref string counterKey)
+ {
+ if (!counterKey.StartsWith(key, StringComparison.InvariantCulture))
+ {
+ return "error";
+ }
+
+ counterKey = counterKey.Substring(key.Length + 1);
+ if (key.Equals("engtype", StringComparison.Ordinal))
+ {
+ return counterKey;
+ }
+
+ var pos = counterKey.IndexOf('_');
+ if (key.Equals("luid", StringComparison.Ordinal))
+ {
+ pos = counterKey.IndexOf('_', pos + 1);
+ }
+
+ var retValue = counterKey.Substring(0, pos);
+ counterKey = counterKey.Substring(pos + 1);
+ return retValue;
+ }
+
+ public void Dispose()
+ {
+ foreach (var counterPair in _gpuCounters)
+ {
+ foreach (var counter in counterPair.Value)
+ {
+ counter.Dispose();
+ }
+ }
+ }
+}
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
new file mode 100644
index 0000000000..bb371353f0
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/MemoryStats.cs
@@ -0,0 +1,100 @@
+// 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.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Windows.Win32;
+
+namespace CoreWidgetProvider.Helpers;
+
+internal sealed partial class MemoryStats : 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);
+
+ public float MemUsage
+ {
+ get; set;
+ }
+
+ public ulong AllMem
+ {
+ get; set;
+ }
+
+ public ulong UsedMem
+ {
+ get; set;
+ }
+
+ public ulong MemCommitted
+ {
+ get; set;
+ }
+
+ public ulong MemCommitLimit
+ {
+ get; set;
+ }
+
+ public ulong MemCached
+ {
+ get; set;
+ }
+
+ public ulong MemPagedPool
+ {
+ get; set;
+ }
+
+ public ulong MemNonPagedPool
+ {
+ get; set;
+ }
+
+ public List MemChartValues { get; set; } = new();
+
+ public void GetData()
+ {
+ Windows.Win32.System.SystemInformation.MEMORYSTATUSEX memStatus = default;
+ memStatus.dwLength = (uint)Marshal.SizeOf();
+ if (PInvoke.GlobalMemoryStatusEx(ref memStatus))
+ {
+ AllMem = memStatus.ullTotalPhys;
+ var availableMem = memStatus.ullAvailPhys;
+ UsedMem = AllMem - availableMem;
+
+ MemUsage = (float)UsedMem / AllMem;
+ lock (MemChartValues)
+ {
+ ChartHelper.AddNextChartValue(MemUsage * 100, MemChartValues);
+ }
+ }
+
+ MemCached = (ulong)_memCached.NextValue();
+ MemCommitted = (ulong)_memCommitted.NextValue();
+ MemCommitLimit = (ulong)_memCommittedLimit.NextValue();
+ MemPagedPool = (ulong)_memPoolPaged.NextValue();
+ MemNonPagedPool = (ulong)_memPoolNonPaged.NextValue();
+ }
+
+ public string CreateMemImageUrl()
+ {
+ return ChartHelper.CreateImageUrl(MemChartValues, ChartHelper.ChartType.Mem);
+ }
+
+ public void 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
new file mode 100644
index 0000000000..d5dc3ac15f
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/NetworkStats.cs
@@ -0,0 +1,169 @@
+// 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.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace CoreWidgetProvider.Helpers;
+
+internal sealed partial class NetworkStats : IDisposable
+{
+ private readonly Dictionary> _networkCounters = new();
+
+ private Dictionary NetworkUsages { get; set; } = new();
+
+ private Dictionary> NetChartValues { get; set; } = new();
+
+ public sealed class Data
+ {
+ public float Usage
+ {
+ get; set;
+ }
+
+ public float Sent
+ {
+ get; set;
+ }
+
+ public float Received
+ {
+ get; set;
+ }
+ }
+
+ public NetworkStats()
+ {
+ InitNetworkPerfCounters();
+ }
+
+ private void InitNetworkPerfCounters()
+ {
+ var perfCounterCategory = new PerformanceCounterCategory("Network Interface");
+ var instanceNames = perfCounterCategory.GetInstanceNames();
+ foreach (var instanceName in instanceNames)
+ {
+ 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());
+ }
+ }
+
+ public void GetData()
+ {
+ float maxUsage = 0;
+ foreach (var networkCounterWithName in _networkCounters)
+ {
+ try
+ {
+ var sent = networkCounterWithName.Value[0].NextValue();
+ var received = networkCounterWithName.Value[1].NextValue();
+ var bandWidth = networkCounterWithName.Value[2].NextValue();
+ if (bandWidth == 0)
+ {
+ continue;
+ }
+
+ var usage = 8 * (sent + received) / bandWidth;
+ var name = networkCounterWithName.Key;
+ NetworkUsages[name].Sent = sent;
+ NetworkUsages[name].Received = received;
+ NetworkUsages[name].Usage = usage;
+
+ var chartValues = NetChartValues[name];
+ lock (chartValues)
+ {
+ ChartHelper.AddNextChartValue(usage * 100, chartValues);
+ }
+
+ if (usage > maxUsage)
+ {
+ maxUsage = usage;
+ }
+ }
+ catch (Exception)
+ {
+ // Log.Error(ex, "Error getting network data.");
+ }
+ }
+ }
+
+ public string CreateNetImageUrl(int netChartIndex)
+ {
+ return ChartHelper.CreateImageUrl(NetChartValues.ElementAt(netChartIndex).Value, ChartHelper.ChartType.Net);
+ }
+
+ public string GetNetworkName(int networkIndex)
+ {
+ if (NetChartValues.Count <= networkIndex)
+ {
+ return string.Empty;
+ }
+
+ return NetChartValues.ElementAt(networkIndex).Key;
+ }
+
+ public Data GetNetworkUsage(int networkIndex)
+ {
+ if (NetChartValues.Count <= networkIndex)
+ {
+ return new Data();
+ }
+
+ var currNetworkName = NetChartValues.ElementAt(networkIndex).Key;
+ if (!NetworkUsages.TryGetValue(currNetworkName, out var value))
+ {
+ return new Data();
+ }
+
+ return value;
+ }
+
+ public int GetPrevNetworkIndex(int networkIndex)
+ {
+ if (NetChartValues.Count == 0)
+ {
+ return 0;
+ }
+
+ if (networkIndex == 0)
+ {
+ return NetChartValues.Count - 1;
+ }
+
+ return networkIndex - 1;
+ }
+
+ public int GetNextNetworkIndex(int networkIndex)
+ {
+ if (NetChartValues.Count == 0)
+ {
+ return 0;
+ }
+
+ if (networkIndex == NetChartValues.Count - 1)
+ {
+ return 0;
+ }
+
+ return networkIndex + 1;
+ }
+
+ public void Dispose()
+ {
+ foreach (var counterPair in _networkCounters)
+ {
+ foreach (var counter in counterPair.Value)
+ {
+ counter.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/Resources.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/Resources.cs
new file mode 100644
index 0000000000..cf51804da9
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/Resources.cs
@@ -0,0 +1,104 @@
+// 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.Text;
+using Microsoft.CmdPal.Core.Common;
+
+namespace CoreWidgetProvider.Helpers;
+
+// This class was pilfered from devhome, but changed much more substantially to
+// get the resources out of our resources.pri the way we need.
+public static class Resources
+{
+ private static readonly Windows.ApplicationModel.Resources.Core.ResourceMap? _map;
+
+ private static readonly string ResourcesPath = "Microsoft.CmdPal.Ext.PerformanceMonitor/Resources";
+
+ static Resources()
+ {
+ try
+ {
+ var currentResourceManager = Windows.ApplicationModel.Resources.Core.ResourceManager.Current;
+ if (currentResourceManager.MainResourceMap is not null)
+ {
+ _map = currentResourceManager.MainResourceMap;
+ }
+ }
+ catch (Exception)
+ {
+ // Resource map not available (e.g., during unit tests)
+ _map = null;
+ }
+ }
+
+ public static string GetResource(string identifier, ILogger? log = null)
+ {
+ if (_map is null)
+ {
+ return identifier;
+ }
+
+ var fullKey = $"{ResourcesPath}/{identifier}";
+
+ var val = _map.GetValue(fullKey);
+#if DEBUG
+ if (val == null)
+ {
+ log?.LogError($"Failed loading resource: {identifier}");
+
+ DebugResources(log);
+ }
+#endif
+ return val!.ValueAsString;
+ }
+
+ public static string ReplaceIdentifersFast(
+ string original)
+ {
+ // walk the string, looking for a pair of '%' characters
+ StringBuilder sb = new();
+ var length = original.Length;
+ for (var i = 0; i < length; i++)
+ {
+ if (original[i] == '%')
+ {
+ var end = original.IndexOf('%', i + 1);
+ if (end > i)
+ {
+ var identifier = original.Substring(i + 1, end - i - 1);
+ var resourceString = GetResource(identifier);
+ sb.Append(resourceString);
+ i = end; // move index to the end '%'
+ continue;
+ }
+ }
+
+ sb.Append(original[i]);
+ }
+
+ return sb.ToString();
+ }
+
+ private static void DebugResources(ILogger? log)
+ {
+ var currentResourceManager = Windows.ApplicationModel.Resources.Core.ResourceManager.Current;
+ StringBuilder sb = new();
+
+ foreach (var (k, v) in currentResourceManager.AllResourceMaps)
+ {
+ sb.AppendLine(k);
+ foreach (var (k2, v2) in v)
+ {
+ sb.Append('\t');
+ sb.AppendLine(k2);
+ }
+
+ sb.AppendLine();
+ }
+
+ log?.LogDebug($"Resource maps:");
+ log?.LogDebug(sb.ToString());
+ }
+}
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
new file mode 100644
index 0000000000..52d0b2c536
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/SystemData.cs
@@ -0,0 +1,26 @@
+// 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;
+
+namespace CoreWidgetProvider.Helpers;
+
+internal sealed partial class SystemData : IDisposable
+{
+ public static MemoryStats MemStats { get; set; } = new MemoryStats();
+
+ public static NetworkStats NetStats { get; set; } = new NetworkStats();
+
+ public static GPUStats GPUStats { get; set; } = new GPUStats();
+
+ public static CPUStats CpuStats { get; set; } = new CPUStats();
+
+ public SystemData()
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/README.md b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/README.md
new file mode 100644
index 0000000000..01ae14f4f5
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/README.md
@@ -0,0 +1,14 @@
+The code in this directory was largely lifted from the [DevHome repo].
+
+The specific directory we're using is
+https://github.com/microsoft/devhome/tree/main/extensions/CoreWidgetProvider
+This has code for all the DevHome performance widgets.
+
+Minimal changes have been made to match our style guidelines.
+Additionally, a much larger change was made to Resources.cs, to match our own
+resource loading needs.
+
+The code was lifted as of commit d52734ce0e33a82af3313d24c3c2979c37b68bab
+
+
+[DevHome repo]: https://github.com/microsoft/devhome/
\ No newline at end of file
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/LoadingTemplate.json b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/LoadingTemplate.json
new file mode 100644
index 0000000000..f931feea31
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/LoadingTemplate.json
@@ -0,0 +1,20 @@
+{
+ "type": "AdaptiveCard",
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
+ "version": "1.5",
+ "body": [
+ {
+ "type": "Container",
+ "items": [
+ {
+ "type": "TextBlock",
+ "text": "%Widget_Template/Loading%",
+ "wrap": true,
+ "horizontalAlignment": "center"
+ }
+ ],
+ "verticalContentAlignment": "center",
+ "height": "stretch"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemCPUUsageTemplate.json b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemCPUUsageTemplate.json
new file mode 100644
index 0000000000..749ca059e2
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemCPUUsageTemplate.json
@@ -0,0 +1,99 @@
+{
+ "type": "AdaptiveCard",
+ "body": [
+ {
+ "type": "Container",
+ "$when": "${errorMessage != null}",
+ "items": [
+ {
+ "type": "TextBlock",
+ "text": "${errorMessage}",
+ "wrap": true,
+ "size": "small"
+ }
+ ],
+ "style": "warning"
+ },
+ {
+ "type": "Container",
+ "$when": "${errorMessage == null}",
+ "items": [
+ {
+ "type": "Image",
+ "url": "${cpuGraphUrl}",
+ "height": "${chartHeight}",
+ "width": "${chartWidth}",
+ "$when": "${$host.widgetSize != \"small\"}",
+ "horizontalAlignment": "center"
+ },
+ {
+ "type": "ColumnSet",
+ "columns": [
+ {
+ "type": "Column",
+ "items": [
+ {
+ "type": "TextBlock",
+ "isSubtle": true,
+ "text": "%CPUUsage_Widget_Template/CPU_Usage%"
+ },
+ {
+ "type": "TextBlock",
+ "size": "large",
+ "weight": "bolder",
+ "text": "${cpuUsage}"
+ }
+ ]
+ },
+ {
+ "type": "Column",
+ "items": [
+ {
+ "type": "TextBlock",
+ "isSubtle": true,
+ "horizontalAlignment": "right",
+ "text": "%CPUUsage_Widget_Template/CPU_Speed%"
+ },
+ {
+ "type": "TextBlock",
+ "size": "large",
+ "horizontalAlignment": "right",
+ "text": "${cpuSpeed}"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "Container",
+ "$when": false,
+ "items": [
+ {
+ "type": "TextBlock",
+ "isSubtle": true,
+ "text": "%CPUUsage_Widget_Template/Processes%",
+ "wrap": true
+ },
+ {
+ "type": "TextBlock",
+ "size": "medium",
+ "text": "${cpuProc1}"
+ },
+ {
+ "type": "TextBlock",
+ "size": "medium",
+ "text": "${cpuProc2}"
+ },
+ {
+ "type": "TextBlock",
+ "size": "medium",
+ "text": "${cpuProc3}"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
+ "version": "1.5"
+}
\ No newline at end of file
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemGPUUsageTemplate.json b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemGPUUsageTemplate.json
new file mode 100644
index 0000000000..24cd7a268e
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemGPUUsageTemplate.json
@@ -0,0 +1,86 @@
+{
+ "type": "AdaptiveCard",
+ "body": [
+ {
+ "type": "Container",
+ "$when": "${errorMessage != null}",
+ "items": [
+ {
+ "type": "TextBlock",
+ "text": "${errorMessage}",
+ "wrap": true,
+ "size": "small"
+ }
+ ],
+ "style": "warning"
+ },
+ {
+ "type": "Container",
+ "$when": "${errorMessage == null}",
+ "items": [
+ {
+ "type": "Image",
+ "url": "${gpuGraphUrl}",
+ "height": "${chartHeight}",
+ "width": "${chartWidth}",
+ "$when": "${$host.widgetSize != \"small\"}",
+ "horizontalAlignment": "center"
+ },
+ {
+ "type": "ColumnSet",
+ "columns": [
+ {
+ "type": "Column",
+ "items": [
+ {
+ "text": "%GPUUsage_Widget_Template/GPU_Usage%",
+ "type": "TextBlock",
+ "size": "small",
+ "isSubtle": true
+ },
+ {
+ "text": "${gpuUsage}",
+ "type": "TextBlock",
+ "size": "large",
+ "weight": "bolder"
+ }
+ ]
+ },
+ {
+ "type": "Column",
+ "items": [
+ {
+ "text": "%GPUUsage_Widget_Template/GPU_Temperature%",
+ "type": "TextBlock",
+ "size": "small",
+ "isSubtle": true,
+ "horizontalAlignment": "right"
+ },
+ {
+ "text": "${gpuTemp}",
+ "type": "TextBlock",
+ "size": "large",
+ "weight": "bolder",
+ "horizontalAlignment": "right"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "text": "%GPUUsage_Widget_Template/GPU_Name%",
+ "type": "TextBlock",
+ "size": "small",
+ "isSubtle": true
+ },
+ {
+ "text": "${gpuName}",
+ "type": "TextBlock",
+ "size": "medium"
+ }
+ ]
+ }
+ ],
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
+ "version": "1.5"
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemMemoryTemplate.json b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemMemoryTemplate.json
new file mode 100644
index 0000000000..188da82fdc
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemMemoryTemplate.json
@@ -0,0 +1,178 @@
+{
+ "type": "AdaptiveCard",
+ "body": [
+ {
+ "type": "Container",
+ "$when": "${errorMessage != null}",
+ "items": [
+ {
+ "type": "TextBlock",
+ "text": "${errorMessage}",
+ "wrap": true,
+ "size": "small"
+ }
+ ],
+ "style": "warning"
+ },
+ {
+ "type": "Container",
+ "$when": "${errorMessage == null}",
+ "items": [
+ {
+ "type": "Image",
+ "url": "${memGraphUrl}",
+ "height": "${chartHeight}",
+ "width": "${chartWidth}",
+ "$when": "${$host.widgetSize != \"small\"}",
+ "horizontalAlignment": "center"
+ },
+ {
+ "type": "ColumnSet",
+ "columns": [
+ {
+ "type": "Column",
+ "items": [
+ {
+ "text": "%Memory_Widget_Template/UsedMemory%",
+ "type": "TextBlock",
+ "size": "small",
+ "isSubtle": true
+ },
+ {
+ "text": "${usedMem}",
+ "type": "TextBlock",
+ "size": "${if($host.widgetSize == \"small\", \"medium\", \"large\")}",
+ "weight": "bolder"
+ }
+ ]
+ },
+ {
+ "type": "Column",
+ "items": [
+ {
+ "text": "%Memory_Widget_Template/AllMemory%",
+ "type": "TextBlock",
+ "size": "small",
+ "isSubtle": true,
+ "horizontalAlignment": "right"
+ },
+ {
+ "text": "${allMem}",
+ "type": "TextBlock",
+ "size": "${if($host.widgetSize == \"small\", \"medium\", \"large\")}",
+ "weight": "bolder",
+ "horizontalAlignment": "right"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "ColumnSet",
+ "columns": [
+ {
+ "type": "Column",
+ "items": [
+ {
+ "text": "%Memory_Widget_Template/Committed%",
+ "type": "TextBlock",
+ "size": "small",
+ "isSubtle": true
+ },
+ {
+ "text": "${committedMem}/${committedLimitMem}",
+ "type": "TextBlock",
+ "size": "medium"
+ }
+ ]
+ },
+ {
+ "type": "Column",
+ "items": [
+ {
+ "text": "%Memory_Widget_Template/Cached%",
+ "type": "TextBlock",
+ "size": "small",
+ "isSubtle": true,
+ "horizontalAlignment": "right"
+ },
+ {
+ "text": "${cachedMem}",
+ "type": "TextBlock",
+ "size": "medium",
+ "horizontalAlignment": "right"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "ColumnSet",
+ "$when": "${$host.widgetSize == \"large\"}",
+ "columns": [
+ {
+ "type": "Column",
+ "items": [
+ {
+ "text": "%Memory_Widget_Template/PagedPool%",
+ "type": "TextBlock",
+ "size": "small",
+ "isSubtle": true
+ },
+ {
+ "text": "${pagedPoolMem}",
+ "type": "TextBlock",
+ "size": "medium"
+ }
+ ]
+ },
+ {
+ "type": "Column",
+ "items": [
+ {
+ "text": "%Memory_Widget_Template/NonPagedPool%",
+ "type": "TextBlock",
+ "size": "small",
+ "isSubtle": true,
+ "horizontalAlignment": "right"
+ },
+ {
+ "text": "${nonPagedPoolMem}",
+ "type": "TextBlock",
+ "size": "medium",
+ "horizontalAlignment": "right"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "ColumnSet",
+ "$when": "${$host.widgetSize != \"small\"}",
+ "columns": [
+ {
+ "type": "Column",
+ "items": [
+ {
+ "text": "%Memory_Widget_Template/MemoryUsage%",
+ "type": "TextBlock",
+ "size": "small",
+ "isSubtle": true,
+ "horizontalAlignment": "right"
+ },
+ {
+ "text": "${memUsage}",
+ "type": "TextBlock",
+ "size": "medium",
+ "horizontalAlignment": "right"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
+ "version": "1.5"
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemNetworkUsageTemplate.json b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemNetworkUsageTemplate.json
new file mode 100644
index 0000000000..e96a611148
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemNetworkUsageTemplate.json
@@ -0,0 +1,88 @@
+{
+ "type": "AdaptiveCard",
+ "body": [
+ {
+ "type": "Container",
+ "$when": "${errorMessage != null}",
+ "items": [
+ {
+ "type": "TextBlock",
+ "text": "${errorMessage}",
+ "wrap": true,
+ "size": "small"
+ }
+ ],
+ "style": "warning"
+ },
+ {
+ "type": "Container",
+ "$when": "${errorMessage == null}",
+ "items": [
+ {
+ "type": "Image",
+ "url": "${netGraphUrl}",
+ "height": "${chartHeight}",
+ "width": "${chartWidth}",
+ "$when": "${$host.widgetSize != \"small\"}",
+ "horizontalAlignment": "center"
+ },
+ {
+ "type": "ColumnSet",
+ "columns": [
+ {
+ "type": "Column",
+ "items": [
+ {
+ "text": "%NetworkUsage_Widget_Template/Sent%",
+ "type": "TextBlock",
+ "spacing": "none",
+ "size": "small",
+ "isSubtle": true
+ },
+ {
+ "text": "${netSent}",
+ "type": "TextBlock",
+ "size": "large",
+ "weight": "bolder"
+ }
+ ]
+ },
+ {
+ "type": "Column",
+ "items": [
+ {
+ "text": "%NetworkUsage_Widget_Template/Received%",
+ "type": "TextBlock",
+ "spacing": "none",
+ "size": "small",
+ "isSubtle": true,
+ "horizontalAlignment": "right"
+ },
+ {
+ "text": "${netReceived}",
+ "type": "TextBlock",
+ "size": "large",
+ "weight": "bolder",
+ "horizontalAlignment": "right"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "text": "%NetworkUsage_Widget_Template/Network_Name%",
+ "type": "TextBlock",
+ "size": "small",
+ "isSubtle": true
+ },
+ {
+ "text": "${networkName}",
+ "type": "TextBlock",
+ "size": "medium"
+ }
+ ]
+ }
+ ],
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
+ "version": "1.5"
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Icons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Icons.cs
new file mode 100644
index 0000000000..3d2fe49e70
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Icons.cs
@@ -0,0 +1,31 @@
+// 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 Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
+
+internal sealed class Icons
+{
+ internal static IconInfo CpuIcon => new("\uE9D9"); // CPU icon
+
+ internal static IconInfo MemoryIcon => new("\uE964"); // Memory icon
+
+ internal static IconInfo DiskIcon => new("\uE977"); // PC1 icon
+
+ internal static IconInfo HardDriveIcon => new("\uEDA2"); // HardDrive icon
+
+ internal static IconInfo NetworkIcon => new("\uEC05"); // Network icon
+
+ internal static IconInfo StackedAreaIcon => new("\uE9D2"); // StackedArea icon
+
+ internal static IconInfo GpuIcon => new("\uE950"); // Component icon
+
+ internal static IconInfo NavigateBackwardIcon => new("\uE72B"); // Previous icon
+
+ internal static IconInfo NavigateForwardIcon => new("\uE72A"); // Next icon
+}
+
+
+#pragma warning restore SA1402 // File may only contain a single type
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj
new file mode 100644
index 0000000000..0e6823c805
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj
@@ -0,0 +1,58 @@
+
+
+
+
+
+ Microsoft.CmdPal.Ext.PerformanceMonitor
+ $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal
+ false
+ false
+
+ Microsoft.CmdPal.Ext.PerformanceMonitor.pri
+ enable
+ preview
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
+
+
+ Resources.resx
+ True
+ True
+
+
+
+
+ Resources.Designer.cs
+ PublicResXFileCodeGenerator
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/NativeMethods.txt b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/NativeMethods.txt
new file mode 100644
index 0000000000..fbc7e91105
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/NativeMethods.txt
@@ -0,0 +1 @@
+GlobalMemoryStatusEx
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/OnLoadStaticPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/OnLoadStaticPage.cs
new file mode 100644
index 0000000000..0c4242240e
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/OnLoadStaticPage.cs
@@ -0,0 +1,123 @@
+// 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 Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using Windows.Foundation;
+
+namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
+
+#pragma warning disable SA1402 // File may only contain a single type
+
+///
+/// Helper class for creating ListPage's which can listen for when they're
+/// loaded and unloaded. This works because CmdPal will attach an event handler
+/// to the ItemsChanged event when the page is added to the UI, and remove it
+/// when the page is removed from the UI.
+///
+/// Subclasses should override the Loaded and Unloaded methods to start/stop
+/// any background work needed to populate the page.
+///
+internal abstract partial class OnLoadStaticListPage : OnLoadBasePage, IListPage
+{
+ private string _searchText = string.Empty;
+
+ public virtual string PlaceholderText { get; set => SetProperty(ref field, value); } = string.Empty;
+
+ public virtual string SearchText { get => _searchText; set => SetProperty(ref _searchText, value); }
+
+ public virtual bool ShowDetails { get; set => SetProperty(ref field, value); }
+
+ public virtual bool HasMoreItems { get; set => SetProperty(ref field, value); }
+
+ public virtual IFilters? Filters { get; set => SetProperty(ref field, value); }
+
+ public virtual IGridProperties? GridProperties { get; set => SetProperty(ref field, value); }
+
+ public virtual ICommandItem? EmptyContent { get; set => SetProperty(ref field, value); }
+
+ public void LoadMore()
+ {
+ }
+
+ protected void SetSearchNoUpdate(string newSearchText)
+ {
+ _searchText = newSearchText;
+ }
+
+ public abstract IListItem[] GetItems();
+}
+
+///
+/// Helper class for creating ContentPage's which can listen for when they're
+/// loaded and unloaded. This works because CmdPal will attach an event handler
+/// to the ItemsChanged event when the page is added to the UI, and remove it
+/// when the page is removed from the UI.
+///
+/// Subclasses should override the Loaded and Unloaded methods to start/stop
+/// any background work needed to populate the page.
+///
+internal abstract partial class OnLoadContentPage : OnLoadBasePage, IContentPage
+{
+ public virtual IDetails? Details { get; set => SetProperty(ref field, value); }
+
+ public virtual IContextItem[] Commands { get; set => SetProperty(ref field, value); } = [];
+
+ public abstract IContent[] GetContent();
+}
+
+internal abstract partial class OnLoadBasePage : Page
+{
+ private int _loadCount;
+
+#pragma warning disable CS0067 // The event is never used
+
+ private event TypedEventHandler