浏览代码

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.
Dan Walmsley 6 月之前
父节点
当前提交
4ff49db604

+ 18 - 0
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>

+ 11 - 1
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();

+ 19 - 21
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 - 0
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;
+}