diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs index 149b410..8771952 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/SensorTreeItemViewModel.cs @@ -30,6 +30,10 @@ public sealed class SensorTreeItemViewModel : ViewModelBase private Visibility _valueColumnVisibility = Visibility.Visible; private Color? _penColor; private TemperatureUnit _temperatureUnit; + private string? _lastValue; + private string? _lastMin; + private string? _lastMax; + private string? _lastToolTip; private SensorTreeItemViewModel(SensorTreeItemKind kind, AppSettings? settings) { @@ -259,10 +263,38 @@ public sealed class SensorTreeItemViewModel : ViewModelBase public void RefreshValues() { - OnPropertyChanged(nameof(Value)); - OnPropertyChanged(nameof(Min)); - OnPropertyChanged(nameof(Max)); - OnPropertyChanged(nameof(ToolTip)); + // Value/Min/Max/ToolTip are computed (no backing field), so raise PropertyChanged only when the formatted text + // 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. + string value = Value; + if (value != _lastValue) + { + _lastValue = value; + OnPropertyChanged(nameof(Value)); + } + + string min = Min; + if (min != _lastMin) + { + _lastMin = min; + OnPropertyChanged(nameof(Min)); + } + + string max = Max; + if (max != _lastMax) + { + _lastMax = max; + OnPropertyChanged(nameof(Max)); + } + + string toolTip = ToolTip; + if (toolTip != _lastToolTip) + { + _lastToolTip = toolTip; + OnPropertyChanged(nameof(ToolTip)); + } + foreach (SensorTreeItemViewModel child in Children) child.RefreshValues(); } diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/ViewModelBase.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/ViewModelBase.cs index e94d890..fd0ec43 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/ViewModelBase.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/ViewModelBase.cs @@ -2,6 +2,7 @@ // 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.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; @@ -10,6 +11,12 @@ namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; public abstract class ViewModelBase : INotifyPropertyChanged { + // Reuse one PropertyChangedEventArgs per property name. WinUI's binding engine handles PropertyChanged natively, so + // each raised event marshals its args across the managed/native boundary and creates a COM-callable wrapper that the + // interop layer retains. Allocating a fresh PropertyChangedEventArgs on every raise — RefreshValues fires four per + // sensor on every update tick — therefore leaked a wrapper per raise (managed-heap growth ~28 MB/min → multi-GB). + private static readonly ConcurrentDictionary EventArgsByName = new(); + public event PropertyChangedEventHandler? PropertyChanged; protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null) @@ -24,6 +31,11 @@ public abstract class ViewModelBase : INotifyPropertyChanged protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + PropertyChangedEventHandler? handler = PropertyChanged; + if (handler == null) + return; + + PropertyChangedEventArgs args = EventArgsByName.GetOrAdd(propertyName ?? string.Empty, static name => new PropertyChangedEventArgs(name)); + handler(this, args); } } diff --git a/LibreHardwareMonitorLib/Hardware/D3DDisplayDevice.cs b/LibreHardwareMonitorLib/Hardware/D3DDisplayDevice.cs index cd88a06..565149f 100644 --- a/LibreHardwareMonitorLib/Hardware/D3DDisplayDevice.cs +++ b/LibreHardwareMonitorLib/Hardware/D3DDisplayDevice.cs @@ -65,78 +65,87 @@ internal static class D3DDisplayDevice if (status != NTSTATUS.STATUS_SUCCESS) return false; - GetAdapterType(out status, adapter, out D3DKMT_ADAPTERTYPE adapterType); - if (status != NTSTATUS.STATUS_SUCCESS) - return false; - - if (adapterType.Anonymous.Anonymous.SoftwareDevice) - return false; - - deviceInfo.Integrated = adapterType.Anonymous.Anonymous.HybridIntegrated; - - GetQueryStatisticsAdapterInformation(out status, adapter, out D3DKMT_QUERYSTATISTICS_ADAPTER_INFORMATION adapterInformation); - if (status != NTSTATUS.STATUS_SUCCESS) - return false; - - uint segmentCount = adapterInformation.NbSegments; - uint nodeCount = adapterInformation.NodeCount; - - deviceInfo.Nodes = new D3DDeviceNodeInfo[nodeCount]; - - DateTime queryTime = DateTime.Now; - - for (uint nodeId = 0; nodeId < nodeCount; nodeId++) + // Once the adapter is open it must be closed on every exit path. The guard clauses below bail out on the first + // failed D3DKMT query, and this method runs on every GPU's Update() tick, so a missing close leaks a kernel + // adapter handle (and its driver-side allocations) once per tick — unbounded over time. Close it in finally. + try { - GetNodeMetaData(out status, adapter, nodeId, out D3DKMT_NODEMETADATA nodeMetaData); + GetAdapterType(out status, adapter, out D3DKMT_ADAPTERTYPE adapterType); if (status != NTSTATUS.STATUS_SUCCESS) return false; - GetQueryStatisticsNode(out status, adapter, nodeId, out D3DKMT_QUERYSTATISTICS_NODE_INFORMATION nodeInformation); + if (adapterType.Anonymous.Anonymous.SoftwareDevice) + return false; + + deviceInfo.Integrated = adapterType.Anonymous.Anonymous.HybridIntegrated; + + GetQueryStatisticsAdapterInformation(out status, adapter, out D3DKMT_QUERYSTATISTICS_ADAPTER_INFORMATION adapterInformation); if (status != NTSTATUS.STATUS_SUCCESS) return false; - deviceInfo.Nodes[nodeId] = new D3DDeviceNodeInfo + uint segmentCount = adapterInformation.NbSegments; + uint nodeCount = adapterInformation.NodeCount; + + deviceInfo.Nodes = new D3DDeviceNodeInfo[nodeCount]; + + DateTime queryTime = DateTime.Now; + + for (uint nodeId = 0; nodeId < nodeCount; nodeId++) { - Id = nodeId, - Name = GetNodeEngineTypeString(nodeMetaData), - RunningTime = nodeInformation.GlobalInformation.RunningTime, - QueryTime = queryTime - }; + GetNodeMetaData(out status, adapter, nodeId, out D3DKMT_NODEMETADATA nodeMetaData); + if (status != NTSTATUS.STATUS_SUCCESS) + return false; + + GetQueryStatisticsNode(out status, adapter, nodeId, out D3DKMT_QUERYSTATISTICS_NODE_INFORMATION nodeInformation); + if (status != NTSTATUS.STATUS_SUCCESS) + return false; + + deviceInfo.Nodes[nodeId] = new D3DDeviceNodeInfo + { + Id = nodeId, + Name = GetNodeEngineTypeString(nodeMetaData), + RunningTime = nodeInformation.GlobalInformation.RunningTime, + QueryTime = queryTime + }; + } + + GetSegmentSize(out status, adapter, out D3DKMT_SEGMENTSIZEINFO segmentSizeInfo); + if (status != NTSTATUS.STATUS_SUCCESS) + return false; + + deviceInfo.GpuSharedLimit = segmentSizeInfo.SharedSystemMemorySize; + deviceInfo.GpuVideoMemoryLimit = segmentSizeInfo.DedicatedVideoMemorySize; + deviceInfo.GpuDedicatedLimit = segmentSizeInfo.DedicatedSystemMemorySize; + + for (uint segmentId = 0; segmentId < segmentCount; segmentId++) + { + GetQueryStatisticsSegment(out status, adapter, segmentId, out D3DKMT_QUERYSTATISTICS_SEGMENT_INFORMATION segmentInformation); + if (status != NTSTATUS.STATUS_SUCCESS) + return false; + + ulong bytesResident = segmentInformation.BytesResident; + ulong bytesCommitted = segmentInformation.BytesCommitted; + + uint aperture = segmentInformation.Aperture; + + if (aperture == 1) + { + deviceInfo.GpuSharedUsed += bytesResident; + deviceInfo.GpuSharedMax += bytesCommitted; + } + else + { + deviceInfo.GpuDedicatedUsed += bytesResident; + deviceInfo.GpuDedicatedMax += bytesCommitted; + } + } + + return true; } - - GetSegmentSize(out status, adapter, out D3DKMT_SEGMENTSIZEINFO segmentSizeInfo); - if (status != NTSTATUS.STATUS_SUCCESS) - return false; - - deviceInfo.GpuSharedLimit = segmentSizeInfo.SharedSystemMemorySize; - deviceInfo.GpuVideoMemoryLimit = segmentSizeInfo.DedicatedVideoMemorySize; - deviceInfo.GpuDedicatedLimit = segmentSizeInfo.DedicatedSystemMemorySize; - - for (uint segmentId = 0; segmentId < segmentCount; segmentId++) + finally { - GetQueryStatisticsSegment(out status, adapter, segmentId, out D3DKMT_QUERYSTATISTICS_SEGMENT_INFORMATION segmentInformation); - if (status != NTSTATUS.STATUS_SUCCESS) - return false; - - ulong bytesResident = segmentInformation.BytesResident; - ulong bytesCommitted = segmentInformation.BytesCommitted; - - uint aperture = segmentInformation.Aperture; - - if (aperture == 1) - { - deviceInfo.GpuSharedUsed += bytesResident; - deviceInfo.GpuSharedMax += bytesCommitted; - } - else - { - deviceInfo.GpuDedicatedUsed += bytesResident; - deviceInfo.GpuDedicatedMax += bytesCommitted; - } + CloseAdapter(out _, adapter); } - - CloseAdapter(out status, adapter); - return status == NTSTATUS.STATUS_SUCCESS; } private static string GetNodeEngineTypeString(D3DKMT_NODEMETADATA nodeMetaData)