Browse Source

Merge pull request #7372 from AvaloniaUI/feature/new-weak-events-for-visual

AffectsRender subscription optimization
pull/8023/head
Dariusz Komosiński 4 years ago
committed by GitHub
parent
commit
ad2ae75187
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      src/Avalonia.Base/Media/Pen.cs
  2. 29
      src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs
  3. 138
      src/Avalonia.Base/Utilities/WeakEvent.cs
  4. 236
      src/Avalonia.Base/Utilities/WeakHashList.cs
  5. 21
      src/Avalonia.Base/Visual.cs
  6. 30
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  7. 17
      src/Avalonia.Controls/TopLevel.cs
  8. 45
      tests/Avalonia.Benchmarks/Visuals/VisualAffectsRenderBenchmarks.cs

26
src/Avalonia.Base/Media/Pen.cs

@ -7,7 +7,7 @@ namespace Avalonia.Media
/// <summary>
/// Describes how a stroke is drawn.
/// </summary>
public sealed class Pen : AvaloniaObject, IPen, IWeakEventSubscriber<EventArgs>
public sealed class Pen : AvaloniaObject, IPen
{
/// <summary>
/// Defines the <see cref="Brush"/> property.
@ -48,7 +48,8 @@ namespace Avalonia.Media
private EventHandler? _invalidated;
private IAffectsRender? _subscribedToBrush;
private IAffectsRender? _subscribedToDashes;
private TargetWeakEventSubscriber<Pen, EventArgs>? _weakSubscriber;
/// <summary>
/// Initializes a new instance of the <see cref="Pen"/> class.
/// </summary>
@ -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<Pen, EventArgs>(
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<EventArgs>.OnEvent(object? sender, WeakEvent ev, EventArgs e)
{
if (ev == InvalidatedWeakEvent)
_invalidated?.Invoke(this, EventArgs.Empty);
}
}
}

29
src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs

@ -9,4 +9,31 @@ namespace Avalonia.Utilities;
public interface IWeakEventSubscriber<in TEventArgs> where TEventArgs : EventArgs
{
void OnEvent(object? sender, WeakEvent ev, TEventArgs e);
}
}
public sealed class WeakEventSubscriber<TEventArgs> : IWeakEventSubscriber<TEventArgs> where TEventArgs : EventArgs
{
public event Action<object?, WeakEvent, TEventArgs>? Event;
void IWeakEventSubscriber<TEventArgs>.OnEvent(object? sender, WeakEvent ev, TEventArgs e)
{
Event?.Invoke(sender, ev, e);
}
}
public sealed class TargetWeakEventSubscriber<TTarget, TEventArgs> : IWeakEventSubscriber<TEventArgs> where TEventArgs : EventArgs
{
private readonly TTarget _target;
private readonly Action<TTarget, object?, WeakEvent, TEventArgs> _dispatchFunc;
public TargetWeakEventSubscriber(TTarget target, Action<TTarget, object?, WeakEvent, TEventArgs> dispatchFunc)
{
_target = target;
_dispatchFunc = dispatchFunc;
}
void IWeakEventSubscriber<TEventArgs>.OnEvent(object? sender, WeakEvent ev, TEventArgs e)
{
_dispatchFunc(_target, sender, ev, e);
}
}

138
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<TSender, TEventArgs> : WeakEvent where TEventArgs : Event
{
if (!_subscriptions.TryGetValue(target, out var subscription))
_subscriptions.Add(target, subscription = new Subscription(this, target));
subscription.Add(new WeakReference<IWeakEventSubscriber<TEventArgs>>(subscriber));
subscription.Add(subscriber);
}
public void Unsubscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
@ -51,11 +52,59 @@ public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : Event
private readonly TSender _target;
private readonly Action _compact;
private WeakReference<IWeakEventSubscriber<TEventArgs>>?[] _data =
new WeakReference<IWeakEventSubscriber<TEventArgs>>[16];
private int _count;
struct Entry
{
WeakReference<IWeakEventSubscriber<TEventArgs>>? _reference;
int _hashCode;
public Entry(IWeakEventSubscriber<TEventArgs> r)
{
if (r == null)
{
_reference = null;
_hashCode = 0;
return;
}
_hashCode = r.GetHashCode();
_reference = new WeakReference<IWeakEventSubscriber<TEventArgs>>(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<TEventArgs> target)
{
if (_reference == null)
{
target = null!;
return false;
}
return _reference.TryGetTarget(out target);
}
public bool Equals(IWeakEventSubscriber<TEventArgs> r)
{
if (_reference == null || r.GetHashCode() != _hashCode)
return false;
return _reference.TryGetTarget(out var target) && target == r;
}
}
private readonly Action _unsubscribe;
private readonly WeakHashList<IWeakEventSubscriber<TEventArgs>> _list = new();
private bool _compactScheduled;
private bool _destroyed;
public Subscription(WeakEvent<TSender, TEventArgs> ev, TSender target)
{
@ -67,48 +116,27 @@ public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : Event
void Destroy()
{
if(_destroyed)
return;
_destroyed = true;
_unsubscribe();
_ev._subscriptions.Remove(_target);
}
public void Add(WeakReference<IWeakEventSubscriber<TEventArgs>> s)
{
if (_count == _data.Length)
{
//Extend capacity
var extendedData = new WeakReference<IWeakEventSubscriber<TEventArgs>>?[_data.Length * 2];
Array.Copy(_data, extendedData, _data.Length);
_data = extendedData;
}
_data[_count] = s;
_count++;
}
public void Add(IWeakEventSubscriber<TEventArgs> s) => _list.Add(s);
public void Remove(IWeakEventSubscriber<TEventArgs> 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<TSender, TEventArgs> : 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<IWeakEventSubscriber<TEventArgs>>.ReturnToSharedPool(alive);
if(_list.NeedCompact && !_compactScheduled)
ScheduleCompact();
}
if (needCompact)
ScheduleCompact();
}
}

236
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<T> where T : class
{
private struct Key
{
public WeakReference<T>? 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<T>(r)
};
public override int GetHashCode() => HashCode;
}
class KeyComparer : IEqualityComparer<Key>
{
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<Key, int>? _dic;
WeakReference<T>?[]? _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<T>[8];
if (_arrCount < _arr.Length)
{
_arr[_arrCount] = new WeakReference<T>(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<T>(item);
return;
}
}
_dic = new Dictionary<Key, int>(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<Key>? toRemove = null;
foreach (var kvp in _dic)
{
if (kvp.Key.Weak?.TryGetTarget(out _) != true)
(toRemove ??= new PooledList<Key>()).Add(kvp.Key);
}
if (toRemove != null)
{
foreach (var k in toRemove)
_dic.Remove(k);
toRemove.Dispose();
}
}
}
private static readonly Stack<PooledList<T>> s_listPool = new();
public static void ReturnToSharedPool(PooledList<T> list)
{
list.Clear();
s_listPool.Push(list);
}
public PooledList<T>? GetAlive(Func<PooledList<T>>? factory = null)
{
PooledList<T>? 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<T>())).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<T>()))
.Add(target!);
else
NeedCompact = true;
}
}
return pooled;
}
}

21
src/Avalonia.Base/Visual.cs

@ -97,12 +97,18 @@ namespace Avalonia
/// </summary>
public static readonly StyledProperty<int> ZIndexProperty =
AvaloniaProperty.Register<Visual, int>(nameof(ZIndex));
private static readonly WeakEvent<IAffectsRender, EventArgs> InvalidatedWeakEvent =
WeakEvent.Register<IAffectsRender>(
(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<Visual, EventArgs>? _affectsRenderWeakSubscriber;
/// <summary>
/// Initializes static members of the <see cref="Visual"/> class.
@ -369,12 +375,21 @@ namespace Avalonia
{
if (e.OldValue is IAffectsRender oldValue)
{
WeakEventHandlerManager.Unsubscribe<EventArgs, T>(oldValue, nameof(oldValue.Invalidated), sender.AffectsRenderInvalidated);
if (sender._affectsRenderWeakSubscriber != null)
InvalidatedWeakEvent.Unsubscribe(oldValue, sender._affectsRenderWeakSubscriber);
}
if (e.NewValue is IAffectsRender newValue)
{
WeakEventHandlerManager.Subscribe<IAffectsRender, EventArgs, T>(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated);
if (sender._affectsRenderWeakSubscriber == null)
{
sender._affectsRenderWeakSubscriber = new TargetWeakEventSubscriber<Visual, EventArgs>(
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();
/// <summary>
/// Called when the <see cref="VisualChildren"/> collection changes.
/// </summary>

30
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.
/// </summary>
public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber<EventArgs>
public class ItemsRepeater : Panel, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property.
@ -60,6 +60,7 @@ namespace Avalonia.Controls
private readonly ViewManager _viewManager;
private readonly ViewportManager _viewportManager;
private readonly TargetWeakEventSubscriber<ItemsRepeater, EventArgs> _layoutWeakSubscriber;
private IEnumerable? _items;
private VirtualizingLayoutContext? _layoutContext;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
@ -74,6 +75,15 @@ namespace Avalonia.Controls
/// </summary>
public ItemsRepeater()
{
_layoutWeakSubscriber = new TargetWeakEventSubscriber<ItemsRepeater, EventArgs>(
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<EventArgs>.OnEvent(object? sender, WeakEvent ev, EventArgs e)
{
if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent)
InvalidateArrange();
else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent)
InvalidateMeasure();
}
private VirtualizingLayoutContext GetLayoutContext()
{
if (_layoutContext == null)

17
src/Avalonia.Controls/TopLevel.cs

@ -34,8 +34,7 @@ namespace Avalonia.Controls
ICloseable,
IStyleHost,
ILogicalRoot,
ITextInputMethodRoot,
IWeakEventSubscriber<ResourcesChangedEventArgs>
ITextInputMethodRoot
{
/// <summary>
/// Defines the <see cref="ClientSize"/> property.
@ -93,6 +92,7 @@ namespace Avalonia.Controls
private WindowTransparencyLevel _actualTransparencyLevel;
private ILayoutManager? _layoutManager;
private Border? _transparencyFallbackBorder;
private TargetWeakEventSubscriber<TopLevel, ResourcesChangedEventArgs>? _resourcesChangesSubscriber;
/// <summary>
/// Initializes static members of the <see cref="TopLevel"/> class.
@ -192,7 +192,13 @@ namespace Avalonia.Controls
if (((IStyleHost)this).StylingParent is IResourceHost applicationResources)
{
ResourcesChangedWeakEvent.Subscribe(applicationResources, this);
_resourcesChangesSubscriber = new TargetWeakEventSubscriber<TopLevel, ResourcesChangedEventArgs>(
this, static (target, _, _, e) =>
{
((ILogical)target).NotifyResourcesChanged(e);
});
ResourcesChangedWeakEvent.Subscribe(applicationResources, _resourcesChangesSubscriber);
}
impl.LostFocus += PlatformImpl_LostFocus;
@ -297,11 +303,6 @@ namespace Avalonia.Controls
/// <inheritdoc/>
IMouseDevice? IInputRoot.MouseDevice => PlatformImpl?.MouseDevice;
void IWeakEventSubscriber<ResourcesChangedEventArgs>.OnEvent(object? sender, WeakEvent ev, ResourcesChangedEventArgs e)
{
((ILogical)this).NotifyResourcesChanged(e);
}
/// <summary>
/// Gets or sets a value indicating whether access keys are shown in the window.
/// </summary>

45
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
{
/// <summary>
/// Defines the <see cref="Pen"/> property.
/// </summary>
public static readonly StyledProperty<IPen> PenProperty =
AvaloniaProperty.Register<Border, IPen>(nameof(Pen));
public IPen Pen
{
get => GetValue(PenProperty);
set => SetValue(PenProperty, value);
}
static TestVisual()
{
AffectsRender<TestVisual>(PenProperty);
}
}
}
Loading…
Cancel
Save