Skip to content

Commit

Permalink
perf: Reduce delegates use in BindingPath property changed
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jeromelaban committed Jan 20, 2022
1 parent 6245c41 commit b93c527
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 30 deletions.
118 changes: 93 additions & 25 deletions src/Uno.UI/DataBinding/BindingPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace Uno.UI.DataBinding
[DebuggerDisplay("Path={_path} DataContext={_dataContext}")]
internal class BindingPath : IDisposable, IValueChangedListener
{
private static List<PropertyChangedRegistrationHandler> _propertyChangedHandlers = new List<PropertyChangedRegistrationHandler>();
private static List<IPropertyChangedRegistrationHandler> _propertyChangedHandlers = new List<IPropertyChangedRegistrationHandler>(2);
private readonly string _path;

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

/// <summary>
/// Defines a delegate that will create a registration on the specified <paramref name="dataContext"/>> for the specified <paramref name="propertyName"/>.
/// Defines a interface that will allow for the creation of a registration on the specified dataContext
/// for the specified propertyName.
/// </summary>
/// <param name="dataContext">The datacontext to use</param>
/// <param name="propertyName">The property in the datacontext</param>
/// <param name="onNewValue">The action to execute when a new value is raised</param>
/// <returns>A disposable that will cleanup resources.</returns>
public delegate IDisposable? PropertyChangedRegistrationHandler(ManagedWeakReference dataContext, string propertyName, Action onNewValue);
public interface IPropertyChangedRegistrationHandler
{
/// <summary>
/// Registere a new <see cref="IPropertyChangedValueHandler"/> for the specified property
/// </summary>
/// <param name="dataContext">The datacontext to use</param>
/// <param name="propertyName">The property in the datacontext</param>
/// <param name="onNewValue">The action to execute when a new value is raised</param>
/// <returns>A disposable that will cleanup resources.</returns>
IDisposable? Register(ManagedWeakReference dataContext, string propertyName, IPropertyChangedValueHandler onNewValue);
}

/// <summary>
/// PropertyChanged value handler.
/// </summary>
/// <remarks>
/// This is an interface to avoid the use of delegates, and delegates type conversion as
/// there are two available signatures. (<see cref="Action"/> and <see cref="DependencyPropertyChangedCallback"/>)
/// </remarks>
public interface IPropertyChangedValueHandler
{
/// <summary>
/// Process a property changed using the <see cref="DependencyPropertyChangedCallback"/> signature.
/// </summary>
void NewValue(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args);

/// <summary>
/// Processa a property changed using <see cref="Action"/>-like signature (e.g. for <see cref="BindingItem"/>)
/// </summary>
void NewValue();
}

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

static BindingPath()
{
RegisterPropertyChangedRegistrationHandler(SubscribeToNotifyPropertyChanged);
RegisterPropertyChangedRegistrationHandler(new BindingPathPropertyChangedRegistrationHandler());
}

/// <summary>
Expand Down Expand Up @@ -122,7 +149,7 @@ internal void CloneShareableObjectsInPath()
/// <remarks>This method exists to provide layer separation,
/// when BindingPath is in the presentation layer, and DependencyProperty is in the (some) Views layer.
/// </remarks>
public static void RegisterPropertyChangedRegistrationHandler(PropertyChangedRegistrationHandler handler)
public static void RegisterPropertyChangedRegistrationHandler(IPropertyChangedRegistrationHandler handler)
{
_propertyChangedHandlers.Add(handler);
}
Expand Down Expand Up @@ -279,6 +306,15 @@ protected virtual void Dispose(bool disposing)
}
}

/// <summary>
/// Property changed registration handler for BindingPath.
/// </summary>
private class BindingPathPropertyChangedRegistrationHandler : IPropertyChangedRegistrationHandler
{
public IDisposable? Register(ManagedWeakReference dataContext, string propertyName, IPropertyChangedValueHandler onNewValue)
=> SubscribeToNotifyPropertyChanged(dataContext, propertyName, onNewValue);
}

#region Miscs helpers
/// <summary>
/// Parse the given string path in parts and create the linked list of binding items in head and tail
Expand Down Expand Up @@ -374,7 +410,7 @@ protected virtual void Dispose(bool disposing)
/// <summary>
/// Subscribes for updates to the INotifyPropertyChanged interface.
/// </summary>
private static IDisposable? SubscribeToNotifyPropertyChanged(ManagedWeakReference dataContextReference, string propertyName, Action newValueAction)
private static IDisposable? SubscribeToNotifyPropertyChanged(ManagedWeakReference dataContextReference, string propertyName, IPropertyChangedValueHandler propertyChangedValueHandler)
{
// Attach to the Notify property changed events
var notify = dataContextReference.Target as System.ComponentModel.INotifyPropertyChanged;
Expand All @@ -386,7 +422,7 @@ protected virtual void Dispose(bool disposing)
propertyName = "Item" + propertyName;
}

var newValueActionWeak = Uno.UI.DataBinding.WeakReferencePool.RentWeakReference(null, newValueAction);
var newValueActionWeak = Uno.UI.DataBinding.WeakReferencePool.RentWeakReference(null, propertyChangedValueHandler);

System.ComponentModel.PropertyChangedEventHandler handler = (s, args) =>
{
Expand All @@ -397,9 +433,10 @@ protected virtual void Dispose(bool disposing)
typeof(BindingPath).Log().Debug($"Property changed for {propertyName} on [{dataContextReference.Target?.GetType()}]");
}
if (!newValueActionWeak.IsDisposed)
if (!newValueActionWeak.IsDisposed
&& newValueActionWeak.Target is IPropertyChangedValueHandler handler)
{
(newValueActionWeak.Target as Action)?.Invoke();
handler.NewValue();
}
}
};
Expand Down Expand Up @@ -808,22 +845,14 @@ private IDisposable SubscribeToPropertyChanged()
for (var i = 0; i < _propertyChangedHandlers.Count; i++)
{
var handler = _propertyChangedHandlers[i];
object? previousValue = default;

Action? updateProperty = () =>
{
var newValue = GetSourceValue();
OnPropertyChanged(previousValue, newValue, shouldRaiseValueChanged: true);
previousValue = newValue;
};
var valueHandler = new PropertyChangedValueHandler(this);

var handlerDisposable = handler(_dataContextWeakStorage!, PropertyName, updateProperty);
var handlerDisposable = handler.Register(_dataContextWeakStorage!, PropertyName, valueHandler);

if (handlerDisposable != null)
{
previousValue = GetSourceValue();
valueHandler.PreviousValue = GetSourceValue();

// We need to keep the reference to the updatePropertyHandler
// in this disposable. The reference is attached to the source's
Expand All @@ -833,7 +862,9 @@ private IDisposable SubscribeToPropertyChanged()
// weak with regards to the delegates that are provided.
disposables.Add(() =>
{
updateProperty = null;
var previousValue = valueHandler.PreviousValue;
valueHandler = null;
handlerDisposable.Dispose();
OnPropertyChanged(previousValue, DependencyProperty.UnsetValue, shouldRaiseValueChanged: false);
});
Expand All @@ -848,6 +879,43 @@ public void Dispose()
_disposed = true;
_propertyChanged.Dispose();
}

/// <summary>
/// Property changed value handler, used to avoid creating a delegate for processing
/// </summary>
/// <remarks>
/// This class is primarily used to avoid the costs associated with creating, storing and invoking delegates,
/// particularly on WebAssembly as of .NET 6 where invoking a delegate requires a context switch from AOT
/// to the interpreter.
/// </remarks>
private class PropertyChangedValueHandler : IPropertyChangedValueHandler, IWeakReferenceProvider
{
private readonly BindingItem _owner;
private readonly ManagedWeakReference _self;

public PropertyChangedValueHandler(BindingItem owner)
{
_owner = owner;
_self = WeakReferencePool.RentSelfWeakReference(this);
}

public object? PreviousValue { get; set; }

public ManagedWeakReference WeakReference
=> _self;

public void NewValue()
{
var newValue = _owner.GetSourceValue();

_owner.OnPropertyChanged(PreviousValue, newValue, shouldRaiseValueChanged: true);

PreviousValue = newValue;
}

public void NewValue(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
=> NewValue();
}
}
}
}
Expand Down
17 changes: 12 additions & 5 deletions src/Uno.UI/UI/Xaml/DependencyObjectStore.Binder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ private string GetOwnerDebugString()
static void InitializeStaticBinder()
{
// Register the ability for the BindingPath to subscribe to dependency property changes.
BindingPath.RegisterPropertyChangedRegistrationHandler(SubscribeToDependencyPropertyChanged);
BindingPath.RegisterPropertyChangedRegistrationHandler(new BindingPathPropertyChangedRegistrationHandler());
}

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

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

if (dp != null)
{
Windows.UI.Xaml.PropertyChangedCallback handler = (s, e) => newValueAction();

return Windows.UI.Xaml.DependencyObjectExtensions
.RegisterDisposablePropertyChangedCallback(dependencyObject, dp, handler);
.RegisterDisposablePropertyChangedCallback(dependencyObject, dp, newValueAction.NewValue);
}
else
{
Expand Down Expand Up @@ -657,6 +655,15 @@ public BindingExpression GetBindingExpression(DependencyProperty dependencyPrope

public Windows.UI.Xaml.Data.Binding? GetBinding(DependencyProperty dependencyProperty)
=> GetBindingExpression(dependencyProperty)?.ParentBinding;

/// <summary>
/// BindingPath Registration handler for DependencyProperty instances
/// </summary>
private class BindingPathPropertyChangedRegistrationHandler : BindingPath.IPropertyChangedRegistrationHandler
{
public IDisposable? Register(ManagedWeakReference dataContext, string propertyName, BindingPath.IPropertyChangedValueHandler onNewValue)
=> SubscribeToDependencyPropertyChanged(dataContext, propertyName, onNewValue);
}
}
}

0 comments on commit b93c527

Please sign in to comment.