From 2c176e5dd738802b31b3a1431c358c1a84141458 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Sat, 30 May 2026 17:45:37 -0500 Subject: [PATCH] WIP - optimizing start times --- .../Services/HardwareMonitorService.cs | 10 ++++ .../ViewModels/MainWindowViewModel.cs | 2 +- LibreHardwareMonitorLib/Hardware/Computer.cs | 51 ++++++++++++++++-- ...dwareStartupTiming-20260530-174110-562.log | 40 ++++++++++++++ docs/optimization-plan.md | 54 +++++++++++++++++++ 5 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 docs/LibreHardwareMonitor.HardwareStartupTiming-20260530-174110-562.log create mode 100644 docs/optimization-plan.md diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs index e270f1a..4e763b7 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs @@ -125,6 +125,16 @@ public sealed class HardwareMonitorService : IDisposable RebuildTree(raiseTreeRebuilt); } + public async Task OpenAsync(bool raiseTreeRebuilt = true, CancellationToken cancellationToken = default) + { + if (_isOpen) + return; + + await Computer.OpenAsync(cancellationToken).ConfigureAwait(false); + _isOpen = true; + RebuildTree(raiseTreeRebuilt); + } + public void Reset() { if (_isOpen) diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs index 7032a35..dbed7d1 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs @@ -676,7 +676,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable try { StatusText = "Initializing hardware sensors..."; - await Task.Run(() => _hardwareMonitor.Open(raiseTreeRebuilt: false)); + await _hardwareMonitor.OpenAsync(raiseTreeRebuilt: false); UpdateRoot(); ApplySensorValuesTimeWindow(); StartWebServerFromSettings(); diff --git a/LibreHardwareMonitorLib/Hardware/Computer.cs b/LibreHardwareMonitorLib/Hardware/Computer.cs index 33d93b2..0293063 100644 --- a/LibreHardwareMonitorLib/Hardware/Computer.cs +++ b/LibreHardwareMonitorLib/Hardware/Computer.cs @@ -516,6 +516,39 @@ public class Computer : IComputer /// enabled. /// public void Open() + { + if (_open) + return; + + OpenInternal(CancellationToken.None); + } + + /// + /// Asynchronously performs the same work as . The required global setup (SMBIOS, mutexes and + /// ) and hardware group discovery run on a background thread; discovery order and behavior + /// match . + /// + /// A task that completes once all enabled hardware groups have been discovered. + public Task OpenAsync() + { + return OpenAsync(CancellationToken.None); + } + + /// + /// Asynchronously performs the same work as , observing the supplied + /// between initialization phases. + /// + /// Token used to cancel initialization before it completes. + /// A task that completes once all enabled hardware groups have been discovered. + public Task OpenAsync(CancellationToken cancellationToken) + { + if (_open) + return Task.CompletedTask; + + return Task.Run(() => OpenInternal(cancellationToken), cancellationToken); + } + + private void OpenInternal(CancellationToken cancellationToken) { if (_open) return; @@ -526,6 +559,7 @@ public class Computer : IComputer { using HardwareStartupTrace startupTrace = HardwareStartupTrace.Create(_settings); + cancellationToken.ThrowIfCancellationRequested(); _smbios = Measure(startupTrace, "SMBios", () => new SMBios()); if (Software.OperatingSystem.IsWindows8OrGreater) @@ -533,9 +567,10 @@ public class Computer : IComputer else startupTrace?.Skip("Mutexes.Open", "Operating system is older than Windows 8."); + cancellationToken.ThrowIfCancellationRequested(); Measure(startupTrace, "OpCode.Open", OpCode.Open); - AddGroups(startupTrace); + AddGroups(startupTrace, cancellationToken); _open = true; } @@ -546,17 +581,21 @@ public class Computer : IComputer } } - private void AddGroups(HardwareStartupTrace startupTrace) + private void AddGroups(HardwareStartupTrace startupTrace, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); if (_motherboardEnabled) AddMeasuredGroup(startupTrace, "MotherboardGroup", () => new MotherboardGroup(_smbios, _settings)); + cancellationToken.ThrowIfCancellationRequested(); if (_cpuEnabled) AddMeasuredGroup(startupTrace, "CpuGroup", () => new CpuGroup(_settings, startupTrace)); + cancellationToken.ThrowIfCancellationRequested(); if (_memoryEnabled) AddMeasuredGroup(startupTrace, "MemoryGroup", () => new MemoryGroup(_settings, startupTrace)); + cancellationToken.ThrowIfCancellationRequested(); if (_gpuEnabled) { AddMeasuredGroup(startupTrace, "AmdGpuGroup", () => new AmdGpuGroup(_settings)); @@ -571,9 +610,11 @@ public class Computer : IComputer AddMeasuredGroup(startupTrace, "IntelGpuGroup", () => new IntelGpuGroup(GetIntelCpus(), _settings)); } + cancellationToken.ThrowIfCancellationRequested(); if (_powerMonitorEnabled) AddMeasuredGroup(startupTrace, "PowerMonitorGroup", () => new PowerMonitorGroup(_settings)); + cancellationToken.ThrowIfCancellationRequested(); if (_controllerEnabled) { AddMeasuredGroup(startupTrace, "TBalancerGroup", () => new TBalancerGroup(_settings)); @@ -586,6 +627,7 @@ public class Computer : IComputer AddMeasuredGroup(startupTrace, "MsiGroup", () => new MsiGroup(_settings)); } + cancellationToken.ThrowIfCancellationRequested(); if (_storageEnabled) AddMeasuredGroup(startupTrace, "StorageGroup", @@ -594,6 +636,7 @@ public class Computer : IComputer DeferStorageDetectionSetting, DeferStorageDetectionEnvironmentVariable); + cancellationToken.ThrowIfCancellationRequested(); if (_networkEnabled) AddMeasuredGroup(startupTrace, "NetworkGroup", @@ -602,12 +645,14 @@ public class Computer : IComputer DeferNetworkDetectionSetting, DeferNetworkDetectionEnvironmentVariable); + cancellationToken.ThrowIfCancellationRequested(); if (_psuEnabled) { AddMeasuredGroup(startupTrace, "CorsairPsuGroup", () => new CorsairPsuGroup(_settings)); AddMeasuredGroup(startupTrace, "MsiPsuGroup", () => new MsiPsuGroup(_settings)); } + cancellationToken.ThrowIfCancellationRequested(); if (_batteryEnabled) AddMeasuredGroup(startupTrace, "BatteryGroup", () => new BatteryGroup(_settings)); } @@ -827,7 +872,7 @@ public class Computer : IComputer StartDeferredGroupRun(); RemoveGroups(); - AddGroups(null); + AddGroups(null, CancellationToken.None); } private void RemoveGroups() diff --git a/docs/LibreHardwareMonitor.HardwareStartupTiming-20260530-174110-562.log b/docs/LibreHardwareMonitor.HardwareStartupTiming-20260530-174110-562.log new file mode 100644 index 0000000..37fdaec --- /dev/null +++ b/docs/LibreHardwareMonitor.HardwareStartupTiming-20260530-174110-562.log @@ -0,0 +1,40 @@ +Libre Hardware Monitor hardware startup timing +Timestamp: 2026-05-30 22:41:10Z +Total elapsed: 239.124 ms + +Phase,ElapsedMs,HardwareCount,Status,Detail +SMBios,3.084,,OK, +Mutexes.Open,1.752,,OK, +OpCode.Open,1.407,,OK, +MotherboardGroup,36.351,1,OK, +CpuGroup.GetProcessorThreads,9.124,,OK,"1 processor(s), 32 thread(s)" +CpuGroup.Processor0.GroupThreadsByCore,0.508,,OK,"24 core(s), 32 thread(s)" +GenericCpu.CpuLoad,0.700,,OK,Available +GenericCpu.EstimateTimeStampCounterFrequency,25.262,,OK, +IntelCpu.CreateMsrModule,0.265,,OK, +IntelCpu.InitialUpdate,0.000,,SKIPPED,Deferred until first regular update. +CpuGroup.Processor0.IntelCpu,37.206,,OK,"13th Gen Intel Core i9-13980HX, 139 sensor(s)" +CpuGroup,47.798,1,OK, +MemoryGroup.Driver.Configure,0.833,,OK, +MemoryGroup.VirtualMemory,0.201,,OK, +MemoryGroup.TotalMemory,0.160,,OK, +MemoryGroup.Driver.LoadDriver,0.123,,OK,Loaded +MemoryGroup.DimmDetection,0.000,,SKIPPED,Deferred to background. +MemoryGroup,2.577,2,OK, +AmdGpuGroup,1.564,0,OK, +NvidiaGroup,0.000,,SKIPPED,Deferred to background. +IntelGpuGroup,76.887,1,OK, +PowerMonitorGroup,1.637,0,OK, +TBalancerGroup,1.902,0,OK, +HeatmasterGroup,0.820,0,OK, +AquaComputerGroup,49.511,0,OK, +AeroCoolGroup,1.070,0,OK, +NzxtGroup,1.038,0,OK, +RazerGroup,0.809,0,OK, +ArcticGroup,0.603,0,OK, +MsiGroup,1.227,0,OK, +StorageGroup,0.000,,SKIPPED,Deferred to background. +NetworkGroup,0.000,,SKIPPED,Deferred to background. +CorsairPsuGroup,1.152,0,OK, +MsiPsuGroup,0.953,0,OK, +BatteryGroup,2.743,1,OK, diff --git a/docs/optimization-plan.md b/docs/optimization-plan.md new file mode 100644 index 0000000..5aa7841 --- /dev/null +++ b/docs/optimization-plan.md @@ -0,0 +1,54 @@ +There doesn’t appear to be a test suite around Computer.Open() in the workspace, so the plan needs to add instrumentation and regression checks before changing +scheduling. I’m going to summarize a conservative path: measure first, make UI progressively populated, then parallelize only independent groups with explicit +resource classes. + +• Updated Plan +└ ✔ Trace current hardware initialization path +✔ Identify blocking and serial discovery hotspots +□ Present prioritized optimization plan + +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +• Findings +Computer.Open() is fully serial today: SMBIOS, mutex setup, OpCode.Open(), then every enabled group in fixed order. Expensive candidates include motherboard LPC/EC/ IPMI, memory SPD driver/SMBus probing, GPU vendor APIs, storage enumeration, network enumeration, USB/serial controller scans, PSUs, power monitors, and battery +device IO. + +Some groups are independent, but not all are safe to parallelize blindly. ISA/PCI/EC/USB sensor paths use shared mutexes and some constructors perform fixed sleeps +or driver/native calls. + +Plan + +1. Add startup timing instrumentation first. + Record per phase: SMBIOS, OpCode.Open, each IGroup constructor, hardware count, exceptions, and elapsed time. Put this behind a debug/log setting so we can + compare before/after without guessing. + +2. Improve perceived startup in WinUI. + Show the shell immediately, then populate groups as they complete. Status should say which group is initializing. This preserves correctness and makes slow + probes visible instead of looking hung. + +3. Split Computer.Open() into staged group discovery. + Keep required global setup serial: + SMBios, Mutexes.Open, OpCode.Open. + Then schedule group creation through a small initialization coordinator. + +4. Parallelize only low-conflict groups first. + Good first candidates: + CpuGroup, NetworkGroup, BatteryGroup, StorageGroup, AMD/NVIDIA GPU groups, non-dependent PSU groups. +5. Add dependency/resource classes. + Use explicit buckets such as Cpu, GpuVendorApi, Storage, Network, Battery, IsaBus, PciBus, Ec, UsbHid, SerialUsb. + Groups in the same risky bucket stay serialized; independent buckets can run concurrently. + Controller groups contain some worst-case sleeps and USB/serial scans. Load them after core CPU/GPU/motherboard/storage sensors, or expose “defer fan/controller + detection” while still defaulting to correctness. + HardwareAdded should fire as each group completes. Computer.Hardware needs lock-safe snapshots so UI and web server can read while discovery continues. + +6. Regression checks. + Compare before/after reports and sensor identifiers on the same machine. The correctness bar should be: same enabled hardware groups eventually discovered, same + sensor identifiers where hardware is present, no lost late-added memory/GPU devices. + +Recommended implementation order: + +1. Timing instrumentation. +2. Progressive WinUI population/status. +3. Async Computer.OpenAsync with serial behavior. +4. Parallelize low-conflict groups. +5. Defer/lazy heavy controller probes. +6. Broaden with resource-bucket scheduling after logs prove hotspots.