From 193de6059e16aa033d5f31af3956d04b5f04e10b Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Mon, 8 Jun 2026 10:37:00 -0500 Subject: [PATCH] wip --- .../Services/HardwareMonitorServiceTests.cs | 35 ++ .../SensorTreeItemViewModelTests.cs | 74 +++++ .../MainWindow.cs | 20 +- .../Services/HardwareMonitorService.cs | 28 +- .../Services/SensorColumnMeasurer.cs | 305 +++++++++++++++--- .../Services/Tracing/FileStartupTracer.cs | 16 +- .../Services/Tracing/StartupTracer.cs | 25 +- .../ViewModels/SensorDisplayColumn.cs | 18 ++ .../ViewModels/SensorTreeItemViewModel.cs | 19 ++ .../Hardware/SettingsParsingTests.cs | 88 +++++ .../Hardware/StartupTraceLogSupportTests.cs | 55 ++++ LibreHardwareMonitorLib/Hardware/Computer.cs | 28 +- .../Hardware/Cpu/GenericCpu.cs | 3 +- .../Hardware/Cpu/IntelCpu.cs | 3 +- .../Hardware/HardwareSettingsKeys.cs | 38 +++ .../Hardware/HardwareStartupTrace.cs | 28 +- .../Hardware/Memory/MemoryGroup.cs | 3 +- .../Hardware/SettingsParsing.cs | 2 +- .../Hardware/StartupTraceLogSupport.cs | 47 +++ 19 files changed, 678 insertions(+), 157 deletions(-) create mode 100644 LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorDisplayColumn.cs create mode 100644 LibreHardwareMonitorLib.Tests/Hardware/SettingsParsingTests.cs create mode 100644 LibreHardwareMonitorLib.Tests/Hardware/StartupTraceLogSupportTests.cs create mode 100644 LibreHardwareMonitorLib/Hardware/HardwareSettingsKeys.cs create mode 100644 LibreHardwareMonitorLib/Hardware/StartupTraceLogSupport.cs diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/Services/HardwareMonitorServiceTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/Services/HardwareMonitorServiceTests.cs index 8a94c72..5c0d6c8 100644 --- a/LibreHardwareMonitor.Windows.WinUI.Tests/Services/HardwareMonitorServiceTests.cs +++ b/LibreHardwareMonitor.Windows.WinUI.Tests/Services/HardwareMonitorServiceTests.cs @@ -3,6 +3,9 @@ // Copyright (C) LibreHardwareMonitor and Contributors. using System; +using System.IO; +using System.Reflection; +using LibreHardwareMonitor.Hardware; using LibreHardwareMonitor.Windows.WinUI.Services; using Xunit; @@ -14,6 +17,31 @@ namespace LibreHardwareMonitor.Windows.WinUI.Tests.Services; // once it is extracted into its own testable unit during Phase 4. public class HardwareMonitorServiceTests { + [Fact] + public void Constructor_AppliesDeferredDetectionDefaultsUsingSharedHardwareKeys() + { + AppSettings settings = CreateBlankSettings(); + using var service = new HardwareMonitorService(settings); + string[] deferredDefaults = + { + HardwareSettingsKeys.MemoryDeferDimmDetection, + HardwareSettingsKeys.CpuDeferInitialUpdate, + HardwareSettingsKeys.CpuDeferTscEstimation, + HardwareSettingsKeys.NvidiaDeferDetection, + HardwareSettingsKeys.StorageDeferDetection, + HardwareSettingsKeys.NetworkDeferDetection, + HardwareSettingsKeys.IntelGpuDeferDetection, + HardwareSettingsKeys.ControllerDeferDetection, + HardwareSettingsKeys.PsuDeferDetection, + }; + + foreach (string key in deferredDefaults) + { + Assert.True(settings.Contains(key), $"Missing default for {key}."); + Assert.True(settings.GetValue(key, false), $"Expected {key} to default to true."); + } + } + [Fact] public void EnableFlags_WriteExpectedSettingKeysAndComputerFlags() { @@ -85,4 +113,11 @@ public class HardwareMonitorServiceTests Assert.False(raised); } + + private static AppSettings CreateBlankSettings() + { + string fileName = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.config"); + ConstructorInfo? ctor = typeof(AppSettings).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(string) }, null); + return (AppSettings)ctor!.Invoke(new object[] { fileName }); + } } diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SensorTreeItemViewModelTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SensorTreeItemViewModelTests.cs index da6b65d..87ba2fb 100644 --- a/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SensorTreeItemViewModelTests.cs +++ b/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SensorTreeItemViewModelTests.cs @@ -183,4 +183,78 @@ public class SensorTreeItemViewModelTests Assert.Contains("60.0", sensorVm.Value); } + + [Fact] + public void RefreshValues_RaisesDisplayColumnChangeOnlyForChangedValue() + { + float? value = 50f; + SensorTreeItemViewModel sensorVm = CreateTemperatureSensorViewModel(() => value, () => 10f, () => 100f); + sensorVm.RefreshValues(); + SensorDisplayColumn changedColumns = SensorDisplayColumn.None; + sensorVm.DisplayColumnsChanged += (_, columns) => changedColumns |= columns; + + value = 60f; + sensorVm.RefreshValues(); + + Assert.Equal(SensorDisplayColumn.Value, changedColumns); + } + + [Fact] + public void RefreshValues_DoesNotRaiseDisplayColumnChangeWhenFormattedValuesAreUnchanged() + { + SensorTreeItemViewModel sensorVm = CreateTemperatureSensorViewModel(() => 50f, () => 10f, () => 100f); + sensorVm.RefreshValues(); + SensorDisplayColumn changedColumns = SensorDisplayColumn.None; + sensorVm.DisplayColumnsChanged += (_, columns) => changedColumns |= columns; + + sensorVm.RefreshValues(); + + Assert.Equal(SensorDisplayColumn.None, changedColumns); + } + + [Fact] + public void Text_RaisesSensorDisplayColumnChange() + { + SensorTreeItemViewModel sensorVm = CreateTemperatureSensorViewModel(() => 50f, () => 10f, () => 100f); + SensorDisplayColumn changedColumns = SensorDisplayColumn.None; + sensorVm.DisplayColumnsChanged += (_, columns) => changedColumns |= columns; + + sensorVm.Text = "Renamed Core"; + + Assert.Equal(SensorDisplayColumn.Sensor, changedColumns); + } + + [Fact] + public void SetTemperatureUnit_RaisesValueMinAndMaxDisplayColumnChanges() + { + SensorTreeItemViewModel sensorVm = CreateTemperatureSensorViewModel(() => 50f, () => 10f, () => 100f); + sensorVm.RefreshValues(); + SensorDisplayColumn changedColumns = SensorDisplayColumn.None; + sensorVm.DisplayColumnsChanged += (_, columns) => changedColumns |= columns; + + sensorVm.SetTemperatureUnit(TemperatureUnit.Fahrenheit); + + Assert.Equal(SensorDisplayColumn.Value | SensorDisplayColumn.Min | SensorDisplayColumn.Max, changedColumns); + } + + private SensorTreeItemViewModel CreateTemperatureSensorViewModel(Func value, Func min, Func max) + { + var mockSensor = new Mock(); + mockSensor.SetupProperty(s => s.Name, "Core 0"); + mockSensor.Setup(s => s.Identifier).Returns(new Identifier("cpu", "0", "temperature", "0")); + mockSensor.Setup(s => s.SensorType).Returns(SensorType.Temperature); + mockSensor.Setup(s => s.Index).Returns(0); + mockSensor.Setup(s => s.Value).Returns(value); + mockSensor.Setup(s => s.Min).Returns(min); + mockSensor.Setup(s => s.Max).Returns(max); + + var mockHardware = new Mock(); + mockHardware.Setup(s => s.Name).Returns("CPU"); + mockHardware.Setup(h => h.HardwareType).Returns(HardwareType.Cpu); + mockHardware.Setup(h => h.Identifier).Returns(new Identifier("cpu", "0")); + mockHardware.Setup(h => h.Sensors).Returns(new[] { mockSensor.Object }); + mockHardware.Setup(h => h.SubHardware).Returns(Array.Empty()); + + return SensorTreeItemViewModel.FromHardware(mockHardware.Object, CreateMockSettings()).Children.First().Children.First(); + } } diff --git a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs index a6981f8..bc326d2 100644 --- a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs +++ b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs @@ -132,6 +132,8 @@ public sealed partial class MainWindow : Window _trayIconService.Update(); SyncGadgetSensors(); ViewModel.RefreshPlotSeries(); + _columnMeasurer.InvalidateAll(); + QueueSensorColumnWidthUpdate(); } if (args.PropertyName == nameof(ViewModel.ShowHiddenSensors)) @@ -520,7 +522,7 @@ public sealed partial class MainWindow : Window SensorTree.RootNodes.Clear(); foreach (SensorTreeItemViewModel item in ViewModel.RootItems) { - TreeViewNode? node = CreateTreeNode(item); + TreeViewNode? node = CreateTreeNode(item, 0); if (node != null) SensorTree.RootNodes.Add(node); } @@ -544,20 +546,20 @@ public sealed partial class MainWindow : Window CompleteStartupTraceIfReady(); } - private TreeViewNode? CreateTreeNode(SensorTreeItemViewModel item) + private TreeViewNode? CreateTreeNode(SensorTreeItemViewModel item, int depth) { if (item.RowVisibility == Visibility.Collapsed) return null; TreeViewNode node = new() { - Content = CreateSensorRow(item), + Content = CreateSensorRow(item, depth), IsExpanded = item.IsExpanded }; foreach (SensorTreeItemViewModel child in item.Children) { - TreeViewNode? childNode = CreateTreeNode(child); + TreeViewNode? childNode = CreateTreeNode(child, depth + 1); if (childNode != null) node.Children.Add(childNode); } @@ -565,9 +567,9 @@ public sealed partial class MainWindow : Window return node; } - private FrameworkElement CreateSensorRow(SensorTreeItemViewModel item) + private FrameworkElement CreateSensorRow(SensorTreeItemViewModel item, int depth) { - Grid row = _columnMeasurer.CreateRowGrid(); + Grid row = _columnMeasurer.CreateRowGrid(item, depth); row.DataContext = item; row.Padding = new Thickness(0, 3, 0, 3); @@ -610,7 +612,11 @@ public sealed partial class MainWindow : Window if (item.Sensor?.Parameters.Count > 0) flyout.Items.Add(CreateMenuItem("Parameters...", async (_, _) => await _dialogService.ShowParametersAsync(item))); - MenuFlyoutItem rename = CreateMenuItem("Rename", async (_, _) => await _dialogService.RenameAsync(item)); + MenuFlyoutItem rename = CreateMenuItem("Rename", async (_, _) => + { + await _dialogService.RenameAsync(item); + QueueSensorColumnWidthUpdate(); + }); rename.IsEnabled = item.CanRename; flyout.Items.Add(rename); diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs index 942c26a..0b15b78 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/HardwareMonitorService.cs @@ -14,16 +14,6 @@ namespace LibreHardwareMonitor.Windows.WinUI.Services; public sealed class HardwareMonitorService : IHardwareMonitorService { - private const string DeferDimmDetectionSetting = "memory.deferDimmDetection"; - private const string DeferCpuInitialUpdateSetting = "cpu.deferInitialUpdate"; - private const string DeferTscEstimationSetting = "cpu.deferTscEstimation"; - private const string DeferNetworkDetectionSetting = "network.deferDetection"; - private const string DeferNvidiaDetectionSetting = "nvidia.deferDetection"; - private const string DeferStorageDetectionSetting = "storage.deferDetection"; - private const string DeferIntelGpuDetectionSetting = "gpu.deferIntelDetection"; - private const string DeferControllerDetectionSetting = "controller.deferDetection"; - private const string DeferPsuDetectionSetting = "psu.deferDetection"; - private readonly object _updateLock = new(); private readonly UpdateVisitor _updateVisitor = new(); private readonly TreeRebuildCoalescer _treeRebuildCoalescer; @@ -230,15 +220,15 @@ public sealed class HardwareMonitorService : IHardwareMonitorService private static readonly string[] WinUiDeferredDetectionDefaults = { - DeferDimmDetectionSetting, - DeferCpuInitialUpdateSetting, - DeferTscEstimationSetting, - DeferNvidiaDetectionSetting, - DeferStorageDetectionSetting, - DeferNetworkDetectionSetting, - DeferIntelGpuDetectionSetting, - DeferControllerDetectionSetting, - DeferPsuDetectionSetting, + HardwareSettingsKeys.MemoryDeferDimmDetection, + HardwareSettingsKeys.CpuDeferInitialUpdate, + HardwareSettingsKeys.CpuDeferTscEstimation, + HardwareSettingsKeys.NvidiaDeferDetection, + HardwareSettingsKeys.StorageDeferDetection, + HardwareSettingsKeys.NetworkDeferDetection, + HardwareSettingsKeys.IntelGpuDeferDetection, + HardwareSettingsKeys.ControllerDeferDetection, + HardwareSettingsKeys.PsuDeferDetection, }; private void ApplyWinUiHardwareDefaults() diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/SensorColumnMeasurer.cs b/LibreHardwareMonitor.Windows.WinUI/Services/SensorColumnMeasurer.cs index 782004f..a6cf37f 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/SensorColumnMeasurer.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/SensorColumnMeasurer.cs @@ -27,18 +27,28 @@ internal sealed class SensorColumnMeasurer private readonly MainWindowViewModel _viewModel; private readonly IStartupTracer _startupTrace; private readonly Dictionary<(string Text, bool Bold), double> _cache = new(); - private readonly List _rowGrids = []; + private readonly List _rows = []; + private readonly Dictionary _rowsByItem = new(); private readonly DispatcherQueueTimer _settleTimer; private readonly double[] _columnWidths = [DefaultDeviceColumnWidth, 120, 120, 120]; + private readonly double[] _measuredColumnWidths = [DefaultDeviceColumnWidth, 120, 120, 120]; private TextBlock? _measurementTextBlock; private Grid? _header; private double _stableDeviceColumnWidth = DefaultDeviceColumnWidth; + private SensorDisplayColumn _dirtyColumns = SensorDisplayColumn.All; private bool _settled; + private bool _requiresFullMeasurement = true; + private bool _lastShowValueColumn; + private bool _lastShowMinColumn; + private bool _lastShowMaxColumn; public SensorColumnMeasurer(DispatcherQueue dispatcherQueue, MainWindowViewModel viewModel, IStartupTracer startupTrace) { _viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel)); _startupTrace = startupTrace; + _lastShowValueColumn = viewModel.ShowValueColumn; + _lastShowMinColumn = viewModel.ShowMinColumn; + _lastShowMaxColumn = viewModel.ShowMaxColumn; _settleTimer = dispatcherQueue.CreateTimer(); _settleTimer.Interval = SettleDelay; @@ -47,7 +57,7 @@ internal sealed class SensorColumnMeasurer public event EventHandler? SettleTriggered; - public int RowCount => _rowGrids.Count; + public int RowCount => _rows.Count; public int CacheEntryCount => _cache.Count; @@ -59,12 +69,20 @@ internal sealed class SensorColumnMeasurer { _stableDeviceColumnWidth = NormalizeDeviceColumnWidth(_viewModel.Settings.GetValue(DeviceColumnWidthSetting, DefaultDeviceColumnWidth)); _columnWidths[0] = _stableDeviceColumnWidth; + InvalidateAll(); } - public Grid CreateRowGrid() + public Grid CreateRowGrid(SensorTreeItemViewModel item, int depth) { + ArgumentNullException.ThrowIfNull(item); + Grid grid = CreateColumnGrid(); - _rowGrids.Add(grid); + RowMeasurement row = new(item, grid, depth); + _rows.Add(row); + _rowsByItem[item] = row; + item.DisplayColumnsChanged += Item_DisplayColumnsChanged; + _requiresFullMeasurement = true; + _dirtyColumns = SensorDisplayColumn.All; return grid; } @@ -77,12 +95,17 @@ internal sealed class SensorColumnMeasurer public void ResetRows() { - _rowGrids.Clear(); + foreach (RowMeasurement row in _rows) + row.Item.DisplayColumnsChanged -= Item_DisplayColumnsChanged; + + _rows.Clear(); + _rowsByItem.Clear(); + InvalidateAll(); } public void ScheduleSettle() { - if (_rowGrids.Count == 0) + if (_rows.Count == 0) return; _settled = false; @@ -95,37 +118,55 @@ internal sealed class SensorColumnMeasurer _settleTimer.Stop(); } + public void InvalidateAll() + { + _requiresFullMeasurement = true; + _dirtyColumns = SensorDisplayColumn.All; + foreach (RowMeasurement row in _rows) + row.DirtyColumns = SensorDisplayColumn.All; + } + public void UpdateWidths() { - double sensorWidth = MeasureText("Sensor", true) + SensorColumnPadding; - double valueWidth = _viewModel.ShowValueColumn ? MeasureText("Value", true) + ValueColumnPadding : 0; - double minWidth = _viewModel.ShowMinColumn ? MeasureText("Min", true) + ValueColumnPadding : 0; - double maxWidth = _viewModel.ShowMaxColumn ? MeasureText("Max", true) + ValueColumnPadding : 0; + if (ColumnVisibilityChanged()) + InvalidateAll(); - foreach (SensorTreeItemViewModel root in _viewModel.RootItems) - MeasureColumnWidths(root, 0, ref sensorWidth, ref valueWidth, ref minWidth, ref maxWidth); + bool widthsChanged = false; + if (_requiresFullMeasurement) + { + MeasureAllRows(); + _requiresFullMeasurement = false; + widthsChanged = UpdateDisplayedColumnWidths(); + } + else if (_dirtyColumns != SensorDisplayColumn.None) + { + MeasureDirtyRows(); + widthsChanged = UpdateDisplayedColumnWidths(); + } - double deviceColumnWidth = NormalizeDeviceColumnWidth(sensorWidth); - if (!_settled) - deviceColumnWidth = Math.Max(deviceColumnWidth, _stableDeviceColumnWidth); - - _columnWidths[0] = deviceColumnWidth; - _columnWidths[1] = Math.Ceiling(valueWidth); - _columnWidths[2] = Math.Ceiling(minWidth); - _columnWidths[3] = Math.Ceiling(maxWidth); - - if (_header != null) - ApplyColumnWidths(_header); - foreach (Grid row in _rowGrids) - ApplyColumnWidths(row); + if (widthsChanged) + ApplyColumnWidthsToRegisteredGrids(); RecordDeviceColumnWidthIfChanged(); } + private void Item_DisplayColumnsChanged(object? sender, SensorDisplayColumn columns) + { + if (columns == SensorDisplayColumn.None || sender is not SensorTreeItemViewModel item) + return; + + if (!_rowsByItem.TryGetValue(item, out RowMeasurement? row)) + return; + + row.DirtyColumns |= columns; + _dirtyColumns |= columns; + } + private void SettleTimer_Tick(DispatcherQueueTimer sender, object args) { _settleTimer.Stop(); _settled = true; + InvalidateAll(); SettleTriggered?.Invoke(this, EventArgs.Empty); } @@ -152,27 +193,183 @@ internal sealed class SensorColumnMeasurer grid.ColumnDefinitions[i].Width = new GridLength(_columnWidths[i]); } - private void MeasureColumnWidths( - SensorTreeItemViewModel item, - int depth, - ref double sensorWidth, - ref double valueWidth, - ref double minWidth, - ref double maxWidth) + private void ApplyColumnWidthsToRegisteredGrids() { - if (item.RowVisibility == Visibility.Visible) + if (_header != null) + ApplyColumnWidths(_header); + + foreach (RowMeasurement row in _rows) + ApplyColumnWidths(row.Grid); + } + + private bool ColumnVisibilityChanged() + { + bool changed = _lastShowValueColumn != _viewModel.ShowValueColumn + || _lastShowMinColumn != _viewModel.ShowMinColumn + || _lastShowMaxColumn != _viewModel.ShowMaxColumn; + + _lastShowValueColumn = _viewModel.ShowValueColumn; + _lastShowMinColumn = _viewModel.ShowMinColumn; + _lastShowMaxColumn = _viewModel.ShowMaxColumn; + return changed; + } + + private void MeasureAllRows() + { + SetHeaderColumnWidths(); + + foreach (RowMeasurement row in _rows) { - sensorWidth = Math.Max(sensorWidth, MeasureText(item.Text) + SensorColumnPadding + depth * TreeIndentWidth); - if (_viewModel.ShowValueColumn) - valueWidth = Math.Max(valueWidth, MeasureText(item.Value) + ValueColumnPadding); - if (_viewModel.ShowMinColumn) - minWidth = Math.Max(minWidth, MeasureText(item.Min) + ValueColumnPadding); - if (_viewModel.ShowMaxColumn) - maxWidth = Math.Max(maxWidth, MeasureText(item.Max) + ValueColumnPadding); + MeasureRowColumn(row, SensorDisplayColumn.Sensor); + MeasureRowColumn(row, SensorDisplayColumn.Value); + MeasureRowColumn(row, SensorDisplayColumn.Min); + MeasureRowColumn(row, SensorDisplayColumn.Max); + row.DirtyColumns = SensorDisplayColumn.None; } - foreach (SensorTreeItemViewModel child in item.Children) - MeasureColumnWidths(child, depth + 1, ref sensorWidth, ref valueWidth, ref minWidth, ref maxWidth); + _dirtyColumns = SensorDisplayColumn.None; + } + + private void MeasureDirtyRows() + { + SensorDisplayColumn dirtyColumns = _dirtyColumns; + foreach (SensorDisplayColumn column in EnumerateColumns(dirtyColumns)) + { + bool shouldRecalculate = false; + int columnIndex = GetColumnIndex(column); + + foreach (RowMeasurement row in _rows) + { + if ((row.DirtyColumns & column) == 0) + continue; + + double oldWidth = row.ColumnWidths[columnIndex]; + double newWidth = GetRowColumnWidth(row, column); + row.ColumnWidths[columnIndex] = newWidth; + + if (newWidth > _measuredColumnWidths[columnIndex]) + { + _measuredColumnWidths[columnIndex] = newWidth; + } + else if (oldWidth >= _measuredColumnWidths[columnIndex] - 0.01 && newWidth < oldWidth) + { + shouldRecalculate = true; + } + + row.DirtyColumns &= ~column; + } + + if (shouldRecalculate) + RecalculateMeasuredColumnWidth(column); + } + + _dirtyColumns = SensorDisplayColumn.None; + foreach (RowMeasurement row in _rows) + _dirtyColumns |= row.DirtyColumns; + } + + private void SetHeaderColumnWidths() + { + _measuredColumnWidths[0] = MeasureText("Sensor", true) + SensorColumnPadding; + _measuredColumnWidths[1] = _viewModel.ShowValueColumn ? MeasureText("Value", true) + ValueColumnPadding : 0; + _measuredColumnWidths[2] = _viewModel.ShowMinColumn ? MeasureText("Min", true) + ValueColumnPadding : 0; + _measuredColumnWidths[3] = _viewModel.ShowMaxColumn ? MeasureText("Max", true) + ValueColumnPadding : 0; + } + + private void MeasureRowColumn(RowMeasurement row, SensorDisplayColumn column) + { + int columnIndex = GetColumnIndex(column); + double width = GetRowColumnWidth(row, column); + row.ColumnWidths[columnIndex] = width; + _measuredColumnWidths[columnIndex] = Math.Max(_measuredColumnWidths[columnIndex], width); + } + + private void RecalculateMeasuredColumnWidth(SensorDisplayColumn column) + { + int columnIndex = GetColumnIndex(column); + SetHeaderColumnWidth(column); + + foreach (RowMeasurement row in _rows) + _measuredColumnWidths[columnIndex] = Math.Max(_measuredColumnWidths[columnIndex], row.ColumnWidths[columnIndex]); + } + + private void SetHeaderColumnWidth(SensorDisplayColumn column) + { + int columnIndex = GetColumnIndex(column); + _measuredColumnWidths[columnIndex] = column switch + { + SensorDisplayColumn.Sensor => MeasureText("Sensor", true) + SensorColumnPadding, + SensorDisplayColumn.Value => _viewModel.ShowValueColumn ? MeasureText("Value", true) + ValueColumnPadding : 0, + SensorDisplayColumn.Min => _viewModel.ShowMinColumn ? MeasureText("Min", true) + ValueColumnPadding : 0, + SensorDisplayColumn.Max => _viewModel.ShowMaxColumn ? MeasureText("Max", true) + ValueColumnPadding : 0, + _ => 0 + }; + } + + private double GetRowColumnWidth(RowMeasurement row, SensorDisplayColumn column) + { + if (row.Item.RowVisibility != Visibility.Visible) + return 0; + + return column switch + { + SensorDisplayColumn.Sensor => MeasureText(row.Item.Text) + SensorColumnPadding + row.Depth * TreeIndentWidth, + SensorDisplayColumn.Value => _viewModel.ShowValueColumn ? MeasureText(row.Item.Value) + ValueColumnPadding : 0, + SensorDisplayColumn.Min => _viewModel.ShowMinColumn ? MeasureText(row.Item.Min) + ValueColumnPadding : 0, + SensorDisplayColumn.Max => _viewModel.ShowMaxColumn ? MeasureText(row.Item.Max) + ValueColumnPadding : 0, + _ => 0 + }; + } + + private bool UpdateDisplayedColumnWidths() + { + double deviceColumnWidth = NormalizeDeviceColumnWidth(_measuredColumnWidths[0]); + if (!_settled) + deviceColumnWidth = Math.Max(deviceColumnWidth, _stableDeviceColumnWidth); + + double[] widths = + [ + deviceColumnWidth, + Math.Ceiling(_measuredColumnWidths[1]), + Math.Ceiling(_measuredColumnWidths[2]), + Math.Ceiling(_measuredColumnWidths[3]) + ]; + + bool changed = false; + for (int i = 0; i < _columnWidths.Length; i++) + { + if (Math.Abs(_columnWidths[i] - widths[i]) < 0.01) + continue; + + _columnWidths[i] = widths[i]; + changed = true; + } + + return changed; + } + + private static IEnumerable EnumerateColumns(SensorDisplayColumn columns) + { + if ((columns & SensorDisplayColumn.Sensor) != 0) + yield return SensorDisplayColumn.Sensor; + if ((columns & SensorDisplayColumn.Value) != 0) + yield return SensorDisplayColumn.Value; + if ((columns & SensorDisplayColumn.Min) != 0) + yield return SensorDisplayColumn.Min; + if ((columns & SensorDisplayColumn.Max) != 0) + yield return SensorDisplayColumn.Max; + } + + private static int GetColumnIndex(SensorDisplayColumn column) + { + return column switch + { + SensorDisplayColumn.Sensor => 0, + SensorDisplayColumn.Value => 1, + SensorDisplayColumn.Min => 2, + SensorDisplayColumn.Max => 3, + _ => throw new ArgumentOutOfRangeException(nameof(column), column, null) + }; } private double MeasureText(string text, bool bold = false) @@ -206,7 +403,7 @@ internal sealed class SensorColumnMeasurer private void RecordDeviceColumnWidthIfChanged() { - if (!_settled || _rowGrids.Count == 0) + if (!_settled || _rows.Count == 0) return; double measuredWidth = NormalizeDeviceColumnWidth(_columnWidths[0]); @@ -216,6 +413,26 @@ internal sealed class SensorColumnMeasurer return; _viewModel.Settings.SetValue(DeviceColumnWidthSetting, measuredWidth); - _startupTrace.Mark("MainWindow.RecordDeviceColumnWidth", FormattableString.Invariant($"width={measuredWidth:F0}, rows={_rowGrids.Count}")); + _startupTrace.Mark("MainWindow.RecordDeviceColumnWidth", FormattableString.Invariant($"width={measuredWidth:F0}, rows={_rows.Count}")); + } + + private sealed class RowMeasurement + { + public RowMeasurement(SensorTreeItemViewModel item, Grid grid, int depth) + { + Item = item; + Grid = grid; + Depth = depth; + } + + public SensorTreeItemViewModel Item { get; } + + public Grid Grid { get; } + + public int Depth { get; } + + public double[] ColumnWidths { get; } = new double[4]; + + public SensorDisplayColumn DirtyColumns { get; set; } = SensorDisplayColumn.All; } } diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/Tracing/FileStartupTracer.cs b/LibreHardwareMonitor.Windows.WinUI/Services/Tracing/FileStartupTracer.cs index 4fc98ca..5857a6c 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/Tracing/FileStartupTracer.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/Tracing/FileStartupTracer.cs @@ -10,6 +10,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using LibreHardwareMonitor.Hardware; namespace LibreHardwareMonitor.Windows.WinUI.Services.Tracing; @@ -167,17 +168,6 @@ internal sealed class FileStartupTracer : IStartupTracer } } - 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) @@ -229,7 +219,7 @@ internal sealed class FileStartupTracer : IStartupTracer foreach (Entry entry in entries.OrderBy(entry => entry.Start)) { - builder.Append(EscapeCsv(entry.Phase)); + builder.Append(StartupTraceLogSupport.EscapeCsv(entry.Phase)); builder.Append(','); builder.Append(entry.Start.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture)); builder.Append(','); @@ -237,7 +227,7 @@ internal sealed class FileStartupTracer : IStartupTracer builder.Append(','); builder.Append(entry.Status); builder.Append(','); - builder.AppendLine(EscapeCsv(entry.Detail)); + builder.AppendLine(StartupTraceLogSupport.EscapeCsv(entry.Detail)); } return builder.ToString(); diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/Tracing/StartupTracer.cs b/LibreHardwareMonitor.Windows.WinUI/Services/Tracing/StartupTracer.cs index 01435a1..1b439fb 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/Tracing/StartupTracer.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/Tracing/StartupTracer.cs @@ -3,7 +3,7 @@ // Copyright (C) LibreHardwareMonitor and Contributors. using System; -using System.IO; +using LibreHardwareMonitor.Hardware; namespace LibreHardwareMonitor.Windows.WinUI.Services.Tracing; @@ -26,31 +26,12 @@ internal static class StartupTracer { 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; + return StartupTraceLogSupport.GetLogFileName("LibreHardwareMonitor.WinUIStartupTiming", 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); + return SettingsParsing.IsTruthy(environmentValue); } } diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorDisplayColumn.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorDisplayColumn.cs new file mode 100644 index 0000000..0095b78 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorDisplayColumn.cs @@ -0,0 +1,18 @@ +// 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; + +namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; + +[Flags] +internal enum SensorDisplayColumn +{ + None = 0, + Sensor = 1, + Value = 2, + Min = 4, + Max = 8, + All = Sensor | Value | Min | Max +} diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs index 8771952..b7362b1 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs @@ -41,6 +41,8 @@ public sealed class SensorTreeItemViewModel : ViewModelBase _settings = settings; } + internal event EventHandler? DisplayColumnsChanged; + public ObservableCollection Children { get; } = []; public bool CanHide => Sensor != null; @@ -151,6 +153,9 @@ public sealed class SensorTreeItemViewModel : ViewModelBase } set { + if (Text == value) + return; + if (Sensor != null) Sensor.Name = value; else if (Hardware != null) @@ -159,6 +164,7 @@ public sealed class SensorTreeItemViewModel : ViewModelBase _text = value; OnPropertyChanged(); + OnDisplayColumnsChanged(SensorDisplayColumn.Sensor); } } @@ -267,11 +273,14 @@ public sealed class SensorTreeItemViewModel : ViewModelBase // actually changed. Unconditionally notifying all four for every item on every update tick made the native // binding engine re-marshal unchanged values across the boundary continuously — most sensors don't change each // tick, so this cuts the per-tick interop churn (and its retained wrappers) by a large factor. + SensorDisplayColumn changedColumns = SensorDisplayColumn.None; + string value = Value; if (value != _lastValue) { _lastValue = value; OnPropertyChanged(nameof(Value)); + changedColumns |= SensorDisplayColumn.Value; } string min = Min; @@ -279,6 +288,7 @@ public sealed class SensorTreeItemViewModel : ViewModelBase { _lastMin = min; OnPropertyChanged(nameof(Min)); + changedColumns |= SensorDisplayColumn.Min; } string max = Max; @@ -286,6 +296,7 @@ public sealed class SensorTreeItemViewModel : ViewModelBase { _lastMax = max; OnPropertyChanged(nameof(Max)); + changedColumns |= SensorDisplayColumn.Max; } string toolTip = ToolTip; @@ -295,6 +306,8 @@ public sealed class SensorTreeItemViewModel : ViewModelBase OnPropertyChanged(nameof(ToolTip)); } + OnDisplayColumnsChanged(changedColumns); + foreach (SensorTreeItemViewModel child in Children) child.RefreshValues(); } @@ -376,6 +389,12 @@ public sealed class SensorTreeItemViewModel : ViewModelBase return changed; } + private void OnDisplayColumnsChanged(SensorDisplayColumn columns) + { + if (columns != SensorDisplayColumn.None) + DisplayColumnsChanged?.Invoke(this, columns); + } + private void UpdateVisibilityState() { bool visible = Kind switch diff --git a/LibreHardwareMonitorLib.Tests/Hardware/SettingsParsingTests.cs b/LibreHardwareMonitorLib.Tests/Hardware/SettingsParsingTests.cs new file mode 100644 index 0000000..2ca3d1d --- /dev/null +++ b/LibreHardwareMonitorLib.Tests/Hardware/SettingsParsingTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using LibreHardwareMonitor.Hardware; +using Xunit; + +namespace LibreHardwareMonitorLib.Tests.Hardware; + +public class SettingsParsingTests +{ + [Theory] + [InlineData("1", true)] + [InlineData("true", true)] + [InlineData("TRUE", true)] + [InlineData("yes", true)] + [InlineData("on", true)] + [InlineData("", false)] + [InlineData("0", false)] + [InlineData("false", false)] + [InlineData("no", false)] + [InlineData("off", false)] + public void IsTruthy_ParsesExpectedTokens(string value, bool expected) + { + Assert.Equal(expected, SettingsParsing.IsTruthy(value)); + } + + [Fact] + public void ShouldDefer_UsesEnvironmentValueBeforeSetting() + { + string environmentVariable = $"{nameof(SettingsParsingTests)}_{Guid.NewGuid():N}"; + DictionarySettings settings = new(); + settings.SetValue("test.defer", "false"); + + try + { + Environment.SetEnvironmentVariable(environmentVariable, "yes"); + + Assert.True(SettingsParsing.ShouldDefer(settings, "test.defer", environmentVariable)); + } + finally + { + Environment.SetEnvironmentVariable(environmentVariable, null); + } + } + + [Fact] + public void ShouldDefer_FallsBackToSettingWhenEnvironmentValueIsBlank() + { + string environmentVariable = $"{nameof(SettingsParsingTests)}_{Guid.NewGuid():N}"; + DictionarySettings settings = new(); + settings.SetValue("test.defer", "true"); + + try + { + Environment.SetEnvironmentVariable(environmentVariable, ""); + + Assert.True(SettingsParsing.ShouldDefer(settings, "test.defer", environmentVariable)); + } + finally + { + Environment.SetEnvironmentVariable(environmentVariable, null); + } + } + + private sealed class DictionarySettings : ISettings + { + private readonly Dictionary _values = new(); + + public bool Contains(string name) + { + return _values.ContainsKey(name); + } + + public void SetValue(string name, string value) + { + _values[name] = value; + } + + public string GetValue(string name, string value) + { + return _values.TryGetValue(name, out string? result) ? result : value; + } + + public void Remove(string name) + { + _values.Remove(name); + } + } +} diff --git a/LibreHardwareMonitorLib.Tests/Hardware/StartupTraceLogSupportTests.cs b/LibreHardwareMonitorLib.Tests/Hardware/StartupTraceLogSupportTests.cs new file mode 100644 index 0000000..e1bbc32 --- /dev/null +++ b/LibreHardwareMonitorLib.Tests/Hardware/StartupTraceLogSupportTests.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using LibreHardwareMonitor.Hardware; +using Xunit; + +namespace LibreHardwareMonitorLib.Tests.Hardware; + +public class StartupTraceLogSupportTests +{ + [Theory] + [InlineData("", "")] + [InlineData("plain", "plain")] + [InlineData("a,b", "\"a,b\"")] + [InlineData("a\"b", "\"a\"\"b\"")] + [InlineData("a\r\nb", "\"a\r\nb\"")] + public void EscapeCsv_EscapesOnlyWhenNeeded(string value, string expected) + { + Assert.Equal(expected, StartupTraceLogSupport.EscapeCsv(value)); + } + + [Fact] + public void GetLogFileName_EmptyPathUsesBaseDirectory() + { + string path = StartupTraceLogSupport.GetLogFileName("TestTrace", ""); + + Assert.Equal(NormalizeDirectory(AppContext.BaseDirectory), NormalizeDirectory(Path.GetDirectoryName(path)!)); + Assert.StartsWith("TestTrace-", Path.GetFileName(path)); + Assert.EndsWith(".log", Path.GetFileName(path)); + } + + [Fact] + public void GetLogFileName_DirectoryPathCombinesGeneratedFileName() + { + string path = StartupTraceLogSupport.GetLogFileName("TestTrace", AppContext.BaseDirectory); + + Assert.Equal(NormalizeDirectory(AppContext.BaseDirectory), NormalizeDirectory(Path.GetDirectoryName(path)!)); + Assert.StartsWith("TestTrace-", Path.GetFileName(path)); + Assert.EndsWith(".log", Path.GetFileName(path)); + } + + [Fact] + public void GetLogFileName_ExplicitFilePathIsReturnedAsConfigured() + { + string configuredPath = Path.Combine(AppContext.BaseDirectory, "explicit-startup.log"); + + string path = StartupTraceLogSupport.GetLogFileName("Ignored", configuredPath); + + Assert.Equal(configuredPath, path); + } + + private static string NormalizeDirectory(string path) + { + return path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } +} diff --git a/LibreHardwareMonitorLib/Hardware/Computer.cs b/LibreHardwareMonitorLib/Hardware/Computer.cs index e5addc6..29f571d 100644 --- a/LibreHardwareMonitorLib/Hardware/Computer.cs +++ b/LibreHardwareMonitorLib/Hardware/Computer.cs @@ -39,17 +39,11 @@ 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 const string DeferIntelGpuDetectionEnvironmentVariable = "LHM_INTEL_GPU_DEFER_DETECTION"; - private const string DeferIntelGpuDetectionSetting = "gpu.deferIntelDetection"; private const string DeferControllerDetectionEnvironmentVariable = "LHM_CONTROLLER_DEFER_DETECTION"; - private const string DeferControllerDetectionSetting = "controller.deferDetection"; private const string DeferPsuDetectionEnvironmentVariable = "LHM_PSU_DEFER_DETECTION"; - private const string DeferPsuDetectionSetting = "psu.deferDetection"; private readonly object _deferredGroupLock = new(); private readonly List _groups = new(); @@ -100,8 +94,6 @@ public class Computer : IComputer /// public event HardwareEventHandler HardwareRemoved; - public event EventHandler HardwareDiscoveryCompleted; - public Task HardwareDiscoveryTask { get @@ -679,7 +671,7 @@ public class Computer : IComputer "NvidiaGroup", () => new NvidiaGroup(_settings), () => _gpuEnabled, - DeferNvidiaDetectionSetting, + HardwareSettingsKeys.NvidiaDeferDetection, DeferNvidiaDetectionEnvironmentVariable); // Intel GPU detection is the most expensive GPU probe but only depends on the (already-created) CPU group, @@ -689,7 +681,7 @@ public class Computer : IComputer "IntelGpuGroup", () => new IntelGpuGroup(GetIntelCpus(), _settings), () => _gpuEnabled && _cpuEnabled, - DeferIntelGpuDetectionSetting, + HardwareSettingsKeys.IntelGpuDeferDetection, DeferIntelGpuDetectionEnvironmentVariable); } @@ -702,7 +694,7 @@ public class Computer : IComputer { // Controllers probe USB/serial buses (with worst-case timeouts), so the whole block can be deferred to a // single background task. It stays sequential there to avoid concurrent serial-port/USB scans. - if (ShouldDeferDetection(DeferControllerDetectionSetting, DeferControllerDetectionEnvironmentVariable)) + if (ShouldDeferDetection(HardwareSettingsKeys.ControllerDeferDetection, DeferControllerDetectionEnvironmentVariable)) { startupTrace?.Skip("ControllerGroups", "Deferred to background."); AddDeferredGroups(() => _controllerEnabled, @@ -734,7 +726,7 @@ public class Computer : IComputer "StorageGroup", () => new StorageGroup(_settings), () => _storageEnabled, - DeferStorageDetectionSetting, + HardwareSettingsKeys.StorageDeferDetection, DeferStorageDetectionEnvironmentVariable); cancellationToken.ThrowIfCancellationRequested(); @@ -743,7 +735,7 @@ public class Computer : IComputer "NetworkGroup", () => new NetworkGroup(_settings), () => _networkEnabled, - DeferNetworkDetectionSetting, + HardwareSettingsKeys.NetworkDeferDetection, DeferNetworkDetectionEnvironmentVariable); cancellationToken.ThrowIfCancellationRequested(); @@ -751,7 +743,7 @@ public class Computer : IComputer { // PSU detection probes HID devices, so it contends with the deferred controllers' USB/HID scans. Defer it // to a background task too, keeping all HID/USB probing off the critical path. - if (ShouldDeferDetection(DeferPsuDetectionSetting, DeferPsuDetectionEnvironmentVariable)) + if (ShouldDeferDetection(HardwareSettingsKeys.PsuDeferDetection, DeferPsuDetectionEnvironmentVariable)) { startupTrace?.Skip("PsuGroups", "Deferred to background."); AddDeferredGroups(() => _psuEnabled, @@ -889,7 +881,7 @@ public class Computer : IComputer cancellationTokenSource.Cancel(); // Mark the run cancelled before draining so the Task.WhenAll continuation in CompleteDeferredGroupRunWhenRegistered - // cannot win the race and raise HardwareDiscoveryCompleted during teardown. + // cannot win the race and complete HardwareDiscoveryTask during teardown. completionSource.TrySetCanceled(); // Wait for in-flight deferred construction to unwind before disposing the token source or returning to a caller @@ -925,17 +917,13 @@ public class Computer : IComputer private void CompleteDeferredGroupRun(TaskCompletionSource completionSource) { - bool completed; lock (_deferredGroupLock) { if (!ReferenceEquals(completionSource, _deferredGroupCompletionSource)) return; - completed = completionSource.TrySetResult(null); + completionSource.TrySetResult(null); } - - if (completed) - HardwareDiscoveryCompleted?.Invoke(this, EventArgs.Empty); } private void CompleteDeferredGroupRunWhenRegistered() diff --git a/LibreHardwareMonitorLib/Hardware/Cpu/GenericCpu.cs b/LibreHardwareMonitorLib/Hardware/Cpu/GenericCpu.cs index 0f09b05..cdc394f 100644 --- a/LibreHardwareMonitorLib/Hardware/Cpu/GenericCpu.cs +++ b/LibreHardwareMonitorLib/Hardware/Cpu/GenericCpu.cs @@ -18,7 +18,6 @@ namespace LibreHardwareMonitor.Hardware.Cpu; public class GenericCpu : Hardware { private const string DeferTscEstimationEnvironmentVariable = "LHM_CPU_DEFER_TSC_ESTIMATION"; - private const string DeferTscEstimationSetting = "cpu.deferTscEstimation"; protected readonly int _coreCount; protected readonly CpuId[][] _cpuId; @@ -271,7 +270,7 @@ public class GenericCpu : Hardware private static bool ShouldDeferTscEstimation(ISettings settings) { - return SettingsParsing.ShouldDefer(settings, DeferTscEstimationSetting, DeferTscEstimationEnvironmentVariable); + return SettingsParsing.ShouldDefer(settings, HardwareSettingsKeys.CpuDeferTscEstimation, DeferTscEstimationEnvironmentVariable); } diff --git a/LibreHardwareMonitorLib/Hardware/Cpu/IntelCpu.cs b/LibreHardwareMonitorLib/Hardware/Cpu/IntelCpu.cs index 4b095a8..0e606c9 100644 --- a/LibreHardwareMonitorLib/Hardware/Cpu/IntelCpu.cs +++ b/LibreHardwareMonitorLib/Hardware/Cpu/IntelCpu.cs @@ -16,7 +16,6 @@ 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; @@ -575,7 +574,7 @@ internal sealed class IntelCpu : GenericCpu private static bool ShouldDeferInitialUpdate(ISettings settings) { - return SettingsParsing.ShouldDefer(settings, DeferInitialUpdateSetting, DeferInitialUpdateEnvironmentVariable); + return SettingsParsing.ShouldDefer(settings, HardwareSettingsKeys.CpuDeferInitialUpdate, DeferInitialUpdateEnvironmentVariable); } public override string GetReport() diff --git a/LibreHardwareMonitorLib/Hardware/HardwareSettingsKeys.cs b/LibreHardwareMonitorLib/Hardware/HardwareSettingsKeys.cs new file mode 100644 index 0000000..721c5a3 --- /dev/null +++ b/LibreHardwareMonitorLib/Hardware/HardwareSettingsKeys.cs @@ -0,0 +1,38 @@ +// 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. + +namespace LibreHardwareMonitor.Hardware; + +/// +/// Setting keys understood by the hardware library. +/// +public static class HardwareSettingsKeys +{ + /// Defers DIMM SPD/thermal-sensor detection until after initial memory hardware is registered. + public const string MemoryDeferDimmDetection = "memory.deferDimmDetection"; + + /// Defers the first CPU update until the regular update loop. + public const string CpuDeferInitialUpdate = "cpu.deferInitialUpdate"; + + /// Defers the CPU timestamp-counter frequency estimate to the background. + public const string CpuDeferTscEstimation = "cpu.deferTscEstimation"; + + /// Defers Nvidia GPU detection to the background. + public const string NvidiaDeferDetection = "nvidia.deferDetection"; + + /// Defers storage device detection to the background. + public const string StorageDeferDetection = "storage.deferDetection"; + + /// Defers network adapter detection to the background. + public const string NetworkDeferDetection = "network.deferDetection"; + + /// Defers Intel integrated GPU detection to the background. + public const string IntelGpuDeferDetection = "gpu.deferIntelDetection"; + + /// Defers controller detection to the background. + public const string ControllerDeferDetection = "controller.deferDetection"; + + /// Defers PSU detection to the background. + public const string PsuDeferDetection = "psu.deferDetection"; +} diff --git a/LibreHardwareMonitorLib/Hardware/HardwareStartupTrace.cs b/LibreHardwareMonitorLib/Hardware/HardwareStartupTrace.cs index af65399..37212af 100644 --- a/LibreHardwareMonitorLib/Hardware/HardwareStartupTrace.cs +++ b/LibreHardwareMonitorLib/Hardware/HardwareStartupTrace.cs @@ -130,18 +130,7 @@ internal sealed class HardwareStartupTrace : IDisposable 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; + return StartupTraceLogSupport.GetLogFileName("LibreHardwareMonitor.HardwareStartupTiming", configuredPath); } private static int? GetHardwareCount(T result) @@ -201,7 +190,7 @@ internal sealed class HardwareStartupTrace : IDisposable foreach (Entry entry in _entries) { - builder.Append(EscapeCsv(entry.Phase)); + builder.Append(StartupTraceLogSupport.EscapeCsv(entry.Phase)); builder.Append(','); builder.Append(entry.Elapsed.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture)); builder.Append(','); @@ -209,23 +198,12 @@ internal sealed class HardwareStartupTrace : IDisposable builder.Append(','); builder.Append(entry.Status); builder.Append(','); - builder.AppendLine(EscapeCsv(entry.Detail)); + builder.AppendLine(StartupTraceLogSupport.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) diff --git a/LibreHardwareMonitorLib/Hardware/Memory/MemoryGroup.cs b/LibreHardwareMonitorLib/Hardware/Memory/MemoryGroup.cs index 2faf2be..f4878ee 100644 --- a/LibreHardwareMonitorLib/Hardware/Memory/MemoryGroup.cs +++ b/LibreHardwareMonitorLib/Hardware/Memory/MemoryGroup.cs @@ -23,7 +23,6 @@ namespace LibreHardwareMonitor.Hardware.Memory; internal class MemoryGroup : IGroup, IHardwareChanged, IHardwareDiscoveryTask { 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 = []; @@ -246,7 +245,7 @@ internal class MemoryGroup : IGroup, IHardwareChanged, IHardwareDiscoveryTask private static bool ShouldDeferDimmDetection(ISettings settings) { - return SettingsParsing.ShouldDefer(settings, DeferDimmDetectionSetting, DeferDimmDetectionEnvironmentVariable); + return SettingsParsing.ShouldDefer(settings, HardwareSettingsKeys.MemoryDeferDimmDetection, DeferDimmDetectionEnvironmentVariable); } private void AddDimms(List accessors, ISettings settings, HardwareStartupTrace startupTrace) diff --git a/LibreHardwareMonitorLib/Hardware/SettingsParsing.cs b/LibreHardwareMonitorLib/Hardware/SettingsParsing.cs index ad6d972..8c4d2ce 100644 --- a/LibreHardwareMonitorLib/Hardware/SettingsParsing.cs +++ b/LibreHardwareMonitorLib/Hardware/SettingsParsing.cs @@ -11,7 +11,7 @@ namespace LibreHardwareMonitor.Hardware; /// and environment variables. Keeps the truthy vocabulary and the env-overrides-setting /// precedence in one place so the components cannot drift. /// -internal static class SettingsParsing +public static class SettingsParsing { /// Returns whether is one of the accepted truthy tokens (1/true/yes/on). public static bool IsTruthy(string value) diff --git a/LibreHardwareMonitorLib/Hardware/StartupTraceLogSupport.cs b/LibreHardwareMonitorLib/Hardware/StartupTraceLogSupport.cs new file mode 100644 index 0000000..2dd8c9a --- /dev/null +++ b/LibreHardwareMonitorLib/Hardware/StartupTraceLogSupport.cs @@ -0,0 +1,47 @@ +// 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.IO; + +namespace LibreHardwareMonitor.Hardware; + +/// +/// Shared file-name and CSV helpers for startup timing logs. +/// +public static class StartupTraceLogSupport +{ + /// + /// Escapes a single CSV field using double quotes when the value contains a comma, quote, or newline. + /// + public 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("\"", "\"\"") + "\""; + } + + /// + /// Resolves a timestamped startup trace log file name from a file-name prefix and optional configured path. + /// + public static string GetLogFileName(string fileNamePrefix, string configuredPath) + { + string fileName = $"{fileNamePrefix}-{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; + } +}