Skip to content

perf: Implement DependencyPropertyChangedEventArgs pool #20556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions src/Uno.UI.Tests/DependencyProperty/Given_DependencyProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1411,9 +1411,9 @@ public void When_DataContext_Changing()
var datacontext2 = new NullablePropertyOwner { MyNullable = 42 };
var datacontext3 = new NullablePropertyOwner { MyNullable = 84 };

var changes = new List<DependencyPropertyChangedEventArgs>();
var values = new List<object>();

SUT.MyNullableChanged += (snd, evt) => changes.Add(evt);
SUT.MyNullableChanged += (snd, evt) => values.Add(evt.NewValue);

SUT.SetBinding(
NullablePropertyOwner.MyNullableProperty,
Expand All @@ -1424,28 +1424,28 @@ public void When_DataContext_Changing()
);

SUT.DataContext = datacontext1;
changes.Count.Should().Be(1);
changes.Last().NewValue.Should().Be(42);
values.Count.Should().Be(1);
values.Last().Should().Be(42);

SUT.DataContext = datacontext2;
changes.Count.Should().Be(1); // Here we ensure we're not receiving a default value, still no changes
values.Count.Should().Be(1); // Here we ensure we're not receiving a default value, still no changes

SUT.DataContext = datacontext3;
changes.Count.Should().Be(2);
changes.Last().NewValue.Should().Be(84);
values.Count.Should().Be(2);
values.Last().Should().Be(84);

SUT.DataContext = null;
changes.Count.Should().Be(3);
changes.Last().NewValue.Should().Be(null);
values.Count.Should().Be(3);
values.Last().Should().Be(null);

var parent = new Border { Child = SUT };

parent.DataContext = datacontext1;
changes.Count.Should().Be(3);
values.Count.Should().Be(3);

SUT.DataContext = DependencyProperty.UnsetValue; // Propagate the datacontext from parent
changes.Count.Should().Be(4);
changes.Last().NewValue.Should().Be(42);
values.Count.Should().Be(4);
values.Last().Should().Be(42);
}

[TestMethod]
Expand Down
5 changes: 5 additions & 0 deletions src/Uno.UI/FeatureConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ public static class DependencyProperty
/// </summary>
public static bool DisableThreadingCheck { get; set; }

/// <summary>
/// Defines how many <see cref="DependencyPropertyChangedEventArgs" /> are pooled.
/// </summary>
public static int DependencyPropertyChangedEventArgsPoolSize { get; set; } = 32;

/// <summary>
/// Enables checks that make sure that <see cref="DependencyObjectStore.GetValue" /> and
/// <see cref="DependencyObjectStore.SetValue" /> are only called on the owner of the property being
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#nullable enable

using System.Diagnostics;

namespace Microsoft.UI.Xaml
{
public partial class DependencyObjectStore
{
internal class DependencyPropertyChangedEventArgsPool
{
private readonly Element[] _elements;
private DependencyPropertyChangedEventArgs? _spare;

private int _index;

public DependencyPropertyChangedEventArgsPool(int size)
{
Debug.Assert(size > 0, "Size must be greater than zero.");

_elements = new Element[size - 1];

_index = -1;
}

public DependencyPropertyChangedEventArgs Rent()
{
var result = _spare;

if (result != null)
{
_spare = null;
}
else
{
result = RentSlow();
}

return result;
}

public DependencyPropertyChangedEventArgs RentSlow()
{
var elements = _elements;

var index = (uint)_index;

// This (store _elements on stack and cast _index/elements.Length to uint) ensures the JIT
// won't emit bound checks for the array access.
if (index < (uint)elements.Length)
{
var result = elements[index].Value;

elements[index].Value = null;

_index--;

return result!;
}

return new();
}

public void Return(DependencyPropertyChangedEventArgs item)
{
if (_spare == null)
{
_spare = item;
}
else
{
ReturnSlow(item);
}
}

private void ReturnSlow(DependencyPropertyChangedEventArgs item)
{
var elements = _elements;

var index = (uint)(_index + 1);

// See RentSlow() comment.
if (index < (uint)elements.Length)
{
elements[index].Value = item;

_index++;
}
}

// We can avoid variance checks when storing elements in the array by using structs.
// Since structs do not support inheritance, they are not subject to variance checks (stelem is used rather than stelem.ref).
// See: https://devblogs.microsoft.com/premier-developer/managed-object-internals-part-3-the-layout-of-a-managed-array-3/
private struct Element
{
public DependencyPropertyChangedEventArgs? Value;
}
}
}
}
39 changes: 18 additions & 21 deletions src/Uno.UI/UI/Xaml/DependencyObjectStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1962,6 +1962,9 @@ DependencyPropertyValuePrecedences newPrecedence
}
}

private static DependencyPropertyChangedEventArgsPool _dpChangedEventArgsPool =
new(Uno.UI.FeatureConfiguration.DependencyProperty.DependencyPropertyChangedEventArgsPoolSize);

private void InvokeCallbacks(
DependencyObject actualInstanceAlias,
DependencyProperty property,
Expand Down Expand Up @@ -2034,16 +2037,19 @@ private void InvokeCallbacks(
// dependency property value through the cache.
propertyMetadata.RaiseBackingFieldUpdate(actualInstanceAlias, newValue);

DependencyPropertyChangedEventArgs? eventArgs = null;
var eventArgs = _dpChangedEventArgsPool.Rent();
eventArgs.PropertyInternal = property;
eventArgs.OldValueInternal = previousValue;
eventArgs.NewValueInternal = newValue;
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
eventArgs.OldPrecedenceInternal = previousPrecedence;
eventArgs.NewPrecedenceInternal = newPrecedence;
eventArgs.BypassesPropagationInternal = bypassesPropagation;
#endif

// Raise the changes for the callback register to the property itself
if (propertyMetadata.HasPropertyChanged)
{
eventArgs ??= new DependencyPropertyChangedEventArgs(property, previousValue, newValue
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
, previousPrecedence, newPrecedence, bypassesPropagation
#endif
);
propertyMetadata.RaisePropertyChangedNoNullCheck(actualInstanceAlias, eventArgs);
}

Expand All @@ -2055,22 +2061,12 @@ private void InvokeCallbacks(
// but before the registered property callbacks
if (actualInstanceAlias is IDependencyObjectInternal doInternal)
{
eventArgs ??= new DependencyPropertyChangedEventArgs(property, previousValue, newValue
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
, previousPrecedence, newPrecedence, bypassesPropagation
#endif
);
doInternal.OnPropertyChanged2(eventArgs);
}

// Raise the changes for the callbacks register through RegisterPropertyChangedCallback.
if (propertyDetails.CanRaisePropertyChanged)
{
eventArgs ??= new DependencyPropertyChangedEventArgs(property, previousValue, newValue
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
, previousPrecedence, newPrecedence, bypassesPropagation
#endif
);
propertyDetails.RaisePropertyChangedNoNullCheck(actualInstanceAlias, eventArgs);
}

Expand All @@ -2079,13 +2075,14 @@ private void InvokeCallbacks(
for (var callbackIndex = 0; callbackIndex < currentCallbacks.Length; callbackIndex++)
{
var callback = currentCallbacks[callbackIndex];
eventArgs ??= new DependencyPropertyChangedEventArgs(property, previousValue, newValue
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
, previousPrecedence, newPrecedence, bypassesPropagation
#endif
);
callback.Invoke(instanceRef, property, eventArgs);
}

// Cleanup to avoid leaks
eventArgs.OldValueInternal = null;
eventArgs.NewValueInternal = null;

_dpChangedEventArgsPool.Return(eventArgs);
}

private void CallChildCallback(DependencyObjectStore childStore, ManagedWeakReference instanceRef, DependencyProperty property, object? newValue)
Expand Down
70 changes: 17 additions & 53 deletions src/Uno.UI/UI/Xaml/DependencyPropertyChangedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,6 @@
using System;
using Uno.UI.DataBinding;
using System.Collections.Generic;
using Uno.Extensions;
using Uno.Foundation.Logging;
using Uno.Diagnostics.Eventing;
using Uno.Disposables;
using System.Linq;
using System.Threading;
using Uno.Collections;
using System.Runtime.CompilerServices;
using System.Diagnostics;
#pragma warning disable IDE1006 // Naming Styles

#if __ANDROID__
using View = Android.Views.View;
#elif __APPLE_UIKIT__
using View = UIKit.UIView;
#endif
using System;

namespace Microsoft.UI.Xaml
{
Expand All @@ -24,68 +9,47 @@ namespace Microsoft.UI.Xaml
/// </summary>
public sealed partial class DependencyPropertyChangedEventArgs : EventArgs
{
internal DependencyPropertyChangedEventArgs(
DependencyProperty property,
object oldValue,
object newValue
internal DependencyProperty PropertyInternal;
internal object NewValueInternal;
internal object OldValueInternal;
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
, DependencyPropertyValuePrecedences oldPrecedence,
DependencyPropertyValuePrecedences newPrecedence,
bool bypassesPropagation
internal DependencyPropertyValuePrecedences NewPrecedenceInternal;
internal DependencyPropertyValuePrecedences OldPrecedenceInternal;
internal bool BypassesPropagationInternal;
#endif
)

internal DependencyPropertyChangedEventArgs()
{
Property = property;
OldValue = oldValue;
NewValue = newValue;
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
OldPrecedence = oldPrecedence;
NewPrecedence = newPrecedence;
BypassesPropagation = bypassesPropagation;
#endif
}

public DependencyProperty Property => PropertyInternal;

/// <summary>
/// Gets the new value of the dependency property.
/// </summary>
public object NewValue { get; }

public object NewValue => NewValueInternal;
/// <summary>
/// Gets the old value of the dependency property.
/// </summary>
public object OldValue { get; }

public DependencyProperty Property { get; }
public object OldValue => OldValueInternal;

#if __APPLE_UIKIT__ || IS_UNIT_TESTS
/// <summary>
/// Gets the dependency property value precedence of the new value
/// </summary>
internal DependencyPropertyValuePrecedences NewPrecedence
{
get;
private set;
}
internal DependencyPropertyValuePrecedences NewPrecedence => NewPrecedenceInternal;

/// <summary>
/// Gets the dependency property value precedence of the old value
/// </summary>
internal DependencyPropertyValuePrecedences OldPrecedence
{
get;
private set;
}
internal DependencyPropertyValuePrecedences OldPrecedence => OldPrecedenceInternal;

/// <summary>
/// Is true if an animated value should be ignored when setting the native
/// value associated to it. Happens in the scenario of GPU bound animations
/// in iOS.
/// </summary>
internal bool BypassesPropagation
{
get;
private set;
}
internal bool BypassesPropagation => BypassesPropagationInternal;
#endif
}
}
Loading