scheduler_test.dart 9.64 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
4

5
import 'dart:async';
6

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/scheduler.dart';
9
import 'package:flutter/services.dart';
10
import 'package:flutter_test/flutter_test.dart';
11

12
import 'scheduler_tester.dart';
13

14
class TestSchedulerBinding extends BindingBase with SchedulerBinding, ServicesBinding {
15 16 17
  final Map<String, List<Map<String, dynamic>>> eventsDispatched = <String, List<Map<String, dynamic>>>{};

  @override
18
  void postEvent(String eventKind, Map<String, dynamic> eventData) {
19 20 21 22 23 24 25
    getEventsDispatched(eventKind).add(eventData);
  }

  List<Map<String, dynamic>> getEventsDispatched(String eventKind) {
    return eventsDispatched.putIfAbsent(eventKind, () => <Map<String, dynamic>>[]);
  }
}
Ian Hickson's avatar
Ian Hickson committed
26

Ian Hickson's avatar
Ian Hickson committed
27
class TestStrategy {
28 29
  int allowedPriority = 10000;

30
  bool shouldRunTaskWithPriority({ required int priority, required SchedulerBinding scheduler }) {
31 32 33 34 35
    return priority >= allowedPriority;
  }
}

void main() {
36
  late TestSchedulerBinding scheduler;
37

38
  setUpAll(() {
39
    scheduler = TestSchedulerBinding();
40 41
  });

Ian Hickson's avatar
Ian Hickson committed
42
  test('Tasks are executed in the right order', () {
43
    final TestStrategy strategy = TestStrategy();
Ian Hickson's avatar
Ian Hickson committed
44
    scheduler.schedulingStrategy = strategy.shouldRunTaskWithPriority;
45 46
    final List<int> input = <int>[2, 23, 23, 11, 0, 80, 3];
    final List<int> executedTasks = <int>[];
47 48

    void scheduleAddingTask(int x) {
49
      scheduler.scheduleTask(() { executedTasks.add(x); }, Priority.idle + x);
50 51
    }

52
    input.forEach(scheduleAddingTask);
Ian Hickson's avatar
Ian Hickson committed
53

54
    strategy.allowedPriority = 100;
55
    for (int i = 0; i < 3; i += 1) {
56
      expect(scheduler.handleEventLoopCallback(), isFalse);
57
    }
58 59 60
    expect(executedTasks.isEmpty, isTrue);

    strategy.allowedPriority = 50;
61
    for (int i = 0; i < 3; i += 1) {
62
      expect(scheduler.handleEventLoopCallback(), i == 0 ? isTrue : isFalse);
63
    }
64
    expect(executedTasks, hasLength(1));
65 66 67 68
    expect(executedTasks.single, equals(80));
    executedTasks.clear();

    strategy.allowedPriority = 20;
69
    for (int i = 0; i < 3; i += 1) {
70
      expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse);
71
    }
72
    expect(executedTasks, hasLength(2));
73 74 75 76 77 78 79 80
    expect(executedTasks[0], equals(23));
    expect(executedTasks[1], equals(23));
    executedTasks.clear();

    scheduleAddingTask(99);
    scheduleAddingTask(19);
    scheduleAddingTask(5);
    scheduleAddingTask(97);
81
    for (int i = 0; i < 3; i += 1) {
82
      expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse);
83
    }
84
    expect(executedTasks, hasLength(2));
85 86 87 88 89
    expect(executedTasks[0], equals(99));
    expect(executedTasks[1], equals(97));
    executedTasks.clear();

    strategy.allowedPriority = 10;
90
    for (int i = 0; i < 3; i += 1) {
91
      expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse);
92
    }
93
    expect(executedTasks, hasLength(2));
94 95 96 97 98
    expect(executedTasks[0], equals(19));
    expect(executedTasks[1], equals(11));
    executedTasks.clear();

    strategy.allowedPriority = 1;
99
    for (int i = 0; i < 4; i += 1) {
100
      expect(scheduler.handleEventLoopCallback(), i < 3 ? isTrue : isFalse);
101
    }
102
    expect(executedTasks, hasLength(3));
103 104 105 106 107 108
    expect(executedTasks[0], equals(5));
    expect(executedTasks[1], equals(3));
    expect(executedTasks[2], equals(2));
    executedTasks.clear();

    strategy.allowedPriority = 0;
109 110
    expect(scheduler.handleEventLoopCallback(), isFalse);
    expect(executedTasks, hasLength(1));
111 112
    expect(executedTasks[0], equals(0));
  });
113 114

  test('2 calls to scheduleWarmUpFrame just schedules it once', () {
115
    final List<VoidCallback> timerQueueTasks = <VoidCallback>[];
116
    bool taskExecuted = false;
117 118 119
    runZoned<void>(
      () {
        // Run it twice without processing the queued tasks.
120
        scheduler.scheduleWarmUpFrame();
121 122 123 124
        scheduler.scheduleWarmUpFrame();
        scheduler.scheduleTask(() { taskExecuted = true; }, Priority.touch);
      },
      zoneSpecification: ZoneSpecification(
125
        createTimer: (Zone self, ZoneDelegate parent, Zone zone, Duration duration, void Function() f) {
126 127
          // Don't actually run the tasks, just record that it was scheduled.
          timerQueueTasks.add(f);
128
          return DummyTimer();
129 130 131 132 133 134 135
        },
      ),
    );

    // scheduleWarmUpFrame scheduled 2 Timers, scheduleTask scheduled 0 because
    // events are locked.
    expect(timerQueueTasks.length, 2);
136
    expect(taskExecuted, false);
137 138 139 140 141

    // Run the timers so that the scheduler is no longer in warm-up state.
    for (final VoidCallback timer in timerQueueTasks) {
      timer();
    }
142 143 144 145

    // As events are locked, make scheduleTask execute after the test or it
    // will execute during following tests and risk failure.
    addTearDown(() => scheduler.handleEventLoopCallback());
146
  });
147 148

  test('Flutter.Frame event fired', () async {
149 150 151 152 153 154 155 156 157 158 159
    SchedulerBinding.instance.platformDispatcher.onReportTimings!(<FrameTiming>[
      FrameTiming(
        vsyncStart: 5000,
        buildStart: 10000,
        buildFinish: 15000,
        rasterStart: 16000,
        rasterFinish: 20000,
        rasterFinishWallTime: 20010,
        frameNumber: 1991,
      ),
    ]);
160 161 162 163 164

    final List<Map<String, dynamic>> events = scheduler.getEventsDispatched('Flutter.Frame');
    expect(events, hasLength(1));

    final Map<String, dynamic> event = events.first;
165
    expect(event['number'], 1991);
166
    expect(event['startTime'], 10000);
167
    expect(event['elapsed'], 15000);
168 169
    expect(event['build'], 5000);
    expect(event['raster'], 4000);
170
    expect(event['vsyncOverhead'], 5000);
171
  });
172

173
  test('TimingsCallback exceptions are caught', () {
174
    FlutterErrorDetails? errorCaught;
175 176 177
    FlutterError.onError = (FlutterErrorDetails details) {
      errorCaught = details;
    };
178
    SchedulerBinding.instance.addTimingsCallback((List<FrameTiming> timings) {
179 180
      throw Exception('Test');
    });
181
    SchedulerBinding.instance.platformDispatcher.onReportTimings!(<FrameTiming>[]);
182
    expect(errorCaught!.exceptionAsString(), equals('Exception: Test'));
183 184
  });

185
  test('currentSystemFrameTimeStamp is the raw timestamp', () {
186 187 188
    // Undo epoch set by previous tests.
    scheduler.resetEpoch();

189 190
    late Duration lastTimeStamp;
    late Duration lastSystemTimeStamp;
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217

    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));
218 219

    timeDilation = 1.0; // restore time dilation, or it will affect other tests
220
  });
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244

  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);
245
    SchedulerBinding.instance.platformDispatcher.onBeginFrame!(Duration.zero);
246
    expect(scheduler.hasScheduledFrame, isFalse);
247
    SchedulerBinding.instance.platformDispatcher.onDrawFrame!();
248 249 250 251 252 253 254
    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);
  });
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279

  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);
  });
280
}
281 282 283 284 285 286 287 288 289 290 291

class DummyTimer implements Timer {
  @override
  void cancel() {}

  @override
  bool get isActive => false;

  @override
  int get tick => 0;
}