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);
}
/// <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>
/// Raises the <see cref="PropertyChanged"/> event.
/// </summary>

12
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@ -31,7 +31,7 @@ namespace Avalonia
/// Gets the <see cref="AvaloniaObject"/> that the property changed on.
/// </summary>
/// <value>The sender object.</value>
public AvaloniaObject Sender { get; }
public AvaloniaObject Sender { get; private set; }
/// <summary>
/// Gets the property that changed.
@ -60,6 +60,16 @@ namespace Avalonia
public BindingPriority Priority { 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 object? GetOldValue();

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

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