feat: make performance monitor a standalone page
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user