Ver código fonte

Reduce memory usage of binding operations.

Dariusz Komosinski 6 anos atrás
pai
commit
8e60e83d4c

+ 10 - 2
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@@ -173,12 +173,20 @@ namespace Avalonia
             Contract.Requires<ArgumentNullException>(type != null);
             Contract.Requires<ArgumentNullException>(name != null);
 
-            if (name.Contains('.'))
+            if (name.Contains("."))
             {
                 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>

+ 14 - 2
src/Avalonia.Base/Data/BindingOperations.cs

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

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

+ 17 - 2
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

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

+ 11 - 1
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@@ -41,7 +41,17 @@ namespace Avalonia.Data.Core
         {
             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);
 
             if (_enableValidation && Next == null)

+ 1 - 1
src/Avalonia.Base/Utilities/IdentifierParser.cs

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

+ 43 - 0
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);
+            }
+        }
+    }
+}