Skip to content

Commit b93c527

Browse files
committed
perf: Reduce delegates use in BindingPath property changed
This change significantly reduces the number of delegates created during the propagation of property changes. This reduces the memory footprint, and improves the invocation performance under Wasm MixedAOT.
1 parent 6245c41 commit b93c527

File tree

2 files changed

+105
-30
lines changed

2 files changed

+105
-30
lines changed

src/Uno.UI/DataBinding/BindingPath.cs

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace Uno.UI.DataBinding
2222
[DebuggerDisplay("Path={_path} DataContext={_dataContext}")]
2323
internal class BindingPath : IDisposable, IValueChangedListener
2424
{
25-
private static List<PropertyChangedRegistrationHandler> _propertyChangedHandlers = new List<PropertyChangedRegistrationHandler>();
25+
private static List<IPropertyChangedRegistrationHandler> _propertyChangedHandlers = new List<IPropertyChangedRegistrationHandler>(2);
2626
private readonly string _path;
2727

2828
private BindingItem? _chain;
@@ -31,13 +31,40 @@ internal class BindingPath : IDisposable, IValueChangedListener
3131
private bool _disposed;
3232

3333
/// <summary>
34-
/// Defines a delegate that will create a registration on the specified <paramref name="dataContext"/>> for the specified <paramref name="propertyName"/>.
34+
/// Defines a interface that will allow for the creation of a registration on the specified dataContext
35+
/// for the specified propertyName.
3536
/// </summary>
36-
/// <param name="dataContext">The datacontext to use</param>
37-
/// <param name="propertyName">The property in the datacontext</param>
38-
/// <param name="onNewValue">The action to execute when a new value is raised</param>
39-
/// <returns>A disposable that will cleanup resources.</returns>
40-
public delegate IDisposable? PropertyChangedRegistrationHandler(ManagedWeakReference dataContext, string propertyName, Action onNewValue);
37+
public interface IPropertyChangedRegistrationHandler
38+
{
39+
/// <summary>
40+
/// Registere a new <see cref="IPropertyChangedValueHandler"/> for the specified property
41+
/// </summary>
42+
/// <param name="dataContext">The datacontext to use</param>
43+
/// <param name="propertyName">The property in the datacontext</param>
44+
/// <param name="onNewValue">The action to execute when a new value is raised</param>
45+
/// <returns>A disposable that will cleanup resources.</returns>
46+
IDisposable? Register(ManagedWeakReference dataContext, string propertyName, IPropertyChangedValueHandler onNewValue);
47+
}
48+
49+
/// <summary>
50+
/// PropertyChanged value handler.
51+
/// </summary>
52+
/// <remarks>
53+
/// This is an interface to avoid the use of delegates, and delegates type conversion as
54+
/// there are two available signatures. (<see cref="Action"/> and <see cref="DependencyPropertyChangedCallback"/>)
55+
/// </remarks>
56+
public interface IPropertyChangedValueHandler
57+
{
58+
/// <summary>
59+
/// Process a property changed using the <see cref="DependencyPropertyChangedCallback"/> signature.
60+
/// </summary>
61+
void NewValue(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args);
62+
63+
/// <summary>
64+
/// Processa a property changed using <see cref="Action"/>-like signature (e.g. for <see cref="BindingItem"/>)
65+
/// </summary>
66+
void NewValue();
67+
}
4168

4269
/// <summary>
4370
/// Provides the new values for the current binding.
@@ -49,7 +76,7 @@ internal class BindingPath : IDisposable, IValueChangedListener
4976

5077
static BindingPath()
5178
{
52-
RegisterPropertyChangedRegistrationHandler(SubscribeToNotifyPropertyChanged);
79+
RegisterPropertyChangedRegistrationHandler(new BindingPathPropertyChangedRegistrationHandler());
5380
}
5481

5582
/// <summary>
@@ -122,7 +149,7 @@ internal void CloneShareableObjectsInPath()
122149
/// <remarks>This method exists to provide layer separation,
123150
/// when BindingPath is in the presentation layer, and DependencyProperty is in the (some) Views layer.
124151
/// </remarks>
125-
public static void RegisterPropertyChangedRegistrationHandler(PropertyChangedRegistrationHandler handler)
152+
public static void RegisterPropertyChangedRegistrationHandler(IPropertyChangedRegistrationHandler handler)
126153
{
127154
_propertyChangedHandlers.Add(handler);
128155
}
@@ -279,6 +306,15 @@ protected virtual void Dispose(bool disposing)
279306
}
280307
}
281308

309+
/// <summary>
310+
/// Property changed registration handler for BindingPath.
311+
/// </summary>
312+
private class BindingPathPropertyChangedRegistrationHandler : IPropertyChangedRegistrationHandler
313+
{
314+
public IDisposable? Register(ManagedWeakReference dataContext, string propertyName, IPropertyChangedValueHandler onNewValue)
315+
=> SubscribeToNotifyPropertyChanged(dataContext, propertyName, onNewValue);
316+
}
317+
282318
#region Miscs helpers
283319
/// <summary>
284320
/// Parse the given string path in parts and create the linked list of binding items in head and tail
@@ -374,7 +410,7 @@ private static void TryPrependItem(
374410
/// <summary>
375411
/// Subscribes for updates to the INotifyPropertyChanged interface.
376412
/// </summary>
377-
private static IDisposable? SubscribeToNotifyPropertyChanged(ManagedWeakReference dataContextReference, string propertyName, Action newValueAction)
413+
private static IDisposable? SubscribeToNotifyPropertyChanged(ManagedWeakReference dataContextReference, string propertyName, IPropertyChangedValueHandler propertyChangedValueHandler)
378414
{
379415
// Attach to the Notify property changed events
380416
var notify = dataContextReference.Target as System.ComponentModel.INotifyPropertyChanged;
@@ -386,7 +422,7 @@ private static void TryPrependItem(
386422
propertyName = "Item" + propertyName;
387423
}
388424

389-
var newValueActionWeak = Uno.UI.DataBinding.WeakReferencePool.RentWeakReference(null, newValueAction);
425+
var newValueActionWeak = Uno.UI.DataBinding.WeakReferencePool.RentWeakReference(null, propertyChangedValueHandler);
390426

391427
System.ComponentModel.PropertyChangedEventHandler handler = (s, args) =>
392428
{
@@ -397,9 +433,10 @@ private static void TryPrependItem(
397433
typeof(BindingPath).Log().Debug($"Property changed for {propertyName} on [{dataContextReference.Target?.GetType()}]");
398434
}
399435

400-
if (!newValueActionWeak.IsDisposed)
436+
if (!newValueActionWeak.IsDisposed
437+
&& newValueActionWeak.Target is IPropertyChangedValueHandler handler)
401438
{
402-
(newValueActionWeak.Target as Action)?.Invoke();
439+
handler.NewValue();
403440
}
404441
}
405442
};
@@ -808,22 +845,14 @@ private IDisposable SubscribeToPropertyChanged()
808845
for (var i = 0; i < _propertyChangedHandlers.Count; i++)
809846
{
810847
var handler = _propertyChangedHandlers[i];
811-
object? previousValue = default;
812848

813-
Action? updateProperty = () =>
814-
{
815-
var newValue = GetSourceValue();
816-
817-
OnPropertyChanged(previousValue, newValue, shouldRaiseValueChanged: true);
818-
819-
previousValue = newValue;
820-
};
849+
var valueHandler = new PropertyChangedValueHandler(this);
821850

822-
var handlerDisposable = handler(_dataContextWeakStorage!, PropertyName, updateProperty);
851+
var handlerDisposable = handler.Register(_dataContextWeakStorage!, PropertyName, valueHandler);
823852

824853
if (handlerDisposable != null)
825854
{
826-
previousValue = GetSourceValue();
855+
valueHandler.PreviousValue = GetSourceValue();
827856

828857
// We need to keep the reference to the updatePropertyHandler
829858
// in this disposable. The reference is attached to the source's
@@ -833,7 +862,9 @@ private IDisposable SubscribeToPropertyChanged()
833862
// weak with regards to the delegates that are provided.
834863
disposables.Add(() =>
835864
{
836-
updateProperty = null;
865+
var previousValue = valueHandler.PreviousValue;
866+
867+
valueHandler = null;
837868
handlerDisposable.Dispose();
838869
OnPropertyChanged(previousValue, DependencyProperty.UnsetValue, shouldRaiseValueChanged: false);
839870
});
@@ -848,6 +879,43 @@ public void Dispose()
848879
_disposed = true;
849880
_propertyChanged.Dispose();
850881
}
882+
883+
/// <summary>
884+
/// Property changed value handler, used to avoid creating a delegate for processing
885+
/// </summary>
886+
/// <remarks>
887+
/// This class is primarily used to avoid the costs associated with creating, storing and invoking delegates,
888+
/// particularly on WebAssembly as of .NET 6 where invoking a delegate requires a context switch from AOT
889+
/// to the interpreter.
890+
/// </remarks>
891+
private class PropertyChangedValueHandler : IPropertyChangedValueHandler, IWeakReferenceProvider
892+
{
893+
private readonly BindingItem _owner;
894+
private readonly ManagedWeakReference _self;
895+
896+
public PropertyChangedValueHandler(BindingItem owner)
897+
{
898+
_owner = owner;
899+
_self = WeakReferencePool.RentSelfWeakReference(this);
900+
}
901+
902+
public object? PreviousValue { get; set; }
903+
904+
public ManagedWeakReference WeakReference
905+
=> _self;
906+
907+
public void NewValue()
908+
{
909+
var newValue = _owner.GetSourceValue();
910+
911+
_owner.OnPropertyChanged(PreviousValue, newValue, shouldRaiseValueChanged: true);
912+
913+
PreviousValue = newValue;
914+
}
915+
916+
public void NewValue(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
917+
=> NewValue();
918+
}
851919
}
852920
}
853921
}

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ private string GetOwnerDebugString()
191191
static void InitializeStaticBinder()
192192
{
193193
// Register the ability for the BindingPath to subscribe to dependency property changes.
194-
BindingPath.RegisterPropertyChangedRegistrationHandler(SubscribeToDependencyPropertyChanged);
194+
BindingPath.RegisterPropertyChangedRegistrationHandler(new BindingPathPropertyChangedRegistrationHandler());
195195
}
196196

197197
internal DependencyProperty DataContextProperty => _dataContextProperty!;
@@ -532,7 +532,7 @@ internal void SetBindingValue(DependencyPropertyDetails propertyDetails, object
532532
/// <param name="newValueAction">The action to execute when a new value is raised</param>
533533
/// <param name="disposeAction">The action to execute when the listener wants to dispose the subscription</param>
534534
/// <returns></returns>
535-
private static IDisposable? SubscribeToDependencyPropertyChanged(ManagedWeakReference dataContextReference, string propertyName, Action newValueAction)
535+
private static IDisposable? SubscribeToDependencyPropertyChanged(ManagedWeakReference dataContextReference, string propertyName, BindingPath.IPropertyChangedValueHandler newValueAction)
536536
{
537537
var dependencyObject = dataContextReference.Target as DependencyObject;
538538

@@ -542,10 +542,8 @@ internal void SetBindingValue(DependencyPropertyDetails propertyDetails, object
542542

543543
if (dp != null)
544544
{
545-
Windows.UI.Xaml.PropertyChangedCallback handler = (s, e) => newValueAction();
546-
547545
return Windows.UI.Xaml.DependencyObjectExtensions
548-
.RegisterDisposablePropertyChangedCallback(dependencyObject, dp, handler);
546+
.RegisterDisposablePropertyChangedCallback(dependencyObject, dp, newValueAction.NewValue);
549547
}
550548
else
551549
{
@@ -657,6 +655,15 @@ public BindingExpression GetBindingExpression(DependencyProperty dependencyPrope
657655

658656
public Windows.UI.Xaml.Data.Binding? GetBinding(DependencyProperty dependencyProperty)
659657
=> GetBindingExpression(dependencyProperty)?.ParentBinding;
658+
659+
/// <summary>
660+
/// BindingPath Registration handler for DependencyProperty instances
661+
/// </summary>
662+
private class BindingPathPropertyChangedRegistrationHandler : BindingPath.IPropertyChangedRegistrationHandler
663+
{
664+
public IDisposable? Register(ManagedWeakReference dataContext, string propertyName, BindingPath.IPropertyChangedValueHandler onNewValue)
665+
=> SubscribeToDependencyPropertyChanged(dataContext, propertyName, onNewValue);
666+
}
660667
}
661668
}
662669

0 commit comments

Comments
 (0)