From 3b874a95671f36493842a7a5a64af2ef27942812 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 10 Feb 2026 06:00:27 -0600 Subject: [PATCH] CmdPal: Port the devhome perf widgets to cmdpal (#45217) 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 --- .github/actions/spell-check/allow/code.txt | 7 + .github/actions/spell-check/allow/names.txt | 3 +- PowerToys.slnx | 4 + .../cmdpal/Microsoft.CmdPal.UI/App.xaml.cs | 2 + .../Microsoft.CmdPal.UI.csproj | 1 + .../DevHome/Enums/WidgetDataState.cs | 13 + .../DevHome/Enums/WidgetPageState.cs | 13 + .../DevHome/Helpers/CPUStats.cs | 146 +++ .../DevHome/Helpers/ChartHelper.cs | 289 ++++++ .../DevHome/Helpers/DataManager.cs | 147 +++ .../DevHome/Helpers/DataType.cs | 35 + .../DevHome/Helpers/GPUStats.cs | 283 ++++++ .../DevHome/Helpers/MemoryStats.cs | 100 ++ .../DevHome/Helpers/NetworkStats.cs | 169 ++++ .../DevHome/Helpers/Resources.cs | 104 ++ .../DevHome/Helpers/SystemData.cs | 26 + .../DevHome/README.md | 14 + .../DevHome/Templates/LoadingTemplate.json | 20 + .../Templates/SystemCPUUsageTemplate.json | 99 ++ .../Templates/SystemGPUUsageTemplate.json | 86 ++ .../Templates/SystemMemoryTemplate.json | 178 ++++ .../Templates/SystemNetworkUsageTemplate.json | 88 ++ .../Icons.cs | 31 + ...osoft.CmdPal.Ext.PerformanceMonitor.csproj | 58 ++ .../NativeMethods.txt | 1 + .../OnLoadStaticPage.cs | 123 +++ .../PerformanceMonitorCommandsProvider.cs | 40 + .../PerformanceWidgetsPage.cs | 926 ++++++++++++++++++ .../Strings/en-US/Resources.resw | 253 +++++ 29 files changed, 3258 insertions(+), 1 deletion(-) create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Enums/WidgetDataState.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Enums/WidgetPageState.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/CPUStats.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/ChartHelper.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/DataManager.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/DataType.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/GPUStats.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/MemoryStats.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/NetworkStats.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/Resources.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/SystemData.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/README.md create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/LoadingTemplate.json create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemCPUUsageTemplate.json create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemGPUUsageTemplate.json create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemMemoryTemplate.json create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Templates/SystemNetworkUsageTemplate.json create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Icons.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/NativeMethods.txt create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/OnLoadStaticPage.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceMonitorCommandsProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceWidgetsPage.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Strings/en-US/Resources.resw 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? InternalItemsChanged; +#pragma warning restore CS0067 // The event is never used + + public event TypedEventHandler 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 diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceMonitorCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceMonitorCommandsProvider.cs new file mode 100644 index 0000000000..7249c97b7d --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceMonitorCommandsProvider.cs @@ -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 }; + // } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceWidgetsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceWidgetsPage.cs new file mode 100644 index 0000000000..4793c44889 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceWidgetsPage.cs @@ -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 + +/// +/// 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. +/// +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(); + } +} + +/// +/// Base class for all the performance monitor widget pages. +/// This handles common stuff like loading their widget JSON +/// and updating it when needed. +/// +internal abstract partial class WidgetPage : OnLoadContentPage +{ + internal event EventHandler? Updated; + + protected Dictionary ContentData { get; } = new(); + + protected WidgetPageState Page { get; set; } = WidgetPageState.Unknown; + + protected Dictionary 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]; + } + + /// + /// 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. + /// + 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(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Strings/en-US/Resources.resw b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Strings/en-US/Resources.resw new file mode 100644 index 0000000000..bcbbbfc001 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Strings/en-US/Resources.resw @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Loading... + Shown in Widget, when loading config file content + + + Submit + Shown in Widget, Tooltip text + + + SSH keychain + + + Local + + + Config file path + + + File not found + + + There are no hosts in this config file. + + + Number of hosts found + + + Connect + + + Processing config file failed + + + System memory + + + Utilization + + + All memory + + + In use (compressed) + + + Committed + + + Cached + + + Non-paged pool + + + Paged pool + + + Utilization + + + Send + + + Receive + + + Name + + + Previous network + + + Next network + + + Ethernet + + + Utilization + + + Name + + + Temperature + + + Previous GPU + + + Next GPU + + + Utilization + + + Speed + + + Processes + + + End process + + + Preview + Shown in Widget, Button text + + + Save + Shown in Widget, Button text + + + Cancel + Shown in Widget, Button text + + + CPU + + + Memory + + + Network + + + GPU + + + Performance monitor + + + CPU Usage + + + CPU Usage: {0} + {0} is the CPU usage percentage + + + ??? + + + CPU Usage: ??? + + + Memory Usage + + + Memory Usage: {0} + {0} is the memory usage percentage + + + ??? + + + Memory Usage: ??? + + + Network Usage + + + Network ({0}): {1} + {0} is the network adapter name, {1} is the usage percentage + + + ??? + + + Network Usage: ??? + + + GPU Usage + + + GPU ({0}): {1} + {0} is the GPU name, {1} is the usage percentage + + + ??? + + + GPU Usage: ??? + + + Open Task Manager + + + Send ↑ + + + Receive ↓ + +