From 67908dfb80ff150bfdde41c2b6d2630e2c4d76f6 Mon Sep 17 00:00:00 2001 From: Yegor Date: Tue, 15 Dec 2020 16:54:52 -0800 Subject: [PATCH] Reschedule engine frame if it arrives in the middle of warm-up (#72115) * Reschedule engine frame if it arrives in the middle of warm-up * user post-frame callback instead --- .../flutter/lib/src/scheduler/binding.dart | 35 +++++++++++++--- .../test/scheduler/scheduler_test.dart | 42 +++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/packages/flutter/lib/src/scheduler/binding.dart b/packages/flutter/lib/src/scheduler/binding.dart index 0fe1ecaf16..52d05d4ba7 100644 --- a/packages/flutter/lib/src/scheduler/binding.dart +++ b/packages/flutter/lib/src/scheduler/binding.dart @@ -954,20 +954,45 @@ mixin SchedulerBinding on BindingBase { int _debugFrameNumber = 0; String? _debugBanner; - bool _ignoreNextEngineDrawFrame = false; + + // Whether the current engine frame needs to be postponed till after the + // warm-up frame. + // + // Engine may begin a frame in the middle of the warm-up frame because the + // warm-up frame is scheduled by timers while the engine frame is scheduled + // by platform specific frame scheduler (e.g. `requestAnimationFrame` on the + // web). When this happens, we let the warm-up frame finish, and postpone the + // engine frame. + bool _rescheduleAfterWarmUpFrame = false; void _handleBeginFrame(Duration rawTimeStamp) { if (_warmUpFrame) { - assert(!_ignoreNextEngineDrawFrame); - _ignoreNextEngineDrawFrame = true; + // "begin frame" and "draw frame" must strictly alternate. Therefore + // _rescheduleAfterWarmUpFrame cannot possibly be true here as it is + // reset by _handleDrawFrame. + assert(!_rescheduleAfterWarmUpFrame); + _rescheduleAfterWarmUpFrame = true; return; } handleBeginFrame(rawTimeStamp); } void _handleDrawFrame() { - if (_ignoreNextEngineDrawFrame) { - _ignoreNextEngineDrawFrame = false; + if (_rescheduleAfterWarmUpFrame) { + _rescheduleAfterWarmUpFrame = false; + // Reschedule in a post-frame callback to allow the draw-frame phase of + // the warm-up frame to finish. + addPostFrameCallback((Duration timeStamp) { + // Force an engine frame. + // + // We need to reset _hasScheduledFrame here because we cancelled the + // original engine frame, and therefore did not run handleBeginFrame + // who is responsible for resetting it. So if a frame callback set this + // to true in the "begin frame" part of the warm-up frame, it will + // still be true here and cause us to skip scheduling an engine frame. + _hasScheduledFrame = false; + scheduleFrame(); + }); return; } handleDrawFrame(); diff --git a/packages/flutter/test/scheduler/scheduler_test.dart b/packages/flutter/test/scheduler/scheduler_test.dart index ea0cba1a5c..cc52cbc7d7 100644 --- a/packages/flutter/test/scheduler/scheduler_test.dart +++ b/packages/flutter/test/scheduler/scheduler_test.dart @@ -129,6 +129,11 @@ void main() { // events are locked. expect(timerQueueTasks.length, 2); expect(taskExecuted, false); + + // Run the timers so that the scheduler is no longer in warm-up state. + for (final VoidCallback timer in timerQueueTasks) { + timer(); + } }); test('Flutter.Frame event fired', () async { @@ -165,6 +170,9 @@ void main() { }); test('currentSystemFrameTimeStamp is the raw timestamp', () { + // Undo epoch set by previous tests. + scheduler.resetEpoch(); + late Duration lastTimeStamp; late Duration lastSystemTimeStamp; @@ -195,6 +203,40 @@ void main() { expect(lastTimeStamp, const Duration(seconds: 3)); // 2s + (8 - 6)s / 2 expect(lastSystemTimeStamp, const Duration(seconds: 8)); }); + + test('Animation frame scheduled in the middle of the warm-up frame', () { + expect(scheduler.schedulerPhase, SchedulerPhase.idle); + final List timers = []; + final ZoneSpecification timerInterceptor = ZoneSpecification( + createTimer: (Zone self, ZoneDelegate parent, Zone zone, Duration duration, void Function() callback) { + timers.add(callback); + return DummyTimer(); + }, + ); + + // Schedule a warm-up frame. + // Expect two timers, one for begin frame, and one for draw frame. + runZoned(scheduler.scheduleWarmUpFrame, zoneSpecification: timerInterceptor); + expect(timers.length, 2); + final VoidCallback warmUpBeginFrame = timers.first; + final VoidCallback warmUpDrawFrame = timers.last; + timers.clear(); + + warmUpBeginFrame(); + + // Simulate an animation frame firing between warm-up begin frame and warm-up draw frame. + // Expect a timer that reschedules the frame. + expect(scheduler.hasScheduledFrame, isFalse); + window.onBeginFrame!(Duration.zero); + expect(scheduler.hasScheduledFrame, isFalse); + window.onDrawFrame!(); + expect(scheduler.hasScheduledFrame, isFalse); + + // The draw frame part of the warm-up frame will run the post-frame + // callback that reschedules the engine frame. + warmUpDrawFrame(); + expect(scheduler.hasScheduledFrame, isTrue); + }); } class DummyTimer implements Timer {