| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'scheduler_tester.dart'; |
| |
| class TestSchedulerBinding extends BindingBase with SchedulerBinding, ServicesBinding { |
| final Map<String, List<Map<String, dynamic>>> eventsDispatched = <String, List<Map<String, dynamic>>>{}; |
| |
| @override |
| void postEvent(String eventKind, Map<String, dynamic> eventData) { |
| getEventsDispatched(eventKind).add(eventData); |
| } |
| |
| List<Map<String, dynamic>> getEventsDispatched(String eventKind) { |
| return eventsDispatched.putIfAbsent(eventKind, () => <Map<String, dynamic>>[]); |
| } |
| } |
| |
| class TestStrategy { |
| int allowedPriority = 10000; |
| |
| bool shouldRunTaskWithPriority({ required int priority, required SchedulerBinding scheduler }) { |
| return priority >= allowedPriority; |
| } |
| } |
| |
| void main() { |
| late TestSchedulerBinding scheduler; |
| |
| setUpAll(() { |
| scheduler = TestSchedulerBinding(); |
| }); |
| |
| test('Tasks are executed in the right order', () { |
| final TestStrategy strategy = TestStrategy(); |
| scheduler.schedulingStrategy = strategy.shouldRunTaskWithPriority; |
| final List<int> input = <int>[2, 23, 23, 11, 0, 80, 3]; |
| final List<int> executedTasks = <int>[]; |
| |
| void scheduleAddingTask(int x) { |
| scheduler.scheduleTask(() { executedTasks.add(x); }, Priority.idle + x); |
| } |
| |
| input.forEach(scheduleAddingTask); |
| |
| strategy.allowedPriority = 100; |
| for (int i = 0; i < 3; i += 1) { |
| expect(scheduler.handleEventLoopCallback(), isFalse); |
| } |
| expect(executedTasks.isEmpty, isTrue); |
| |
| strategy.allowedPriority = 50; |
| for (int i = 0; i < 3; i += 1) { |
| expect(scheduler.handleEventLoopCallback(), i == 0 ? isTrue : isFalse); |
| } |
| expect(executedTasks, hasLength(1)); |
| expect(executedTasks.single, equals(80)); |
| executedTasks.clear(); |
| |
| strategy.allowedPriority = 20; |
| for (int i = 0; i < 3; i += 1) { |
| expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse); |
| } |
| expect(executedTasks, hasLength(2)); |
| expect(executedTasks[0], equals(23)); |
| expect(executedTasks[1], equals(23)); |
| executedTasks.clear(); |
| |
| scheduleAddingTask(99); |
| scheduleAddingTask(19); |
| scheduleAddingTask(5); |
| scheduleAddingTask(97); |
| for (int i = 0; i < 3; i += 1) { |
| expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse); |
| } |
| expect(executedTasks, hasLength(2)); |
| expect(executedTasks[0], equals(99)); |
| expect(executedTasks[1], equals(97)); |
| executedTasks.clear(); |
| |
| strategy.allowedPriority = 10; |
| for (int i = 0; i < 3; i += 1) { |
| expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse); |
| } |
| expect(executedTasks, hasLength(2)); |
| expect(executedTasks[0], equals(19)); |
| expect(executedTasks[1], equals(11)); |
| executedTasks.clear(); |
| |
| strategy.allowedPriority = 1; |
| for (int i = 0; i < 4; i += 1) { |
| expect(scheduler.handleEventLoopCallback(), i < 3 ? isTrue : isFalse); |
| } |
| expect(executedTasks, hasLength(3)); |
| expect(executedTasks[0], equals(5)); |
| expect(executedTasks[1], equals(3)); |
| expect(executedTasks[2], equals(2)); |
| executedTasks.clear(); |
| |
| strategy.allowedPriority = 0; |
| expect(scheduler.handleEventLoopCallback(), isFalse); |
| expect(executedTasks, hasLength(1)); |
| expect(executedTasks[0], equals(0)); |
| }); |
| |
| test('2 calls to scheduleWarmUpFrame just schedules it once', () { |
| final List<VoidCallback> timerQueueTasks = <VoidCallback>[]; |
| bool taskExecuted = false; |
| runZoned<void>( |
| () { |
| // Run it twice without processing the queued tasks. |
| scheduler.scheduleWarmUpFrame(); |
| scheduler.scheduleWarmUpFrame(); |
| scheduler.scheduleTask(() { taskExecuted = true; }, Priority.touch); |
| }, |
| zoneSpecification: ZoneSpecification( |
| createTimer: (Zone self, ZoneDelegate parent, Zone zone, Duration duration, void Function() f) { |
| // Don't actually run the tasks, just record that it was scheduled. |
| timerQueueTasks.add(f); |
| return DummyTimer(); |
| }, |
| ), |
| ); |
| |
| // scheduleWarmUpFrame scheduled 2 Timers, scheduleTask scheduled 0 because |
| // 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(); |
| } |
| |
| // As events are locked, make scheduleTask execute after the test or it |
| // will execute during following tests and risk failure. |
| addTearDown(() => scheduler.handleEventLoopCallback()); |
| }); |
| |
| test('Flutter.Frame event fired', () async { |
| SchedulerBinding.instance.platformDispatcher.onReportTimings!(<FrameTiming>[ |
| FrameTiming( |
| vsyncStart: 5000, |
| buildStart: 10000, |
| buildFinish: 15000, |
| rasterStart: 16000, |
| rasterFinish: 20000, |
| rasterFinishWallTime: 20010, |
| frameNumber: 1991, |
| ), |
| ]); |
| |
| final List<Map<String, dynamic>> events = scheduler.getEventsDispatched('Flutter.Frame'); |
| expect(events, hasLength(1)); |
| |
| final Map<String, dynamic> event = events.first; |
| expect(event['number'], 1991); |
| expect(event['startTime'], 10000); |
| expect(event['elapsed'], 15000); |
| expect(event['build'], 5000); |
| expect(event['raster'], 4000); |
| expect(event['vsyncOverhead'], 5000); |
| }); |
| |
| test('TimingsCallback exceptions are caught', () { |
| FlutterErrorDetails? errorCaught; |
| FlutterError.onError = (FlutterErrorDetails details) { |
| errorCaught = details; |
| }; |
| SchedulerBinding.instance.addTimingsCallback((List<FrameTiming> timings) { |
| throw Exception('Test'); |
| }); |
| SchedulerBinding.instance.platformDispatcher.onReportTimings!(<FrameTiming>[]); |
| expect(errorCaught!.exceptionAsString(), equals('Exception: Test')); |
| }); |
| |
| test('currentSystemFrameTimeStamp is the raw timestamp', () { |
| // Undo epoch set by previous tests. |
| scheduler.resetEpoch(); |
| |
| late Duration lastTimeStamp; |
| late Duration lastSystemTimeStamp; |
| |
| void frameCallback(Duration timeStamp) { |
| expect(timeStamp, scheduler.currentFrameTimeStamp); |
| lastTimeStamp = scheduler.currentFrameTimeStamp; |
| lastSystemTimeStamp = scheduler.currentSystemFrameTimeStamp; |
| } |
| |
| scheduler.scheduleFrameCallback(frameCallback); |
| tick(const Duration(seconds: 2)); |
| expect(lastTimeStamp, Duration.zero); |
| expect(lastSystemTimeStamp, const Duration(seconds: 2)); |
| |
| scheduler.scheduleFrameCallback(frameCallback); |
| tick(const Duration(seconds: 4)); |
| expect(lastTimeStamp, const Duration(seconds: 2)); |
| expect(lastSystemTimeStamp, const Duration(seconds: 4)); |
| |
| timeDilation = 2; |
| scheduler.scheduleFrameCallback(frameCallback); |
| tick(const Duration(seconds: 6)); |
| expect(lastTimeStamp, const Duration(seconds: 2)); // timeDilation calls SchedulerBinding.resetEpoch |
| expect(lastSystemTimeStamp, const Duration(seconds: 6)); |
| |
| scheduler.scheduleFrameCallback(frameCallback); |
| tick(const Duration(seconds: 8)); |
| expect(lastTimeStamp, const Duration(seconds: 3)); // 2s + (8 - 6)s / 2 |
| expect(lastSystemTimeStamp, const Duration(seconds: 8)); |
| |
| timeDilation = 1.0; // restore time dilation, or it will affect other tests |
| }); |
| |
| test('Animation frame scheduled in the middle of the warm-up frame', () { |
| expect(scheduler.schedulerPhase, SchedulerPhase.idle); |
| final List<VoidCallback> timers = <VoidCallback>[]; |
| 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<void>(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); |
| SchedulerBinding.instance.platformDispatcher.onBeginFrame!(Duration.zero); |
| expect(scheduler.hasScheduledFrame, isFalse); |
| SchedulerBinding.instance.platformDispatcher.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); |
| }); |
| |
| test('Can schedule futures to completion', () async { |
| bool isCompleted = false; |
| |
| // `Future` is disallowed in this file due to the import of |
| // scheduler_tester.dart so annotations cannot be specified. |
| // ignore: always_specify_types |
| final result = scheduler.scheduleTask( |
| () async { |
| // Yield, so if awaiting `result` did not wait for completion of this |
| // task, the assertion on `isCompleted` will fail. |
| await null; |
| await null; |
| |
| isCompleted = true; |
| return 1; |
| }, |
| Priority.idle, |
| ); |
| |
| scheduler.handleEventLoopCallback(); |
| await result; |
| |
| expect(isCompleted, true); |
| }); |
| } |
| |
| class DummyTimer implements Timer { |
| @override |
| void cancel() {} |
| |
| @override |
| bool get isActive => false; |
| |
| @override |
| int get tick => 0; |
| } |