feat: make performance monitor a standalone page

This commit is contained in:
zhengxuan.zhang
2026-08-18 11:12:24 +08:00
parent 955997d430
commit f4624727ac
8 changed files with 222 additions and 8 deletions
+2
View File
@@ -66,6 +66,7 @@ using XplorePlane.Services.Inspection.Tasks;
using XplorePlane.ViewModels;
using XplorePlane.ViewModels.Cnc;
using XplorePlane.ViewModels.Debug;
using XplorePlane.ViewModels.Diagnostics;
using XplorePlane.ViewModels.ImageProcessing;
using XplorePlane.ViewModels.Inspection.Documentation;
using XplorePlane.ViewModels.Main;
@@ -925,6 +926,7 @@ namespace XplorePlane
containerRegistry.RegisterSingleton<IDebugPanelConfigService, DebugPanelConfigService>();
containerRegistry.Register<DebugPanelViewModel>();
containerRegistry.Register<DebugPanelWindow>();
containerRegistry.Register<SystemPerformanceMonitorViewModel>();
// 注册检测配方服务(单例)
containerRegistry.RegisterSingleton<IRecipeService, RecipeService>();
@@ -27,7 +27,6 @@ namespace XplorePlane.ViewModels.Debug
public StateDisplayViewModel StateDisplay { get; }
public EventLogViewModel EventLog { get; }
public SnapshotManagerViewModel SnapshotManager { get; }
public PerformanceMonitorViewModel PerformanceMonitor { get; }
public DelegateCommand SaveLayoutCommand { get; }
public DelegateCommand ResetLayoutCommand { get; }
@@ -53,7 +52,6 @@ namespace XplorePlane.ViewModels.Debug
StateDisplay = new StateDisplayViewModel(appStateService, loggerService, _dispatcher);
EventLog = new EventLogViewModel(appStateService, loggerService, _dispatcher);
SnapshotManager = new SnapshotManagerViewModel(appStateService, loggerService, _dispatcher);
PerformanceMonitor = new PerformanceMonitorViewModel(appStateService, loggerService, _dispatcher);
SaveLayoutCommand = new DelegateCommand(SaveLayout);
ResetLayoutCommand = new DelegateCommand(ResetLayout);
@@ -73,7 +71,6 @@ namespace XplorePlane.ViewModels.Debug
CurrentConfig = _configService.LoadConfig();
StateDisplay.Initialize();
EventLog.Initialize();
PerformanceMonitor.Initialize();
ApplyFilterConfig();
_initialized = true;
@@ -99,7 +96,6 @@ namespace XplorePlane.ViewModels.Debug
StateDisplay.Dispose();
EventLog.Dispose();
SnapshotManager.Dispose();
PerformanceMonitor.Dispose();
_disposed = true;
}
@@ -189,4 +185,4 @@ namespace XplorePlane.ViewModels.Debug
EventLog.ApplyFilter();
}
}
}
}
@@ -0,0 +1,148 @@
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Threading;
using XP.Common.Logging.Interfaces;
using XplorePlane.Services.Storage;
namespace XplorePlane.ViewModels.Diagnostics
{
public sealed class SystemPerformanceMonitorViewModel : BindableBase, IDisposable
{
private readonly ILoggerService _logger;
private readonly IXpDataPathService _pathService;
private readonly DispatcherTimer _timer;
private readonly Process _process = Process.GetCurrentProcess();
private TimeSpan _lastProcessCpu;
private DateTime _lastSample = DateTime.UtcNow;
private ulong _lastIdle, _lastKernel, _lastUser;
private bool _disposed;
public ObservableCollection<DiskUsageRow> Disks { get; } = new();
public ObservableCollection<string> Alerts { get; } = new();
public double CpuUsage { get; private set; }
public double ProcessCpuUsage { get; private set; }
public double MemoryUsage { get; private set; }
public string MemoryText { get; private set; } = "读取中";
public string ProcessMemoryText { get; private set; } = "读取中";
public string OverallStatus { get; private set; } = "读取中";
public string LastUpdatedText { get; private set; } = "尚未采样";
public bool HasAlerts => Alerts.Count > 0;
public SystemPerformanceMonitorViewModel(ILoggerService logger, IXpDataPathService pathService)
{
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<SystemPerformanceMonitorViewModel>();
_pathService = pathService ?? throw new ArgumentNullException(nameof(pathService));
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
_timer.Tick += (_, _) => Update();
}
public void Initialize()
{
if (_disposed) return;
Update();
_timer.Start();
}
public void Dispose()
{
if (_disposed) return;
_timer.Stop();
_process.Dispose();
_disposed = true;
}
private void Update()
{
try
{
var now = DateTime.UtcNow;
var elapsed = Math.Max(0.1, (now - _lastSample).TotalSeconds);
_process.Refresh();
var processCpu = _process.TotalProcessorTime;
ProcessCpuUsage = Math.Clamp((processCpu - _lastProcessCpu).TotalSeconds / (elapsed * Environment.ProcessorCount) * 100, 0, 100);
_lastProcessCpu = processCpu;
_lastSample = now;
RaisePropertyChanged(nameof(ProcessCpuUsage));
if (GetSystemTimes(out var idle, out var kernel, out var user) && _lastKernel != 0)
{
var total = (kernel - _lastKernel) + (user - _lastUser);
CpuUsage = total == 0 ? 0 : Math.Clamp((double)(total - (idle - _lastIdle)) / total * 100, 0, 100);
RaisePropertyChanged(nameof(CpuUsage));
}
_lastIdle = idle; _lastKernel = kernel; _lastUser = user;
var memory = new MemoryStatusEx { Length = (uint)Marshal.SizeOf<MemoryStatusEx>() };
if (GlobalMemoryStatusEx(ref memory))
{
MemoryUsage = memory.MemoryLoad;
MemoryText = $"{FormatBytes((long)(memory.TotalPhys - memory.AvailPhys))} / {FormatBytes((long)memory.TotalPhys)}";
RaisePropertyChanged(nameof(MemoryUsage));
RaisePropertyChanged(nameof(MemoryText));
}
ProcessMemoryText = FormatBytes(_process.WorkingSet64);
RaisePropertyChanged(nameof(ProcessMemoryText));
UpdateDisks();
LastUpdatedText = $"更新于 {DateTime.Now:HH:mm:ss}";
OverallStatus = HasAlerts ? $"{Alerts.Count} 项告警" : "系统正常";
RaisePropertyChanged(nameof(LastUpdatedText));
RaisePropertyChanged(nameof(OverallStatus));
RaisePropertyChanged(nameof(HasAlerts));
}
catch (Exception ex)
{
_logger.Warn("读取系统性能指标失败:{Message}", ex.Message);
}
}
private void UpdateDisks()
{
var paths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { [_pathService.RootPath] = "XP 数据 / 检测结果" };
foreach (var drive in DriveInfo.GetDrives().Where(d => d.IsReady)) paths.TryAdd(drive.RootDirectory.FullName, "系统 / 应用数据");
Disks.Clear(); Alerts.Clear();
foreach (var pair in paths)
{
try
{
var drive = new DriveInfo(Path.GetPathRoot(pair.Key)!);
var total = drive.TotalSize; var free = drive.AvailableFreeSpace;
var usage = total <= 0 ? 0 : (double)(total - free) / total * 100;
var status = free < 5L * 1024 * 1024 * 1024 || usage >= 95 ? "严重" : free < 10L * 1024 * 1024 * 1024 || usage >= 90 ? "警告" : free < 20L * 1024 * 1024 * 1024 || usage >= 80 ? "注意" : "正常";
Disks.Add(new DiskUsageRow { Drive = drive.Name, Total = FormatBytes(total), Used = FormatBytes(total - free), Free = FormatBytes(free), Usage = $"{usage:F0}%", Purpose = pair.Value, Status = status });
if (status != "正常") Alerts.Add($"{drive.Name} 剩余空间 {FormatBytes(free)},状态:{status}。建议清理历史数据。");
}
catch (Exception ex) { _logger.Warn("读取磁盘信息失败:{Path}{Message}", pair.Key, ex.Message); }
}
}
private static string FormatBytes(long value)
{
string[] units = { "B", "GB", "TB" }; double size = value; var index = 0;
while (size >= 1024 && index < units.Length - 1) { size /= 1024; index++; }
return index == 0 ? $"{size:F0} {units[index]}" : $"{size:F1} {units[index]}";
}
[DllImport("kernel32.dll")] private static extern bool GetSystemTimesNative(out FileTime idle, out FileTime kernel, out FileTime user);
[DllImport("kernel32.dll")] private static extern bool GlobalMemoryStatusEx(ref MemoryStatusEx status);
private static bool GetSystemTimes(out ulong idle, out ulong kernel, out ulong user) { var ok = GetSystemTimesNative(out var i, out var k, out var u); idle = i.ToUInt64(); kernel = k.ToUInt64(); user = u.ToUInt64(); return ok; }
[StructLayout(LayoutKind.Sequential)] private struct FileTime { public uint Low; public uint High; public ulong ToUInt64() => ((ulong)High << 32) | Low; }
[StructLayout(LayoutKind.Sequential)] private struct MemoryStatusEx { public uint Length, MemoryLoad; public ulong TotalPhys, AvailPhys, TotalPageFile, AvailPageFile, TotalVirtual, AvailVirtual, AvailExtendedVirtual; }
}
public sealed class DiskUsageRow
{
public string Drive { get; init; } = string.Empty;
public string Total { get; init; } = string.Empty;
public string Used { get; init; } = string.Empty;
public string Free { get; init; } = string.Empty;
public string Usage { get; init; } = string.Empty;
public string Purpose { get; init; } = string.Empty;
public string Status { get; init; } = string.Empty;
}
}
@@ -29,6 +29,7 @@ using XplorePlane.Services.Security;
using XplorePlane.Services.Storage;
using XplorePlane.ViewModels.Cnc;
using XplorePlane.ViewModels.Debug;
using XplorePlane.ViewModels.Diagnostics;
using XplorePlane.ViewModels.Inspection.Documentation;
using XplorePlane.Views.Cnc;
using XplorePlane.Views.Debug;
@@ -191,6 +192,7 @@ namespace XplorePlane.ViewModels.Main
public DelegateCommand CheckForUpdateCommand { get; }
public DelegateCommand OpenSettingsCommand { get; }
public DelegateCommand OpenDebugPanelCommand { get; }
public DelegateCommand OpenPerformanceMonitorCommand { get; }
public DelegateCommand BrowseDataRootPathCommand { get; }
public DelegateCommand ResetDataRootPathCommand { get; }
public DelegateCommand SaveDataRootPathCommand { get; }
@@ -743,6 +745,7 @@ namespace XplorePlane.ViewModels.Main
OpenSettingsCommand = new DelegateCommand(ExecuteOpenSettings);
OpenDebugPanelCommand = new DelegateCommand(ExecuteOpenDebugPanel);
OpenPerformanceMonitorCommand = new DelegateCommand(ExecuteOpenPerformanceMonitor);
BrowseDataRootPathCommand = new DelegateCommand(ExecuteBrowseDataRootPath);
ResetDataRootPathCommand = new DelegateCommand(ExecuteResetDataRootPath);
SaveDataRootPathCommand = new DelegateCommand(ExecuteSaveDataRootPath);
@@ -1071,6 +1074,25 @@ namespace XplorePlane.ViewModels.Main
}
}
private void ExecuteOpenPerformanceMonitor()
{
try
{
_windowLauncher.ShowOrActivate("SystemPerformanceMonitor",
() =>
{
var viewModel = _containerProvider.Resolve<SystemPerformanceMonitorViewModel>();
var window = new Views.Diagnostics.SystemPerformanceMonitorWindow { DataContext = viewModel };
return window;
}, "性能监控");
}
catch (Exception ex)
{
_logger.Error(ex, "打开性能监控失败");
_dialogs.Show("Main_OpenDebugFailed", "Dialog_Error", ApplicationMessageKind.Error, ex.Message);
}
}
private void ExecuteBrowseDataRootPath()
{
try
@@ -47,9 +47,6 @@
<TabItem Header="快照管理">
<views:SnapshotManagerView DataContext="{Binding SnapshotManager}" />
</TabItem>
<TabItem Header="性能监控">
<views:PerformanceMonitorView DataContext="{Binding PerformanceMonitor}" />
</TabItem>
</TabControl>
</Grid>
@@ -0,0 +1,22 @@
<Window x:Class="XplorePlane.Views.Diagnostics.SystemPerformanceMonitorWindow"
FontFamily="{StaticResource NovaFontFamily}"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
Title="性能监控" Width="1080" Height="720" MinWidth="900" MinHeight="560"
WindowStartupLocation="CenterOwner" Background="{StaticResource NovaSurfaceContainerLowBrush}">
<Window.Resources><BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" /></Window.Resources>
<Grid Margin="20">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<DockPanel Grid.Row="0" Margin="0,0,0,16"><TextBlock Text="性能监控" FontSize="{StaticResource NovaFontSizeTitleLarge}" FontWeight="SemiBold"/><TextBlock DockPanel.Dock="Right" Text="{Binding LastUpdatedText}" Foreground="{StaticResource NovaOnSurfaceVariantBrush}" VerticalAlignment="Bottom"/></DockPanel>
<UniformGrid Grid.Row="1" Columns="4" Margin="0,0,0,16">
<Border Background="{StaticResource NovaSurfaceContainerBrush}" BorderBrush="{StaticResource NovaOutlineVariantBrush}" BorderThickness="1" CornerRadius="{StaticResource NovaRadiusXs}" Padding="14" Margin="0,0,10,0"><StackPanel><TextBlock Text="系统状态" Foreground="{StaticResource NovaOnSurfaceVariantBrush}"/><TextBlock Text="{Binding OverallStatus}" FontSize="{StaticResource NovaFontSizeSubtitle}" FontWeight="SemiBold"/></StackPanel></Border>
<Border Background="{StaticResource NovaSurfaceContainerBrush}" BorderBrush="{StaticResource NovaOutlineVariantBrush}" BorderThickness="1" CornerRadius="{StaticResource NovaRadiusXs}" Padding="14" Margin="0,0,10,0"><StackPanel><TextBlock Text="CPU" Foreground="{StaticResource NovaOnSurfaceVariantBrush}"/><TextBlock Text="{Binding CpuUsage, StringFormat={}{0:F0}%}" FontSize="{StaticResource NovaFontSizeSubtitle}" FontWeight="SemiBold"/><TextBlock Text="{Binding ProcessCpuUsage, StringFormat=XP 进程 {0:F1}%}" Foreground="{StaticResource NovaOnSurfaceVariantBrush}"/></StackPanel></Border>
<Border Background="{StaticResource NovaSurfaceContainerBrush}" BorderBrush="{StaticResource NovaOutlineVariantBrush}" BorderThickness="1" CornerRadius="{StaticResource NovaRadiusXs}" Padding="14" Margin="0,0,10,0"><StackPanel><TextBlock Text="内存" Foreground="{StaticResource NovaOnSurfaceVariantBrush}"/><TextBlock Text="{Binding MemoryUsage, StringFormat={}{0:F0}%}" FontSize="{StaticResource NovaFontSizeSubtitle}" FontWeight="SemiBold"/><TextBlock Text="{Binding MemoryText}" Foreground="{StaticResource NovaOnSurfaceVariantBrush}"/></StackPanel></Border>
<Border Background="{StaticResource NovaSurfaceContainerBrush}" BorderBrush="{StaticResource NovaOutlineVariantBrush}" BorderThickness="1" CornerRadius="{StaticResource NovaRadiusXs}" Padding="14"><StackPanel><TextBlock Text="XP 进程内存" Foreground="{StaticResource NovaOnSurfaceVariantBrush}"/><TextBlock Text="{Binding ProcessMemoryText}" FontSize="{StaticResource NovaFontSizeSubtitle}" FontWeight="SemiBold"/></StackPanel></Border>
</UniformGrid>
<GroupBox Grid.Row="2" Header="磁盘空间" Margin="0,0,0,16"><telerik:RadGridView ItemsSource="{Binding Disks}" AutoGenerateColumns="False" IsReadOnly="True" ShowGroupPanel="False" CanUserGroupColumns="False" RowHeight="32" MinHeight="120" MaxHeight="190"><telerik:RadGridView.Columns><telerik:GridViewDataColumn Header="磁盘" DataMemberBinding="{Binding Drive}" Width="80"/><telerik:GridViewDataColumn Header="总容量" DataMemberBinding="{Binding Total}" Width="100"/><telerik:GridViewDataColumn Header="已用" DataMemberBinding="{Binding Used}" Width="100"/><telerik:GridViewDataColumn Header="剩余" DataMemberBinding="{Binding Free}" Width="100"/><telerik:GridViewDataColumn Header="使用率" DataMemberBinding="{Binding Usage}" Width="90"/><telerik:GridViewDataColumn Header="用途" DataMemberBinding="{Binding Purpose}" Width="*"/><telerik:GridViewDataColumn Header="状态" DataMemberBinding="{Binding Status}" Width="80"/></telerik:RadGridView.Columns></telerik:RadGridView></GroupBox>
<GroupBox Grid.Row="3" Header="当前告警" Margin="0,0,0,16" Visibility="{Binding HasAlerts, Converter={StaticResource BooleanToVisibilityConverter}}"><ItemsControl ItemsSource="{Binding Alerts}"><ItemsControl.ItemTemplate><DataTemplate><TextBlock Text="⚠ {Binding}" Foreground="{StaticResource NovaWarningBrush}" Margin="4" TextWrapping="Wrap"/></DataTemplate></ItemsControl.ItemTemplate></ItemsControl></GroupBox>
<TextBlock Grid.Row="4" Text="磁盘空间不足时,XP 将提前提示用户清理相关数据。" Foreground="{StaticResource NovaOnSurfaceVariantBrush}" VerticalAlignment="Top"/>
</Grid>
</Window>
@@ -0,0 +1,19 @@
using System;
using System.ComponentModel;
using System.Windows;
using XplorePlane.ViewModels.Diagnostics;
namespace XplorePlane.Views.Diagnostics
{
public partial class SystemPerformanceMonitorWindow : Window
{
public SystemPerformanceMonitorWindow()
{
InitializeComponent();
Loaded += (_, _) => (DataContext as SystemPerformanceMonitorViewModel)?.Initialize();
Closing += OnClosing;
}
private void OnClosing(object sender, CancelEventArgs e) => (DataContext as SystemPerformanceMonitorViewModel)?.Dispose();
}
}
+8
View File
@@ -802,6 +802,14 @@
Command="{Binding OpenDebugPanelCommand}"
Text="{loc:Localization Key=Main_DebugPanel}" />
<telerik:RadRibbonButton
telerik:ScreenTip.Description="查看 CPU、内存和磁盘空间使用情况"
telerik:ScreenTip.Title="性能监控"
Command="{Binding OpenPerformanceMonitorCommand}"
Size="Large"
LargeImage="/Assets/Icons/tools.png"
SmallImage="/Assets/Icons/tools.png"
Text="性能监控" />
<telerik:RadRibbonButton
telerik:ScreenTip.Description="打开检测报告配置窗口"
telerik:ScreenTip.Title="{loc:Localization Key=Main_Report}"
Command="{Binding OpenReportConfigCommand}"