MVP port complete
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using LibreHardwareMonitor.Windows.WinUI.Services;
|
||||
using Microsoft.UI.Xaml;
|
||||
using IOPath = System.IO.Path;
|
||||
|
||||
@@ -13,19 +14,27 @@ namespace LibreHardwareMonitor.Windows.WinUI;
|
||||
public partial class App : Application
|
||||
{
|
||||
private bool _launchStarted;
|
||||
private readonly WinUiStartupTrace? _startupTrace;
|
||||
private Window? _window;
|
||||
|
||||
public App()
|
||||
{
|
||||
UnhandledException += App_UnhandledException;
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
|
||||
InitializeComponent();
|
||||
_startupTrace = WinUiStartupTrace.Create();
|
||||
_startupTrace?.Mark("App.Constructor.Begin");
|
||||
MeasureStartup("App.WireExceptionHandlers", () =>
|
||||
{
|
||||
UnhandledException += App_UnhandledException;
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
|
||||
});
|
||||
MeasureStartup("App.InitializeComponent", InitializeComponent);
|
||||
_startupTrace?.Mark("App.Constructor.Complete");
|
||||
_startupTrace?.Flush();
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
LaunchMainWindow();
|
||||
MeasureStartup("App.OnLaunched", LaunchMainWindow);
|
||||
}
|
||||
|
||||
private void LaunchMainWindow()
|
||||
@@ -36,18 +45,42 @@ public partial class App : Application
|
||||
_launchStarted = true;
|
||||
try
|
||||
{
|
||||
MainWindow mainWindow = new();
|
||||
_startupTrace?.Mark("App.LaunchMainWindow.Begin");
|
||||
MainWindow mainWindow = MeasureStartup("App.CreateMainWindow", () => new MainWindow(_startupTrace));
|
||||
_window = mainWindow;
|
||||
_window.Activate();
|
||||
mainWindow.StartMonitoringAfterActivation();
|
||||
MeasureStartup("App.ActivateWindow", mainWindow.Activate);
|
||||
MeasureStartup("App.StartMonitoringAfterActivation", mainWindow.StartMonitoringAfterActivation);
|
||||
_startupTrace?.Mark("App.LaunchMainWindow.Complete");
|
||||
_startupTrace?.Flush();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_startupTrace?.Mark("App.LaunchMainWindow.Exception", $"{ex.GetType().FullName}: {ex.Message}");
|
||||
_startupTrace?.Flush();
|
||||
WriteExceptionLog("Window launch failed", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void MeasureStartup(string phase, Action action)
|
||||
{
|
||||
if (_startupTrace == null)
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
|
||||
_startupTrace.Measure(phase, action);
|
||||
}
|
||||
|
||||
private T MeasureStartup<T>(string phase, Func<T> action)
|
||||
{
|
||||
if (_startupTrace == null)
|
||||
return action();
|
||||
|
||||
return _startupTrace.Measure(phase, action);
|
||||
}
|
||||
|
||||
private static void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs args)
|
||||
{
|
||||
WriteExceptionLog("WinUI unhandled exception", args.Exception);
|
||||
|
||||
@@ -35,87 +35,139 @@ namespace LibreHardwareMonitor.Windows.WinUI;
|
||||
|
||||
public sealed class MainWindow : Window
|
||||
{
|
||||
private static readonly TimeSpan DeviceColumnWidthSettleDelay = TimeSpan.FromSeconds(5);
|
||||
|
||||
private const double DefaultSensorColumnWidth = 320;
|
||||
private const double MaximumSensorColumnWidth = 4096;
|
||||
private const double MinimumSensorColumnWidth = 120;
|
||||
private const double SensorColumnPadding = 72;
|
||||
private const double SensorTreeIndentWidth = 20;
|
||||
private const string DeviceColumnWidthSetting = "winui.deviceColumnWidth";
|
||||
private const double ValueColumnPadding = 18;
|
||||
private const int MaxTextMeasurementCacheEntries = 4096;
|
||||
|
||||
private readonly AppWindow _appWindow;
|
||||
private readonly Grid _contentGrid;
|
||||
private readonly DispatcherQueueTimer _deviceColumnWidthSettleTimer;
|
||||
private readonly DispatcherQueueTimer _timer;
|
||||
private readonly Canvas _plotCanvas;
|
||||
private readonly Grid _plotPane;
|
||||
private readonly Grid _rootGrid;
|
||||
private readonly TreeView _sensorTree;
|
||||
private readonly Grid _sensorPane;
|
||||
private readonly WinUiStartupTrace? _startupTrace;
|
||||
private readonly Dictionary<(string Text, bool Bold), double> _textMeasurementCache = new();
|
||||
private readonly List<Grid> _sensorRowGrids = [];
|
||||
private readonly double[] _sensorColumnWidths = [320, 120, 120, 120];
|
||||
private readonly double[] _sensorColumnWidths = [DefaultSensorColumnWidth, 120, 120, 120];
|
||||
private Grid? _sensorHeader;
|
||||
private double _stableDeviceColumnWidth = DefaultSensorColumnWidth;
|
||||
private bool _deviceColumnWidthSettled;
|
||||
private bool _firstLayoutRecorded;
|
||||
private bool _isUpdating;
|
||||
private bool _isMonitoringStarted;
|
||||
private bool _startupCompletionRequested;
|
||||
private bool _startupCompleteRecorded;
|
||||
private bool _sensorTreeRebuildQueued;
|
||||
private bool _sensorColumnWidthUpdateQueued;
|
||||
|
||||
public MainWindow()
|
||||
public MainWindow() : this(null)
|
||||
{
|
||||
AppSettings settings = AppSettings.LoadDefault();
|
||||
ViewModel = new MainWindowViewModel(settings);
|
||||
}
|
||||
|
||||
IntPtr hwnd = WindowNative.GetWindowHandle(this);
|
||||
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
|
||||
_appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
_appWindow.Title = "Libre Hardware Monitor";
|
||||
internal MainWindow(WinUiStartupTrace? startupTrace)
|
||||
{
|
||||
_startupTrace = startupTrace;
|
||||
_startupTrace?.Mark("MainWindow.Constructor.Begin");
|
||||
AppSettings settings = MeasureStartup("MainWindow.LoadSettings", AppSettings.LoadDefault);
|
||||
ViewModel = MeasureStartup("MainWindow.CreateViewModel", () => new MainWindowViewModel(settings, _startupTrace));
|
||||
MeasureStartup("MainWindow.ApplySavedDeviceColumnWidth", () => ApplySavedDeviceColumnWidth(settings), () => FormattableString.Invariant($"width={_sensorColumnWidths[0]:F0}"));
|
||||
|
||||
Grid root = BuildRoot();
|
||||
Content = root;
|
||||
_rootGrid = root;
|
||||
|
||||
_contentGrid = (Grid)root.Children[1];
|
||||
_sensorPane = (Grid)_contentGrid.Children[0];
|
||||
_plotPane = (Grid)_contentGrid.Children[1];
|
||||
_sensorTree = (TreeView)((Grid)_sensorPane.Children[1]).Children[0];
|
||||
_plotCanvas = (Canvas)_plotPane.Children[1];
|
||||
|
||||
RestoreWindowBounds();
|
||||
MaximizeWindow();
|
||||
ApplyTheme();
|
||||
UpdatePlotLayout();
|
||||
|
||||
_timer = DispatcherQueue.CreateTimer();
|
||||
_timer.Interval = ViewModel.UpdateInterval;
|
||||
_timer.Tick += UpdateTimer_Tick;
|
||||
|
||||
ViewModel.PropertyChanged += (_, args) =>
|
||||
_appWindow = MeasureStartup("MainWindow.GetAppWindow", () =>
|
||||
{
|
||||
if (args.PropertyName is nameof(ViewModel.UpdateInterval) or nameof(ViewModel.UpdateIntervalIndex))
|
||||
_timer.Interval = ViewModel.UpdateInterval;
|
||||
IntPtr hwnd = WindowNative.GetWindowHandle(this);
|
||||
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
|
||||
AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
appWindow.Title = "Libre Hardware Monitor";
|
||||
return appWindow;
|
||||
});
|
||||
|
||||
if (args.PropertyName is nameof(ViewModel.ShowPlot)
|
||||
or nameof(ViewModel.PlotLocation)
|
||||
or nameof(ViewModel.PlotGridColumn)
|
||||
or nameof(ViewModel.PlotGridRow))
|
||||
UpdatePlotLayout();
|
||||
Grid root = MeasureStartup("MainWindow.BuildRoot", BuildRoot);
|
||||
MeasureStartup("MainWindow.AssignContent", () => Content = root);
|
||||
_rootGrid = root;
|
||||
_rootGrid.Loaded += RootGrid_Loaded;
|
||||
_rootGrid.LayoutUpdated += RootGrid_LayoutUpdated;
|
||||
|
||||
if (args.PropertyName == nameof(ViewModel.ShowHiddenSensors))
|
||||
RebuildSensorTree();
|
||||
(Grid contentGrid, Grid sensorPane, Grid plotPane, TreeView sensorTree, Canvas plotCanvas) = MeasureStartup("MainWindow.ResolveControls", () =>
|
||||
{
|
||||
Grid resolvedContentGrid = (Grid)root.Children[1];
|
||||
Grid resolvedSensorPane = (Grid)resolvedContentGrid.Children[0];
|
||||
Grid resolvedPlotPane = (Grid)resolvedContentGrid.Children[1];
|
||||
TreeView resolvedSensorTree = (TreeView)((Grid)resolvedSensorPane.Children[1]).Children[0];
|
||||
Canvas resolvedPlotCanvas = (Canvas)resolvedPlotPane.Children[1];
|
||||
return (resolvedContentGrid, resolvedSensorPane, resolvedPlotPane, resolvedSensorTree, resolvedPlotCanvas);
|
||||
});
|
||||
_contentGrid = contentGrid;
|
||||
_sensorPane = sensorPane;
|
||||
_plotPane = plotPane;
|
||||
_sensorTree = sensorTree;
|
||||
_plotCanvas = plotCanvas;
|
||||
|
||||
if (args.PropertyName is nameof(ViewModel.ShowValueColumn)
|
||||
or nameof(ViewModel.ShowMinColumn)
|
||||
or nameof(ViewModel.ShowMaxColumn))
|
||||
UpdateSensorColumnWidths();
|
||||
MeasureStartup("MainWindow.RestoreWindowBounds", RestoreWindowBounds);
|
||||
MeasureStartup("MainWindow.MaximizeWindow", MaximizeWindow);
|
||||
MeasureStartup("MainWindow.ApplyTheme", ApplyTheme);
|
||||
MeasureStartup("MainWindow.UpdatePlotLayout", UpdatePlotLayout);
|
||||
|
||||
if (args.PropertyName == nameof(ViewModel.ThemeMode))
|
||||
ApplyTheme();
|
||||
};
|
||||
ViewModel.RootItems.CollectionChanged += RootItems_CollectionChanged;
|
||||
ViewModel.PlotInvalidated += (_, _) => DrawPlot();
|
||||
(_timer, _deviceColumnWidthSettleTimer) = MeasureStartup("MainWindow.CreateTimers", () =>
|
||||
{
|
||||
DispatcherQueueTimer timer = DispatcherQueue.CreateTimer();
|
||||
timer.Interval = ViewModel.UpdateInterval;
|
||||
timer.Tick += UpdateTimer_Tick;
|
||||
|
||||
Closed += MainWindow_Closed;
|
||||
DispatcherQueueTimer settleTimer = DispatcherQueue.CreateTimer();
|
||||
settleTimer.Interval = DeviceColumnWidthSettleDelay;
|
||||
settleTimer.Tick += DeviceColumnWidthSettleTimer_Tick;
|
||||
|
||||
return (timer, settleTimer);
|
||||
});
|
||||
|
||||
MeasureStartup("MainWindow.WireEvents", () =>
|
||||
{
|
||||
ViewModel.PropertyChanged += (_, args) =>
|
||||
{
|
||||
if (args.PropertyName is nameof(ViewModel.UpdateInterval) or nameof(ViewModel.UpdateIntervalIndex))
|
||||
_timer.Interval = ViewModel.UpdateInterval;
|
||||
|
||||
if (args.PropertyName is nameof(ViewModel.ShowPlot)
|
||||
or nameof(ViewModel.PlotLocation)
|
||||
or nameof(ViewModel.PlotGridColumn)
|
||||
or nameof(ViewModel.PlotGridRow))
|
||||
UpdatePlotLayout();
|
||||
|
||||
if (args.PropertyName == nameof(ViewModel.ShowHiddenSensors))
|
||||
QueueSensorTreeRebuild();
|
||||
|
||||
if (args.PropertyName is nameof(ViewModel.ShowValueColumn)
|
||||
or nameof(ViewModel.ShowMinColumn)
|
||||
or nameof(ViewModel.ShowMaxColumn))
|
||||
QueueSensorColumnWidthUpdate();
|
||||
|
||||
if (args.PropertyName == nameof(ViewModel.ThemeMode))
|
||||
ApplyTheme();
|
||||
};
|
||||
ViewModel.RootItems.CollectionChanged += RootItems_CollectionChanged;
|
||||
ViewModel.PlotInvalidated += (_, _) => DrawPlot();
|
||||
|
||||
Closed += MainWindow_Closed;
|
||||
});
|
||||
_startupTrace?.Mark("MainWindow.Constructor.Complete");
|
||||
}
|
||||
|
||||
public MainWindowViewModel ViewModel { get; }
|
||||
|
||||
public void StartMonitoringAfterActivation()
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(async () =>
|
||||
_startupTrace?.Mark("MainWindow.StartMonitoringAfterActivation.Queued");
|
||||
if (!DispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
if (_isMonitoringStarted)
|
||||
return;
|
||||
@@ -123,16 +175,88 @@ public sealed class MainWindow : Window
|
||||
_isMonitoringStarted = true;
|
||||
try
|
||||
{
|
||||
await Task.Delay(250);
|
||||
await ViewModel.StartAsync();
|
||||
RebuildSensorTree();
|
||||
_timer.Start();
|
||||
_startupTrace?.Mark("MainWindow.StartMonitoringAfterActivation.Begin");
|
||||
await MeasureStartupAsync("MainWindowViewModel.StartAsync", ViewModel.StartAsync);
|
||||
MeasureStartup("MainWindow.StartTimer", _timer.Start);
|
||||
_startupTrace?.Mark("MainWindow.StartMonitoringAfterActivation.Complete");
|
||||
RequestStartupTraceComplete();
|
||||
_startupTrace?.Flush();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_startupTrace?.Mark("MainWindow.StartMonitoringAfterActivation.Exception", $"{ex.GetType().FullName}: {ex.Message}");
|
||||
_startupTrace?.Flush();
|
||||
RecordRuntimeError("Hardware initialization failed", ex);
|
||||
}
|
||||
});
|
||||
}))
|
||||
{
|
||||
_startupTrace?.Mark("MainWindow.StartMonitoringAfterActivation.EnqueueFailed");
|
||||
_startupTrace?.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetRootSizeDetail()
|
||||
{
|
||||
return FormattableString.Invariant($"root={_rootGrid.ActualWidth:F0}x{_rootGrid.ActualHeight:F0}, rows={_sensorRowGrids.Count}, rootNodes={_sensorTree.RootNodes.Count}");
|
||||
}
|
||||
|
||||
private void RootGrid_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_rootGrid.Loaded -= RootGrid_Loaded;
|
||||
_startupTrace?.Mark("MainWindow.RootLoaded", GetRootSizeDetail());
|
||||
_startupTrace?.Flush();
|
||||
}
|
||||
|
||||
private void RootGrid_LayoutUpdated(object? sender, object e)
|
||||
{
|
||||
if (_firstLayoutRecorded)
|
||||
return;
|
||||
|
||||
_firstLayoutRecorded = true;
|
||||
_rootGrid.LayoutUpdated -= RootGrid_LayoutUpdated;
|
||||
_startupTrace?.Mark("MainWindow.FirstLayoutUpdated", GetRootSizeDetail());
|
||||
_startupTrace?.Flush();
|
||||
}
|
||||
|
||||
private void MeasureStartup(string phase, Action action)
|
||||
{
|
||||
if (_startupTrace is not { IsComplete: false })
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
|
||||
_startupTrace.Measure(phase, action);
|
||||
}
|
||||
|
||||
private void MeasureStartup(string phase, Action action, Func<string> getDetail)
|
||||
{
|
||||
if (_startupTrace is not { IsComplete: false })
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
|
||||
_startupTrace.Measure(phase, action, getDetail);
|
||||
}
|
||||
|
||||
private T MeasureStartup<T>(string phase, Func<T> action)
|
||||
{
|
||||
if (_startupTrace is not { IsComplete: false })
|
||||
return action();
|
||||
|
||||
return _startupTrace.Measure(phase, action);
|
||||
}
|
||||
|
||||
private async Task MeasureStartupAsync(string phase, Func<Task> action)
|
||||
{
|
||||
if (_startupTrace is not { IsComplete: false })
|
||||
{
|
||||
await action();
|
||||
return;
|
||||
}
|
||||
|
||||
await _startupTrace.MeasureAsync(phase, action);
|
||||
}
|
||||
|
||||
private Grid BuildRoot()
|
||||
@@ -148,7 +272,7 @@ public sealed class MainWindow : Window
|
||||
}
|
||||
};
|
||||
|
||||
root.Children.Add(BuildMenuBar());
|
||||
root.Children.Add(MeasureStartup("MainWindow.BuildMenuBar", BuildMenuBar));
|
||||
|
||||
Grid contentGrid = new()
|
||||
{
|
||||
@@ -163,10 +287,11 @@ public sealed class MainWindow : Window
|
||||
new RowDefinition { Height = new GridLength(220) }
|
||||
}
|
||||
};
|
||||
Bind(contentGrid, UIElement.IsHitTestVisibleProperty, ViewModel, nameof(ViewModel.IsHardwareInteractionEnabled));
|
||||
Grid.SetRow(contentGrid, 1);
|
||||
|
||||
contentGrid.Children.Add(BuildSensorPane());
|
||||
contentGrid.Children.Add(BuildPlotPane());
|
||||
contentGrid.Children.Add(MeasureStartup("MainWindow.BuildSensorPane", BuildSensorPane));
|
||||
contentGrid.Children.Add(MeasureStartup("MainWindow.BuildPlotPane", BuildPlotPane));
|
||||
root.Children.Add(contentGrid);
|
||||
|
||||
TextBlock statusText = new()
|
||||
@@ -178,12 +303,15 @@ public sealed class MainWindow : Window
|
||||
Grid.SetRow(statusText, 2);
|
||||
root.Children.Add(statusText);
|
||||
|
||||
root.Children.Add(BuildLoadingOverlay());
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private MenuBar BuildMenuBar()
|
||||
{
|
||||
MenuBar menuBar = new();
|
||||
Bind(menuBar, Control.IsEnabledProperty, ViewModel, nameof(ViewModel.IsHardwareInteractionEnabled));
|
||||
|
||||
MenuBarItem file = new() { Title = "File" };
|
||||
file.Items.Add(CreateMenuItem("Save Report...", async (_, _) => await SaveReportAsync()));
|
||||
@@ -319,6 +447,43 @@ public sealed class MainWindow : Window
|
||||
return menuBar;
|
||||
}
|
||||
|
||||
private Grid BuildLoadingOverlay()
|
||||
{
|
||||
Grid overlay = new()
|
||||
{
|
||||
Background = new SolidColorBrush(global::Windows.UI.Color.FromArgb(150, 128, 128, 128)),
|
||||
Visibility = ViewModel.HardwareLoadingVisibility,
|
||||
IsHitTestVisible = true
|
||||
};
|
||||
Bind(overlay, UIElement.VisibilityProperty, ViewModel, nameof(ViewModel.HardwareLoadingVisibility));
|
||||
Grid.SetRowSpan(overlay, 3);
|
||||
Canvas.SetZIndex(overlay, 1000);
|
||||
|
||||
StackPanel content = new()
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Spacing = 12
|
||||
};
|
||||
ProgressRing progressRing = new()
|
||||
{
|
||||
Width = 64,
|
||||
Height = 64,
|
||||
IsIndeterminate = true
|
||||
};
|
||||
TextBlock text = new()
|
||||
{
|
||||
Text = "Loading hardware devices...",
|
||||
FontWeight = new global::Windows.UI.Text.FontWeight { Weight = 600 },
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
content.Children.Add(progressRing);
|
||||
content.Children.Add(text);
|
||||
overlay.Children.Add(content);
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
private Grid BuildSensorPane()
|
||||
{
|
||||
Grid pane = new()
|
||||
@@ -397,7 +562,39 @@ public sealed class MainWindow : Window
|
||||
|
||||
private void RootItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
RebuildSensorTree();
|
||||
QueueSensorTreeRebuild();
|
||||
}
|
||||
|
||||
private void QueueSensorTreeRebuild()
|
||||
{
|
||||
if (_sensorTreeRebuildQueued)
|
||||
return;
|
||||
|
||||
_sensorTreeRebuildQueued = true;
|
||||
if (!DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
_sensorTreeRebuildQueued = false;
|
||||
RebuildSensorTree();
|
||||
}))
|
||||
{
|
||||
_sensorTreeRebuildQueued = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueSensorColumnWidthUpdate()
|
||||
{
|
||||
if (_sensorColumnWidthUpdateQueued)
|
||||
return;
|
||||
|
||||
_sensorColumnWidthUpdateQueued = true;
|
||||
if (!DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
_sensorColumnWidthUpdateQueued = false;
|
||||
UpdateSensorColumnWidths();
|
||||
}))
|
||||
{
|
||||
_sensorColumnWidthUpdateQueued = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void UpdateTimer_Tick(DispatcherQueueTimer sender, object args)
|
||||
@@ -425,19 +622,37 @@ public sealed class MainWindow : Window
|
||||
|
||||
private void RecordRuntimeError(string message, Exception exception)
|
||||
{
|
||||
_startupTrace?.Mark("MainWindow.RuntimeError", $"{message}: {exception.GetType().FullName}: {exception.Message}");
|
||||
_startupTrace?.Flush();
|
||||
string logPath = IOPath.Combine(AppContext.BaseDirectory, "LibreHardwareMonitor.Windows.WinUI.startup.log");
|
||||
File.WriteAllText(logPath, exception.ToString());
|
||||
ViewModel.SetStatusText($"{message}. See {IOPath.GetFileName(logPath)}.");
|
||||
}
|
||||
|
||||
private void ApplySavedDeviceColumnWidth(AppSettings settings)
|
||||
{
|
||||
_stableDeviceColumnWidth = NormalizeDeviceColumnWidth(settings.GetValue(DeviceColumnWidthSetting, DefaultSensorColumnWidth));
|
||||
_sensorColumnWidths[0] = _stableDeviceColumnWidth;
|
||||
}
|
||||
|
||||
private void MainWindow_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
_timer.Stop();
|
||||
_deviceColumnWidthSettleTimer.Stop();
|
||||
SaveWindowBounds();
|
||||
ViewModel.Dispose();
|
||||
_startupTrace?.Dispose();
|
||||
}
|
||||
|
||||
private void RebuildSensorTree()
|
||||
{
|
||||
MeasureStartup("MainWindow.RebuildSensorTree", RebuildSensorTreeCore, GetRootSizeDetail);
|
||||
ScheduleDeviceColumnWidthSettle();
|
||||
if (_startupCompletionRequested)
|
||||
CompleteStartupTraceIfReady();
|
||||
}
|
||||
|
||||
private void RebuildSensorTreeCore()
|
||||
{
|
||||
if (_sensorTree == null)
|
||||
return;
|
||||
@@ -454,6 +669,39 @@ public sealed class MainWindow : Window
|
||||
UpdateSensorColumnWidths();
|
||||
}
|
||||
|
||||
private void CompleteStartupTraceIfReady()
|
||||
{
|
||||
if (_startupCompleteRecorded || _startupTrace is not { IsComplete: false } || _sensorRowGrids.Count == 0)
|
||||
return;
|
||||
|
||||
_startupCompleteRecorded = true;
|
||||
_startupTrace.Complete("MainWindow.StartupComplete", GetRootSizeDetail());
|
||||
}
|
||||
|
||||
private void RequestStartupTraceComplete()
|
||||
{
|
||||
_startupCompletionRequested = true;
|
||||
if (!DispatcherQueue.TryEnqueue(CompleteStartupTraceIfReady))
|
||||
CompleteStartupTraceIfReady();
|
||||
}
|
||||
|
||||
private void DeviceColumnWidthSettleTimer_Tick(DispatcherQueueTimer sender, object args)
|
||||
{
|
||||
_deviceColumnWidthSettleTimer.Stop();
|
||||
_deviceColumnWidthSettled = true;
|
||||
UpdateSensorColumnWidths();
|
||||
}
|
||||
|
||||
private void ScheduleDeviceColumnWidthSettle()
|
||||
{
|
||||
if (_sensorRowGrids.Count == 0)
|
||||
return;
|
||||
|
||||
_deviceColumnWidthSettled = false;
|
||||
_deviceColumnWidthSettleTimer.Stop();
|
||||
_deviceColumnWidthSettleTimer.Start();
|
||||
}
|
||||
|
||||
private static DataTemplate CreateTreeViewNodeContentTemplate()
|
||||
{
|
||||
const string xaml = """
|
||||
@@ -1065,6 +1313,11 @@ public sealed class MainWindow : Window
|
||||
}
|
||||
|
||||
private void UpdateSensorColumnWidths()
|
||||
{
|
||||
MeasureStartup("MainWindow.UpdateSensorColumnWidths", UpdateSensorColumnWidthsCore, () => FormattableString.Invariant($"rows={_sensorRowGrids.Count}, cacheEntries={_textMeasurementCache.Count}, deviceWidth={_sensorColumnWidths[0]:F0}, settled={_deviceColumnWidthSettled}"));
|
||||
}
|
||||
|
||||
private void UpdateSensorColumnWidthsCore()
|
||||
{
|
||||
double sensorWidth = MeasureText("Sensor", true) + SensorColumnPadding;
|
||||
double valueWidth = ViewModel.ShowValueColumn ? MeasureText("Value", true) + ValueColumnPadding : 0;
|
||||
@@ -1074,7 +1327,11 @@ public sealed class MainWindow : Window
|
||||
foreach (SensorTreeItemViewModel root in ViewModel.RootItems)
|
||||
MeasureSensorColumnWidths(root, 0, ref sensorWidth, ref valueWidth, ref minWidth, ref maxWidth);
|
||||
|
||||
_sensorColumnWidths[0] = Math.Ceiling(sensorWidth);
|
||||
double deviceColumnWidth = NormalizeDeviceColumnWidth(sensorWidth);
|
||||
if (!_deviceColumnWidthSettled)
|
||||
deviceColumnWidth = Math.Max(deviceColumnWidth, _stableDeviceColumnWidth);
|
||||
|
||||
_sensorColumnWidths[0] = deviceColumnWidth;
|
||||
_sensorColumnWidths[1] = Math.Ceiling(valueWidth);
|
||||
_sensorColumnWidths[2] = Math.Ceiling(minWidth);
|
||||
_sensorColumnWidths[3] = Math.Ceiling(maxWidth);
|
||||
@@ -1083,6 +1340,8 @@ public sealed class MainWindow : Window
|
||||
ApplySensorColumnWidths(_sensorHeader);
|
||||
foreach (Grid row in _sensorRowGrids)
|
||||
ApplySensorColumnWidths(row);
|
||||
|
||||
RecordDeviceColumnWidthIfChanged();
|
||||
}
|
||||
|
||||
private void MeasureSensorColumnWidths(
|
||||
@@ -1108,15 +1367,47 @@ public sealed class MainWindow : Window
|
||||
MeasureSensorColumnWidths(child, depth + 1, ref sensorWidth, ref valueWidth, ref minWidth, ref maxWidth);
|
||||
}
|
||||
|
||||
private static double MeasureText(string text, bool bold = false)
|
||||
private double MeasureText(string text, bool bold = false)
|
||||
{
|
||||
(string Text, bool Bold) key = (text, bold);
|
||||
if (_textMeasurementCache.TryGetValue(key, out double width))
|
||||
return width;
|
||||
|
||||
if (_textMeasurementCache.Count >= MaxTextMeasurementCacheEntries)
|
||||
_textMeasurementCache.Clear();
|
||||
|
||||
TextBlock textBlock = new()
|
||||
{
|
||||
Text = text,
|
||||
FontWeight = new global::Windows.UI.Text.FontWeight { Weight = bold ? (ushort)600 : (ushort)400 }
|
||||
};
|
||||
textBlock.Measure(new global::Windows.Foundation.Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
return textBlock.DesiredSize.Width;
|
||||
width = textBlock.DesiredSize.Width;
|
||||
_textMeasurementCache[key] = width;
|
||||
return width;
|
||||
}
|
||||
|
||||
private static double NormalizeDeviceColumnWidth(double width)
|
||||
{
|
||||
if (!double.IsFinite(width))
|
||||
width = DefaultSensorColumnWidth;
|
||||
|
||||
return Math.Ceiling(Math.Clamp(width, MinimumSensorColumnWidth, MaximumSensorColumnWidth));
|
||||
}
|
||||
|
||||
private void RecordDeviceColumnWidthIfChanged()
|
||||
{
|
||||
if (!_deviceColumnWidthSettled || _sensorRowGrids.Count == 0)
|
||||
return;
|
||||
|
||||
double measuredWidth = NormalizeDeviceColumnWidth(_sensorColumnWidths[0]);
|
||||
_stableDeviceColumnWidth = measuredWidth;
|
||||
double savedWidth = NormalizeDeviceColumnWidth(ViewModel.Settings.GetValue(DeviceColumnWidthSetting, DefaultSensorColumnWidth));
|
||||
if (Math.Abs(measuredWidth - savedWidth) < 0.5)
|
||||
return;
|
||||
|
||||
ViewModel.Settings.SetValue(DeviceColumnWidthSetting, measuredWidth);
|
||||
_startupTrace?.Mark("MainWindow.RecordDeviceColumnWidth", FormattableString.Invariant($"width={measuredWidth:F0}, rows={_sensorRowGrids.Count}"));
|
||||
}
|
||||
|
||||
private void ApplySensorColumnWidths(Grid grid)
|
||||
|
||||
@@ -137,6 +137,8 @@ public sealed class HardwareMonitorService : IDisposable
|
||||
await Computer.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
_isOpen = true;
|
||||
RebuildTree(raiseTreeRebuilt);
|
||||
await Computer.HardwareDiscoveryTask.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
RebuildTree(raiseTreeRebuilt);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
// Copyright (C) LibreHardwareMonitor and Contributors.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibreHardwareMonitor.Windows.WinUI.Services;
|
||||
|
||||
internal sealed class WinUiStartupTrace : IDisposable
|
||||
{
|
||||
private const string EnabledEnvironmentVariable = "LHM_WINUI_STARTUP_TIMING";
|
||||
private const string PathEnvironmentVariable = "LHM_WINUI_STARTUP_TIMING_PATH";
|
||||
|
||||
private readonly List<Entry> _entries = new();
|
||||
private readonly string _fileName;
|
||||
private readonly object _lock = new();
|
||||
private readonly Stopwatch _totalStopwatch;
|
||||
private bool _completed;
|
||||
private bool _disposed;
|
||||
|
||||
private WinUiStartupTrace()
|
||||
{
|
||||
_fileName = GetLogFileName();
|
||||
_totalStopwatch = Stopwatch.StartNew();
|
||||
Mark("WinUIStartupTrace.Begin");
|
||||
}
|
||||
|
||||
public bool IsComplete
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
return _completed || _disposed;
|
||||
}
|
||||
}
|
||||
|
||||
public static WinUiStartupTrace? Create()
|
||||
{
|
||||
return IsEnabled() ? new WinUiStartupTrace() : null;
|
||||
}
|
||||
|
||||
public void Complete(string phase, string detail = "")
|
||||
{
|
||||
bool shouldWrite;
|
||||
lock (_lock)
|
||||
{
|
||||
shouldWrite = !_completed && !_disposed;
|
||||
if (shouldWrite)
|
||||
{
|
||||
AddEntryLocked(new Entry(phase, _totalStopwatch.Elapsed, TimeSpan.Zero, "COMPLETE", detail));
|
||||
_completed = true;
|
||||
_totalStopwatch.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldWrite)
|
||||
WriteLog();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
bool shouldWrite;
|
||||
lock (_lock)
|
||||
{
|
||||
shouldWrite = !_disposed;
|
||||
if (shouldWrite)
|
||||
{
|
||||
if (!_completed)
|
||||
{
|
||||
AddEntryLocked(new Entry("WinUIStartupTrace.Dispose", _totalStopwatch.Elapsed, TimeSpan.Zero, "DISPOSED", ""));
|
||||
_totalStopwatch.Stop();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldWrite)
|
||||
WriteLog();
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
WriteLog();
|
||||
}
|
||||
|
||||
public void Mark(string phase, string detail = "")
|
||||
{
|
||||
AddEntry(phase, TimeSpan.Zero, "MARK", detail);
|
||||
}
|
||||
|
||||
public void Measure(string phase, Action action)
|
||||
{
|
||||
Measure(phase, action, null);
|
||||
}
|
||||
|
||||
public void Measure(string phase, Action action, Func<string>? getDetail)
|
||||
{
|
||||
TimeSpan start = _totalStopwatch.Elapsed;
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
action();
|
||||
stopwatch.Stop();
|
||||
AddEntry(phase, start, stopwatch.Elapsed, "OK", GetDetail(getDetail));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
AddEntry(phase, start, stopwatch.Elapsed, "ERROR", $"{ex.GetType().FullName}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public T Measure<T>(string phase, Func<T> action)
|
||||
{
|
||||
return Measure(phase, action, null);
|
||||
}
|
||||
|
||||
public T Measure<T>(string phase, Func<T> action, Func<T, string>? getDetail)
|
||||
{
|
||||
TimeSpan start = _totalStopwatch.Elapsed;
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
T result = action();
|
||||
stopwatch.Stop();
|
||||
AddEntry(phase, start, stopwatch.Elapsed, "OK", GetDetail(result, getDetail));
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
AddEntry(phase, start, stopwatch.Elapsed, "ERROR", $"{ex.GetType().FullName}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MeasureAsync(string phase, Func<Task> action)
|
||||
{
|
||||
await MeasureAsync(phase, action, null);
|
||||
}
|
||||
|
||||
public async Task MeasureAsync(string phase, Func<Task> action, Func<string>? getDetail)
|
||||
{
|
||||
TimeSpan start = _totalStopwatch.Elapsed;
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
await action();
|
||||
stopwatch.Stop();
|
||||
AddEntry(phase, start, stopwatch.Elapsed, "OK", GetDetail(getDetail));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
AddEntry(phase, start, stopwatch.Elapsed, "ERROR", $"{ex.GetType().FullName}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T> MeasureAsync<T>(string phase, Func<Task<T>> action)
|
||||
{
|
||||
return await MeasureAsync(phase, action, null);
|
||||
}
|
||||
|
||||
public async Task<T> MeasureAsync<T>(string phase, Func<Task<T>> action, Func<T, string>? getDetail)
|
||||
{
|
||||
TimeSpan start = _totalStopwatch.Elapsed;
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
T result = await action();
|
||||
stopwatch.Stop();
|
||||
AddEntry(phase, start, stopwatch.Elapsed, "OK", GetDetail(result, getDetail));
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
AddEntry(phase, start, stopwatch.Elapsed, "ERROR", $"{ex.GetType().FullName}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return "";
|
||||
|
||||
if (!value.Contains(",") && !value.Contains("\"") && !value.Contains("\r") && !value.Contains("\n"))
|
||||
return value;
|
||||
|
||||
return "\"" + value.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
private static string GetDetail(Func<string>? getDetail)
|
||||
{
|
||||
if (getDetail == null)
|
||||
return "";
|
||||
|
||||
try
|
||||
{
|
||||
return getDetail() ?? "";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Detail unavailable: {ex.GetType().FullName}: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDetail<T>(T result, Func<T, string>? getDetail)
|
||||
{
|
||||
if (getDetail == null)
|
||||
return "";
|
||||
|
||||
try
|
||||
{
|
||||
return getDetail(result) ?? "";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Detail unavailable: {ex.GetType().FullName}: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetLogFileName()
|
||||
{
|
||||
string? configuredPath = Environment.GetEnvironmentVariable(PathEnvironmentVariable);
|
||||
|
||||
string fileName = $"LibreHardwareMonitor.WinUIStartupTiming-{DateTime.Now:yyyyMMdd-HHmmss-fff}.log";
|
||||
if (string.IsNullOrWhiteSpace(configuredPath))
|
||||
return Path.Combine(AppContext.BaseDirectory, fileName);
|
||||
|
||||
if (configuredPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
|
||||
|| configuredPath.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal)
|
||||
|| Directory.Exists(configuredPath))
|
||||
{
|
||||
return Path.Combine(configuredPath, fileName);
|
||||
}
|
||||
|
||||
return configuredPath;
|
||||
}
|
||||
|
||||
private static bool IsEnabled()
|
||||
{
|
||||
string environmentValue = Environment.GetEnvironmentVariable(EnabledEnvironmentVariable) ?? "";
|
||||
return IsTruthy(environmentValue);
|
||||
}
|
||||
|
||||
private static bool IsTruthy(string value)
|
||||
{
|
||||
return value.Equals("1", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("true", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("yes", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("on", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void AddEntry(string phase, TimeSpan elapsed, string status, string detail)
|
||||
{
|
||||
AddEntry(phase, _totalStopwatch.Elapsed, elapsed, status, detail);
|
||||
}
|
||||
|
||||
private void AddEntry(string phase, TimeSpan start, TimeSpan elapsed, string status, string detail)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_completed || _disposed)
|
||||
return;
|
||||
|
||||
AddEntryLocked(new Entry(phase, start, elapsed, status, detail));
|
||||
}
|
||||
}
|
||||
|
||||
private void AddEntryLocked(Entry entry)
|
||||
{
|
||||
_entries.Add(entry);
|
||||
Debug.WriteLine($"WinUI startup: {entry}");
|
||||
}
|
||||
|
||||
private string BuildLog(IReadOnlyList<Entry> entries, TimeSpan totalElapsed)
|
||||
{
|
||||
StringBuilder builder = new();
|
||||
builder.AppendLine("Libre Hardware Monitor WinUI startup timing");
|
||||
builder.Append("Timestamp: ");
|
||||
builder.AppendLine(DateTimeOffset.Now.ToString("u", CultureInfo.InvariantCulture));
|
||||
builder.Append("Total elapsed: ");
|
||||
builder.Append(totalElapsed.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture));
|
||||
builder.AppendLine(" ms");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("Phase,StartMs,ElapsedMs,Status,Detail");
|
||||
|
||||
foreach (Entry entry in entries.OrderBy(entry => entry.Start))
|
||||
{
|
||||
builder.Append(EscapeCsv(entry.Phase));
|
||||
builder.Append(',');
|
||||
builder.Append(entry.Start.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture));
|
||||
builder.Append(',');
|
||||
builder.Append(entry.Elapsed.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture));
|
||||
builder.Append(',');
|
||||
builder.Append(entry.Status);
|
||||
builder.Append(',');
|
||||
builder.AppendLine(EscapeCsv(entry.Detail));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private void WriteLog()
|
||||
{
|
||||
Entry[] entries;
|
||||
TimeSpan totalElapsed;
|
||||
lock (_lock)
|
||||
{
|
||||
entries = _entries.ToArray();
|
||||
totalElapsed = _totalStopwatch.Elapsed;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string? directory = Path.GetDirectoryName(_fileName);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
File.WriteAllText(_fileName, BuildLog(entries, totalElapsed));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Startup tracing must never affect application startup.
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Entry
|
||||
{
|
||||
public Entry(string phase, TimeSpan start, TimeSpan elapsed, string status, string detail)
|
||||
{
|
||||
Phase = phase;
|
||||
Start = start;
|
||||
Elapsed = elapsed;
|
||||
Status = status;
|
||||
Detail = detail;
|
||||
}
|
||||
|
||||
public string Detail { get; }
|
||||
|
||||
public TimeSpan Elapsed { get; }
|
||||
|
||||
public string Phase { get; }
|
||||
|
||||
public TimeSpan Start { get; }
|
||||
|
||||
public string Status { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Phase}: start {Start.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture)} ms, elapsed {Elapsed.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture)} ms, {Status}, {Detail}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
|
||||
private readonly Dictionary<string, PlotSeriesViewModel> _plotSeriesByIdentifier = new();
|
||||
private readonly RemoteWebServer _remoteWebServer;
|
||||
private readonly StartupService _startupService = new();
|
||||
private readonly WinUiStartupTrace? _startupTrace;
|
||||
private AppThemeMode _themeMode;
|
||||
private int _loggingIntervalIndex;
|
||||
private PlotLocation _plotLocation;
|
||||
@@ -92,6 +93,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
|
||||
private bool _showPlot;
|
||||
private bool _showValueColumn;
|
||||
private bool _throttleAtaUpdate;
|
||||
private bool _isHardwareLoading = true;
|
||||
private bool _isStarted;
|
||||
private bool _isStarting;
|
||||
private int _rootUpdateQueued;
|
||||
@@ -99,9 +101,15 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
|
||||
private TemperatureUnit _temperatureUnit;
|
||||
private int _updateIntervalIndex;
|
||||
|
||||
public MainWindowViewModel(AppSettings settings)
|
||||
public MainWindowViewModel(AppSettings settings) : this(settings, null)
|
||||
{
|
||||
}
|
||||
|
||||
internal MainWindowViewModel(AppSettings settings, WinUiStartupTrace? startupTrace)
|
||||
{
|
||||
Settings = settings;
|
||||
_startupTrace = startupTrace;
|
||||
_startupTrace?.Mark("MainWindowViewModel.Constructor.Begin");
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
_hardwareMonitor = new HardwareMonitorService(settings);
|
||||
_logger = new Logger(_hardwareMonitor.Computer);
|
||||
@@ -131,6 +139,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
|
||||
StorageDevice.ThrottleInterval = _throttleAtaUpdate ? TimeSpan.FromSeconds(30) : TimeSpan.Zero;
|
||||
_logger.LoggingInterval = LoggingIntervals[_loggingIntervalIndex];
|
||||
_logger.FileRotationMethod = (LoggerFileRotation)Math.Clamp(settings.GetValue("logger.fileRotation", 0), 0, 1);
|
||||
_startupTrace?.Mark("MainWindowViewModel.Constructor.Complete");
|
||||
}
|
||||
|
||||
public event EventHandler? PlotInvalidated;
|
||||
@@ -530,6 +539,23 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
|
||||
|
||||
public bool IsWebServerUnavailable => _remoteWebServer.PlatformNotSupported;
|
||||
|
||||
public bool IsHardwareInteractionEnabled => !IsHardwareLoading;
|
||||
|
||||
public bool IsHardwareLoading
|
||||
{
|
||||
get => _isHardwareLoading;
|
||||
private set
|
||||
{
|
||||
if (!SetProperty(ref _isHardwareLoading, value))
|
||||
return;
|
||||
|
||||
OnPropertyChanged(nameof(HardwareLoadingVisibility));
|
||||
OnPropertyChanged(nameof(IsHardwareInteractionEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
public Visibility HardwareLoadingVisibility => IsHardwareLoading ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public string WebServerUrl
|
||||
{
|
||||
get
|
||||
@@ -675,16 +701,22 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
|
||||
_isStarting = true;
|
||||
try
|
||||
{
|
||||
IsHardwareLoading = true;
|
||||
_startupTrace?.Mark("MainWindowViewModel.StartAsync.Begin");
|
||||
StatusText = "Initializing hardware sensors...";
|
||||
await _hardwareMonitor.OpenAsync(raiseTreeRebuilt: false);
|
||||
UpdateRoot();
|
||||
ApplySensorValuesTimeWindow();
|
||||
StartWebServerFromSettings();
|
||||
UpdateStatus();
|
||||
await MeasureStartupAsync("MainWindowViewModel.HardwareMonitor.OpenAsync", () => _hardwareMonitor.OpenAsync(raiseTreeRebuilt: false));
|
||||
MeasureStartup("MainWindowViewModel.UpdateRoot", UpdateRoot, GetRootDetail);
|
||||
StatusText = "Reading sensor values...";
|
||||
await MeasureStartupAsync("MainWindowViewModel.InitialSensorValueUpdate", () => RefreshSensorValuesAsync(trackPlotPoints: false, logSensors: false));
|
||||
MeasureStartup("MainWindowViewModel.StartWebServerFromSettings", StartWebServerFromSettings);
|
||||
MeasureStartup("MainWindowViewModel.UpdateStatus", UpdateStatus, GetRootDetail);
|
||||
_isStarted = true;
|
||||
_startupTrace?.Mark("MainWindowViewModel.StartAsync.Complete", GetRootDetail());
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsHardwareLoading = false;
|
||||
_startupTrace?.Mark("MainWindowViewModel.HardwareLoadingComplete", GetRootDetail());
|
||||
_isStarting = false;
|
||||
}
|
||||
}
|
||||
@@ -694,13 +726,19 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
|
||||
if (!_isStarted)
|
||||
return;
|
||||
|
||||
await RefreshSensorValuesAsync(trackPlotPoints: true, logSensors: LogSensors);
|
||||
}
|
||||
|
||||
private async Task RefreshSensorValuesAsync(bool trackPlotPoints, bool logSensors)
|
||||
{
|
||||
await _hardwareMonitor.UpdateAsync();
|
||||
|
||||
SensorTreeItemViewModel? root = RootItems.FirstOrDefault();
|
||||
root?.RefreshValues();
|
||||
TrackPlotPoints();
|
||||
if (trackPlotPoints)
|
||||
TrackPlotPoints();
|
||||
|
||||
if (LogSensors)
|
||||
if (logSensors)
|
||||
_logger.Log();
|
||||
|
||||
UpdateStatus();
|
||||
@@ -742,6 +780,54 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
|
||||
visitor.VisitComputer(_hardwareMonitor.Computer);
|
||||
}
|
||||
|
||||
private string GetRootDetail()
|
||||
{
|
||||
int hardwareCount = _hardwareMonitor.Computer.Hardware.Count;
|
||||
int sensorCount = RootItems.FirstOrDefault()?.EnumerateSensors().Count() ?? 0;
|
||||
return $"{hardwareCount} hardware device(s), {sensorCount} sensor(s)";
|
||||
}
|
||||
|
||||
private void MeasureStartup(string phase, Action action)
|
||||
{
|
||||
if (_startupTrace is not { IsComplete: false })
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
|
||||
_startupTrace.Measure(phase, action);
|
||||
}
|
||||
|
||||
private void MeasureStartup(string phase, Action action, Func<string> getDetail)
|
||||
{
|
||||
if (_startupTrace is not { IsComplete: false })
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
|
||||
_startupTrace.Measure(phase, action, getDetail);
|
||||
}
|
||||
|
||||
private T MeasureStartup<T>(string phase, Func<T> action)
|
||||
{
|
||||
if (_startupTrace is not { IsComplete: false })
|
||||
return action();
|
||||
|
||||
return _startupTrace.Measure(phase, action);
|
||||
}
|
||||
|
||||
private async Task MeasureStartupAsync(string phase, Func<Task> action)
|
||||
{
|
||||
if (_startupTrace is not { IsComplete: false })
|
||||
{
|
||||
await action();
|
||||
return;
|
||||
}
|
||||
|
||||
await _startupTrace.MeasureAsync(phase, action);
|
||||
}
|
||||
|
||||
private void NotifyColumnVisibilityChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(ValueColumnVisibility));
|
||||
@@ -822,11 +908,11 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
|
||||
|
||||
private void UpdateRoot()
|
||||
{
|
||||
RootItems.Clear();
|
||||
SensorTreeItemViewModel root = _hardwareMonitor.Root;
|
||||
root.Configure(TemperatureUnit, ShowHiddenSensors, ShowValueColumn, ShowMinColumn, ShowMaxColumn);
|
||||
RootItems.Add(root);
|
||||
ApplySensorValuesTimeWindow();
|
||||
MeasureStartup("MainWindowViewModel.RootItems.Clear", RootItems.Clear);
|
||||
SensorTreeItemViewModel root = MeasureStartup("MainWindowViewModel.GetHardwareRoot", () => _hardwareMonitor.Root);
|
||||
MeasureStartup("MainWindowViewModel.ConfigureRoot", () => root.Configure(TemperatureUnit, ShowHiddenSensors, ShowValueColumn, ShowMinColumn, ShowMaxColumn), GetRootDetail);
|
||||
MeasureStartup("MainWindowViewModel.RootItems.Add", () => RootItems.Add(root), GetRootDetail);
|
||||
MeasureStartup("MainWindowViewModel.ApplySensorValuesTimeWindow", ApplySensorValuesTimeWindow, GetRootDetail);
|
||||
}
|
||||
|
||||
private void UpdateStatus()
|
||||
|
||||
@@ -176,13 +176,7 @@ public sealed class SensorTreeItemViewModel : ViewModelBase
|
||||
public TemperatureUnit TemperatureUnit
|
||||
{
|
||||
get => _temperatureUnit;
|
||||
private set
|
||||
{
|
||||
if (!SetProperty(ref _temperatureUnit, value))
|
||||
return;
|
||||
|
||||
RefreshValues();
|
||||
}
|
||||
private set => SetProperty(ref _temperatureUnit, value);
|
||||
}
|
||||
|
||||
public string Value => Sensor == null ? "" : SensorFormatter.FormatValue(Sensor, Sensor.Value, TemperatureUnit);
|
||||
@@ -295,9 +289,8 @@ public sealed class SensorTreeItemViewModel : ViewModelBase
|
||||
|
||||
public void SetTemperatureUnit(TemperatureUnit temperatureUnit)
|
||||
{
|
||||
TemperatureUnit = temperatureUnit;
|
||||
foreach (SensorTreeItemViewModel child in Children)
|
||||
child.SetTemperatureUnit(temperatureUnit);
|
||||
if (SetTemperatureUnitCore(temperatureUnit))
|
||||
RefreshValues();
|
||||
}
|
||||
|
||||
private static SensorTreeItemViewModel FromSensor(ISensor sensor, AppSettings settings)
|
||||
@@ -333,6 +326,15 @@ public sealed class SensorTreeItemViewModel : ViewModelBase
|
||||
return item;
|
||||
}
|
||||
|
||||
private bool SetTemperatureUnitCore(TemperatureUnit temperatureUnit)
|
||||
{
|
||||
bool changed = SetProperty(ref _temperatureUnit, temperatureUnit, nameof(TemperatureUnit));
|
||||
foreach (SensorTreeItemViewModel child in Children)
|
||||
changed |= child.SetTemperatureUnitCore(temperatureUnit);
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private void UpdateVisibilityState()
|
||||
{
|
||||
bool visible = Kind switch
|
||||
|
||||
@@ -50,6 +50,7 @@ public class Computer : IComputer
|
||||
private const string DeferPsuDetectionEnvironmentVariable = "LHM_PSU_DEFER_DETECTION";
|
||||
private const string DeferPsuDetectionSetting = "psu.deferDetection";
|
||||
|
||||
private readonly object _deferredGroupLock = new();
|
||||
private readonly List<IGroup> _groups = new();
|
||||
private readonly object _lock = new();
|
||||
private readonly ISettings _settings;
|
||||
@@ -67,6 +68,8 @@ public class Computer : IComputer
|
||||
private SMBios _smbios;
|
||||
private bool _storageEnabled;
|
||||
private CancellationTokenSource _deferredGroupCancellationTokenSource;
|
||||
private TaskCompletionSource _deferredGroupCompletionSource = CreateCompletedTaskCompletionSource();
|
||||
private List<Task> _deferredGroupTasks = [];
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="IComputer" /> instance with basic initial <see cref="Settings" />.
|
||||
@@ -91,6 +94,17 @@ public class Computer : IComputer
|
||||
/// <inheritdoc />
|
||||
public event HardwareEventHandler HardwareRemoved;
|
||||
|
||||
public event EventHandler HardwareDiscoveryCompleted;
|
||||
|
||||
public Task HardwareDiscoveryTask
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_deferredGroupLock)
|
||||
return _deferredGroupCompletionSource.Task;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IList<IHardware> Hardware
|
||||
{
|
||||
@@ -466,6 +480,8 @@ public class Computer : IComputer
|
||||
hardwareChanged.HardwareAdded += HardwareAddedEvent;
|
||||
hardwareChanged.HardwareRemoved += HardwareRemovedEvent;
|
||||
}
|
||||
|
||||
TrackGroupHardwareDiscoveryTask(group);
|
||||
}
|
||||
|
||||
if (HardwareAdded != null)
|
||||
@@ -577,6 +593,7 @@ public class Computer : IComputer
|
||||
Measure(startupTrace, "OpCode.Open", OpCode.Open);
|
||||
|
||||
AddGroups(startupTrace, cancellationToken);
|
||||
CompleteDeferredGroupRunWhenRegistered();
|
||||
|
||||
_open = true;
|
||||
}
|
||||
@@ -742,7 +759,7 @@ public class Computer : IComputer
|
||||
}
|
||||
|
||||
CancellationToken cancellationToken = cancellationTokenSource.Token;
|
||||
Task.Run(() =>
|
||||
Task task = Task.Run(() =>
|
||||
{
|
||||
IGroup group = null;
|
||||
try
|
||||
@@ -768,6 +785,7 @@ public class Computer : IComputer
|
||||
|
||||
Add(group);
|
||||
});
|
||||
TrackDeferredGroupTask(task);
|
||||
}
|
||||
|
||||
private void AddDeferredGroups(Func<bool> isEnabled, params Func<IGroup>[] createGroups)
|
||||
@@ -789,7 +807,7 @@ public class Computer : IComputer
|
||||
}
|
||||
|
||||
CancellationToken cancellationToken = cancellationTokenSource.Token;
|
||||
Task.Run(() =>
|
||||
Task task = Task.Run(() =>
|
||||
{
|
||||
foreach (Func<IGroup> createGroup in createGroups)
|
||||
{
|
||||
@@ -821,6 +839,7 @@ public class Computer : IComputer
|
||||
Add(group);
|
||||
}
|
||||
});
|
||||
TrackDeferredGroupTask(task);
|
||||
}
|
||||
|
||||
private bool ShouldDeferDetection(string settingName, string environmentVariable)
|
||||
@@ -843,6 +862,12 @@ public class Computer : IComputer
|
||||
private void StartDeferredGroupRun()
|
||||
{
|
||||
CancelDeferredGroupRun();
|
||||
lock (_deferredGroupLock)
|
||||
{
|
||||
_deferredGroupCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_deferredGroupTasks = [];
|
||||
}
|
||||
|
||||
_deferredGroupCancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
@@ -850,12 +875,81 @@ public class Computer : IComputer
|
||||
{
|
||||
CancellationTokenSource cancellationTokenSource = _deferredGroupCancellationTokenSource;
|
||||
_deferredGroupCancellationTokenSource = null;
|
||||
TaskCompletionSource completionSource;
|
||||
|
||||
lock (_deferredGroupLock)
|
||||
{
|
||||
completionSource = _deferredGroupCompletionSource;
|
||||
_deferredGroupTasks = [];
|
||||
}
|
||||
|
||||
if (cancellationTokenSource == null)
|
||||
{
|
||||
completionSource.TrySetCanceled();
|
||||
return;
|
||||
}
|
||||
|
||||
cancellationTokenSource.Cancel();
|
||||
cancellationTokenSource.Dispose();
|
||||
completionSource.TrySetCanceled();
|
||||
}
|
||||
|
||||
private static TaskCompletionSource CreateCompletedTaskCompletionSource()
|
||||
{
|
||||
TaskCompletionSource completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
completionSource.SetResult();
|
||||
return completionSource;
|
||||
}
|
||||
|
||||
private void CompleteDeferredGroupRun(TaskCompletionSource completionSource)
|
||||
{
|
||||
bool completed;
|
||||
lock (_deferredGroupLock)
|
||||
{
|
||||
if (!ReferenceEquals(completionSource, _deferredGroupCompletionSource))
|
||||
return;
|
||||
|
||||
completed = completionSource.TrySetResult();
|
||||
}
|
||||
|
||||
if (completed)
|
||||
HardwareDiscoveryCompleted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void CompleteDeferredGroupRunWhenRegistered()
|
||||
{
|
||||
TaskCompletionSource completionSource;
|
||||
Task[] tasks;
|
||||
lock (_deferredGroupLock)
|
||||
{
|
||||
completionSource = _deferredGroupCompletionSource;
|
||||
tasks = _deferredGroupTasks.ToArray();
|
||||
}
|
||||
|
||||
if (tasks.Length == 0)
|
||||
{
|
||||
CompleteDeferredGroupRun(completionSource);
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.WhenAll(tasks).ContinueWith(_ => CompleteDeferredGroupRun(completionSource),
|
||||
CancellationToken.None,
|
||||
TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
|
||||
private void TrackDeferredGroupTask(Task task)
|
||||
{
|
||||
lock (_deferredGroupLock)
|
||||
_deferredGroupTasks.Add(task);
|
||||
}
|
||||
|
||||
private void TrackGroupHardwareDiscoveryTask(IGroup group)
|
||||
{
|
||||
if (group is not IHardwareDiscoveryTask hardwareDiscoveryTask || hardwareDiscoveryTask.HardwareDiscoveryTask.IsCompleted)
|
||||
return;
|
||||
|
||||
TrackDeferredGroupTask(hardwareDiscoveryTask.HardwareDiscoveryTask);
|
||||
}
|
||||
|
||||
private static void NewSection(TextWriter writer)
|
||||
@@ -969,6 +1063,7 @@ public class Computer : IComputer
|
||||
StartDeferredGroupRun();
|
||||
RemoveGroups();
|
||||
AddGroups(null, CancellationToken.None);
|
||||
CompleteDeferredGroupRunWhenRegistered();
|
||||
}
|
||||
|
||||
private void RemoveGroups()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
// Copyright (C) LibreHardwareMonitor and Contributors.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibreHardwareMonitor.Hardware;
|
||||
|
||||
internal interface IHardwareDiscoveryTask
|
||||
{
|
||||
Task HardwareDiscoveryTask { get; }
|
||||
}
|
||||
@@ -19,7 +19,7 @@ using RAMSPDToolkit.Windows.Driver;
|
||||
|
||||
namespace LibreHardwareMonitor.Hardware.Memory;
|
||||
|
||||
internal class MemoryGroup : IGroup, IHardwareChanged
|
||||
internal class MemoryGroup : IGroup, IHardwareChanged, IHardwareDiscoveryTask
|
||||
{
|
||||
private const string DeferDimmDetectionEnvironmentVariable = "LHM_MEMORY_DEFER_DIMM_DETECTION";
|
||||
private const string DeferDimmDetectionSetting = "memory.deferDimmDetection";
|
||||
@@ -28,6 +28,7 @@ internal class MemoryGroup : IGroup, IHardwareChanged
|
||||
private List<Hardware> _hardware = [];
|
||||
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private Task _hardwareDiscoveryTask = Task.CompletedTask;
|
||||
private Exception _lastException;
|
||||
private bool _disposed = false;
|
||||
|
||||
@@ -73,6 +74,8 @@ internal class MemoryGroup : IGroup, IHardwareChanged
|
||||
|
||||
public IReadOnlyList<IHardware> Hardware => _hardware;
|
||||
|
||||
public Task HardwareDiscoveryTask => _hardwareDiscoveryTask;
|
||||
|
||||
public string GetReport()
|
||||
{
|
||||
StringBuilder report = new();
|
||||
@@ -158,7 +161,7 @@ internal class MemoryGroup : IGroup, IHardwareChanged
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
CancellationToken cancellationToken = _cancellationTokenSource.Token;
|
||||
|
||||
Task.Run(async () =>
|
||||
_hardwareDiscoveryTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user