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.