diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs index 2f8fdd9..e270f1a 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs @@ -3,7 +3,9 @@ // Copyright (C) LibreHardwareMonitor and Contributors. using System; +using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using LibreHardwareMonitor.Hardware; using LibreHardwareMonitor.Hardware.Storage; @@ -13,13 +15,21 @@ namespace LibreHardwareMonitor.Windows.WinUI.Services; public sealed class HardwareMonitorService : IDisposable { + private const string DeferDimmDetectionSetting = "memory.deferDimmDetection"; + private const string DeferCpuInitialUpdateSetting = "cpu.deferInitialUpdate"; + private const string DeferNetworkDetectionSetting = "network.deferDetection"; + private const string DeferNvidiaDetectionSetting = "nvidia.deferDetection"; + private const string DeferStorageDetectionSetting = "storage.deferDetection"; + private readonly object _updateLock = new(); private readonly UpdateVisitor _updateVisitor = new(); private bool _isOpen; + private int _treeRebuildQueued; public HardwareMonitorService(AppSettings settings) { Settings = settings; + ApplyWinUiHardwareDefaults(); Computer = new Computer(settings); Computer.HardwareAdded += HardwareChanged; Computer.HardwareRemoved += HardwareChanged; @@ -181,6 +191,24 @@ public sealed class HardwareMonitorService : IDisposable ForceDriveWakeup = Settings.GetValue("forceDriveWakeupItem", false); } + private void ApplyWinUiHardwareDefaults() + { + if (!Settings.Contains(DeferDimmDetectionSetting)) + Settings.SetValue(DeferDimmDetectionSetting, true); + + if (!Settings.Contains(DeferCpuInitialUpdateSetting)) + Settings.SetValue(DeferCpuInitialUpdateSetting, true); + + if (!Settings.Contains(DeferNvidiaDetectionSetting)) + Settings.SetValue(DeferNvidiaDetectionSetting, true); + + if (!Settings.Contains(DeferStorageDetectionSetting)) + Settings.SetValue(DeferStorageDetectionSetting, true); + + if (!Settings.Contains(DeferNetworkDetectionSetting)) + Settings.SetValue(DeferNetworkDetectionSetting, true); + } + private void SetHardwareEnabled(string settingName, bool value, Action setter) { setter(value); @@ -192,6 +220,31 @@ public sealed class HardwareMonitorService : IDisposable private void HardwareChanged(IHardware hardware) { if (_isOpen) - RebuildTree(); + QueueTreeRebuild(); + } + + private void QueueTreeRebuild() + { + if (Interlocked.Exchange(ref _treeRebuildQueued, 1) == 1) + return; + + _ = Task.Run(async () => + { + try + { + await Task.Delay(100).ConfigureAwait(false); + + if (_isOpen) + RebuildTree(); + } + catch (Exception ex) + { + Debug.WriteLine($"Hardware tree rebuild failed: {ex}"); + } + finally + { + Interlocked.Exchange(ref _treeRebuildQueued, 0); + } + }); } } diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs index 1acd452..7032a35 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs @@ -6,11 +6,13 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Threading; using System.Threading.Tasks; using LibreHardwareMonitor.Hardware; using LibreHardwareMonitor.Hardware.Storage; using LibreHardwareMonitor.Windows.WinUI.Services; using LibreHardwareMonitor.Windows.WinUI.Utilities; +using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Windows.UI; @@ -73,6 +75,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable ]; private readonly HardwareMonitorService _hardwareMonitor; + private readonly DispatcherQueue _dispatcherQueue; private readonly Logger _logger; private readonly Dictionary _plotSeriesByIdentifier = new(); private readonly RemoteWebServer _remoteWebServer; @@ -91,6 +94,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable private bool _throttleAtaUpdate; private bool _isStarted; private bool _isStarting; + private int _rootUpdateQueued; private string _statusText = ""; private TemperatureUnit _temperatureUnit; private int _updateIntervalIndex; @@ -98,6 +102,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable public MainWindowViewModel(AppSettings settings) { Settings = settings; + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); _hardwareMonitor = new HardwareMonitorService(settings); _logger = new Logger(_hardwareMonitor.Computer); _remoteWebServer = new RemoteWebServer( @@ -703,7 +708,31 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable private void HardwareMonitor_TreeRebuilt(object? sender, EventArgs e) { - UpdateRoot(); + if (_dispatcherQueue.HasThreadAccess) + { + UpdateRoot(); + UpdateStatus(); + return; + } + + if (Interlocked.Exchange(ref _rootUpdateQueued, 1) == 1) + return; + + if (!_dispatcherQueue.TryEnqueue(() => + { + try + { + UpdateRoot(); + UpdateStatus(); + } + finally + { + Interlocked.Exchange(ref _rootUpdateQueued, 0); + } + })) + { + Interlocked.Exchange(ref _rootUpdateQueued, 0); + } } private void ApplySensorValuesTimeWindow() diff --git a/LibreHardwareMonitorLib/Hardware/Computer.cs b/LibreHardwareMonitorLib/Hardware/Computer.cs index 7a23d01..33d93b2 100644 --- a/LibreHardwareMonitorLib/Hardware/Computer.cs +++ b/LibreHardwareMonitorLib/Hardware/Computer.cs @@ -9,6 +9,8 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using LibreHardwareMonitor.Hardware.Battery; using LibreHardwareMonitor.Hardware.Controller.AeroCool; using LibreHardwareMonitor.Hardware.Controller.AquaComputer; @@ -35,6 +37,13 @@ namespace LibreHardwareMonitor.Hardware; /// public class Computer : IComputer { + private const string DeferNetworkDetectionEnvironmentVariable = "LHM_NETWORK_DEFER_DETECTION"; + private const string DeferNetworkDetectionSetting = "network.deferDetection"; + private const string DeferNvidiaDetectionEnvironmentVariable = "LHM_NVIDIA_DEFER_DETECTION"; + private const string DeferNvidiaDetectionSetting = "nvidia.deferDetection"; + private const string DeferStorageDetectionEnvironmentVariable = "LHM_STORAGE_DEFER_DETECTION"; + private const string DeferStorageDetectionSetting = "storage.deferDetection"; + private readonly List _groups = new(); private readonly object _lock = new(); private readonly ISettings _settings; @@ -51,6 +60,7 @@ public class Computer : IComputer private bool _psuEnabled; private SMBios _smbios; private bool _storageEnabled; + private CancellationTokenSource _deferredGroupCancellationTokenSource; /// /// Creates a new instance with basic initial . @@ -510,67 +520,201 @@ public class Computer : IComputer if (_open) return; - _smbios = new SMBios(); + StartDeferredGroupRun(); - if (Software.OperatingSystem.IsWindows8OrGreater) - Mutexes.Open(); + try + { + using HardwareStartupTrace startupTrace = HardwareStartupTrace.Create(_settings); - OpCode.Open(); + _smbios = Measure(startupTrace, "SMBios", () => new SMBios()); - AddGroups(); + if (Software.OperatingSystem.IsWindows8OrGreater) + Measure(startupTrace, "Mutexes.Open", Mutexes.Open); + else + startupTrace?.Skip("Mutexes.Open", "Operating system is older than Windows 8."); - _open = true; + Measure(startupTrace, "OpCode.Open", OpCode.Open); + + AddGroups(startupTrace); + + _open = true; + } + catch + { + CancelDeferredGroupRun(); + throw; + } } - private void AddGroups() + private void AddGroups(HardwareStartupTrace startupTrace) { if (_motherboardEnabled) - Add(new MotherboardGroup(_smbios, _settings)); + AddMeasuredGroup(startupTrace, "MotherboardGroup", () => new MotherboardGroup(_smbios, _settings)); if (_cpuEnabled) - Add(new CpuGroup(_settings)); + AddMeasuredGroup(startupTrace, "CpuGroup", () => new CpuGroup(_settings, startupTrace)); if (_memoryEnabled) - Add(new MemoryGroup(_settings)); + AddMeasuredGroup(startupTrace, "MemoryGroup", () => new MemoryGroup(_settings, startupTrace)); if (_gpuEnabled) { - Add(new AmdGpuGroup(_settings)); - Add(new NvidiaGroup(_settings)); + AddMeasuredGroup(startupTrace, "AmdGpuGroup", () => new AmdGpuGroup(_settings)); + AddMeasuredGroup(startupTrace, + "NvidiaGroup", + () => new NvidiaGroup(_settings), + () => _gpuEnabled, + DeferNvidiaDetectionSetting, + DeferNvidiaDetectionEnvironmentVariable); if (_cpuEnabled) - Add(new IntelGpuGroup(GetIntelCpus(), _settings)); + AddMeasuredGroup(startupTrace, "IntelGpuGroup", () => new IntelGpuGroup(GetIntelCpus(), _settings)); } if (_powerMonitorEnabled) - Add(new PowerMonitorGroup(_settings)); + AddMeasuredGroup(startupTrace, "PowerMonitorGroup", () => new PowerMonitorGroup(_settings)); if (_controllerEnabled) { - Add(new TBalancerGroup(_settings)); - Add(new HeatmasterGroup(_settings)); - Add(new AquaComputerGroup(_settings)); - Add(new AeroCoolGroup(_settings)); - Add(new NzxtGroup(_settings)); - Add(new RazerGroup(_settings)); - Add(new ArcticGroup(_settings)); - Add(new MsiGroup(_settings)); + AddMeasuredGroup(startupTrace, "TBalancerGroup", () => new TBalancerGroup(_settings)); + AddMeasuredGroup(startupTrace, "HeatmasterGroup", () => new HeatmasterGroup(_settings)); + AddMeasuredGroup(startupTrace, "AquaComputerGroup", () => new AquaComputerGroup(_settings)); + AddMeasuredGroup(startupTrace, "AeroCoolGroup", () => new AeroCoolGroup(_settings)); + AddMeasuredGroup(startupTrace, "NzxtGroup", () => new NzxtGroup(_settings)); + AddMeasuredGroup(startupTrace, "RazerGroup", () => new RazerGroup(_settings)); + AddMeasuredGroup(startupTrace, "ArcticGroup", () => new ArcticGroup(_settings)); + AddMeasuredGroup(startupTrace, "MsiGroup", () => new MsiGroup(_settings)); } if (_storageEnabled) - Add(new StorageGroup(_settings)); + AddMeasuredGroup(startupTrace, + "StorageGroup", + () => new StorageGroup(_settings), + () => _storageEnabled, + DeferStorageDetectionSetting, + DeferStorageDetectionEnvironmentVariable); if (_networkEnabled) - Add(new NetworkGroup(_settings)); + AddMeasuredGroup(startupTrace, + "NetworkGroup", + () => new NetworkGroup(_settings), + () => _networkEnabled, + DeferNetworkDetectionSetting, + DeferNetworkDetectionEnvironmentVariable); if (_psuEnabled) { - Add(new CorsairPsuGroup(_settings)); - Add(new MsiPsuGroup(_settings)); + AddMeasuredGroup(startupTrace, "CorsairPsuGroup", () => new CorsairPsuGroup(_settings)); + AddMeasuredGroup(startupTrace, "MsiPsuGroup", () => new MsiPsuGroup(_settings)); } if (_batteryEnabled) - Add(new BatteryGroup(_settings)); + AddMeasuredGroup(startupTrace, "BatteryGroup", () => new BatteryGroup(_settings)); + } + + private static void Measure(HardwareStartupTrace startupTrace, string phase, Action action) + { + if (startupTrace != null) + startupTrace.Measure(phase, action); + else + action(); + } + + private static T Measure(HardwareStartupTrace startupTrace, string phase, Func action) + { + return startupTrace != null ? startupTrace.Measure(phase, action) : action(); + } + + private void AddMeasuredGroup(HardwareStartupTrace startupTrace, string phase, Func createGroup) + { + Add(Measure(startupTrace, phase, createGroup)); + } + + private void AddMeasuredGroup(HardwareStartupTrace startupTrace, string phase, Func createGroup, Func isEnabled, string deferSettingName, string deferEnvironmentVariable) + { + if (!ShouldDeferDetection(deferSettingName, deferEnvironmentVariable)) + { + AddMeasuredGroup(startupTrace, phase, createGroup); + return; + } + + startupTrace?.Skip(phase, "Deferred to background."); + AddDeferredGroup(createGroup, isEnabled); + } + + private void AddDeferredGroup(Func createGroup, Func isEnabled) + { + CancellationTokenSource cancellationTokenSource = _deferredGroupCancellationTokenSource; + if (cancellationTokenSource == null) + { + if (isEnabled()) + Add(createGroup()); + + return; + } + + CancellationToken cancellationToken = cancellationTokenSource.Token; + Task.Run(() => + { + IGroup group = null; + try + { + group = createGroup(); + } + catch (Exception ex) + { + if (!cancellationToken.IsCancellationRequested) + System.Diagnostics.Debug.WriteLine($"Deferred hardware detection failed: {ex}"); + + return; + } + + if (group == null) + return; + + if (cancellationToken.IsCancellationRequested || !isEnabled()) + { + group.Close(); + return; + } + + Add(group); + }); + } + + private bool ShouldDeferDetection(string settingName, string environmentVariable) + { + string environmentValue = Environment.GetEnvironmentVariable(environmentVariable); + if (!string.IsNullOrWhiteSpace(environmentValue)) + return IsTruthy(environmentValue); + + return IsTruthy(_settings.GetValue(settingName, "false")); + } + + 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 StartDeferredGroupRun() + { + CancelDeferredGroupRun(); + _deferredGroupCancellationTokenSource = new CancellationTokenSource(); + } + + private void CancelDeferredGroupRun() + { + CancellationTokenSource cancellationTokenSource = _deferredGroupCancellationTokenSource; + _deferredGroupCancellationTokenSource = null; + + if (cancellationTokenSource == null) + return; + + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); } private static void NewSection(TextWriter writer) @@ -655,6 +799,8 @@ public class Computer : IComputer if (!_open) return; + CancelDeferredGroupRun(); + lock (_lock) { while (_groups.Count > 0) @@ -679,8 +825,9 @@ public class Computer : IComputer if (!_open) return; + StartDeferredGroupRun(); RemoveGroups(); - AddGroups(); + AddGroups(null); } private void RemoveGroups() diff --git a/LibreHardwareMonitorLib/Hardware/Cpu/CpuGroup.cs b/LibreHardwareMonitorLib/Hardware/Cpu/CpuGroup.cs index d35201e..64f878c 100644 --- a/LibreHardwareMonitorLib/Hardware/Cpu/CpuGroup.cs +++ b/LibreHardwareMonitorLib/Hardware/Cpu/CpuGroup.cs @@ -1,7 +1,7 @@ // 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. -// Partial Copyright (C) Michael Möller and Contributors. +// Partial Copyright (C) Michael M�ller and Contributors. // All Rights Reserved. using System; @@ -17,8 +17,15 @@ internal class CpuGroup : IGroup private readonly CpuId[][][] _threads; public CpuGroup(ISettings settings) + : this(settings, null) + { } + + internal CpuGroup(ISettings settings, HardwareStartupTrace startupTrace) { - CpuId[][] processorThreads = GetProcessorThreads(); + CpuId[][] processorThreads = Measure(startupTrace, + "CpuGroup.GetProcessorThreads", + GetProcessorThreads, + DescribeProcessorThreads); _threads = new CpuId[processorThreads.Length][][]; int index = 0; @@ -27,44 +34,18 @@ internal class CpuGroup : IGroup if (threads.Length == 0) continue; - CpuId[][] coreThreads = GroupThreadsByCore(threads); + CpuId[][] coreThreads = Measure(startupTrace, + $"CpuGroup.Processor{index}.GroupThreadsByCore", + () => GroupThreadsByCore(threads), + cores => $"{cores.Length} core(s), {threads.Length} thread(s)"); _threads[index] = coreThreads; - switch (threads[0].Vendor) - { - case Vendor.Intel: - _hardware.Add(new IntelCpu(index, coreThreads, settings)); - break; - case Vendor.AMD: - switch (threads[0].Family) - { - case 0x0F: - _hardware.Add(new Amd0FCpu(index, coreThreads, settings)); - break; - case 0x10: - case 0x11: - case 0x12: - case 0x14: - case 0x15: - case 0x16: - // TODO: https://github.com/namazso/PawnIO.Modules/issues/32 - //_hardware.Add(new Amd10Cpu(index, coreThreads, settings)); - break; - case 0x17: - case 0x19: - case 0x1A: - _hardware.Add(new Amd17Cpu(index, coreThreads, settings)); - break; - default: - _hardware.Add(new GenericCpu(index, coreThreads, settings)); - break; - } - - break; - default: - _hardware.Add(new GenericCpu(index, coreThreads, settings)); - break; - } + GenericCpu cpu = Measure(startupTrace, + $"CpuGroup.Processor{index}.{GetCpuConstructorName(threads[0])}", + () => CreateCpu(index, coreThreads, settings, startupTrace), + DescribeCpu); + if (cpu != null) + _hardware.Add(cpu); index++; } @@ -123,6 +104,88 @@ internal class CpuGroup : IGroup } } + private static GenericCpu CreateCpu(int index, CpuId[][] coreThreads, ISettings settings, HardwareStartupTrace startupTrace) + { + CpuId thread = coreThreads[0][0]; + + switch (thread.Vendor) + { + case Vendor.Intel: + return new IntelCpu(index, coreThreads, settings, startupTrace); + case Vendor.AMD: + switch (thread.Family) + { + case 0x0F: + return new Amd0FCpu(index, coreThreads, settings); + case 0x10: + case 0x11: + case 0x12: + case 0x14: + case 0x15: + case 0x16: + // TODO: https://github.com/namazso/PawnIO.Modules/issues/32 + return null; + case 0x17: + case 0x19: + case 0x1A: + return new Amd17Cpu(index, coreThreads, settings); + default: + return new GenericCpu(index, coreThreads, settings); + } + default: + return new GenericCpu(index, coreThreads, settings); + } + } + + private static string GetCpuConstructorName(CpuId thread) + { + switch (thread.Vendor) + { + case Vendor.Intel: + return nameof(IntelCpu); + case Vendor.AMD: + switch (thread.Family) + { + case 0x0F: + return nameof(Amd0FCpu); + case 0x10: + case 0x11: + case 0x12: + case 0x14: + case 0x15: + case 0x16: + return "UnsupportedAmdCpu"; + case 0x17: + case 0x19: + case 0x1A: + return nameof(Amd17Cpu); + default: + return nameof(GenericCpu); + } + default: + return nameof(GenericCpu); + } + } + + private static string DescribeProcessorThreads(CpuId[][] processorThreads) + { + int threadCount = 0; + foreach (CpuId[] threads in processorThreads) + threadCount += threads.Length; + + return $"{processorThreads.Length} processor(s), {threadCount} thread(s)"; + } + + private static string DescribeCpu(GenericCpu cpu) + { + return cpu != null ? $"{cpu.Name}, {cpu.Sensors.Length} sensor(s)" : "Unsupported CPU family"; + } + + private static T Measure(HardwareStartupTrace startupTrace, string phase, Func action, Func getDetail) + { + return startupTrace != null ? startupTrace.Measure(phase, action, getDetail) : action(); + } + private static CpuId[][] GetProcessorThreads() { List threads = new(); @@ -210,3 +273,4 @@ internal class CpuGroup : IGroup } } } + diff --git a/LibreHardwareMonitorLib/Hardware/Cpu/GenericCpu.cs b/LibreHardwareMonitorLib/Hardware/Cpu/GenericCpu.cs index 61010f1..6b8e715 100644 --- a/LibreHardwareMonitorLib/Hardware/Cpu/GenericCpu.cs +++ b/LibreHardwareMonitorLib/Hardware/Cpu/GenericCpu.cs @@ -34,7 +34,11 @@ public class GenericCpu : Hardware private long _lastTime; private ulong _lastTimeStampCount; - public GenericCpu(int processorIndex, CpuId[][] cpuId, ISettings settings) : base(cpuId[0][0].Name, CreateIdentifier(cpuId[0][0].Vendor, processorIndex), settings) + public GenericCpu(int processorIndex, CpuId[][] cpuId, ISettings settings) + : this(processorIndex, cpuId, settings, null) + { } + + internal GenericCpu(int processorIndex, CpuId[][] cpuId, ISettings settings, HardwareStartupTrace startupTrace) : base(cpuId[0][0].Name, CreateIdentifier(cpuId[0][0].Vendor, processorIndex), settings) { _cpuId = cpuId; _vendor = cpuId[0][0].Vendor; @@ -59,7 +63,7 @@ public class GenericCpu : Hardware _totalLoad = _coreCount > 1 ? new Sensor("CPU Total", 0, SensorType.Load, this, settings) : null; _maxLoad = _coreCount > 1 ? new Sensor("CPU Core Max", 1, SensorType.Load, this, settings) : null; - _cpuLoad = new CpuLoad(cpuId); + _cpuLoad = Measure(startupTrace, "GenericCpu.CpuLoad", () => new CpuLoad(cpuId), load => load.IsAvailable ? "Available" : "Unavailable"); if (_cpuLoad.IsAvailable) { _threadLoads = new Sensor[_threadCount]; @@ -93,12 +97,19 @@ public class GenericCpu : Hardware if (HasTimeStampCounter) { GroupAffinity previousAffinity = ThreadAffinity.Set(cpuId[0][0].Affinity); - EstimateTimeStampCounterFrequency(out _estimatedTimeStampCounterFrequency, out _estimatedTimeStampCounterFrequencyError); + double estimatedTimeStampCounterFrequency = 0; + double estimatedTimeStampCounterFrequencyError = 0; + Measure(startupTrace, + "GenericCpu.EstimateTimeStampCounterFrequency", + () => EstimateTimeStampCounterFrequency(out estimatedTimeStampCounterFrequency, out estimatedTimeStampCounterFrequencyError)); + _estimatedTimeStampCounterFrequency = estimatedTimeStampCounterFrequency; + _estimatedTimeStampCounterFrequencyError = estimatedTimeStampCounterFrequencyError; ThreadAffinity.Set(previousAffinity); } else { _estimatedTimeStampCounterFrequency = 0; + _estimatedTimeStampCounterFrequencyError = 0; } TimeStampCounterFrequency = _estimatedTimeStampCounterFrequency; @@ -142,6 +153,19 @@ public class GenericCpu : Hardware return new Identifier(s, processorIndex.ToString(CultureInfo.InvariantCulture)); } + private static void Measure(HardwareStartupTrace startupTrace, string phase, Action action) + { + if (startupTrace != null) + startupTrace.Measure(phase, action); + else + action(); + } + + private static T Measure(HardwareStartupTrace startupTrace, string phase, Func action, Func getDetail) + { + return startupTrace != null ? startupTrace.Measure(phase, action, getDetail) : action(); + } + private static void EstimateTimeStampCounterFrequency(out double frequency, out double error) { // preload the function diff --git a/LibreHardwareMonitorLib/Hardware/Cpu/IntelCpu.cs b/LibreHardwareMonitorLib/Hardware/Cpu/IntelCpu.cs index 972b6cf..4d4424b 100644 --- a/LibreHardwareMonitorLib/Hardware/Cpu/IntelCpu.cs +++ b/LibreHardwareMonitorLib/Hardware/Cpu/IntelCpu.cs @@ -14,6 +14,9 @@ namespace LibreHardwareMonitor.Hardware.Cpu; internal sealed class IntelCpu : GenericCpu { + private const string DeferInitialUpdateEnvironmentVariable = "LHM_CPU_DEFER_INITIAL_UPDATE"; + private const string DeferInitialUpdateSetting = "cpu.deferInitialUpdate"; + private readonly Sensor _busClock; private readonly Sensor _coreAvg; private readonly Sensor[] _coreClocks; @@ -36,12 +39,16 @@ internal sealed class IntelCpu : GenericCpu private readonly IntelMsr _pawnModule; - public IntelCpu(int processorIndex, CpuId[][] cpuId, ISettings settings) : base(processorIndex, cpuId, settings) + public IntelCpu(int processorIndex, CpuId[][] cpuId, ISettings settings) + : this(processorIndex, cpuId, settings, null) + { } + + internal IntelCpu(int processorIndex, CpuId[][] cpuId, ISettings settings, HardwareStartupTrace startupTrace) : base(processorIndex, cpuId, settings, startupTrace) { if (Software.OperatingSystem.IsUnix) return; - _pawnModule = new IntelMsr(); + _pawnModule = Measure(startupTrace, "IntelCpu.CreateMsrModule", () => new IntelMsr()); uint eax; @@ -527,7 +534,17 @@ internal sealed class IntelCpu : GenericCpu ActivateSensor(_coreVIDs[i]); } - Update(); + if (ShouldDeferInitialUpdate(settings)) + { + if (HasTimeStampCounter && _timeStampCounterMultiplier > 0) + ActivateSensor(_busClock); + + startupTrace?.Skip("IntelCpu.InitialUpdate", "Deferred until first regular update."); + } + else + { + Measure(startupTrace, "IntelCpu.InitialUpdate", Update); + } } public float EnergyUnitsMultiplier { get; } @@ -555,6 +572,36 @@ internal sealed class IntelCpu : GenericCpu return result; } + private static void Measure(HardwareStartupTrace startupTrace, string phase, Action action) + { + if (startupTrace != null) + startupTrace.Measure(phase, action); + else + action(); + } + + private static T Measure(HardwareStartupTrace startupTrace, string phase, Func action) + { + return startupTrace != null ? startupTrace.Measure(phase, action) : action(); + } + + private static bool ShouldDeferInitialUpdate(ISettings settings) + { + string environmentValue = Environment.GetEnvironmentVariable(DeferInitialUpdateEnvironmentVariable); + if (!string.IsNullOrWhiteSpace(environmentValue)) + return IsTruthy(environmentValue); + + return IsTruthy(settings.GetValue(DeferInitialUpdateSetting, "false")); + } + + 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); + } + public override string GetReport() { StringBuilder r = new(); diff --git a/LibreHardwareMonitorLib/Hardware/HardwareStartupTrace.cs b/LibreHardwareMonitorLib/Hardware/HardwareStartupTrace.cs new file mode 100644 index 0000000..c26c6ea --- /dev/null +++ b/LibreHardwareMonitorLib/Hardware/HardwareStartupTrace.cs @@ -0,0 +1,240 @@ +// 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.Text; + +namespace LibreHardwareMonitor.Hardware; + +internal sealed class HardwareStartupTrace : IDisposable +{ + private const string EnabledEnvironmentVariable = "LHM_HARDWARE_STARTUP_TIMING"; + private const string PathEnvironmentVariable = "LHM_HARDWARE_STARTUP_TIMING_PATH"; + private const string EnabledSetting = "diagnostics.hardwareStartupTiming"; + private const string PathSetting = "diagnostics.hardwareStartupTimingPath"; + + private readonly List _entries = new(); + private readonly string _fileName; + private readonly Stopwatch _totalStopwatch; + private bool _disposed; + + private HardwareStartupTrace(ISettings settings) + { + _fileName = GetLogFileName(settings); + _totalStopwatch = Stopwatch.StartNew(); + } + + public static HardwareStartupTrace Create(ISettings settings) + { + if (!IsEnabled(settings)) + return null; + + return new HardwareStartupTrace(settings); + } + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _totalStopwatch.Stop(); + WriteLog(); + } + + public T Measure(string phase, Func action) + { + return Measure(phase, action, null); + } + + public T Measure(string phase, Func action, Func getDetail) + { + Stopwatch stopwatch = Stopwatch.StartNew(); + try + { + T result = action(); + stopwatch.Stop(); + AddEntry(phase, stopwatch.Elapsed, "OK", GetHardwareCount(result), GetDetail(result, getDetail)); + return result; + } + catch (Exception ex) + { + stopwatch.Stop(); + AddEntry(phase, stopwatch.Elapsed, "ERROR", null, $"{ex.GetType().FullName}: {ex.Message}"); + throw; + } + } + + public void Measure(string phase, Action action) + { + Stopwatch stopwatch = Stopwatch.StartNew(); + try + { + action(); + stopwatch.Stop(); + AddEntry(phase, stopwatch.Elapsed, "OK", null, ""); + } + catch (Exception ex) + { + stopwatch.Stop(); + AddEntry(phase, stopwatch.Elapsed, "ERROR", null, $"{ex.GetType().FullName}: {ex.Message}"); + throw; + } + } + + public void Skip(string phase, string reason) + { + AddEntry(phase, TimeSpan.Zero, "SKIPPED", null, reason); + } + + private static bool IsEnabled(ISettings settings) + { + string settingValue = settings.GetValue(EnabledSetting, "false"); + string environmentValue = Environment.GetEnvironmentVariable(EnabledEnvironmentVariable) ?? ""; + + return IsTruthy(settingValue) || 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 static string GetLogFileName(ISettings settings) + { + string configuredPath = Environment.GetEnvironmentVariable(PathEnvironmentVariable); + if (string.IsNullOrWhiteSpace(configuredPath)) + configuredPath = settings.GetValue(PathSetting, ""); + + string fileName = $"LibreHardwareMonitor.HardwareStartupTiming-{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 int? GetHardwareCount(T result) + { + return result is IGroup group ? group.Hardware.Count : null; + } + + 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 void AddEntry(string phase, TimeSpan elapsed, string status, int? hardwareCount, string detail) + { + Entry entry = new(phase, elapsed, status, hardwareCount, detail); + _entries.Add(entry); + Debug.WriteLine($"Hardware startup: {entry}"); + } + + private void WriteLog() + { + try + { + string directory = Path.GetDirectoryName(_fileName); + if (!string.IsNullOrEmpty(directory)) + Directory.CreateDirectory(directory); + + File.WriteAllText(_fileName, BuildLog()); + } + catch + { + // Startup tracing must never affect hardware initialization. + } + } + + private string BuildLog() + { + StringBuilder builder = new(); + builder.AppendLine("Libre Hardware Monitor hardware startup timing"); + builder.Append("Timestamp: "); + builder.AppendLine(DateTimeOffset.Now.ToString("u", CultureInfo.InvariantCulture)); + builder.Append("Total elapsed: "); + builder.Append(_totalStopwatch.Elapsed.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture)); + builder.AppendLine(" ms"); + builder.AppendLine(); + builder.AppendLine("Phase,ElapsedMs,HardwareCount,Status,Detail"); + + foreach (Entry entry in _entries) + { + builder.Append(EscapeCsv(entry.Phase)); + builder.Append(','); + builder.Append(entry.Elapsed.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture)); + builder.Append(','); + builder.Append(entry.HardwareCount?.ToString(CultureInfo.InvariantCulture) ?? ""); + builder.Append(','); + builder.Append(entry.Status); + builder.Append(','); + builder.AppendLine(EscapeCsv(entry.Detail)); + } + + return builder.ToString(); + } + + 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 sealed class Entry + { + public Entry(string phase, TimeSpan elapsed, string status, int? hardwareCount, string detail) + { + Phase = phase; + Elapsed = elapsed; + Status = status; + HardwareCount = hardwareCount; + Detail = detail; + } + + public string Phase { get; } + + public TimeSpan Elapsed { get; } + + public string Status { get; } + + public int? HardwareCount { get; } + + public string Detail { get; } + + public override string ToString() + { + string hardwareCount = HardwareCount?.ToString(CultureInfo.InvariantCulture) ?? "-"; + return $"{Phase}: {Elapsed.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture)} ms, {Status}, {hardwareCount} hardware, {Detail}"; + } + } +} diff --git a/LibreHardwareMonitorLib/Hardware/Memory/MemoryGroup.cs b/LibreHardwareMonitorLib/Hardware/Memory/MemoryGroup.cs index 8f31c07..4443248 100644 --- a/LibreHardwareMonitorLib/Hardware/Memory/MemoryGroup.cs +++ b/LibreHardwareMonitorLib/Hardware/Memory/MemoryGroup.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -20,6 +21,9 @@ namespace LibreHardwareMonitor.Hardware.Memory; internal class MemoryGroup : IGroup, IHardwareChanged { + private const string DeferDimmDetectionEnvironmentVariable = "LHM_MEMORY_DEFER_DIMM_DETECTION"; + private const string DeferDimmDetectionSetting = "memory.deferDimmDetection"; + private static readonly TimeSpan _retryInterval = TimeSpan.FromSeconds(2.5); private static readonly object _lock = new(); private List _hardware = []; @@ -28,23 +32,35 @@ internal class MemoryGroup : IGroup, IHardwareChanged private bool _disposed = false; public MemoryGroup(ISettings settings) + : this(settings, null) + { } + + internal MemoryGroup(ISettings settings, HardwareStartupTrace startupTrace) { - if (DriverManager.Driver is null || !DriverManager.Driver.IsOpen) + Measure(startupTrace, "MemoryGroup.Driver.Configure", () => { - // Assign implementation of IDriver. - DriverManager.Driver = new RAMSPDToolkitDriver(); - SMBusManager.UseWMI = false; - } + if (DriverManager.Driver is null || !DriverManager.Driver.IsOpen) + { + // Assign implementation of IDriver. + DriverManager.Driver = new RAMSPDToolkitDriver(); + SMBusManager.UseWMI = false; + } + }); - _hardware.Add(new VirtualMemory(settings)); - _hardware.Add(new TotalMemory(settings)); + _hardware.Add(Measure(startupTrace, "MemoryGroup.VirtualMemory", () => new VirtualMemory(settings))); + _hardware.Add(Measure(startupTrace, "MemoryGroup.TotalMemory", () => new TotalMemory(settings))); - if (DriverManager.Driver == null || !DriverManager.LoadDriver()) + if (!Measure(startupTrace, "MemoryGroup.Driver.LoadDriver", () => DriverManager.Driver != null && DriverManager.LoadDriver(), loaded => loaded ? "Loaded" : "Unavailable")) { return; } - if (!TryAddDimms(settings)) + if (ShouldDeferDimmDetection(settings)) + { + startupTrace?.Skip("MemoryGroup.DimmDetection", "Deferred to background."); + StartDimmDetectionTask(settings, TimeSpan.Zero); + } + else if (!TryAddDimms(settings, startupTrace)) { StartRetryTask(settings); } @@ -96,6 +112,11 @@ internal class MemoryGroup : IGroup, IHardwareChanged } private bool TryAddDimms(ISettings settings) + { + return TryAddDimms(settings, null); + } + + private bool TryAddDimms(ISettings settings, HardwareStartupTrace startupTrace) { try { @@ -106,9 +127,14 @@ internal class MemoryGroup : IGroup, IHardwareChanged return false; } - if (DetectThermalSensors(out List accessors)) + List accessors = []; + bool detected = Measure(startupTrace, + "MemoryGroup.DetectThermalSensors", + () => DetectThermalSensors(out accessors, startupTrace), + result => result ? $"{accessors.Count} DIMM accessor(s)" : "No DIMM accessors"); + if (detected) { - AddDimms(accessors, settings); + Measure(startupTrace, "MemoryGroup.AddDimms", () => AddDimms(accessors, settings, startupTrace)); return true; } } @@ -123,41 +149,67 @@ internal class MemoryGroup : IGroup, IHardwareChanged } private void StartRetryTask(ISettings settings) + { + StartDimmDetectionTask(settings, _retryInterval); + } + + private void StartDimmDetectionTask(ISettings settings, TimeSpan initialDelay) { _cancellationTokenSource = new CancellationTokenSource(); + CancellationToken cancellationToken = _cancellationTokenSource.Token; Task.Run(async () => { - int retryRemaining = 5; - - while (!_cancellationTokenSource.IsCancellationRequested && --retryRemaining > 0) + try { - await Task.Delay(TimeSpan.FromSeconds(2.5), _cancellationTokenSource.Token).ConfigureAwait(false); + int retryRemaining = 5; + TimeSpan delay = initialDelay; - if (TryAddDimms(settings)) + while (!cancellationToken.IsCancellationRequested && retryRemaining-- > 0) { - break; + if (delay > TimeSpan.Zero) + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + + if (TryAddDimms(settings)) + break; + + delay = _retryInterval; } } - }, _cancellationTokenSource.Token); + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + } + }, cancellationToken); } private static bool DetectThermalSensors(out List accessors) + { + return DetectThermalSensors(out accessors, null); + } + + private static bool DetectThermalSensors(out List accessors, HardwareStartupTrace startupTrace) { accessors = []; bool ramDetected = false; - SMBusManager.DetectSMBuses(); + Measure(startupTrace, "MemoryGroup.SMBus.DetectSMBuses", SMBusManager.DetectSMBuses); //Go through detected SMBuses + int busIndex = 0; foreach (SMBusInterface smbus in SMBusManager.RegisteredSMBuses) { + string busName = $"{smbus.GetType().Name}[{busIndex}]"; + //Go through possible RAM slots for (byte i = SPDConstants.SPD_BEGIN; i <= SPDConstants.SPD_END; ++i) { //Detect type of RAM, if available - SPDDetector detector = new(smbus, i); + string address = "0x" + i.ToString("X2", CultureInfo.InvariantCulture); + SPDDetector detector = Measure(startupTrace, + $"MemoryGroup.SPDDetector.{busName}.{address}", + () => new SPDDetector(smbus, i), + result => result.Accessor != null ? $"Detected {result.Accessor.GetType().Name} index {result.Accessor.Index}" : "No RAM"); //RAM available and detected if (detector.Accessor != null) @@ -168,12 +220,31 @@ internal class MemoryGroup : IGroup, IHardwareChanged ramDetected = true; } } + + busIndex++; } return ramDetected; } - private void AddDimms(List accessors, ISettings settings) + private static bool ShouldDeferDimmDetection(ISettings settings) + { + string environmentValue = Environment.GetEnvironmentVariable(DeferDimmDetectionEnvironmentVariable); + if (!string.IsNullOrWhiteSpace(environmentValue)) + return IsTruthy(environmentValue); + + return IsTruthy(settings.GetValue(DeferDimmDetectionSetting, "false")); + } + + 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 AddDimms(List accessors, ISettings settings, HardwareStartupTrace startupTrace) { List additions = []; @@ -181,12 +252,16 @@ internal class MemoryGroup : IGroup, IHardwareChanged { //Default value string name = $"DIMM #{ram.Index}"; + string phasePrefix = $"MemoryGroup.DIMM{ram.Index}"; //Check if we can switch to the correct page - if (ram.ChangePage(PageData.ModulePartNumber)) - name = $"{ram.GetModuleManufacturerString()} - {ram.ModulePartNumber()} (#{ram.Index})"; + if (Measure(startupTrace, $"{phasePrefix}.ChangePage", () => ram.ChangePage(PageData.ModulePartNumber), changed => changed ? "Module part number page selected" : "Module part number page unavailable")) + name = Measure(startupTrace, $"{phasePrefix}.ReadName", () => $"{ram.GetModuleManufacturerString()} - {ram.ModulePartNumber()} (#{ram.Index})", result => result); - DimmMemory memory = new(ram, name, new Identifier("memory", "dimm", $"{ram.Index}"), settings); + DimmMemory memory = Measure(startupTrace, + $"{phasePrefix}.CreateHardware", + () => new DimmMemory(ram, name, new Identifier("memory", "dimm", $"{ram.Index}"), settings), + memory => $"{memory.Sensors.Length} sensor(s)"); additions.Add(memory); } @@ -194,4 +269,22 @@ internal class MemoryGroup : IGroup, IHardwareChanged foreach (Hardware hardware in additions) HardwareAdded?.Invoke(hardware); } + + private static void Measure(HardwareStartupTrace startupTrace, string phase, Action action) + { + if (startupTrace != null) + startupTrace.Measure(phase, action); + else + action(); + } + + private static T Measure(HardwareStartupTrace startupTrace, string phase, Func action) + { + return startupTrace != null ? startupTrace.Measure(phase, action) : action(); + } + + private static T Measure(HardwareStartupTrace startupTrace, string phase, Func action, Func getDetail) + { + return startupTrace != null ? startupTrace.Measure(phase, action, getDetail) : action(); + } }