diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SensorTreeItemViewModelTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SensorTreeItemViewModelTests.cs index c4cc012..da6b65d 100644 --- a/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SensorTreeItemViewModelTests.cs +++ b/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SensorTreeItemViewModelTests.cs @@ -141,8 +141,9 @@ public class SensorTreeItemViewModelTests sensorVm.IsVisible = false; Assert.Equal(Visibility.Collapsed, sensorVm.RowVisibility); - - // typeGroup doesn't update automatically in ViewModel when child changes, so skip assert + + // Hiding the group's only visible sensor now collapses the parent group too (no empty group header left behind). + Assert.Equal(Visibility.Collapsed, typeGroup.RowVisibility); vm.SetShowHiddenSensors(true); diff --git a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs index af543c8..64f4240 100644 --- a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs +++ b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs @@ -70,6 +70,8 @@ public sealed class MainWindow : Window private bool _isUpdating; private bool _initialWindowStateApplied; private bool _isClosingForExit; + private bool _isShuttingDown; + private bool _runtimeErrorLogged; private bool _isMainWindowHidden; private bool _isMonitoringStarted; private bool _startupCompletionRequested; @@ -635,13 +637,18 @@ public sealed class MainWindow : Window private async void UpdateTimer_Tick(DispatcherQueueTimer sender, object args) { - if (_isUpdating) + if (_isUpdating || _isShuttingDown) return; _isUpdating = true; try { await ViewModel.UpdateAsync(); + + // The window may have been closed (and the view model/computer disposed) while we awaited the update. + if (_isShuttingDown) + return; + UpdateSensorColumnWidths(); _trayIconService.Update(); _gadgetWindow?.UpdateSensors(ViewModel.GetGadgetSensorItems()); @@ -649,7 +656,7 @@ public sealed class MainWindow : Window } catch (Exception ex) { - _timer.Stop(); + // A transient update failure must not permanently freeze the UI: keep the timer running and just record it. RecordRuntimeError("Sensor update failed", ex); } finally @@ -662,8 +669,24 @@ public sealed class MainWindow : Window { _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()); + + // Use a dedicated runtime log (not the startup log that App.xaml.cs appends to) and write it only once, so a + // recurring per-tick failure neither clobbers the startup diagnostics nor grows the file without bound. + string logPath = IOPath.Combine(AppContext.BaseDirectory, "LibreHardwareMonitor.Windows.WinUI.runtime.log"); + if (!_runtimeErrorLogged) + { + try + { + File.WriteAllText(logPath, exception.ToString()); + } + catch + { + // Never let diagnostic logging throw out of the update loop. + } + + _runtimeErrorLogged = true; + } + ViewModel.SetStatusText($"{message}. See {IOPath.GetFileName(logPath)}."); } @@ -682,6 +705,7 @@ public sealed class MainWindow : Window return; } + _isShuttingDown = true; _timer.Stop(); _deviceColumnWidthSettleTimer.Stop(); _trayIconService.Dispose(); diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/AppSettings.cs b/LibreHardwareMonitor.Windows.WinUI/Services/AppSettings.cs index dd00102..ef5b0f1 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/AppSettings.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/AppSettings.cs @@ -15,7 +15,10 @@ namespace LibreHardwareMonitor.Windows.WinUI.Services; public sealed class AppSettings : ISettings { - private readonly IDictionary _settings = new Dictionary(); + // ISettings is read and written concurrently from background hardware-discovery threads (deferred group/sensor + // construction) as well as the UI thread, so every access to the backing dictionary is guarded by _lock. + private readonly Dictionary _settings = new(); + private readonly object _lock = new(); private AppSettings(string fileName) { @@ -35,91 +38,107 @@ public sealed class AppSettings : ISettings public bool Contains(string name) { - return _settings.ContainsKey(name); + lock (_lock) + return _settings.ContainsKey(name); } public string GetValue(string name, string value) { - return _settings.TryGetValue(name, out string? result) ? result : value; + lock (_lock) + return _settings.TryGetValue(name, out string? result) ? result : value; } public int GetValue(string name, int value) { - return _settings.TryGetValue(name, out string? result) && int.TryParse(result, out int parsedValue) - ? parsedValue - : value; + lock (_lock) + return _settings.TryGetValue(name, out string? result) && int.TryParse(result, out int parsedValue) + ? parsedValue + : value; } public float GetValue(string name, float value) { - return _settings.TryGetValue(name, out string? result) - && float.TryParse(result, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedValue) - ? parsedValue - : value; + lock (_lock) + return _settings.TryGetValue(name, out string? result) + && float.TryParse(result, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedValue) + ? parsedValue + : value; } public double GetValue(string name, double value) { - return _settings.TryGetValue(name, out string? result) - && double.TryParse(result, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedValue) - ? parsedValue - : value; + lock (_lock) + return _settings.TryGetValue(name, out string? result) + && double.TryParse(result, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedValue) + ? parsedValue + : value; } public bool GetValue(string name, bool value) { - return _settings.TryGetValue(name, out string? result) ? result == "true" : value; + lock (_lock) + return _settings.TryGetValue(name, out string? result) ? result == "true" : value; } public Color GetValue(string name, Color value) { - if (_settings.TryGetValue(name, out string? result) - && uint.TryParse(result, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint parsedValue)) + lock (_lock) { - return Color.FromArgb( - (byte)((parsedValue >> 24) & 0xff), - (byte)((parsedValue >> 16) & 0xff), - (byte)((parsedValue >> 8) & 0xff), - (byte)(parsedValue & 0xff)); - } + if (_settings.TryGetValue(name, out string? result) + && uint.TryParse(result, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint parsedValue)) + { + return Color.FromArgb( + (byte)((parsedValue >> 24) & 0xff), + (byte)((parsedValue >> 16) & 0xff), + (byte)((parsedValue >> 8) & 0xff), + (byte)(parsedValue & 0xff)); + } - return value; + return value; + } } public void SetValue(string name, string value) { - _settings[name] = value; + lock (_lock) + _settings[name] = value; } public void SetValue(string name, int value) { - _settings[name] = value.ToString(CultureInfo.InvariantCulture); + lock (_lock) + _settings[name] = value.ToString(CultureInfo.InvariantCulture); } public void SetValue(string name, float value) { - _settings[name] = value.ToString(CultureInfo.InvariantCulture); + lock (_lock) + _settings[name] = value.ToString(CultureInfo.InvariantCulture); } public void SetValue(string name, double value) { - _settings[name] = value.ToString(CultureInfo.InvariantCulture); + lock (_lock) + _settings[name] = value.ToString(CultureInfo.InvariantCulture); } public void SetValue(string name, bool value) { - _settings[name] = value ? "true" : "false"; + lock (_lock) + _settings[name] = value ? "true" : "false"; } public void SetValue(string name, Color value) { uint argb = (uint)((value.A << 24) | (value.R << 16) | (value.G << 8) | value.B); - _settings[name] = argb.ToString("X8", CultureInfo.InvariantCulture); + lock (_lock) + _settings[name] = argb.ToString("X8", CultureInfo.InvariantCulture); } public void Remove(string name) { - _settings.Remove(name); + lock (_lock) + _settings.Remove(name); } public void Load() @@ -149,7 +168,10 @@ public sealed class AppSettings : ISettings XmlAttribute? keyAttribute = child.Attributes?["key"]; XmlAttribute? valueAttribute = child.Attributes?["value"]; if (keyAttribute?.Value != null && valueAttribute?.Value != null) - _settings[keyAttribute.Value] = valueAttribute.Value; + { + lock (_lock) + _settings[keyAttribute.Value] = valueAttribute.Value; + } } } } @@ -165,7 +187,11 @@ public sealed class AppSettings : ISettings XmlElement appSettings = doc.CreateElement("appSettings"); configuration.AppendChild(appSettings); - foreach (KeyValuePair setting in _settings) + List> snapshot; + lock (_lock) + snapshot = new List>(_settings); + + foreach (KeyValuePair setting in snapshot) { XmlElement add = doc.CreateElement("add"); add.SetAttribute("key", setting.Key); diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs index 59f36bb..6e51f67 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs @@ -29,6 +29,7 @@ public sealed class HardwareMonitorService : IDisposable private readonly UpdateVisitor _updateVisitor = new(); private bool _isOpen; private int _treeRebuildQueued; + private volatile bool _treeRebuildDirty; public HardwareMonitorService(AppSettings settings) { @@ -44,6 +45,12 @@ public sealed class HardwareMonitorService : IDisposable public Computer Computer { get; } + /// + /// Lock guarding access to live sensor collections (values and active-sensor sets). Held by + /// while sensors are updated; other readers (tree rebuild, the remote web server) take it to read consistently. + /// + public object SensorReadLock => _updateLock; + public AppSettings Settings { get; } public SensorTreeItemViewModel Root { get; private set; } = SensorTreeItemViewModel.CreateRoot(Environment.MachineName); @@ -177,8 +184,14 @@ public sealed class HardwareMonitorService : IDisposable public void RebuildTree(bool raiseTreeRebuilt = true) { SensorTreeItemViewModel root = SensorTreeItemViewModel.CreateRoot(Environment.MachineName); - foreach (IHardware hardware in Computer.Hardware.OrderBy(hardware => hardware.HardwareType).ThenBy(hardware => hardware.Name, StringComparer.OrdinalIgnoreCase)) - root.Children.Add(SensorTreeItemViewModel.FromHardware(hardware, Settings)); + + // Building the tree reads each hardware's live sensor set (a HashSet). Hold the update lock so it cannot run + // concurrently with UpdateAsync()'s Computer.Accept, which mutates that set via ActivateSensor on another thread. + lock (_updateLock) + { + foreach (IHardware hardware in Computer.Hardware.OrderBy(hardware => hardware.HardwareType).ThenBy(hardware => hardware.Name, StringComparer.OrdinalIgnoreCase)) + root.Children.Add(SensorTreeItemViewModel.FromHardware(hardware, Settings)); + } Root = root; if (raiseTreeRebuilt) @@ -187,6 +200,9 @@ public sealed class HardwareMonitorService : IDisposable public void Dispose() { + // Clear before closing so any tree-rebuild task still pending its delay bails instead of rebuilding from a + // half-closed Computer. + _isOpen = false; Computer.HardwareAdded -= HardwareChanged; Computer.HardwareRemoved -= HardwareChanged; Computer.Close(); @@ -247,12 +263,18 @@ public sealed class HardwareMonitorService : IDisposable private void HardwareChanged(IHardware hardware) { + // Storage discovery is deferred, so drives usually appear after ForceDriveWakeup was applied at startup (against + // an empty hardware list). Apply the current setting to each newly discovered drive so it actually takes effect. + if (hardware is StorageDevice storageDevice) + storageDevice.ForceWakeup = Settings.GetValue("forceDriveWakeupItem", false); + if (_isOpen) QueueTreeRebuild(); } private void QueueTreeRebuild() { + _treeRebuildDirty = true; if (Interlocked.Exchange(ref _treeRebuildQueued, 1) == 1) return; @@ -262,8 +284,14 @@ public sealed class HardwareMonitorService : IDisposable { await Task.Delay(100).ConfigureAwait(false); - if (_isOpen) - RebuildTree(); + // Rebuild until no change has arrived since the last rebuild began, so a HardwareAdded/Removed that lands + // while a rebuild is in flight is not coalesced away and lost. + while (_treeRebuildDirty) + { + _treeRebuildDirty = false; + if (_isOpen) + RebuildTree(); + } } catch (Exception ex) { @@ -272,6 +300,10 @@ public sealed class HardwareMonitorService : IDisposable finally { Interlocked.Exchange(ref _treeRebuildQueued, 0); + + // A change may have slipped in between the loop's exit and clearing the flag; re-queue so it is honored. + if (_treeRebuildDirty) + QueueTreeRebuild(); } }); } diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/RemoteWebServer.cs b/LibreHardwareMonitor.Windows.WinUI/Services/RemoteWebServer.cs index 0dce8af..952243f 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/RemoteWebServer.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/RemoteWebServer.cs @@ -25,6 +25,7 @@ namespace LibreHardwareMonitor.Windows.WinUI.Services; public sealed class RemoteWebServer : IDisposable { private readonly IComputer _computer; + private readonly object _sensorReadLock; private readonly Func _rootProvider; private readonly Version _version = typeof(RemoteWebServer).Assembly.GetName().Version ?? new Version(0, 0); private CancellationTokenSource? _cts; @@ -34,6 +35,7 @@ public sealed class RemoteWebServer : IDisposable public RemoteWebServer( Func rootProvider, IComputer computer, + object sensorReadLock, string listenerIp, int listenerPort, bool authEnabled, @@ -42,6 +44,7 @@ public sealed class RemoteWebServer : IDisposable { _rootProvider = rootProvider; _computer = computer; + _sensorReadLock = sensorReadLock; ListenerIp = listenerIp; ListenerPort = listenerPort; AuthEnabled = authEnabled; @@ -196,7 +199,10 @@ public sealed class RemoteWebServer : IDisposable return; } - string requestedFile = request.RawUrl?.TrimStart('/') ?? ""; + // Match on the path component only (AbsolutePath excludes the query string) and compare endpoints exactly. + // Prefix matching previously let any static asset whose name starts with an endpoint name (e.g. + // "metrics.html", "sensor-icons.css") be hijacked by the API handlers. + string requestedFile = (request.Url?.AbsolutePath ?? request.RawUrl ?? "").TrimStart('/'); if (requestedFile.Length == 0) requestedFile = "index.html"; @@ -206,13 +212,13 @@ public sealed class RemoteWebServer : IDisposable return; } - if (requestedFile.StartsWith("metrics", StringComparison.OrdinalIgnoreCase)) + if (requestedFile.Equals("metrics", StringComparison.OrdinalIgnoreCase)) { await SendPrometheusAsync(context.Response, request); return; } - if (requestedFile.StartsWith("Sensor", StringComparison.OrdinalIgnoreCase)) + if (requestedFile.Equals("Sensor", StringComparison.OrdinalIgnoreCase)) { Dictionary result = []; HandleSensorRequest(request, result); @@ -220,7 +226,7 @@ public sealed class RemoteWebServer : IDisposable return; } - if (requestedFile.StartsWith("ResetAllMinMax", StringComparison.OrdinalIgnoreCase)) + if (requestedFile.Equals("ResetAllMinMax", StringComparison.OrdinalIgnoreCase)) { _computer.Accept(new SensorVisitor(sensor => { @@ -427,7 +433,12 @@ public sealed class RemoteWebServer : IDisposable private async Task SendPrometheusAsync(HttpListenerResponse response, HttpListenerRequest request) { Dictionary settings = ParsePrometheusSettings(request); - string content = GeneratePrometheusResponse(_rootProvider(), settings); + + // Serialize against the sensor update loop: GeneratePrometheusResponse enumerates each sensor's live Values ring + // buffer, which the update thread mutates concurrently (an unsynchronized enumeration would throw). + string content; + lock (_sensorReadLock) + content = GeneratePrometheusResponse(_rootProvider(), settings); response.AddHeader("Cache-Control", "no-cache"); response.AddHeader("Access-Control-Allow-Origin", "*"); diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/TrayIconService.cs b/LibreHardwareMonitor.Windows.WinUI/Services/TrayIconService.cs index 4e2fde6..5400964 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/TrayIconService.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/TrayIconService.cs @@ -202,7 +202,14 @@ public sealed class TrayIconService : IDisposable private IntPtr WindowSubclassProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam, nuint subclassId, nuint refData) { if (message == CallbackMessage) - HandleTrayCallback((uint)wParam.ToInt64(), (uint)lParam.ToInt64()); + { + // With NOTIFYICON_VERSION_4 the event is packed into lParam: LOWORD = the mouse/keyboard message, + // HIWORD = the icon id. (wParam carries the cursor coordinates, which we don't use.) + uint packed = unchecked((uint)lParam.ToInt64()); + uint mouseMessage = packed & 0xFFFF; + uint iconId = (packed >> 16) & 0xFFFF; + HandleTrayCallback(iconId, mouseMessage); + } return DefSubclassProc(hWnd, message, wParam, lParam); } @@ -360,7 +367,7 @@ public sealed class TrayIconService : IDisposable BitmapInfo bitmapInfo = BitmapInfo.Create(IconSize, IconSize); bitmap = CreateDIBSection(hdc, ref bitmapInfo, 0, out _, IntPtr.Zero, 0); if (bitmap == IntPtr.Zero) - return _mainIconHandle; + return IntPtr.Zero; // never return the shared main icon: callers DestroyIcon() the returned handle oldBitmap = SelectObject(memoryDc, bitmap); WinUIColor color = GetSensorTrayColor(sensor); diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs index 4c38833..1115f70 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs @@ -116,6 +116,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable _remoteWebServer = new RemoteWebServer( () => RootItems.FirstOrDefault(), _hardwareMonitor.Computer, + _hardwareMonitor.SensorReadLock, settings.GetValue("listenerIp", "?"), settings.GetValue("listenerPort", 8085), settings.GetValue("authenticationEnabled", false), @@ -958,9 +959,11 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable _plotSeriesByIdentifier[identifier] = series; PlotSeries.Add(series); } - else + else if (sensorItem.PenColor.HasValue) { - series.Color = GetPlotColor(sensorItem); + // Only honor an explicit user pen color for an existing series; keep the auto-assigned color stable. + // (Recomputing it from the live series count made existing lines shift/collide colors every tick.) + series.Color = sensorItem.PenColor.Value; } series.Points.Add(new PlotPointViewModel(now, value.Value)); diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs index 6752634..149b410 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs @@ -18,6 +18,7 @@ namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; public sealed class SensorTreeItemViewModel : ViewModelBase { private readonly AppSettings? _settings; + private SensorTreeItemViewModel? _parent; private string? _expandedSettingName; private bool _isExpanded = true; private bool _isVisible = true; @@ -68,6 +69,10 @@ public sealed class SensorTreeItemViewModel : ViewModelBase _settings?.SetValue(new Identifier(Sensor.Identifier, "hidden").ToString(), !value); UpdateVisibilityState(); + + // A SensorType group's visibility depends on whether any child is visible, so recompute the parent too; + // otherwise hiding the last visible sensor leaves an empty group header (and the reverse on show). + _parent?.UpdateVisibilityState(); } } @@ -320,7 +325,11 @@ public sealed class SensorTreeItemViewModel : ViewModelBase }; foreach (ISensor sensor in sensors) - item.Children.Add(FromSensor(sensor, settings)); + { + SensorTreeItemViewModel child = FromSensor(sensor, settings); + child._parent = item; + item.Children.Add(child); + } item.UpdateVisibilityState(); return item; diff --git a/LibreHardwareMonitorLib/Hardware/Computer.cs b/LibreHardwareMonitorLib/Hardware/Computer.cs index 6d0425b..8da07ce 100644 --- a/LibreHardwareMonitorLib/Hardware/Computer.cs +++ b/LibreHardwareMonitorLib/Hardware/Computer.cs @@ -465,30 +465,54 @@ public class Computer : IComputer private void Add(IGroup group) { - if (group == null) - return; + AddCore(group, CancellationToken.None, null); + } + /// + /// Inserts while holding . The deferred discovery paths pass the run's + /// and so the final eligibility check and the + /// membership mutation are atomic with respect to 's drain: a background task can therefore never + /// add (and leak) a group after the computer has already been torn down. Returns whether the group was added. + /// + private bool AddCore(IGroup group, CancellationToken cancellationToken, Func isEnabled) + { + if (group == null) + return false; + + bool added = false; lock (_lock) { - if (_groups.Contains(group)) - return; - - _groups.Add(group); - - if (group is IHardwareChanged hardwareChanged) + if (!cancellationToken.IsCancellationRequested && (isEnabled == null || isEnabled()) && !_groups.Contains(group)) { - hardwareChanged.HardwareAdded += HardwareAddedEvent; - hardwareChanged.HardwareRemoved += HardwareRemovedEvent; + _groups.Add(group); + + if (group is IHardwareChanged hardwareChanged) + { + hardwareChanged.HardwareAdded += HardwareAddedEvent; + hardwareChanged.HardwareRemoved += HardwareRemovedEvent; + } + + TrackGroupHardwareDiscoveryTask(group); + added = true; } - - TrackGroupHardwareDiscoveryTask(group); } - if (HardwareAdded != null) + if (added) { - foreach (IHardware hardware in group.Hardware) - HardwareAdded(hardware); + if (HardwareAdded != null) + { + foreach (IHardware hardware in group.Hardware) + HardwareAdded(hardware); + } } + else if (isEnabled != null) + { + // Only the deferred path owns the group's lifetime here; if we declined to add it (run cancelled or the + // category was disabled meanwhile), close it so its USB/HID/serial handles are not leaked. + group.Close(); + } + + return added; } private void Remove(IGroup group) @@ -777,13 +801,7 @@ public class Computer : IComputer if (group == null) return; - if (cancellationToken.IsCancellationRequested || !isEnabled()) - { - group.Close(); - return; - } - - Add(group); + AddCore(group, cancellationToken, isEnabled); }); TrackDeferredGroupTask(task); } @@ -830,13 +848,9 @@ public class Computer : IComputer if (group == null) continue; - if (cancellationToken.IsCancellationRequested || !isEnabled()) - { - group.Close(); + // Stop the sequential block if the run was cancelled or the category was disabled while we were probing. + if (!AddCore(group, cancellationToken, isEnabled)) return; - } - - Add(group); } }); TrackDeferredGroupTask(task); diff --git a/LibreHardwareMonitorLib/Hardware/Cpu/IntelCpu.cs b/LibreHardwareMonitorLib/Hardware/Cpu/IntelCpu.cs index 4d4424b..254c753 100644 --- a/LibreHardwareMonitorLib/Hardware/Cpu/IntelCpu.cs +++ b/LibreHardwareMonitorLib/Hardware/Cpu/IntelCpu.cs @@ -678,7 +678,12 @@ internal sealed class IntelCpu : GenericCpu } } - if (HasTimeStampCounter && _timeStampCounterMultiplier > 0) + double timeStampCounterFrequency = TimeStampCounterFrequency; + + // When TSC estimation is deferred to the background, TimeStampCounterFrequency is 0 until the estimate lands (or + // the base class self-corrects it from real TSC deltas after a couple of updates). Skip the bus/core clock math + // until it is known so the clock sensors keep their previous value instead of momentarily reporting 0 MHz. + if (HasTimeStampCounter && _timeStampCounterMultiplier > 0 && timeStampCounterFrequency > 0) { double newBusClock = 0; for (int i = 0; i < _coreClocks.Length; i++) @@ -686,7 +691,7 @@ internal sealed class IntelCpu : GenericCpu System.Threading.Thread.Sleep(1); if (_pawnModule.ReadMsr(IA32_PERF_STATUS, out eax, out _, _cpuId[i][0].Affinity)) { - newBusClock = TimeStampCounterFrequency / _timeStampCounterMultiplier; + newBusClock = timeStampCounterFrequency / _timeStampCounterMultiplier; switch (_microArchitecture) { case MicroArchitecture.Nehalem: @@ -727,7 +732,7 @@ internal sealed class IntelCpu : GenericCpu else { // if IA32_PERF_STATUS is not available, assume TSC frequency - _coreClocks[i].Value = (float)TimeStampCounterFrequency; + _coreClocks[i].Value = (float)timeStampCounterFrequency; } }