Browse Source

Avoid closures and extra allocations caused by transition observables.

Dariusz Komosinski 4 years ago
parent
commit
5765007ecd

+ 15 - 0
src/Avalonia.Animation/AnimatorDrivenTransition.cs

@@ -0,0 +1,15 @@
+using System;
+using Avalonia.Animation.Animators;
+
+namespace Avalonia.Animation
+{
+    public class AnimatorDrivenTransition<T, TAnimator> : Transition<T> where TAnimator : Animator<T>, new()
+    {
+        private static readonly TAnimator s_animator = new TAnimator();
+
+        public override IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue)
+        {
+            return new AnimatorTransitionObservable<T, TAnimator>(s_animator, progress, oldValue, newValue);
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Animation/AnimatorTransitionObservable.cs

@@ -0,0 +1,24 @@
+using System;
+using Avalonia.Animation.Animators;
+
+namespace Avalonia.Animation
+{
+    public class AnimatorTransitionObservable<T, TAnimator> : TransitionObservableBase<T> where TAnimator : Animator<T>
+    {
+        private readonly TAnimator _animator;
+        private readonly T _oldValue;
+        private readonly T _newValue;
+
+        public AnimatorTransitionObservable(TAnimator animator, IObservable<double> progress, T oldValue, T newValue) : base(progress)
+        {
+            _animator = animator;
+            _oldValue = oldValue;
+            _newValue = newValue;
+        }
+
+        protected override T ProduceValue(double progress)
+        {
+            return _animator.Interpolate(progress, _oldValue, _newValue);
+        }
+    }
+}

+ 2 - 4
src/Avalonia.Animation/Transition`1.cs → src/Avalonia.Animation/Transition.cs

@@ -1,7 +1,5 @@
-using System;
-using System.Reactive.Linq;
+using System;
 using Avalonia.Animation.Easings;
-using Avalonia.Animation.Utils;
 
 namespace Avalonia.Animation
 {
@@ -56,4 +54,4 @@ namespace Avalonia.Animation
             return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
         }
     }
-}
+}

+ 17 - 6
src/Avalonia.Animation/TransitionInstance.cs

@@ -1,8 +1,4 @@
-using Avalonia.Metadata;
 using System;
-using System.Reactive.Linq;
-using Avalonia.Animation.Easings;
-using Avalonia.Animation.Utils;
 using Avalonia.Reactive;
 using Avalonia.Utilities;
 
@@ -11,7 +7,7 @@ namespace Avalonia.Animation
     /// <summary>
     /// Handles the timing and lifetime of a <see cref="Transition{T}"/>.
     /// </summary>
-    internal class TransitionInstance : SingleSubscriberObservableBase<double>
+    internal class TransitionInstance : SingleSubscriberObservableBase<double>, IObserver<TimeSpan>
     {
         private IDisposable _timerSubscription;
         private TimeSpan _delay;
@@ -76,8 +72,23 @@ namespace Avalonia.Animation
         protected override void Subscribed()
         {
             _clock = new Clock(_baseClock);
-            _timerSubscription = _clock.Subscribe(TimerTick);
+            _timerSubscription = _clock.Subscribe(this);
             PublishNext(0.0d);
         }
+
+        void IObserver<TimeSpan>.OnCompleted()
+        {
+            PublishCompleted();
+        }
+
+        void IObserver<TimeSpan>.OnError(Exception error)
+        {
+            PublishError(error);
+        }
+
+        void IObserver<TimeSpan>.OnNext(TimeSpan value)
+        {
+            TimerTick(value);
+        }
     }
 }

+ 45 - 0
src/Avalonia.Animation/TransitionObservableBase.cs

@@ -0,0 +1,45 @@
+using System;
+using Avalonia.Reactive;
+
+#nullable enable
+
+namespace Avalonia.Animation
+{
+    public abstract class TransitionObservableBase<T> : SingleSubscriberObservableBase<T>, IObserver<double>
+    {
+        private readonly IObservable<double> _progress;
+        private IDisposable? _progressSubscription;
+
+        protected TransitionObservableBase(IObservable<double> progress)
+        {
+            _progress = progress;
+        }
+
+        protected override void Unsubscribed()
+        {
+            _progressSubscription?.Dispose();
+        }
+
+        protected override void Subscribed()
+        {
+            _progressSubscription = _progress.Subscribe(this);
+        }
+
+        protected abstract T ProduceValue(double progress);
+
+        void IObserver<double>.OnCompleted()
+        {
+            PublishCompleted();
+        }
+
+        void IObserver<double>.OnError(Exception error)
+        {
+            PublishError(error);
+        }
+
+        void IObserver<double>.OnNext(double value)
+        {
+            PublishNext(ProduceValue(value));
+        }
+    }
+}

+ 5 - 2
src/Avalonia.Animation/Transitions/DoubleTransition.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Reactive.Linq;
-
 using Avalonia.Animation.Animators;
 
 namespace Avalonia.Animation
@@ -8,7 +7,11 @@ namespace Avalonia.Animation
     /// <summary>
     /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="double"/> types.
     /// </summary>  
-    public class DoubleTransition : Transition<double>
+    public class DoubleTransition : AnimatorDrivenTransition<double, DoubleAnimator>
+    {
+    }
+
+    public class DoubleTransitionOld : Transition<double>
     {
         private static readonly DoubleAnimator s_animator = new DoubleAnimator();
 

+ 8 - 10
src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs

@@ -1,28 +1,26 @@
 using System;
-using System.Reactive.Linq;
 using Avalonia.Animation.Animators;
 using Avalonia.Media;
+using Avalonia.Media.Transformation;
+
+#nullable enable
 
 namespace Avalonia.Animation
 {
     public class TransformOperationsTransition : Transition<ITransform>
     {
-        private static readonly TransformOperationsAnimator _operationsAnimator =  new TransformOperationsAnimator();
+        private static readonly TransformOperationsAnimator s_operationsAnimator = new TransformOperationsAnimator();
 
-        public override IObservable<ITransform> DoTransition(IObservable<double> progress,
+        public override IObservable<ITransform> DoTransition(
+            IObservable<double> progress,
             ITransform oldValue,
             ITransform newValue)
         {
             var oldTransform = TransformOperationsAnimator.EnsureOperations(oldValue);
             var newTransform = TransformOperationsAnimator.EnsureOperations(newValue);
 
-            return progress
-                .Select(p =>
-                {
-                    var f = Easing.Ease(p);
-
-                    return _operationsAnimator.Interpolate(f, oldTransform, newTransform);
-                });
+            return new AnimatorTransitionObservable<TransformOperations, TransformOperationsAnimator>(
+                s_operationsAnimator, progress, oldTransform, newTransform);
         }
     }
 }

+ 92 - 0
tests/Avalonia.Benchmarks/Animations/TransitionBenchmark.cs

@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Reactive.Subjects;
+using System.Runtime.CompilerServices;
+using Avalonia.Animation;
+using Avalonia.Layout;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Animations
+{
+    [MemoryDiagnoser]
+    public class TransitionBenchmark
+    {
+        private readonly DoubleTransition _transition;
+        private readonly DoubleTransitionOld _oldTransition;
+        private readonly int _frameCount;
+        private readonly Subject<double> _timeProducer;
+        private readonly List<double> _producedValues;
+
+        public TransitionBenchmark()
+        {
+            _frameCount = 100;
+
+            _oldTransition = new DoubleTransitionOld
+            {
+                Duration = TimeSpan.FromMilliseconds(_frameCount), Property = Layoutable.WidthProperty
+            };
+
+            _transition = new DoubleTransition
+            {
+                Duration = TimeSpan.FromMilliseconds(_frameCount), Property = Layoutable.WidthProperty
+            };
+
+            _timeProducer = new Subject<double>();
+            _producedValues = new List<double>(_frameCount);
+        }
+
+        [Benchmark(Baseline = true)]
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        public void OldTransition()
+        {
+            TransitionCommon(_oldTransition);
+        }
+
+        [Benchmark]
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        public void NewTransition()
+        {
+            TransitionCommon(_transition);
+        }
+
+        private void TransitionCommon(Transition<double> transition)
+        {
+            var transitionObs = transition.DoTransition(_timeProducer, 0, 1);
+
+            _producedValues.Clear();
+
+            using var transitionSub = transitionObs.Subscribe(new AddValueObserver(_producedValues));
+
+            for (int i = 0; i < _frameCount; i++)
+            {
+                _timeProducer.OnNext(TimeSpan.FromMilliseconds(i).TotalSeconds);
+            }
+
+            Debug.Assert(_producedValues.Count == _frameCount);
+        }
+
+        private class AddValueObserver : IObserver<double>
+        {
+            private readonly List<double> _values;
+
+            public AddValueObserver(List<double> values)
+            {
+                _values = values;
+            }
+
+            public void OnCompleted()
+            {
+            }
+
+            public void OnError(Exception error)
+            {
+            }
+
+            public void OnNext(double value)
+            {
+                _values.Add(value);
+            }
+        }
+    }
+}