diff --git a/src/Avalonia.Base/Media/Pen.cs b/src/Avalonia.Base/Media/Pen.cs index 65ba851100..bda0e5cb99 100644 --- a/src/Avalonia.Base/Media/Pen.cs +++ b/src/Avalonia.Base/Media/Pen.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media /// /// Describes how a stroke is drawn. /// - public sealed class Pen : AvaloniaObject, IPen, IWeakEventSubscriber + public sealed class Pen : AvaloniaObject, IPen { /// /// Defines the property. @@ -48,7 +48,8 @@ namespace Avalonia.Media private EventHandler? _invalidated; private IAffectsRender? _subscribedToBrush; private IAffectsRender? _subscribedToDashes; - + private TargetWeakEventSubscriber? _weakSubscriber; + /// /// Initializes a new instance of the class. /// @@ -207,13 +208,24 @@ namespace Avalonia.Media { if ((_invalidated == null || field != value) && field != null) { - InvalidatedWeakEvent.Unsubscribe(field, this); + if (_weakSubscriber != null) + InvalidatedWeakEvent.Unsubscribe(field, _weakSubscriber); field = null; } if (_invalidated != null && field != value && value is IAffectsRender affectsRender) { - InvalidatedWeakEvent.Subscribe(affectsRender, this); + if (_weakSubscriber == null) + { + _weakSubscriber = new TargetWeakEventSubscriber( + this, static (target, _, ev, _) => + { + if (ev == InvalidatedWeakEvent) + target._invalidated?.Invoke(target, EventArgs.Empty); + }); + } + + InvalidatedWeakEvent.Subscribe(affectsRender, _weakSubscriber); field = affectsRender; } } @@ -223,11 +235,5 @@ namespace Avalonia.Media UpdateSubscription(ref _subscribedToBrush, Brush); UpdateSubscription(ref _subscribedToDashes, DashStyle); } - - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) - { - if (ev == InvalidatedWeakEvent) - _invalidated?.Invoke(this, EventArgs.Empty); - } } } diff --git a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs index e48c0cb111..6cf8c605a7 100644 --- a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs +++ b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs @@ -9,4 +9,31 @@ namespace Avalonia.Utilities; public interface IWeakEventSubscriber where TEventArgs : EventArgs { void OnEvent(object? sender, WeakEvent ev, TEventArgs e); -} \ No newline at end of file +} + +public sealed class WeakEventSubscriber : IWeakEventSubscriber where TEventArgs : EventArgs +{ + public event Action? Event; + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, TEventArgs e) + { + Event?.Invoke(sender, ev, e); + } +} + +public sealed class TargetWeakEventSubscriber : IWeakEventSubscriber where TEventArgs : EventArgs +{ + private readonly TTarget _target; + private readonly Action _dispatchFunc; + + public TargetWeakEventSubscriber(TTarget target, Action dispatchFunc) + { + _target = target; + _dispatchFunc = dispatchFunc; + } + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, TEventArgs e) + { + _dispatchFunc(_target, sender, ev, e); + } +} diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 0b32015a8a..e72606bf70 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -36,7 +37,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { if (!_subscriptions.TryGetValue(target, out var subscription)) _subscriptions.Add(target, subscription = new Subscription(this, target)); - subscription.Add(new WeakReference>(subscriber)); + subscription.Add(subscriber); } public void Unsubscribe(TSender target, IWeakEventSubscriber subscriber) @@ -51,11 +52,59 @@ public class WeakEvent : WeakEvent where TEventArgs : Event private readonly TSender _target; private readonly Action _compact; - private WeakReference>?[] _data = - new WeakReference>[16]; - private int _count; + struct Entry + { + WeakReference>? _reference; + int _hashCode; + + public Entry(IWeakEventSubscriber r) + { + if (r == null) + { + _reference = null; + _hashCode = 0; + return; + } + + _hashCode = r.GetHashCode(); + _reference = new WeakReference>(r); + } + + public bool IsEmpty + { + get + { + if (_reference == null) + return true; + if (_reference.TryGetTarget(out _)) + return false; + _reference = null; + return true; + } + } + + public bool TryGetTarget([MaybeNullWhen(false)]out IWeakEventSubscriber target) + { + if (_reference == null) + { + target = null!; + return false; + } + return _reference.TryGetTarget(out target); + } + + public bool Equals(IWeakEventSubscriber r) + { + if (_reference == null || r.GetHashCode() != _hashCode) + return false; + return _reference.TryGetTarget(out var target) && target == r; + } + } + private readonly Action _unsubscribe; + private readonly WeakHashList> _list = new(); private bool _compactScheduled; + private bool _destroyed; public Subscription(WeakEvent ev, TSender target) { @@ -67,48 +116,27 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Destroy() { + if(_destroyed) + return; + _destroyed = true; _unsubscribe(); _ev._subscriptions.Remove(_target); } - public void Add(WeakReference> s) - { - if (_count == _data.Length) - { - //Extend capacity - var extendedData = new WeakReference>?[_data.Length * 2]; - Array.Copy(_data, extendedData, _data.Length); - _data = extendedData; - } - - _data[_count] = s; - _count++; - } + public void Add(IWeakEventSubscriber s) => _list.Add(s); public void Remove(IWeakEventSubscriber s) { - var removed = false; - - for (int c = 0; c < _count; ++c) - { - var reference = _data[c]; - - if (reference != null && reference.TryGetTarget(out var instance) && instance == s) - { - _data[c] = null; - removed = true; - } - } - - if (removed) - { + _list.Remove(s); + if(_list.IsEmpty) + Destroy(); + else if(_list.NeedCompact && _compactScheduled) ScheduleCompact(); - } } void ScheduleCompact() { - if(_compactScheduled) + if(_compactScheduled || _destroyed) return; _compactScheduled = true; Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); @@ -116,43 +144,27 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Compact() { + if(!_compactScheduled) + return; _compactScheduled = false; - int empty = -1; - for (var c = 0; c < _count; c++) - { - var r = _data[c]; - //Mark current index as first empty - if (r == null && empty == -1) - empty = c; - //If current element isn't null and we have an empty one - if (r != null && empty != -1) - { - _data[c] = null; - _data[empty] = r; - empty++; - } - } - - if (empty != -1) - _count = empty; - if (_count == 0) + _list.Compact(); + if (_list.IsEmpty) Destroy(); } void OnEvent(object? sender, TEventArgs eventArgs) { - var needCompact = false; - for (var c = 0; c < _count; c++) + var alive = _list.GetAlive(); + if(alive == null) + Destroy(); + else { - var r = _data[c]; - if (r?.TryGetTarget(out var sub) == true) - sub!.OnEvent(_target, _ev, eventArgs); - else - needCompact = true; + foreach(var item in alive.Span) + item.OnEvent(_target, _ev, eventArgs); + WeakHashList>.ReturnToSharedPool(alive); + if(_list.NeedCompact && !_compactScheduled) + ScheduleCompact(); } - - if (needCompact) - ScheduleCompact(); } } diff --git a/src/Avalonia.Base/Utilities/WeakHashList.cs b/src/Avalonia.Base/Utilities/WeakHashList.cs new file mode 100644 index 0000000000..32668872da --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakHashList.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Utilities; + +internal class WeakHashList where T : class +{ + private struct Key + { + public WeakReference? Weak; + public T? Strong; + public int HashCode; + + public static Key MakeStrong(T r) => new() + { + HashCode = r.GetHashCode(), + Strong = r + }; + + public static Key MakeWeak(T r) => new() + { + HashCode = r.GetHashCode(), + Weak = new WeakReference(r) + }; + + public override int GetHashCode() => HashCode; + } + + class KeyComparer : IEqualityComparer + { + public bool Equals(Key x, Key y) + { + if (x.HashCode != y.HashCode) + return false; + if (x.Strong != null) + { + if (y.Strong != null) + return x.Strong == y.Strong; + if (y.Weak == null) + return false; + return y.Weak.TryGetTarget(out var weakTarget) && weakTarget == x.Strong; + } + else if (y.Strong != null) + { + if (x.Weak == null) + return false; + return x.Weak.TryGetTarget(out var weakTarget) && weakTarget == y.Strong; + } + else + { + if (x.Weak == null || x.Weak.TryGetTarget(out var xTarget) == false) + return y.Weak?.TryGetTarget(out _) != true; + return y.Weak?.TryGetTarget(out var yTarget) == true && xTarget == yTarget; + } + } + + public int GetHashCode(Key obj) => obj.HashCode; + public static KeyComparer Instance = new(); + } + + Dictionary? _dic; + WeakReference?[]? _arr; + int _arrCount; + + public bool IsEmpty => _dic == null || _dic.Count == 0; + public bool NeedCompact { get; private set; } + + public void Add(T item) + { + if (_dic != null) + { + var strongKey = Key.MakeStrong(item); + if (_dic.TryGetValue(strongKey, out var cnt)) + _dic[strongKey] = cnt + 1; + else + _dic[Key.MakeWeak(item)] = 1; + return; + } + + if (_arr == null) + _arr = new WeakReference[8]; + + if (_arrCount < _arr.Length) + { + _arr[_arrCount] = new WeakReference(item); + _arrCount++; + return; + } + + // Check if something is dead + for (var c = 0; c < _arrCount; c++) + { + if (_arr[c]!.TryGetTarget(out _) == false) + { + _arr[c] = new WeakReference(item); + return; + } + } + + _dic = new Dictionary(KeyComparer.Instance); + foreach (var existing in _arr) + { + if (existing!.TryGetTarget(out var target)) + Add(target); + } + _arr = null; + + } + + public void Remove(T item) + { + if (_arr != null) + { + for (var c = 0; c < _arr.Length; c++) + { + if (_arr[c]?.TryGetTarget(out var target) == true && target == item) + { + _arr[c] = null; + Compact(); + return; + } + } + } + else if (_dic != null) + { + var strongKey = Key.MakeStrong(item); + + if (_dic.TryGetValue(strongKey, out var cnt)) + { + if (cnt > 1) + { + _dic[strongKey] = cnt - 1; + return; + } + } + + _dic.Remove(strongKey); + } + } + + private void ArrCompact() + { + if (_arr != null) + { + int empty = -1; + for (var c = 0; c < _arrCount; c++) + { + var r = _arr[c]; + //Mark current index as first empty + if (r == null && empty == -1) + empty = c; + //If current element isn't null and we have an empty one + if (r != null && empty != -1) + { + _arr[c] = null; + _arr[empty] = r; + empty++; + } + } + + if (empty != -1) + _arrCount = empty; + } + } + + public void Compact() + { + if (_dic != null) + { + PooledList? toRemove = null; + foreach (var kvp in _dic) + { + if (kvp.Key.Weak?.TryGetTarget(out _) != true) + (toRemove ??= new PooledList()).Add(kvp.Key); + } + + if (toRemove != null) + { + foreach (var k in toRemove) + _dic.Remove(k); + toRemove.Dispose(); + } + } + } + + private static readonly Stack> s_listPool = new(); + + public static void ReturnToSharedPool(PooledList list) + { + list.Clear(); + s_listPool.Push(list); + } + + public PooledList? GetAlive(Func>? factory = null) + { + PooledList? pooled = null; + if (_arr != null) + { + bool needCompact = false; + for (var c = 0; c < _arrCount; c++) + { + if (_arr[c]?.TryGetTarget(out var target) == true) + (pooled ??= factory?.Invoke() + ?? (s_listPool.Count > 0 + ? s_listPool.Pop() + : new PooledList())).Add(target!); + else + { + _arr[c] = null; + needCompact = true; + } + } + if(needCompact) + ArrCompact(); + return pooled; + } + if (_dic != null) + { + + foreach (var kvp in _dic) + { + if (kvp.Key.Weak?.TryGetTarget(out var target) == true) + (pooled ??= factory?.Invoke() + ?? (s_listPool.Count > 0 + ? s_listPool.Pop() + : new PooledList())) + .Add(target!); + else + NeedCompact = true; + } + } + + return pooled; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 26a7d5d3b8..4fd21f02f9 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -97,12 +97,18 @@ namespace Avalonia /// public static readonly StyledProperty ZIndexProperty = AvaloniaProperty.Register(nameof(ZIndex)); + + private static readonly WeakEvent InvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.Invalidated += h, + (s, h) => s.Invalidated -= h); private Rect _bounds; private TransformedBounds? _transformedBounds; private IRenderRoot? _visualRoot; private IVisual? _visualParent; private bool _hasMirrorTransform; + private TargetWeakEventSubscriber? _affectsRenderWeakSubscriber; /// /// Initializes static members of the class. @@ -369,12 +375,21 @@ namespace Avalonia { if (e.OldValue is IAffectsRender oldValue) { - WeakEventHandlerManager.Unsubscribe(oldValue, nameof(oldValue.Invalidated), sender.AffectsRenderInvalidated); + if (sender._affectsRenderWeakSubscriber != null) + InvalidatedWeakEvent.Unsubscribe(oldValue, sender._affectsRenderWeakSubscriber); } if (e.NewValue is IAffectsRender newValue) { - WeakEventHandlerManager.Subscribe(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated); + if (sender._affectsRenderWeakSubscriber == null) + { + sender._affectsRenderWeakSubscriber = new TargetWeakEventSubscriber( + sender, static (target, _, _, _) => + { + target.InvalidateVisual(); + }); + } + InvalidatedWeakEvent.Subscribe(newValue, sender._affectsRenderWeakSubscriber); } sender.InvalidateVisual(); @@ -625,8 +640,6 @@ namespace Avalonia OnVisualParentChanged(old, value); } - private void AffectsRenderInvalidated(object? sender, EventArgs e) => InvalidateVisual(); - /// /// Called when the collection changes. /// diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 09c0e58332..c63adb0bf6 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber + public class ItemsRepeater : Panel, IChildIndexProvider { /// /// Defines the property. @@ -60,6 +60,7 @@ namespace Avalonia.Controls private readonly ViewManager _viewManager; private readonly ViewportManager _viewportManager; + private readonly TargetWeakEventSubscriber _layoutWeakSubscriber; private IEnumerable? _items; private VirtualizingLayoutContext? _layoutContext; private EventHandler? _childIndexChanged; @@ -74,6 +75,15 @@ namespace Avalonia.Controls /// public ItemsRepeater() { + _layoutWeakSubscriber = new TargetWeakEventSubscriber( + this, static (target, _, ev, _) => + { + if (ev == AttachedLayout.ArrangeInvalidatedWeakEvent) + target.InvalidateArrange(); + else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) + target.InvalidateMeasure(); + }); + _viewManager = new ViewManager(this); _viewportManager = new ViewportManager(this); KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once); @@ -728,8 +738,8 @@ namespace Avalonia.Controls { oldValue.UninitializeForContext(LayoutContext); - AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this); - AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this); + AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber); + AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -747,8 +757,8 @@ namespace Avalonia.Controls { newValue.InitializeForContext(LayoutContext); - AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this); - AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this); + AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); + AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; @@ -798,15 +808,7 @@ namespace Avalonia.Controls { _viewportManager.OnBringIntoViewRequested(e); } - - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) - { - if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent) - InvalidateArrange(); - else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) - InvalidateMeasure(); - } - + private VirtualizingLayoutContext GetLayoutContext() { if (_layoutContext == null) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 75a34659a2..57fb82485c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -34,8 +34,7 @@ namespace Avalonia.Controls ICloseable, IStyleHost, ILogicalRoot, - ITextInputMethodRoot, - IWeakEventSubscriber + ITextInputMethodRoot { /// /// Defines the property. @@ -93,6 +92,7 @@ namespace Avalonia.Controls private WindowTransparencyLevel _actualTransparencyLevel; private ILayoutManager? _layoutManager; private Border? _transparencyFallbackBorder; + private TargetWeakEventSubscriber? _resourcesChangesSubscriber; /// /// Initializes static members of the class. @@ -192,7 +192,13 @@ namespace Avalonia.Controls if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { - ResourcesChangedWeakEvent.Subscribe(applicationResources, this); + _resourcesChangesSubscriber = new TargetWeakEventSubscriber( + this, static (target, _, _, e) => + { + ((ILogical)target).NotifyResourcesChanged(e); + }); + + ResourcesChangedWeakEvent.Subscribe(applicationResources, _resourcesChangesSubscriber); } impl.LostFocus += PlatformImpl_LostFocus; @@ -297,11 +303,6 @@ namespace Avalonia.Controls /// IMouseDevice? IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, ResourcesChangedEventArgs e) - { - ((ILogical)this).NotifyResourcesChanged(e); - } - /// /// Gets or sets a value indicating whether access keys are shown in the window. /// diff --git a/tests/Avalonia.Benchmarks/Visuals/VisualAffectsRenderBenchmarks.cs b/tests/Avalonia.Benchmarks/Visuals/VisualAffectsRenderBenchmarks.cs new file mode 100644 index 0000000000..5b85a068d6 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Visuals/VisualAffectsRenderBenchmarks.cs @@ -0,0 +1,45 @@ +using Avalonia.Controls; +using Avalonia.Media; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Visuals; + +[MemoryDiagnoser] +public class VisualAffectsRenderBenchmarks +{ + private readonly TestVisual _target; + private readonly IPen _pen; + + public VisualAffectsRenderBenchmarks() + { + _target = new TestVisual(); + _pen = new Pen(Brushes.Black); + } + + [Benchmark] + public void SetPropertyThatAffectsRender() + { + _target.Pen = _pen; + _target.Pen = null; + } + + private class TestVisual : Visual + { + /// + /// Defines the property. + /// + public static readonly StyledProperty PenProperty = + AvaloniaProperty.Register(nameof(Pen)); + + public IPen Pen + { + get => GetValue(PenProperty); + set => SetValue(PenProperty, value); + } + + static TestVisual() + { + AffectsRender(PenProperty); + } + } +}