Browse Source

Make UiThreadRenderTimer to calculate the next tick time based on expected FPS (#17121)

* Make UiThreadRenderTimer to calculate the next tick time based on expected FPS

* Address review
Nikita Tsukanov 1 year ago
parent
commit
8742dbdf78
1 changed files with 59 additions and 23 deletions
  1. 59 23
      src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs

+ 59 - 23
src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs

@@ -3,39 +3,75 @@ using System.Diagnostics;
 using Avalonia.Metadata;
 using Avalonia.Reactive;
 using Avalonia.Threading;
+using System;
+using System.Diagnostics;
+using Avalonia.Metadata;
+using Avalonia.Threading;
 
-namespace Avalonia.Rendering
+namespace Avalonia.Rendering;
+
+/// <summary>
+/// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms 
+/// </summary>
+[PrivateApi]
+public class UiThreadRenderTimer : DefaultRenderTimer
 {
+    private readonly Stopwatch _clock = Stopwatch.StartNew();
     /// <summary>
-    /// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms 
+    /// Initializes a new instance of the <see cref="UiThreadRenderTimer"/> class.
     /// </summary>
-    [PrivateApi]
-    public class UiThreadRenderTimer : DefaultRenderTimer
+    /// <param name="framesPerSecond">The number of frames per second at which the loop should run.</param>
+    public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond)
+    {
+    }
+
+    /// <inheritdoc />
+    public override bool RunsInBackground => false;
+    
+    class TimerInstance : IDisposable
     {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="UiThreadRenderTimer"/> class.
-        /// </summary>
-        /// <param name="framesPerSecond">The number of frames per second at which the loop should run.</param>
-        public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond)
+        private UiThreadRenderTimer _parent;
+        private readonly Action<TimeSpan> _tick;
+        private DispatcherTimer _timer = new DispatcherTimer(DispatcherPriority.Render);
+
+        public TimerInstance(UiThreadRenderTimer parent, Action<TimeSpan> tick)
         {
+            _parent = parent;
+            _tick = tick;
+            _timer.Tick += OnTick;
+            _timer.Interval = Interval;
+            Interval = TimeSpan.FromSeconds(1.0 / _parent.FramesPerSecond);
+            _timer.Start();
         }
 
-        /// <inheritdoc />
-        public override bool RunsInBackground => false;
-
-        /// <inheritdoc />
-        protected override IDisposable StartCore(Action<TimeSpan> tick)
+        private void OnTick(object? sender, EventArgs e)
         {
-            bool cancelled = false;
-            var st = Stopwatch.StartNew();
-            DispatcherTimer.Run(() =>
+            var tickedAt = _parent._clock.Elapsed;
+            var nextTickAt = tickedAt + Interval;
+            try
             {
-                if (cancelled)
-                    return false;
-                tick(st.Elapsed);
-                return !cancelled;
-            }, TimeSpan.FromSeconds(1.0 / FramesPerSecond), DispatcherPriority.UiThreadRender);
-            return Disposable.Create(() => cancelled = true);
+                _tick(tickedAt);
+            }
+            finally
+            {
+                var afterTick = _parent._clock.Elapsed;
+                var interval = nextTickAt - afterTick;
+                if (interval < s_minInterval)
+                    // We are way overdue, but shouldn't cause starvation in other areas
+                    interval = s_minInterval;
+                _timer.Interval = interval;
+            }
         }
+
+        private static readonly TimeSpan s_minInterval = TimeSpan.FromMilliseconds(1);
+        private TimeSpan Interval { get; }
+
+        public void Dispose() => _timer.Stop();
+    }
+    
+    /// <inheritdoc />
+    protected override IDisposable StartCore(Action<TimeSpan> tick)
+    {
+        return new TimerInstance(this, tick);
     }
 }