Skip to content

Commit 015ab89

Browse files
ebaricheDevTKSS
authored andcommitted
Merge pull request unoplatform#20556 from unoplatform/dev/eb/dpcea-pool
perf: Implement DependencyPropertyChangedEventArgs pool
2 parents 264ffcd + 1e9d5a2 commit 015ab89

File tree

5 files changed

+151
-86
lines changed

5 files changed

+151
-86
lines changed

src/Uno.UI.Tests/DependencyProperty/Given_DependencyProperty.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,9 +1411,9 @@ public void When_DataContext_Changing()
14111411
var datacontext2 = new NullablePropertyOwner { MyNullable = 42 };
14121412
var datacontext3 = new NullablePropertyOwner { MyNullable = 84 };
14131413

1414-
var changes = new List<DependencyPropertyChangedEventArgs>();
1414+
var values = new List<object>();
14151415

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

14181418
SUT.SetBinding(
14191419
NullablePropertyOwner.MyNullableProperty,
@@ -1424,28 +1424,28 @@ public void When_DataContext_Changing()
14241424
);
14251425

14261426
SUT.DataContext = datacontext1;
1427-
changes.Count.Should().Be(1);
1428-
changes.Last().NewValue.Should().Be(42);
1427+
values.Count.Should().Be(1);
1428+
values.Last().Should().Be(42);
14291429

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

14331433
SUT.DataContext = datacontext3;
1434-
changes.Count.Should().Be(2);
1435-
changes.Last().NewValue.Should().Be(84);
1434+
values.Count.Should().Be(2);
1435+
values.Last().Should().Be(84);
14361436

14371437
SUT.DataContext = null;
1438-
changes.Count.Should().Be(3);
1439-
changes.Last().NewValue.Should().Be(null);
1438+
values.Count.Should().Be(3);
1439+
values.Last().Should().Be(null);
14401440

14411441
var parent = new Border { Child = SUT };
14421442

14431443
parent.DataContext = datacontext1;
1444-
changes.Count.Should().Be(3);
1444+
values.Count.Should().Be(3);
14451445

14461446
SUT.DataContext = DependencyProperty.UnsetValue; // Propagate the datacontext from parent
1447-
changes.Count.Should().Be(4);
1448-
changes.Last().NewValue.Should().Be(42);
1447+
values.Count.Should().Be(4);
1448+
values.Last().Should().Be(42);
14491449
}
14501450

14511451
[TestMethod]

src/Uno.UI/FeatureConfiguration.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,11 @@ public static class DependencyProperty
934934
/// </summary>
935935
public static bool DisableThreadingCheck { get; set; }
936936

937+
/// <summary>
938+
/// Defines how many <see cref="DependencyPropertyChangedEventArgs" /> are pooled.
939+
/// </summary>
940+
public static int DependencyPropertyChangedEventArgsPoolSize { get; set; } = 32;
941+
937942
/// <summary>
938943
/// Enables checks that make sure that <see cref="DependencyObjectStore.GetValue" /> and
939944
/// <see cref="DependencyObjectStore.SetValue" /> are only called on the owner of the property being
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#nullable enable
2+
3+
using System.Diagnostics;
4+
5+
namespace Microsoft.UI.Xaml
6+
{
7+
public partial class DependencyObjectStore
8+
{
9+
internal class DependencyPropertyChangedEventArgsPool
10+
{
11+
private readonly Element[] _elements;
12+
private DependencyPropertyChangedEventArgs? _spare;
13+
14+
private int _index;
15+
16+
public DependencyPropertyChangedEventArgsPool(int size)
17+
{
18+
Debug.Assert(size > 0, "Size must be greater than zero.");
19+
20+
_elements = new Element[size - 1];
21+
22+
_index = -1;
23+
}
24+
25+
public DependencyPropertyChangedEventArgs Rent()
26+
{
27+
var result = _spare;
28+
29+
if (result != null)
30+
{
31+
_spare = null;
32+
}
33+
else
34+
{
35+
result = RentSlow();
36+
}
37+
38+
return result;
39+
}
40+
41+
public DependencyPropertyChangedEventArgs RentSlow()
42+
{
43+
var elements = _elements;
44+
45+
var index = (uint)_index;
46+
47+
// This (store _elements on stack and cast _index/elements.Length to uint) ensures the JIT
48+
// won't emit bound checks for the array access.
49+
if (index < (uint)elements.Length)
50+
{
51+
var result = elements[index].Value;
52+
53+
elements[index].Value = null;
54+
55+
_index--;
56+
57+
return result!;
58+
}
59+
60+
return new();
61+
}
62+
63+
public void Return(DependencyPropertyChangedEventArgs item)
64+
{
65+
if (_spare == null)
66+
{
67+
_spare = item;
68+
}
69+
else
70+
{
71+
ReturnSlow(item);
72+
}
73+
}
74+
75+
private void ReturnSlow(DependencyPropertyChangedEventArgs item)
76+
{
77+
var elements = _elements;
78+
79+
var index = (uint)(_index + 1);
80+
81+
// See RentSlow() comment.
82+
if (index < (uint)elements.Length)
83+
{
84+
elements[index].Value = item;
85+
86+
_index++;
87+
}
88+
}
89+
90+
// We can avoid variance checks when storing elements in the array by using structs.
91+
// Since structs do not support inheritance, they are not subject to variance checks (stelem is used rather than stelem.ref).
92+
// See: https://devblogs.microsoft.com/premier-developer/managed-object-internals-part-3-the-layout-of-a-managed-array-3/
93+
private struct Element
94+
{
95+
public DependencyPropertyChangedEventArgs? Value;
96+
}
97+
}
98+
}
99+
}

src/Uno.UI/UI/Xaml/DependencyObjectStore.cs

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1962,6 +1962,9 @@ DependencyPropertyValuePrecedences newPrecedence
19621962
}
19631963
}
19641964

1965+
private static DependencyPropertyChangedEventArgsPool _dpChangedEventArgsPool =
1966+
new(Uno.UI.FeatureConfiguration.DependencyProperty.DependencyPropertyChangedEventArgsPoolSize);
1967+
19651968
private void InvokeCallbacks(
19661969
DependencyObject actualInstanceAlias,
19671970
DependencyProperty property,
@@ -2034,16 +2037,19 @@ private void InvokeCallbacks(
20342037
// dependency property value through the cache.
20352038
propertyMetadata.RaiseBackingFieldUpdate(actualInstanceAlias, newValue);
20362039

2037-
DependencyPropertyChangedEventArgs? eventArgs = null;
2040+
var eventArgs = _dpChangedEventArgsPool.Rent();
2041+
eventArgs.PropertyInternal = property;
2042+
eventArgs.OldValueInternal = previousValue;
2043+
eventArgs.NewValueInternal = newValue;
2044+
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
2045+
eventArgs.OldPrecedenceInternal = previousPrecedence;
2046+
eventArgs.NewPrecedenceInternal = newPrecedence;
2047+
eventArgs.BypassesPropagationInternal = bypassesPropagation;
2048+
#endif
20382049

20392050
// Raise the changes for the callback register to the property itself
20402051
if (propertyMetadata.HasPropertyChanged)
20412052
{
2042-
eventArgs ??= new DependencyPropertyChangedEventArgs(property, previousValue, newValue
2043-
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
2044-
, previousPrecedence, newPrecedence, bypassesPropagation
2045-
#endif
2046-
);
20472053
propertyMetadata.RaisePropertyChangedNoNullCheck(actualInstanceAlias, eventArgs);
20482054
}
20492055

@@ -2055,22 +2061,12 @@ private void InvokeCallbacks(
20552061
// but before the registered property callbacks
20562062
if (actualInstanceAlias is IDependencyObjectInternal doInternal)
20572063
{
2058-
eventArgs ??= new DependencyPropertyChangedEventArgs(property, previousValue, newValue
2059-
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
2060-
, previousPrecedence, newPrecedence, bypassesPropagation
2061-
#endif
2062-
);
20632064
doInternal.OnPropertyChanged2(eventArgs);
20642065
}
20652066

20662067
// Raise the changes for the callbacks register through RegisterPropertyChangedCallback.
20672068
if (propertyDetails.CanRaisePropertyChanged)
20682069
{
2069-
eventArgs ??= new DependencyPropertyChangedEventArgs(property, previousValue, newValue
2070-
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
2071-
, previousPrecedence, newPrecedence, bypassesPropagation
2072-
#endif
2073-
);
20742070
propertyDetails.RaisePropertyChangedNoNullCheck(actualInstanceAlias, eventArgs);
20752071
}
20762072

@@ -2079,13 +2075,14 @@ private void InvokeCallbacks(
20792075
for (var callbackIndex = 0; callbackIndex < currentCallbacks.Length; callbackIndex++)
20802076
{
20812077
var callback = currentCallbacks[callbackIndex];
2082-
eventArgs ??= new DependencyPropertyChangedEventArgs(property, previousValue, newValue
2083-
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
2084-
, previousPrecedence, newPrecedence, bypassesPropagation
2085-
#endif
2086-
);
20872078
callback.Invoke(instanceRef, property, eventArgs);
20882079
}
2080+
2081+
// Cleanup to avoid leaks
2082+
eventArgs.OldValueInternal = null;
2083+
eventArgs.NewValueInternal = null;
2084+
2085+
_dpChangedEventArgsPool.Return(eventArgs);
20892086
}
20902087

20912088
private void CallChildCallback(DependencyObjectStore childStore, ManagedWeakReference instanceRef, DependencyProperty property, object? newValue)
Lines changed: 17 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,6 @@
1-
using System;
2-
using Uno.UI.DataBinding;
3-
using System.Collections.Generic;
4-
using Uno.Extensions;
5-
using Uno.Foundation.Logging;
6-
using Uno.Diagnostics.Eventing;
7-
using Uno.Disposables;
8-
using System.Linq;
9-
using System.Threading;
10-
using Uno.Collections;
11-
using System.Runtime.CompilerServices;
12-
using System.Diagnostics;
1+
#pragma warning disable IDE1006 // Naming Styles
132

14-
#if __ANDROID__
15-
using View = Android.Views.View;
16-
#elif __APPLE_UIKIT__
17-
using View = UIKit.UIView;
18-
#endif
3+
using System;
194

205
namespace Microsoft.UI.Xaml
216
{
@@ -24,68 +9,47 @@ namespace Microsoft.UI.Xaml
249
/// </summary>
2510
public sealed partial class DependencyPropertyChangedEventArgs : EventArgs
2611
{
27-
internal DependencyPropertyChangedEventArgs(
28-
DependencyProperty property,
29-
object oldValue,
30-
object newValue
12+
internal DependencyProperty PropertyInternal;
13+
internal object NewValueInternal;
14+
internal object OldValueInternal;
3115
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
32-
, DependencyPropertyValuePrecedences oldPrecedence,
33-
DependencyPropertyValuePrecedences newPrecedence,
34-
bool bypassesPropagation
16+
internal DependencyPropertyValuePrecedences NewPrecedenceInternal;
17+
internal DependencyPropertyValuePrecedences OldPrecedenceInternal;
18+
internal bool BypassesPropagationInternal;
3519
#endif
36-
)
20+
21+
internal DependencyPropertyChangedEventArgs()
3722
{
38-
Property = property;
39-
OldValue = oldValue;
40-
NewValue = newValue;
41-
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
42-
OldPrecedence = oldPrecedence;
43-
NewPrecedence = newPrecedence;
44-
BypassesPropagation = bypassesPropagation;
45-
#endif
4623
}
4724

25+
public DependencyProperty Property => PropertyInternal;
26+
4827
/// <summary>
4928
/// Gets the new value of the dependency property.
5029
/// </summary>
51-
public object NewValue { get; }
52-
30+
public object NewValue => NewValueInternal;
5331
/// <summary>
5432
/// Gets the old value of the dependency property.
5533
/// </summary>
56-
public object OldValue { get; }
57-
58-
public DependencyProperty Property { get; }
34+
public object OldValue => OldValueInternal;
5935

6036
#if __APPLE_UIKIT__ || IS_UNIT_TESTS
6137
/// <summary>
6238
/// Gets the dependency property value precedence of the new value
6339
/// </summary>
64-
internal DependencyPropertyValuePrecedences NewPrecedence
65-
{
66-
get;
67-
private set;
68-
}
40+
internal DependencyPropertyValuePrecedences NewPrecedence => NewPrecedenceInternal;
6941

7042
/// <summary>
7143
/// Gets the dependency property value precedence of the old value
7244
/// </summary>
73-
internal DependencyPropertyValuePrecedences OldPrecedence
74-
{
75-
get;
76-
private set;
77-
}
45+
internal DependencyPropertyValuePrecedences OldPrecedence => OldPrecedenceInternal;
7846

7947
/// <summary>
8048
/// Is true if an animated value should be ignored when setting the native
8149
/// value associated to it. Happens in the scenario of GPU bound animations
8250
/// in iOS.
8351
/// </summary>
84-
internal bool BypassesPropagation
85-
{
86-
get;
87-
private set;
88-
}
52+
internal bool BypassesPropagation => BypassesPropagationInternal;
8953
#endif
9054
}
9155
}

0 commit comments

Comments
 (0)