Browse Source

Reduce memory usage of binding operations.

pull/3265/head
Dariusz Komosinski 6 years ago
parent
commit
8e60e83d4c
  1. 12
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  2. 16
      src/Avalonia.Base/Data/BindingOperations.cs
  3. 6
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  4. 19
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  5. 12
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  6. 2
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  7. 43
      tests/Avalonia.Benchmarks/Data/BindingOperations.cs

12
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -173,12 +173,20 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(type != null); Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(name != null); Contract.Requires<ArgumentNullException>(name != null);
if (name.Contains('.')) if (name.Contains("."))
{ {
throw new InvalidOperationException("Attached properties not supported."); throw new InvalidOperationException("Attached properties not supported.");
} }
return GetRegistered(type).FirstOrDefault(x => x.Name == name); foreach (AvaloniaProperty x in GetRegistered(type))
{
if (x.Name == name)
{
return x;
}
}
return null;
} }
/// <summary> /// <summary>

16
src/Avalonia.Base/Data/BindingOperations.cs

@ -56,22 +56,34 @@ namespace Avalonia.Data
if (source != null) if (source != null)
{ {
// Perf: Avoid allocating closure in the outer scope.
var targetCopy = target;
var propertyCopy = property;
var bindingCopy = binding;
return source return source
.Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
.Take(1) .Take(1)
.Subscribe(x => target.SetValue(property, x, binding.Priority)); .Subscribe(x => targetCopy.SetValue(propertyCopy, x, bindingCopy.Priority));
} }
else else
{ {
target.SetValue(property, binding.Value, binding.Priority); target.SetValue(property, binding.Value, binding.Priority);
return Disposable.Empty; return Disposable.Empty;
} }
case BindingMode.OneWayToSource: case BindingMode.OneWayToSource:
{
// Perf: Avoid allocating closure in the outer scope.
var bindingCopy = binding;
return Observable.CombineLatest( return Observable.CombineLatest(
binding.Observable, binding.Observable,
target.GetObservable(property), target.GetObservable(property),
(_, v) => v) (_, v) => v)
.Subscribe(x => binding.Subject.OnNext(x)); .Subscribe(x => bindingCopy.Subject.OnNext(x));
}
default: default:
throw new ArgumentException("Invalid binding mode."); throw new ArgumentException("Invalid binding mode.");
} }

6
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -21,7 +21,7 @@ namespace Avalonia.Data.Core
/// An ordered collection of property accessor plugins that can be used to customize /// An ordered collection of property accessor plugins that can be used to customize
/// the reading and subscription of property values on a type. /// the reading and subscription of property values on a type.
/// </summary> /// </summary>
public static readonly IList<IPropertyAccessorPlugin> PropertyAccessors = public static readonly List<IPropertyAccessorPlugin> PropertyAccessors =
new List<IPropertyAccessorPlugin> new List<IPropertyAccessorPlugin>
{ {
new AvaloniaPropertyAccessorPlugin(), new AvaloniaPropertyAccessorPlugin(),
@ -33,7 +33,7 @@ namespace Avalonia.Data.Core
/// An ordered collection of validation checker plugins that can be used to customize /// An ordered collection of validation checker plugins that can be used to customize
/// the validation of view model and model data. /// the validation of view model and model data.
/// </summary> /// </summary>
public static readonly IList<IDataValidationPlugin> DataValidators = public static readonly List<IDataValidationPlugin> DataValidators =
new List<IDataValidationPlugin> new List<IDataValidationPlugin>
{ {
new DataAnnotationsValidationPlugin(), new DataAnnotationsValidationPlugin(),
@ -45,7 +45,7 @@ namespace Avalonia.Data.Core
/// An ordered collection of stream plugins that can be used to customize the behavior /// An ordered collection of stream plugins that can be used to customize the behavior
/// of the '^' stream binding operator. /// of the '^' stream binding operator.
/// </summary> /// </summary>
public static readonly IList<IStreamPlugin> StreamHandlers = public static readonly List<IStreamPlugin> StreamHandlers =
new List<IStreamPlugin> new List<IStreamPlugin>
{ {
new TaskStreamPlugin(), new TaskStreamPlugin(),

19
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -3,6 +3,7 @@
using System; using System;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Runtime.ExceptionServices;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
@ -76,7 +77,7 @@ namespace Avalonia.Data.Core.Plugins
return false; return false;
} }
private class Accessor : PropertyAccessorBase private class Accessor : PropertyAccessorBase, IObserver<object>
{ {
private readonly WeakReference<AvaloniaObject> _reference; private readonly WeakReference<AvaloniaObject> _reference;
private readonly AvaloniaProperty _property; private readonly AvaloniaProperty _property;
@ -117,7 +118,7 @@ namespace Avalonia.Data.Core.Plugins
protected override void SubscribeCore() protected override void SubscribeCore()
{ {
_subscription = Instance?.GetObservable(_property).Subscribe(PublishValue); _subscription = Instance?.GetObservable(_property).Subscribe(this);
} }
protected override void UnsubscribeCore() protected override void UnsubscribeCore()
@ -125,6 +126,20 @@ namespace Avalonia.Data.Core.Plugins
_subscription?.Dispose(); _subscription?.Dispose();
_subscription = null; _subscription = null;
} }
void IObserver<object>.OnCompleted()
{
}
void IObserver<object>.OnError(Exception error)
{
ExceptionDispatchInfo.Capture(error).Throw();
}
void IObserver<object>.OnNext(object value)
{
PublishValue(value);
}
} }
} }
} }

12
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -41,7 +41,17 @@ namespace Avalonia.Data.Core
{ {
reference.TryGetTarget(out object target); reference.TryGetTarget(out object target);
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName)); IPropertyAccessorPlugin plugin = null;
foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors)
{
if (x.Match(target, PropertyName))
{
plugin = x;
break;
}
}
var accessor = plugin?.Start(reference, PropertyName); var accessor = plugin?.Start(reference, PropertyName);
if (_enableValidation && Next == null) if (_enableValidation && Next == null)

2
src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -15,7 +15,7 @@ namespace Avalonia.Utilities
{ {
if (IsValidIdentifierStart(r.Peek)) if (IsValidIdentifierStart(r.Peek))
{ {
return r.TakeWhile(IsValidIdentifierChar); return r.TakeWhile(c => IsValidIdentifierChar(c));
} }
else else
{ {

43
tests/Avalonia.Benchmarks/Data/BindingOperations.cs

@ -0,0 +1,43 @@
using Avalonia.Data;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Data
{
[MemoryDiagnoser, InProcess]
public class BindingsBenchmark
{
[Benchmark]
public void TwoWayBinding_Via_Binding()
{
var instance = new TestClass();
var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay)
{
Source = instance
};
instance.Bind(TestClass.IntValueProperty, binding);
}
private class TestClass : AvaloniaObject
{
public static readonly StyledProperty<int> IntValueProperty =
AvaloniaProperty.Register<TestClass, int>(nameof(IntValue));
public static readonly StyledProperty<int> BoundValueProperty =
AvaloniaProperty.Register<TestClass, int>(nameof(BoundValue));
public int IntValue
{
get => GetValue(IntValueProperty);
set => SetValue(IntValueProperty, value);
}
public int BoundValue
{
get => GetValue(BoundValueProperty);
set => SetValue(BoundValueProperty, value);
}
}
}
}
Loading…
Cancel
Save