Compare commits

...

3 Commits

Author SHA1 Message Date
Mike Griese
bcb9a2a205 spel 2026-01-30 12:35:33 -06:00
Mike Griese
14c10b192a didn't need these two 2026-01-30 06:39:33 -06:00
Mike Griese
5da332fc9b CmdPal: Port the devhome perf widgets to cmdpal
Pretty direct port of the code, to prove it works.

There's definitely some improvement to be made here, esp WRT to the
network and GPU listing - networks should all just be listed.
Or at least automatically track the active one. And GPU should aggregate
a bunch of stats.

And we can probably add the details to these list items.

But most importantly, _it works_.

re: #45201
2026-01-30 06:33:56 -06:00
29 changed files with 3257 additions and 1 deletions

View File

@@ -38,6 +38,7 @@ Gbps
gcode
Heatshrink
Mbits
Kbits
MBs
mkv
msix
@@ -97,6 +98,7 @@ Yubico
Perplexity
Groq
svgl
devhome
# KEYS
@@ -342,3 +344,7 @@ reportbug
#ffmpeg
crf
nostdin
# Performance counter keys
engtype
Nonpaged

View File

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

View File

@@ -218,6 +218,10 @@
<Platform Solution="*|x64" Project="x64" />
<Deploy />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />

View File

@@ -12,6 +12,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;
@@ -167,6 +168,7 @@ public partial class App : Application, IDisposable
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
services.AddSingleton<ICommandProvider, PerformanceMonitorCommandsProvider>();
}
private static void AddUIServices(ServiceCollection services)

View File

@@ -141,6 +141,7 @@
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.PerformanceMonitor\Microsoft.CmdPal.Ext.PerformanceMonitor.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj" />

View File

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

View File

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

View File

@@ -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<Process, PerformanceCounter> _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<float> 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<Process, float>();
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();
}
}
}

View File

@@ -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<float> chartValues, ChartType type)
{
var chartStr = CreateChart(chartValues, type);
return "data:image/svg+xml;utf8," + chartStr;
}
/// <summary>
/// Creates an SVG image for the chart.
/// </summary>
/// <param name="chartValues">The values to plot on the chart</param>
/// <param name="type">The type of chart. Each chart type uses different colors.</param>
/// <remarks>
/// The SVG is made of three shapes: <br/>
/// 1. A colored line, plotting the points on the graph <br/>
/// 2. A transparent line, outlining the gradient under the graph <br/>
/// 3. A grey box, outlining the entire image <br/>
/// The SVG also contains a definition for the fill gradient.
/// </remarks>
/// <returns>A string representing the chart as an SVG image.</returns>
public static string CreateChart(List<float> chartValues, ChartType type)
{
// The SVG created by this method will look similar to this:
/*
<svg height="102" width="264">
<defs>
<linearGradient x1="0%" x2="0%" y1="0%" y2="100%" id="gradientId">
<stop offset="0%" style="stop-color:rgb(222,104,242);stop-opacity:0.4" />
<stop offset="95%" style="stop-color:rgb(125,0,138);stop-opacity:0.25" />
</linearGradient>
</defs>
<polyline points="1,91 10,71 253,51 262,31 262,101 1,101" style="fill:url(#gradientId);stroke:transparent" />
<polyline points="1,91 10,71 253,51 262,31" style="fill:none;stroke:rgb(222,104,242);stroke-width:1" />
<rect height="102" width="264" style="fill:none;stroke:lightgrey;stroke-width:1" />
</svg>
*/
// The following code can be uncommented for testing when a static image is desired.
/* chartValues.Clear();
chartValues = new List<float>
{
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<float> 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<float> chartValues)
{
if (chartValues.Count >= MaxChartValues)
{
chartValues.RemoveAt(0);
}
chartValues.Add(value);
}
}

View File

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

View File

@@ -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
{
/// <summary>
/// CPU related data.
/// </summary>
CPU,
/// <summary>
/// CPU related data, including the top processes.
/// Calculating the top processes takes a lot longer,
/// so by default we don't.
/// </summary>
CpuWithTopProcesses,
/// <summary>
/// Memory related data.
/// </summary>
Memory,
/// <summary>
/// GPU related data.
/// </summary>
GPU,
/// <summary>
/// Network related data.
/// </summary>
Network,
}

View File

@@ -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<int, List<PerformanceCounter>> _gpuCounters = new();
private readonly List<Data> _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<float> 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<PerformanceCounter>? 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<PerformanceCounter>();
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();
}
}
}
}

View File

@@ -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<float> MemChartValues { get; set; } = new();
public void GetData()
{
Windows.Win32.System.SystemInformation.MEMORYSTATUSEX memStatus = default;
memStatus.dwLength = (uint)Marshal.SizeOf<Windows.Win32.System.SystemInformation.MEMORYSTATUSEX>();
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();
}
}

View File

@@ -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<string, List<PerformanceCounter>> _networkCounters = new();
private Dictionary<string, Data> NetworkUsages { get; set; } = new();
private Dictionary<string, List<float>> 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<PerformanceCounter>();
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Sent/sec", instanceName));
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Received/sec", instanceName));
instanceCounters.Add(new PerformanceCounter("Network Interface", "Current Bandwidth", instanceName));
_networkCounters.Add(instanceName, instanceCounters);
NetChartValues.Add(instanceName, new List<float>());
NetworkUsages.Add(instanceName, new Data());
}
}
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();
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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"
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,58 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<Import Project="..\Common.ExtDependencies.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.PerformanceMonitor</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>Microsoft.CmdPal.Ext.PerformanceMonitor.pri</ProjectPriFileName>
<nullable>enable</nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Diagnostics.PerformanceCounter" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\Core\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj" />
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Update="DevHome\Templates\SystemCPUUsageTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="DevHome\Templates\SystemGPUUsageTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="DevHome\Templates\SystemMemoryTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="DevHome\Templates\SystemNetworkUsageTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1 @@
GlobalMemoryStatusEx

View File

@@ -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
/// <summary>
/// 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.
/// </summary>
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();
}
/// <summary>
/// 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.
/// </summary>
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<object, IItemsChangedEventArgs>? InternalItemsChanged;
#pragma warning restore CS0067 // The event is never used
public event TypedEventHandler<object, IItemsChangedEventArgs> ItemsChanged
{
add
{
InternalItemsChanged += value;
if (_loadCount == 0)
{
Loaded();
}
_loadCount++;
}
remove
{
InternalItemsChanged -= value;
_loadCount--;
_loadCount = Math.Max(0, _loadCount);
if (_loadCount == 0)
{
Unloaded();
}
}
}
protected abstract void Loaded();
protected abstract void Unloaded();
protected void RaiseItemsChanged(int totalItems = -1)
{
try
{
// TODO #181 - This is the same thing that BaseObservable has to deal with.
InternalItemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems));
}
catch
{
}
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,40 @@
// 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;
public partial class PerformanceMonitorCommandsProvider : CommandProvider
{
private readonly ICommandItem[] _commands;
private readonly ICommandItem _band;
public PerformanceMonitorCommandsProvider()
{
DisplayName = Resources.GetResource("Performance_Monitor_Title");
Id = "PerformanceMonitor";
Icon = Icons.StackedAreaIcon;
var page = new PerformanceWidgetsPage(false);
var band = new PerformanceWidgetsPage(true);
_band = new CommandItem(band) { Title = DisplayName };
_commands = [
new CommandItem(page) { Title = DisplayName },
];
}
public override ICommandItem[] TopLevelCommands()
{
return _commands;
}
// Soon...
// public override ICommandItem[]? GetDockBands()
// {
// return new ICommandItem[] { _band };
// }
}

View File

@@ -0,0 +1,926 @@
// 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.IO;
using System.Text;
using System.Text.Json.Nodes;
using CoreWidgetProvider.Helpers;
using CoreWidgetProvider.Widgets.Enums;
using Microsoft.CmdPal.Core.Common;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
#pragma warning disable SA1402 // File may only contain a single type
/// <summary>
/// Page for displaying performance monitor widgets. Can be used as both a list
/// in the main window, or as a band in the dock.
/// By using OnLoadStaticListPage, we can get onload/onunload events to start/stop
/// the data gathering.
/// </summary>
internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDisposable
{
public override string Id => "com.microsoft.cmdpal.performanceWidget";
public override string Title => Resources.GetResource("Performance_Monitor_Title");
public override IconInfo Icon => Icons.StackedAreaIcon;
private readonly bool _isBandPage;
private readonly SystemCPUUsageWidgetPage _cpuPage = new();
private readonly ListItem _cpuItem;
private readonly SystemMemoryUsageWidgetPage _memoryPage = new();
private readonly ListItem _memoryItem;
private readonly SystemNetworkUsageWidgetPage _networkPage = new();
private readonly ListItem _networkItem;
private readonly SystemGPUUsageWidgetPage _gpuPage = new();
private readonly ListItem _gpuItem;
// For bands, we want two bands, one for up and one for down
private ListItem? _networkUpItem;
private ListItem? _networkDownItem;
private string _networkUpSpeed = string.Empty;
private string _networkDownSpeed = string.Empty;
public PerformanceWidgetsPage(bool isBandPage = false)
{
_isBandPage = isBandPage;
_cpuItem = new ListItem(_cpuPage)
{
Title = _cpuPage.GetItemTitle(isBandPage),
MoreCommands = _cpuPage.Commands,
};
_cpuPage.Updated += (s, e) =>
{
_cpuItem.Title = _cpuPage.GetItemTitle(isBandPage);
};
_memoryItem = new ListItem(_memoryPage)
{
Title = _memoryPage.GetItemTitle(isBandPage),
MoreCommands = _memoryPage.Commands,
};
_memoryPage.Updated += (s, e) =>
{
_memoryItem.Title = _memoryPage.GetItemTitle(isBandPage);
};
_networkItem = new ListItem(_networkPage)
{
Title = _networkPage.GetItemTitle(isBandPage),
MoreCommands = _networkPage.Commands,
};
_networkPage.Updated += (s, e) =>
{
_networkItem.Title = _networkPage.GetItemTitle(isBandPage);
_networkUpSpeed = _networkPage.GetUpSpeed();
_networkDownSpeed = _networkPage.GetDownSpeed();
_networkDownItem?.Title = $"{_networkDownSpeed}";
_networkUpItem?.Title = $"{_networkUpSpeed}";
};
_gpuItem = new ListItem(_gpuPage)
{
Title = _gpuPage.GetItemTitle(isBandPage),
MoreCommands = _gpuPage.Commands,
};
_gpuPage.Updated += (s, e) =>
{
_gpuItem.Title = _gpuPage.GetItemTitle(isBandPage);
};
if (_isBandPage)
{
// add subtitles to them all
_cpuItem.Subtitle = Resources.GetResource("CPU_Usage_Subtitle");
_memoryItem.Subtitle = Resources.GetResource("Memory_Usage_Subtitle");
_networkItem.Subtitle = Resources.GetResource("Network_Usage_Subtitle");
_gpuItem.Subtitle = Resources.GetResource("GPU_Usage_Subtitle");
}
}
protected override void Loaded()
{
_cpuPage.PushActivate();
_memoryPage.PushActivate();
_networkPage.PushActivate();
_gpuPage.PushActivate();
}
protected override void Unloaded()
{
_cpuPage.PopActivate();
_memoryPage.PopActivate();
_networkPage.PopActivate();
_gpuPage.PopActivate();
}
public override IListItem[] GetItems()
{
if (!_isBandPage)
{
// TODO add details
return new[] { _cpuItem, _memoryItem, _networkItem, _gpuItem };
}
else
{
_networkUpItem = new ListItem(_networkPage)
{
Title = $"{_networkUpSpeed}",
Subtitle = Resources.GetResource("Network_Send_Subtitle"),
MoreCommands = _networkPage.Commands,
};
_networkDownItem = new ListItem(_networkPage)
{
Title = $"{_networkDownSpeed}",
Subtitle = Resources.GetResource("Network_Receive_Subtitle"),
MoreCommands = _networkPage.Commands,
};
return new[] { _cpuItem, _memoryItem, _networkDownItem, _networkUpItem, _gpuItem };
}
}
public void Dispose()
{
_cpuPage.Dispose();
_memoryPage.Dispose();
_networkPage.Dispose();
_gpuPage.Dispose();
}
}
/// <summary>
/// Base class for all the performance monitor widget pages.
/// This handles common stuff like loading their widget JSON
/// and updating it when needed.
/// </summary>
internal abstract partial class WidgetPage : OnLoadContentPage
{
internal event EventHandler? Updated;
protected Dictionary<string, string> ContentData { get; } = new();
protected WidgetPageState Page { get; set; } = WidgetPageState.Unknown;
protected Dictionary<WidgetPageState, string> Template { get; set; } = new();
protected JsonObject ContentDataJson
{
get
{
var json = new JsonObject();
lock (ContentData)
{
foreach (var kvp in ContentData)
{
if (kvp.Value is not null)
{
json[kvp.Key] = kvp.Value;
}
}
}
return json;
}
}
private readonly FormContent _formContent = new();
public void UpdateWidget()
{
lock (ContentData)
{
LoadContentData();
}
_formContent.DataJson = ContentDataJson.ToJsonString();
Updated?.Invoke(this, EventArgs.Empty);
}
protected abstract void LoadContentData();
protected abstract string GetTemplatePath(WidgetPageState page);
protected string GetTemplateForPage(WidgetPageState page)
{
if (Template.TryGetValue(page, out var value))
{
CoreLogger.LogDebug($"Using cached template for {page}");
return value;
}
try
{
var path = Path.Combine(Package.Current.EffectivePath, GetTemplatePath(page));
var template = File.ReadAllText(path, Encoding.Default) ?? throw new FileNotFoundException(path);
template = Resources.ReplaceIdentifersFast(template);
CoreLogger.LogDebug($"Caching template for {page}");
Template[page] = template;
return template;
}
catch (Exception e)
{
CoreLogger.LogError("Error getting template.", e);
return string.Empty;
}
}
public override IContent[] GetContent()
{
_formContent.TemplateJson = GetTemplateForPage(WidgetPageState.Content);
return [_formContent];
}
/// <summary>
/// Increment our tracker of how many pages have needed us active. This is a
/// little wackier than just OnLoad/Unload. Both the ListPage for
/// PerformanceWidgetsPage itself, AND the widget itself need the stats to
/// be updating. So we use a counter to track how many "clients" need us
/// active. When either is activated, we'll start updating. When both are
/// removed, we'll stop updating.
/// </summary>
internal virtual void PushActivate()
{
_loadCount++;
}
internal virtual void PopActivate()
{
_loadCount--;
}
private int _loadCount;
protected bool IsActive => _loadCount > 0;
protected override void Loaded()
{
PushActivate();
}
protected override void Unloaded()
{
PopActivate();
}
internal static string FloatToPercentString(float value)
{
return ((int)(value * 100)).ToString(CultureInfo.InvariantCulture) + "%";
}
}
internal sealed partial class SystemCPUUsageWidgetPage : WidgetPage, IDisposable
{
public override string Title => Resources.GetResource("CPU_Usage_Title");
public override string Id => "com.microsoft.cmdpal.cpu_widget";
public override IconInfo Icon => Icons.CpuIcon;
private readonly DataManager _dataManager;
public SystemCPUUsageWidgetPage()
{
_dataManager = new(DataType.CPU, () => UpdateWidget());
Commands = [
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
protected override void LoadContentData()
{
// CoreLogger.LogDebug("Getting CPU stats");
try
{
ContentData.Clear();
var timer = Stopwatch.StartNew();
var currentData = _dataManager.GetCPUStats();
var dataDuration = timer.ElapsedMilliseconds;
ContentData["cpuUsage"] = FloatToPercentString(currentData.CpuUsage);
ContentData["cpuSpeed"] = SpeedToString(currentData.CpuSpeed);
ContentData["cpuGraphUrl"] = currentData.CreateCPUImageUrl();
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
// ContentData["cpuProc1"] = currentData.GetCpuProcessText(0);
// ContentData["cpuProc2"] = currentData.GetCpuProcessText(1);
// ContentData["cpuProc3"] = currentData.GetCpuProcessText(2);
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
// CoreLogger.LogDebug($"CPU stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
// DataState = WidgetDataState.Okay;
}
catch (Exception e)
{
// Log.Error(e, "Error retrieving stats.");
ContentData.Clear();
ContentData["errorMessage"] = e.Message;
// ContentData = content.ToJsonString();
// DataState = WidgetDataState.Failed;
return;
}
}
protected override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.Content => @"DevHome\Templates\SystemCPUUsageTemplate.json",
WidgetPageState.Loading => @"DevHome\Templates\SystemCPUUsageTemplate.json",
_ => throw new NotImplementedException(),
};
}
public string GetItemTitle(bool isBandPage)
{
if (ContentData.TryGetValue("cpuUsage", out var usage))
{
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("CPU_Usage_Label"), usage);
}
else
{
return isBandPage ? Resources.GetResource("CPU_Usage_Unknown") : Resources.GetResource("CPU_Usage_Unknown_Label");
}
}
private string SpeedToString(float cpuSpeed)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.00} GHz", cpuSpeed / 1000);
}
internal override void PushActivate()
{
base.PushActivate();
if (IsActive)
{
_dataManager.Start();
}
}
internal override void PopActivate()
{
base.PopActivate();
if (!IsActive)
{
_dataManager.Stop();
}
}
public void Dispose()
{
_dataManager.Dispose();
}
}
internal sealed partial class SystemMemoryUsageWidgetPage : WidgetPage, IDisposable
{
public override string Id => "com.microsoft.cmdpal.memory_widget";
public override string Title => Resources.GetResource("Memory_Usage_Title");
public override IconInfo Icon => Icons.MemoryIcon;
private readonly DataManager _dataManager;
public SystemMemoryUsageWidgetPage()
{
_dataManager = new(DataType.Memory, () => UpdateWidget());
Commands = [
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
protected override void LoadContentData()
{
// CoreLogger.LogDebug("Getting Memory stats");
try
{
ContentData.Clear();
var timer = Stopwatch.StartNew();
var currentData = _dataManager.GetMemoryStats();
var dataDuration = timer.ElapsedMilliseconds;
ContentData["allMem"] = MemUlongToString(currentData.AllMem);
ContentData["usedMem"] = MemUlongToString(currentData.UsedMem);
ContentData["memUsage"] = FloatToPercentString(currentData.MemUsage);
ContentData["committedMem"] = MemUlongToString(currentData.MemCommitted);
ContentData["committedLimitMem"] = MemUlongToString(currentData.MemCommitLimit);
ContentData["cachedMem"] = MemUlongToString(currentData.MemCached);
ContentData["pagedPoolMem"] = MemUlongToString(currentData.MemPagedPool);
ContentData["nonPagedPoolMem"] = MemUlongToString(currentData.MemNonPagedPool);
ContentData["memGraphUrl"] = currentData.CreateMemImageUrl();
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
// CoreLogger.LogDebug($"Memory stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
}
catch (Exception e)
{
ContentData.Clear();
ContentData["errorMessage"] = e.Message;
return;
}
}
protected override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.Content => @"DevHome\Templates\SystemMemoryTemplate.json",
WidgetPageState.Loading => @"DevHome\Templates\SystemMemoryTemplate.json",
_ => throw new NotImplementedException(),
};
}
public string GetItemTitle(bool isBandPage)
{
if (ContentData.TryGetValue("memUsage", out var usage))
{
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("Memory_Usage_Label"), usage);
}
else
{
return isBandPage ? Resources.GetResource("Memory_Usage_Unknown") : Resources.GetResource("Memory_Usage_Unknown_Label");
}
}
private string MemUlongToString(ulong memBytes)
{
if (memBytes < 1024)
{
return memBytes.ToString(CultureInfo.InvariantCulture) + " B";
}
var memSize = memBytes / 1024.0;
if (memSize < 1024)
{
return memSize.ToString("0.00", CultureInfo.InvariantCulture) + " kB";
}
memSize /= 1024;
if (memSize < 1024)
{
return memSize.ToString("0.00", CultureInfo.InvariantCulture) + " MB";
}
memSize /= 1024;
return memSize.ToString("0.00", CultureInfo.InvariantCulture) + " GB";
}
internal override void PushActivate()
{
base.PushActivate();
if (IsActive)
{
_dataManager.Start();
}
}
internal override void PopActivate()
{
base.PopActivate();
if (!IsActive)
{
_dataManager.Stop();
}
}
public void Dispose()
{
_dataManager.Dispose();
}
}
internal sealed partial class SystemNetworkUsageWidgetPage : WidgetPage, IDisposable
{
public override string Id => "com.microsoft.cmdpal.network_widget";
public override string Title => Resources.GetResource("Network_Usage_Title");
public override IconInfo Icon => Icons.NetworkIcon;
private readonly DataManager _dataManager;
private int _networkIndex;
public SystemNetworkUsageWidgetPage()
{
_dataManager = new(DataType.Network, () => UpdateWidget());
Commands = [
new CommandContextItem(new PrevNetworkCommand(this) { Name = Resources.GetResource("Previous_Network_Title") }),
new CommandContextItem(new NextNetworkCommand(this) { Name = Resources.GetResource("Next_Network_Title") }),
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
protected override void LoadContentData()
{
// CoreLogger.LogDebug("Getting Network stats");
try
{
ContentData.Clear();
var timer = Stopwatch.StartNew();
var currentData = _dataManager.GetNetworkStats();
var dataDuration = timer.ElapsedMilliseconds;
var netName = currentData.GetNetworkName(_networkIndex);
var networkStats = currentData.GetNetworkUsage(_networkIndex);
ContentData["networkUsage"] = FloatToPercentString(networkStats.Usage);
ContentData["netSent"] = BytesToBitsPerSecString(networkStats.Sent);
ContentData["netReceived"] = BytesToBitsPerSecString(networkStats.Received);
ContentData["networkName"] = netName;
ContentData["netGraphUrl"] = currentData.CreateNetImageUrl(_networkIndex);
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
// CoreLogger.LogDebug($"Network stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
}
catch (Exception e)
{
ContentData.Clear();
ContentData["errorMessage"] = e.Message;
return;
}
}
protected override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.Content => @"DevHome\Templates\SystemNetworkUsageTemplate.json",
WidgetPageState.Loading => @"DevHome\Templates\SystemNetworkUsageTemplate.json",
_ => throw new NotImplementedException(),
};
}
public string GetItemTitle(bool isBandPage)
{
if (ContentData.TryGetValue("networkName", out var name) && ContentData.TryGetValue("networkUsage", out var usage))
{
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("Network_Usage_Label"), name, usage);
}
else
{
return isBandPage ? Resources.GetResource("Network_Usage_Unknown") : Resources.GetResource("Network_Usage_Unknown_Label");
}
}
// up/down speed is always used for bands
public string GetUpSpeed()
{
if (ContentData.TryGetValue("netSent", out var upSpeed))
{
return upSpeed;
}
else
{
return "???";
}
}
public string GetDownSpeed()
{
if (ContentData.TryGetValue("netReceived", out var downSpeed))
{
return downSpeed;
}
else
{
return "???";
}
}
private string BytesToBitsPerSecString(float value)
{
// Bytes to bits
value *= 8;
// bits to Kbits
value /= 1024;
if (value < 1024)
{
if (value < 100)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Kbps", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} Kbps", value);
}
// Kbits to Mbits
value /= 1024;
if (value < 1024)
{
if (value < 100)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Mbps", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} Mbps", value);
}
// Mbits to Gbits
value /= 1024;
if (value < 100)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Gbps", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} Gbps", value);
}
internal override void PushActivate()
{
base.PushActivate();
if (IsActive)
{
_dataManager.Start();
}
}
internal override void PopActivate()
{
base.PopActivate();
if (!IsActive)
{
_dataManager.Stop();
}
}
private void HandlePrevNetwork()
{
_networkIndex = _dataManager.GetNetworkStats().GetPrevNetworkIndex(_networkIndex);
UpdateWidget();
}
private void HandleNextNetwork()
{
_networkIndex = _dataManager.GetNetworkStats().GetNextNetworkIndex(_networkIndex);
UpdateWidget();
}
public void Dispose()
{
_dataManager.Dispose();
}
private sealed partial class PrevNetworkCommand : InvokableCommand
{
private readonly SystemNetworkUsageWidgetPage _page;
public PrevNetworkCommand(SystemNetworkUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.network_widget.prev";
public override IconInfo Icon => Icons.NavigateBackwardIcon;
public override ICommandResult Invoke()
{
_page.HandlePrevNetwork();
return CommandResult.KeepOpen();
}
}
private sealed partial class NextNetworkCommand : InvokableCommand
{
private readonly SystemNetworkUsageWidgetPage _page;
public NextNetworkCommand(SystemNetworkUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.network_widget.next";
public override IconInfo Icon => Icons.NavigateForwardIcon;
public override ICommandResult Invoke()
{
_page.HandleNextNetwork();
return CommandResult.KeepOpen();
}
}
}
internal sealed partial class SystemGPUUsageWidgetPage : WidgetPage, IDisposable
{
public override string Id => "com.microsoft.cmdpal.gpu_widget";
public override string Title => Resources.GetResource("GPU_Usage_Title");
public override IconInfo Icon => Icons.GpuIcon;
private readonly DataManager _dataManager;
private readonly string _gpuActiveEngType = "3D";
private int _gpuActiveIndex;
public SystemGPUUsageWidgetPage()
{
_dataManager = new(DataType.GPU, () => UpdateWidget());
Commands = [
new CommandContextItem(new PrevGPUCommand(this) { Name = Resources.GetResource("Previous_GPU_Title") }),
new CommandContextItem(new NextGPUCommand(this) { Name = Resources.GetResource("Next_GPU_Title") }),
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
protected override void LoadContentData()
{
// CoreLogger.LogDebug("Getting GPU stats");
try
{
ContentData.Clear();
var timer = Stopwatch.StartNew();
var stats = _dataManager.GetGPUStats();
var dataDuration = timer.ElapsedMilliseconds;
var gpuName = stats.GetGPUName(_gpuActiveIndex);
ContentData["gpuUsage"] = FloatToPercentString(stats.GetGPUUsage(_gpuActiveIndex, _gpuActiveEngType));
ContentData["gpuName"] = gpuName;
ContentData["gpuTemp"] = stats.GetGPUTemperature(_gpuActiveIndex);
ContentData["gpuGraphUrl"] = stats.CreateGPUImageUrl(_gpuActiveIndex);
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
// CoreLogger.LogDebug($"GPU stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
}
catch (Exception e)
{
ContentData.Clear();
ContentData["errorMessage"] = e.Message;
return;
}
}
protected override string GetTemplatePath(WidgetPageState page)
{
return page switch
{
WidgetPageState.Content => @"DevHome\Templates\SystemGPUUsageTemplate.json",
WidgetPageState.Loading => @"DevHome\Templates\SystemGPUUsageTemplate.json",
_ => throw new NotImplementedException(),
};
}
public string GetItemTitle(bool isBandPage)
{
if (ContentData.TryGetValue("gpuName", out var name) && ContentData.TryGetValue("gpuUsage", out var usage))
{
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("GPU_Usage_Label"), name, usage);
}
else
{
return isBandPage ? Resources.GetResource("GPU_Usage_Unknown") : Resources.GetResource("GPU_Usage_Unknown_Label");
}
}
internal override void PushActivate()
{
base.PushActivate();
if (IsActive)
{
_dataManager.Start();
}
}
internal override void PopActivate()
{
base.PopActivate();
if (!IsActive)
{
_dataManager.Stop();
}
}
private void HandlePrevGPU()
{
_gpuActiveIndex = _dataManager.GetGPUStats().GetPrevGPUIndex(_gpuActiveIndex);
UpdateWidget();
}
private void HandleNextGPU()
{
_gpuActiveIndex = _dataManager.GetGPUStats().GetNextGPUIndex(_gpuActiveIndex);
UpdateWidget();
}
public void Dispose()
{
_dataManager.Dispose();
}
private sealed partial class PrevGPUCommand : InvokableCommand
{
private readonly SystemGPUUsageWidgetPage _page;
public PrevGPUCommand(SystemGPUUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.gpu_widget.prev";
public override IconInfo Icon => Icons.NavigateBackwardIcon;
public override ICommandResult Invoke()
{
_page.HandlePrevGPU();
return CommandResult.KeepOpen();
}
}
private sealed partial class NextGPUCommand : InvokableCommand
{
private readonly SystemGPUUsageWidgetPage _page;
public NextGPUCommand(SystemGPUUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.gpu_widget.next";
public override IconInfo Icon => Icons.NavigateForwardIcon;
public override ICommandResult Invoke()
{
_page.HandleNextGPU();
return CommandResult.KeepOpen();
}
}
}
internal sealed partial class OpenTaskManagerCommand : InvokableCommand
{
internal static readonly OpenTaskManagerCommand Instance = new();
public override string Id => "com.microsoft.cmdpal.open_task_manager";
public override IconInfo Icon => Icons.StackedAreaIcon; // StackedAreaIcon looks like task manager's icon
public override string Name => Resources.GetResource("Open_Task_Manager_Title");
public override ICommandResult Invoke()
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = "taskmgr.exe",
UseShellExecute = true,
});
}
catch (Exception e)
{
CoreLogger.LogError("Error launching Task Manager.", e);
}
return CommandResult.Hide();
}
}

View File

@@ -0,0 +1,253 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Widget_Template.Loading" xml:space="preserve">
<value>Loading...</value>
<comment>Shown in Widget, when loading config file content</comment>
</data>
<data name="Widget_Template_Tooltip.Submit" xml:space="preserve">
<value>Submit</value>
<comment>Shown in Widget, Tooltip text</comment>
</data>
<data name="SSH_Widget_Template.Name" xml:space="preserve">
<value>SSH keychain</value>
</data>
<data name="SSH_Widget_Template.Target" xml:space="preserve">
<value>Local</value>
</data>
<data name="SSH_Widget_Template.ConfigFilePath" xml:space="preserve">
<value>Config file path</value>
</data>
<data name="SSH_Widget_Template.ConfigFileNotFound" xml:space="preserve">
<value>File not found</value>
</data>
<data name="SSH_Widget_Template.EmptyHosts" xml:space="preserve">
<value>There are no hosts in this config file.</value>
</data>
<data name="SSH_Widget_Template.NumOfHosts" xml:space="preserve">
<value>Number of hosts found</value>
</data>
<data name="SSH_Widget_Template.Connect" xml:space="preserve">
<value>Connect</value>
</data>
<data name="SSH_Widget_Template.ErrorProcessingConfigFile" xml:space="preserve">
<value>Processing config file failed</value>
</data>
<data name="Memory_Widget_Template.SystemMemory" xml:space="preserve">
<value>System memory</value>
</data>
<data name="Memory_Widget_Template.MemoryUsage" xml:space="preserve">
<value>Utilization</value>
</data>
<data name="Memory_Widget_Template.AllMemory" xml:space="preserve">
<value>All memory</value>
</data>
<data name="Memory_Widget_Template.UsedMemory" xml:space="preserve">
<value>In use (compressed)</value>
</data>
<data name="Memory_Widget_Template.Committed" xml:space="preserve">
<value>Committed</value>
</data>
<data name="Memory_Widget_Template.Cached" xml:space="preserve">
<value>Cached</value>
</data>
<data name="Memory_Widget_Template.NonPagedPool" xml:space="preserve">
<value>Non-paged pool</value>
</data>
<data name="Memory_Widget_Template.PagedPool" xml:space="preserve">
<value>Paged pool</value>
</data>
<data name="NetworkUsage_Widget_Template.Network_Usage" xml:space="preserve">
<value>Utilization</value>
</data>
<data name="NetworkUsage_Widget_Template.Sent" xml:space="preserve">
<value>Send</value>
</data>
<data name="NetworkUsage_Widget_Template.Received" xml:space="preserve">
<value>Receive</value>
</data>
<data name="NetworkUsage_Widget_Template.Network_Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="Previous_Network_Title" xml:space="preserve">
<value>Previous network</value>
</data>
<data name="Next_Network_Title" xml:space="preserve">
<value>Next network</value>
</data>
<data name="NetworkUsage_Widget_Template.Ethernet_Heading" xml:space="preserve">
<value>Ethernet</value>
</data>
<data name="GPUUsage_Widget_Template.GPU_Usage" xml:space="preserve">
<value>Utilization</value>
</data>
<data name="GPUUsage_Widget_Template.GPU_Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="GPUUsage_Widget_Template.GPU_Temperature" xml:space="preserve">
<value>Temperature</value>
</data>
<data name="Previous_GPU_Title" xml:space="preserve">
<value>Previous GPU</value>
</data>
<data name="Next_GPU_Title" xml:space="preserve">
<value>Next GPU</value>
</data>
<data name="CPUUsage_Widget_Template.CPU_Usage" xml:space="preserve">
<value>Utilization</value>
</data>
<data name="CPUUsage_Widget_Template.CPU_Speed" xml:space="preserve">
<value>Speed</value>
</data>
<data name="CPUUsage_Widget_Template.Processes" xml:space="preserve">
<value>Processes</value>
</data>
<data name="CPUUsage_Widget_Template.End_Process" xml:space="preserve">
<value>End process</value>
</data>
<data name="Widget_Template_Button.Preview" xml:space="preserve">
<value>Preview</value>
<comment>Shown in Widget, Button text</comment>
</data>
<data name="Widget_Template_Button.Save" xml:space="preserve">
<value>Save</value>
<comment>Shown in Widget, Button text</comment>
</data>
<data name="Widget_Template_Button.Cancel" xml:space="preserve">
<value>Cancel</value>
<comment>Shown in Widget, Button text</comment>
</data>
<data name="CPU_Usage_Subtitle" xml:space="preserve">
<value>CPU</value>
</data>
<data name="Memory_Usage_Subtitle" xml:space="preserve">
<value>Memory</value>
</data>
<data name="Network_Usage_Subtitle" xml:space="preserve">
<value>Network</value>
</data>
<data name="GPU_Usage_Subtitle" xml:space="preserve">
<value>GPU</value>
</data>
<data name="Performance_Monitor_Title" xml:space="preserve">
<value>Performance monitor</value>
</data>
<data name="CPU_Usage_Title" xml:space="preserve">
<value>CPU Usage</value>
</data>
<data name="CPU_Usage_Label" xml:space="preserve">
<value>CPU Usage: {0}</value>
<comment>{0} is the CPU usage percentage</comment>
</data>
<data name="CPU_Usage_Unknown" xml:space="preserve">
<value>???</value>
</data>
<data name="CPU_Usage_Unknown_Label" xml:space="preserve">
<value>CPU Usage: ???</value>
</data>
<data name="Memory_Usage_Title" xml:space="preserve">
<value>Memory Usage</value>
</data>
<data name="Memory_Usage_Label" xml:space="preserve">
<value>Memory Usage: {0}</value>
<comment>{0} is the memory usage percentage</comment>
</data>
<data name="Memory_Usage_Unknown" xml:space="preserve">
<value>???</value>
</data>
<data name="Memory_Usage_Unknown_Label" xml:space="preserve">
<value>Memory Usage: ???</value>
</data>
<data name="Network_Usage_Title" xml:space="preserve">
<value>Network Usage</value>
</data>
<data name="Network_Usage_Label" xml:space="preserve">
<value>Network ({0}): {1}</value>
<comment>{0} is the network adapter name, {1} is the usage percentage</comment>
</data>
<data name="Network_Usage_Unknown" xml:space="preserve">
<value>???</value>
</data>
<data name="Network_Usage_Unknown_Label" xml:space="preserve">
<value>Network Usage: ???</value>
</data>
<data name="GPU_Usage_Title" xml:space="preserve">
<value>GPU Usage</value>
</data>
<data name="GPU_Usage_Label" xml:space="preserve">
<value>GPU ({0}): {1}</value>
<comment>{0} is the GPU name, {1} is the usage percentage</comment>
</data>
<data name="GPU_Usage_Unknown" xml:space="preserve">
<value>???</value>
</data>
<data name="GPU_Usage_Unknown_Label" xml:space="preserve">
<value>GPU Usage: ???</value>
</data>
<data name="Open_Task_Manager_Title" xml:space="preserve">
<value>Open Task Manager</value>
</data>
<data name="Network_Send_Subtitle" xml:space="preserve">
<value>Send ↑</value>
</data>
<data name="Network_Receive_Subtitle" xml:space="preserve">
<value>Receive ↓</value>
</data>
</root>