From a1cffce9fdd5b82fea195328b167b2babddc9072 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Sat, 30 May 2026 19:47:04 -0500 Subject: [PATCH] MVP port complete --- .../App.xaml.cs | 49 ++- .../MainWindow.cs | 411 +++++++++++++++--- .../Services/HardwareMonitorService.cs | 2 + .../Services/WinUiStartupTrace.cs | 366 ++++++++++++++++ .../ViewModels/MainWindowViewModel.cs | 112 ++++- .../ViewModels/SensorTreeItemViewModel.cs | 22 +- LibreHardwareMonitorLib/Hardware/Computer.cs | 99 ++++- .../Hardware/IHardwareDiscoveryTask.cs | 12 + .../Hardware/Memory/MemoryGroup.cs | 7 +- 9 files changed, 985 insertions(+), 95 deletions(-) create mode 100644 LibreHardwareMonitor.Windows.WinUI/Services/WinUiStartupTrace.cs create mode 100644 LibreHardwareMonitorLib/Hardware/IHardwareDiscoveryTask.cs diff --git a/LibreHardwareMonitor.Windows.WinUI/App.xaml.cs b/LibreHardwareMonitor.Windows.WinUI/App.xaml.cs index a22a639..f768fba 100644 --- a/LibreHardwareMonitor.Windows.WinUI/App.xaml.cs +++ b/LibreHardwareMonitor.Windows.WinUI/App.xaml.cs @@ -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(string phase, Func 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); diff --git a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs index 84da256..fdad3c4 100644 --- a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs +++ b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs @@ -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 _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 getDetail) + { + if (_startupTrace is not { IsComplete: false }) + { + action(); + return; + } + + _startupTrace.Measure(phase, action, getDetail); + } + + private T MeasureStartup(string phase, Func action) + { + if (_startupTrace is not { IsComplete: false }) + return action(); + + return _startupTrace.Measure(phase, action); + } + + private async Task MeasureStartupAsync(string phase, Func 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) diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs index 9933c7a..59f36bb 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs @@ -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() diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/WinUiStartupTrace.cs b/LibreHardwareMonitor.Windows.WinUI/Services/WinUiStartupTrace.cs new file mode 100644 index 0000000..63144f0 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Services/WinUiStartupTrace.cs @@ -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 _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? 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(string phase, Func action) + { + return Measure(phase, action, null); + } + + public T Measure(string phase, Func action, Func? 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 action) + { + await MeasureAsync(phase, action, null); + } + + public async Task MeasureAsync(string phase, Func action, Func? 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 MeasureAsync(string phase, Func> action) + { + return await MeasureAsync(phase, action, null); + } + + public async Task MeasureAsync(string phase, Func> action, Func? 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? getDetail) + { + if (getDetail == null) + return ""; + + try + { + return getDetail() ?? ""; + } + catch (Exception ex) + { + return $"Detail unavailable: {ex.GetType().FullName}: {ex.Message}"; + } + } + + private static string GetDetail(T result, Func? 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 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}"; + } + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs index dbed7d1..a168822 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs @@ -80,6 +80,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable private readonly Dictionary _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 getDetail) + { + if (_startupTrace is not { IsComplete: false }) + { + action(); + return; + } + + _startupTrace.Measure(phase, action, getDetail); + } + + private T MeasureStartup(string phase, Func action) + { + if (_startupTrace is not { IsComplete: false }) + return action(); + + return _startupTrace.Measure(phase, action); + } + + private async Task MeasureStartupAsync(string phase, Func 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() diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs index 8d8d91e..6752634 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs @@ -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 diff --git a/LibreHardwareMonitorLib/Hardware/Computer.cs b/LibreHardwareMonitorLib/Hardware/Computer.cs index 3fba96e..ff6eb40 100644 --- a/LibreHardwareMonitorLib/Hardware/Computer.cs +++ b/LibreHardwareMonitorLib/Hardware/Computer.cs @@ -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 _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 _deferredGroupTasks = []; /// /// Creates a new instance with basic initial . @@ -91,6 +94,17 @@ public class Computer : IComputer /// public event HardwareEventHandler HardwareRemoved; + public event EventHandler HardwareDiscoveryCompleted; + + public Task HardwareDiscoveryTask + { + get + { + lock (_deferredGroupLock) + return _deferredGroupCompletionSource.Task; + } + } + /// public IList 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 isEnabled, params Func[] createGroups) @@ -789,7 +807,7 @@ public class Computer : IComputer } CancellationToken cancellationToken = cancellationTokenSource.Token; - Task.Run(() => + Task task = Task.Run(() => { foreach (Func 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() diff --git a/LibreHardwareMonitorLib/Hardware/IHardwareDiscoveryTask.cs b/LibreHardwareMonitorLib/Hardware/IHardwareDiscoveryTask.cs new file mode 100644 index 0000000..4a7f3dd --- /dev/null +++ b/LibreHardwareMonitorLib/Hardware/IHardwareDiscoveryTask.cs @@ -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; } +} diff --git a/LibreHardwareMonitorLib/Hardware/Memory/MemoryGroup.cs b/LibreHardwareMonitorLib/Hardware/Memory/MemoryGroup.cs index 4443248..9f06823 100644 --- a/LibreHardwareMonitorLib/Hardware/Memory/MemoryGroup.cs +++ b/LibreHardwareMonitorLib/Hardware/Memory/MemoryGroup.cs @@ -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 = []; 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 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 {