Browse Source

Optimization: Add an optimized path for notifying property changes for inherited va… (#18223)

* Add an optimized path for notifying property changes for inherited values

Avoids many allocations of event args.

* fix unit tests with hack to prove concept.

* update docs and remove value tuple that could cause allocation.

* add a benchmark for inherited property change notifications.
pull/18558/head
Dan Walmsley 10 months ago
committed by GitHub
parent
commit
4ff49db604
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      src/Avalonia.Base/AvaloniaObject.cs
  2. 12
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  3. 40
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  4. 46
      tests/Avalonia.Benchmarks/Data/InheritedProperties.cs

18
src/Avalonia.Base/AvaloniaObject.cs

@ -749,6 +749,24 @@ namespace Avalonia
RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue, true); RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue, true);
} }
/// <summary>
/// This is an optimized path for <see cref="RaisePropertyChanged{T}(Avalonia.DirectPropertyBase{T},T,T)"/>.
/// This will reuse the event args in situations where many allocations would otherwise happen.
/// </summary>
/// <param name="args">Avalonia property change args</param>
/// <param name="inpcArgs">INPC event args/</param>
internal void RaisePropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> args, PropertyChangedEventArgs? inpcArgs)
{
OnPropertyChangedCore(args);
if (args.IsEffectiveValueChange && inpcArgs is not null)
{
args.Property.NotifyChanged(args);
_propertyChanged?.Invoke(this, args);
_inpcChanged?.Invoke(this, inpcArgs);
}
}
/// <summary> /// <summary>
/// Raises the <see cref="PropertyChanged"/> event. /// Raises the <see cref="PropertyChanged"/> event.
/// </summary> /// </summary>

12
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@ -31,7 +31,7 @@ namespace Avalonia
/// Gets the <see cref="AvaloniaObject"/> that the property changed on. /// Gets the <see cref="AvaloniaObject"/> that the property changed on.
/// </summary> /// </summary>
/// <value>The sender object.</value> /// <value>The sender object.</value>
public AvaloniaObject Sender { get; } public AvaloniaObject Sender { get; private set; }
/// <summary> /// <summary>
/// Gets the property that changed. /// Gets the property that changed.
@ -60,6 +60,16 @@ namespace Avalonia
public BindingPriority Priority { get; private set; } public BindingPriority Priority { get; private set; }
internal bool IsEffectiveValueChange { get; private set; } internal bool IsEffectiveValueChange { get; private set; }
/// <summary>
/// Sets the Sender property.
/// This is purely for reuse in some code paths where multiple allocations may occur.
/// </summary>
/// <param name="sender">The sender object.</param>
internal void SetSender(AvaloniaObject sender)
{
Sender = sender;
}
protected abstract AvaloniaProperty GetProperty(); protected abstract AvaloniaProperty GetProperty();
protected abstract object? GetOldValue(); protected abstract object? GetOldValue();

40
src/Avalonia.Base/PropertyStore/ValueStore.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
@ -590,10 +591,13 @@ namespace Avalonia.PropertyStore
return; return;
var count = children.Count; var count = children.Count;
var apArgs = new AvaloniaPropertyChangedEventArgs<T>(Owner, property, oldValue, value.Value, BindingPriority.Inherited, true);
var incpArgs = new PropertyChangedEventArgs(property.Name);
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, value.Value); children[i].GetValueStore().OnAncestorInheritedValueChanged(apArgs, incpArgs);
} }
} }
@ -614,9 +618,12 @@ namespace Avalonia.PropertyStore
{ {
var count = children.Count; var count = children.Count;
var apArgs = new AvaloniaPropertyChangedEventArgs<T>(Owner, property, oldValue, newValue, BindingPriority.Inherited, true);
var incpArgs = new PropertyChangedEventArgs(property.Name);
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue); children[i].GetValueStore().OnAncestorInheritedValueChanged(apArgs, incpArgs);
} }
} }
} }
@ -639,33 +646,24 @@ namespace Avalonia.PropertyStore
} }
} }
} }
/// <summary> /// <summary>
/// Called when an inherited property changes on the value store of the inheritance ancestor. /// Called when an inherited property changes on the value store of the inheritance ancestor.
/// </summary> /// </summary>
/// <typeparam name="T">The property type.</typeparam> /// <typeparam name="T">The property type.</typeparam>
/// <param name="property">The property.</param> /// <param name="apArgs">Avalonia Property EventArgs to reuse.</param>
/// <param name="oldValue">The old value of the property.</param> /// <param name="args">PropertyChangedEventArgs to reuse</param>
/// <param name="newValue">The new value of the property.</param> public void OnAncestorInheritedValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> apArgs, PropertyChangedEventArgs? args)
public void OnAncestorInheritedValueChanged<T>(
StyledProperty<T> property,
T oldValue,
T newValue)
{ {
Debug.Assert(property.Inherits);
// If the inherited value is set locally, propagation stops here. // If the inherited value is set locally, propagation stops here.
if (_effectiveValues.ContainsKey(property)) if (_effectiveValues.ContainsKey(apArgs.Property))
return; return;
using var notifying = PropertyNotifying.Start(Owner, property); using var notifying = PropertyNotifying.Start(Owner, apArgs.Property);
Owner.RaisePropertyChanged( apArgs.SetSender(Owner);
property, Owner.RaisePropertyChanged(apArgs, args);
oldValue,
newValue,
BindingPriority.Inherited,
true);
var children = Owner.GetInheritanceChildren(); var children = Owner.GetInheritanceChildren();
@ -676,7 +674,7 @@ namespace Avalonia.PropertyStore
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue); children[i].GetValueStore().OnAncestorInheritedValueChanged(apArgs, args);
} }
} }

46
tests/Avalonia.Benchmarks/Data/InheritedProperties.cs

@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Data;
[MemoryDiagnoser]
public class InheritedProperties
{
private readonly TestRoot _root;
private readonly List<Control> _controls = new();
public InheritedProperties()
{
var panel = new StackPanel();
_root = new TestRoot
{
Child = panel,
Renderer = new NullRenderer()
};
_controls.Add(panel);
_controls = ControlHierarchyCreator.CreateChildren(_controls, panel, 3, 5, 5);
_root.LayoutManager.ExecuteInitialLayoutPass();
}
[Benchmark, MethodImpl(MethodImplOptions.NoInlining)]
public void ChangeDataContext()
{
TestDataContext[] dataContexts = [new(), new(), new()];
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < dataContexts.Length; j++)
{
_root.DataContext = dataContexts[j];
}
}
}
public class TestDataContext;
}
Loading…
Cancel
Save