While the app was running, working set grew ~20 MB/min (reaching tens of GB over a day). dotnet-gcdump traced it to COM-callable wrappers accumulating from the per-tick binding interop: SensorTreeItemViewModel.RefreshValues raised PropertyChanged for Value/Min/Max/ToolTip on every sensor every update tick, and ViewModelBase allocated a new PropertyChangedEventArgs per raise. WinUI's binding engine is native, so each raised event marshals its args across the boundary and retains a wrapper. - ViewModelBase: reuse one cached PropertyChangedEventArgs per property name. - SensorTreeItemViewModel.RefreshValues: raise PropertyChanged only when the formatted text actually changed. Also close a leaked D3DKMT adapter handle: D3DDisplayDevice.GetDeviceInfoByIdentifier opened the adapter but skipped CloseAdapter on every early-return failure path, and it runs on each GPU's Update() tick. Close it in a finally block. Verified with dotnet-gcdump over an 18-minute soak: managed heap and live object count stay flat (~50 MB) instead of climbing 47 -> 470 MB. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
42 lines
1.8 KiB
C#
42 lines
1.8 KiB
C#
// 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.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
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<string, PropertyChangedEventArgs> EventArgsByName = new();
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
|
{
|
|
if (EqualityComparer<T>.Default.Equals(field, value))
|
|
return false;
|
|
|
|
field = value;
|
|
OnPropertyChanged(propertyName);
|
|
return true;
|
|
}
|
|
|
|
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
|
{
|
|
PropertyChangedEventHandler? handler = PropertyChanged;
|
|
if (handler == null)
|
|
return;
|
|
|
|
PropertyChangedEventArgs args = EventArgsByName.GetOrAdd(propertyName ?? string.Empty, static name => new PropertyChangedEventArgs(name));
|
|
handler(this, args);
|
|
}
|
|
}
|