binding.dart 19.4 KB
Newer Older
1 2 3 4 5
// Copyright 2015 The Chromium 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';
6
import 'dart:collection';
7
import 'dart:developer';
8 9
import 'dart:ui' as ui show window;
import 'dart:ui' show VoidCallback;
10

11
import 'package:collection/collection.dart';
12
import 'package:flutter/foundation.dart';
13

14 15
import 'priority.dart';

16 17
export 'dart:ui' show VoidCallback;

18
/// Slows down animations by this factor to help in development.
19 20 21 22 23 24 25 26 27 28
double get timeDilation => _timeDilation;
double _timeDilation = 1.0;
set timeDilation(double value) {
  if (_timeDilation == value)
    return;
  // We need to resetEpoch first so that we capture start of the epoch with the
  // current time dilation.
  SchedulerBinding.instance?.resetEpoch();
  _timeDilation = value;
}
29 30 31 32 33 34 35

/// A frame-related callback from the scheduler.
///
/// The timeStamp is the number of milliseconds since the beginning of the
/// scheduler's epoch. Use timeStamp to determine how far to advance animation
/// timelines so that all the animations in the system are synchronized to a
/// common time base.
36
typedef void FrameCallback(Duration timeStamp);
37

38
/// Signature for the [SchedulerBinding.schedulingStrategy] callback. Called
39 40 41 42 43 44 45
/// whenever the system needs to decide whether a task at a given
/// priority needs to be run.
///
/// Return true if a task with the given priority should be executed
/// at this time, false otherwise.
///
/// See also [defaultSchedulingStrategy].
46
typedef bool SchedulingStrategy({ int priority, SchedulerBinding scheduler });
Ian Hickson's avatar
Ian Hickson committed
47

48
class _TaskEntry {
49
  const _TaskEntry(this.task, this.priority);
50
  final VoidCallback task;
51
  final int priority;
52
}
53

54 55 56 57
class _FrameCallbackEntry {
  _FrameCallbackEntry(this.callback, { bool rescheduling: false }) {
    assert(() {
      if (rescheduling) {
58 59 60 61 62 63 64 65 66 67 68 69 70 71
        assert(() {
          if (currentCallbackStack == null) {
            throw new FlutterError(
              'addFrameCallback or scheduleFrameCallback called with rescheduling true, but no callback is in scope.\n'
              'The "rescheduling" argument should only be set to true if the '
              'callback is being reregistered from within the callback itself, '
              'and only then if the callback itself is entirely synchronous. '
              'If this is the initial registration of the callback, or if the '
              'callback is asynchronous, then do not use the "rescheduling" '
              'argument.'
            );
          }
          return true;
        });
72 73 74 75 76 77 78 79 80 81
        stack = currentCallbackStack;
      } else {
        stack = StackTrace.current;
      }
      return true;
    });
  }
  static StackTrace currentCallbackStack;
  final FrameCallback callback;
  StackTrace stack;
82 83
}

84
/// Scheduler for running the following:
85
///
86 87 88 89
/// * _Frame callbacks_, triggered by the system's
///   [ui.window.onBeginFrame] callback, for synchronising the
///   application's behavior to the system's display. For example, the
///   rendering layer uses this to drive its rendering pipeline.
90
///
91 92 93
/// * Non-rendering tasks, to be run between frames. These are given a
///   priority and are executed in priority order according to a
///   [schedulingStrategy].
94
abstract class SchedulerBinding extends BindingBase {
Ian Hickson's avatar
Ian Hickson committed
95

96
  @override
Ian Hickson's avatar
Ian Hickson committed
97 98 99
  void initInstances() {
    super.initInstances();
    _instance = this;
100
    ui.window.onBeginFrame = handleBeginFrame;
101
  }
102

103 104 105
  /// The current [SchedulerBinding], if one has been created.
  static SchedulerBinding get instance => _instance;
  static SchedulerBinding _instance;
Ian Hickson's avatar
Ian Hickson committed
106

107 108 109 110
  @override
  void initServiceExtensions() {
    super.initServiceExtensions();
    registerNumericServiceExtension(
111
      name: 'timeDilation',
112 113 114 115 116 117 118
      getter: () => timeDilation,
      setter: (double value) {
        timeDilation = value;
      }
    );
  }

119
  /// The strategy to use when deciding whether to run a task or not.
120
  ///
121
  /// Defaults to [defaultSchedulingStrategy].
Ian Hickson's avatar
Ian Hickson committed
122
  SchedulingStrategy schedulingStrategy = defaultSchedulingStrategy;
123

Hixie's avatar
Hixie committed
124 125 126
  static int _taskSorter (_TaskEntry e1, _TaskEntry e2) {
    return -e1.priority.compareTo(e2.priority);
  }
127
  final PriorityQueue<_TaskEntry> _taskQueue = new HeapPriorityQueue<_TaskEntry>(_taskSorter);
128

129 130 131 132 133 134 135
  /// Schedules the given `task` with the given `priority`.
  ///
  /// Tasks will be executed between frames, in priority order,
  /// excluding tasks that are skipped by the current
  /// [schedulingStrategy]. Tasks should be short (as in, up to a
  /// millisecond), so as to not cause the regular frame callbacks to
  /// get delayed.
136
  void scheduleTask(VoidCallback task, Priority priority) {
137
    bool isFirstTask = _taskQueue.isEmpty;
138
    _taskQueue.add(new _TaskEntry(task, priority.value));
139
    if (isFirstTask)
140
      _ensureEventLoopCallback();
141 142
  }

143 144 145 146 147 148 149 150 151 152 153
  // Whether this scheduler already requested to be called from the event loop.
  bool _hasRequestedAnEventLoopCallback = false;

  // Ensures that the scheduler is awakened by the event loop.
  void _ensureEventLoopCallback() {
    if (_hasRequestedAnEventLoopCallback)
      return;
    Timer.run(handleEventLoopCallback);
    _hasRequestedAnEventLoopCallback = true;
  }

154
  /// Called by the system when there is time to run tasks.
155 156 157 158 159
  void handleEventLoopCallback() {
    _hasRequestedAnEventLoopCallback = false;
    _runTasks();
  }

160
  // Called when the system wakes up and at the end of each frame.
161 162
  void _runTasks() {
    if (_taskQueue.isEmpty)
163
      return;
164
    _TaskEntry entry = _taskQueue.first;
165 166 167
    // TODO(floitsch): for now we only expose the priority. It might
    // be interesting to provide more info (like, how long the task
    // ran the last time, or how long is left in this frame).
Ian Hickson's avatar
Ian Hickson committed
168
    if (schedulingStrategy(priority: entry.priority, scheduler: this)) {
169
      try {
170
        (_taskQueue.removeFirst().task)();
171
      } finally {
172 173
        if (_taskQueue.isNotEmpty)
          _ensureEventLoopCallback();
174 175
      }
    } else {
176 177
      // TODO(floitsch): we shouldn't need to request a frame. Just schedule
      // an event-loop callback.
178
      scheduleFrame();
179 180 181
    }
  }

182
  int _nextFrameCallbackId = 0; // positive
183
  Map<int, _FrameCallbackEntry> _transientCallbacks = <int, _FrameCallbackEntry>{};
184
  final Set<int> _removedIds = new HashSet<int>();
185

186 187 188
  /// The current number of transient frame callbacks scheduled.
  ///
  /// This is reset to zero just before all the currently scheduled
189
  /// transient callbacks are called, at the start of a frame.
190 191 192 193
  ///
  /// This number is primarily exposed so that tests can verify that
  /// there are no unexpected transient callbacks still registered
  /// after a test's resources have been gracefully disposed.
194 195 196
  int get transientCallbackCount => _transientCallbacks.length;

  /// Schedules the given frame callback.
197
  ///
198
  /// Adds the given callback to the list of frame callbacks and ensures that a
199
  /// frame is scheduled.
200
  ///
201 202 203 204 205 206 207 208
  /// If this is a one-off registration, ignore the `rescheduling` argument.
  ///
  /// If this is a callback that will be reregistered each time it fires, then
  /// when you reregister the callback, set the `rescheduling` argument to true.
  /// This has no effect in release builds, but in debug builds, it ensures that
  /// the stack trace that is stored for this callback is the original stack
  /// trace for when the callback was _first_ registered, rather than the stack
  /// trace for when the callback is reregistered. This makes it easier to track
209
  /// down the original reason that a particular callback was called. If
210 211
  /// `rescheduling` is true, the call must be in the context of a frame
  /// callback.
212 213 214
  ///
  /// Callbacks registered with this method can be canceled using
  /// [cancelFrameCallbackWithId].
215
  int scheduleFrameCallback(FrameCallback callback, { bool rescheduling: false }) {
216
    scheduleFrame();
217
    return addFrameCallback(callback, rescheduling: rescheduling);
218 219 220 221 222 223 224
  }

  /// Adds a frame callback.
  ///
  /// Frame callbacks are executed at the beginning of a frame (see
  /// [handleBeginFrame]).
  ///
225 226
  /// These callbacks are executed in the order in which they have
  /// been added.
227
  ///
228
  /// Callbacks registered with this method will not be called until
229 230 231
  /// a frame is requested. To register a callback and ensure that a
  /// frame is immediately scheduled, use [scheduleFrameCallback].
  ///
232 233 234 235 236 237 238 239
  /// If this is a one-off registration, ignore the `rescheduling` argument.
  ///
  /// If this is a callback that will be reregistered each time it fires, then
  /// when you reregister the callback, set the `rescheduling` argument to true.
  /// This has no effect in release builds, but in debug builds, it ensures that
  /// the stack trace that is stored for this callback is the original stack
  /// trace for when the callback was _first_ registered, rather than the stack
  /// trace for when the callback is reregistered. This makes it easier to track
240
  /// down the original reason that a particular callback was called. If
241 242
  /// `rescheduling` is true, the call must be in the context of a frame
  /// callback.
243 244 245
  ///
  /// Callbacks registered with this method can be canceled using
  /// [cancelFrameCallbackWithId].
246
  int addFrameCallback(FrameCallback callback, { bool rescheduling: false }) {
247
    _nextFrameCallbackId += 1;
248
    _transientCallbacks[_nextFrameCallbackId] = new _FrameCallbackEntry(callback, rescheduling: rescheduling);
249 250 251 252
    return _nextFrameCallbackId;
  }

  /// Cancels the callback of the given [id].
253 254
  ///
  /// Removes the given callback from the list of frame callbacks. If a frame
255 256 257 258
  /// has been requested, this does not also cancel that request.
  ///
  /// Frame callbacks are registered using [scheduleFrameCallback] or
  /// [addFrameCallback].
259
  void cancelFrameCallbackWithId(int id) {
260 261 262 263 264
    assert(id > 0);
    _transientCallbacks.remove(id);
    _removedIds.add(id);
  }

265 266 267 268 269 270
  /// Asserts that there are no registered transient callbacks; if
  /// there are, prints their locations and throws an exception.
  ///
  /// This is expected to be called at the end of tests (the
  /// flutter_test framework does it automatically in normal cases).
  ///
271
  /// Call this method when you expect there to be no transient
272 273 274 275
  /// callbacks registered, in an assert statement with a message that
  /// you want printed when a transient callback is registered:
  ///
  /// ```dart
276
  /// assert(SchedulerBinding.instance.debugAssertNoTransientCallbacks(
277 278 279 280 281 282 283 284 285 286 287 288
  ///   'A leak of transient callbacks was detected while doing foo.'
  /// ));
  /// ```
  ///
  /// Does nothing if asserts are disabled. Always returns true.
  bool debugAssertNoTransientCallbacks(String reason) {
    assert(() {
      if (transientCallbackCount > 0) {
        FlutterError.reportError(new FlutterErrorDetails(
          exception: reason,
          library: 'scheduler library',
          informationCollector: (StringBuffer information) {
289 290 291 292 293 294 295 296 297 298 299
            if (transientCallbackCount == 1) {
              information.writeln(
                'There was one transient callback left. '
                'The stack traces for when it was registered is as follows:'
              );
            } else {
              information.writeln(
                'There were $transientCallbackCount transient callbacks left. '
                'The stack traces for when they were registered are as follows:'
              );
            }
300 301 302 303 304 305 306 307 308 309 310 311 312
            for (int id in _transientCallbacks.keys) {
              _FrameCallbackEntry entry = _transientCallbacks[id];
              information.writeln('-- callback $id --');
              information.writeln(entry.stack);
            }
          }
        ));
      }
      return true;
    });
    return true;
  }

313
  final List<FrameCallback> _persistentCallbacks = new List<FrameCallback>();
314

315 316
  /// Adds a persistent frame callback.
  ///
317
  /// Persistent callbacks are called after transient
318
  /// (non-persistent) frame callbacks.
319
  ///
320 321 322 323
  /// Does *not* request a new frame. Conceptually, persistent frame
  /// callbacks are observers of "begin frame" events. Since they are
  /// executed after the transient frame callbacks they can drive the
  /// rendering pipeline.
324
  void addPersistentFrameCallback(FrameCallback callback) {
325 326 327
    _persistentCallbacks.add(callback);
  }

328
  final List<FrameCallback> _postFrameCallbacks = new List<FrameCallback>();
329 330 331

  /// Schedule a callback for the end of this frame.
  ///
332
  /// Does *not* request a new frame.
333
  ///
334 335 336 337 338 339
  /// This callback is run during a frame, just after the persistent
  /// frame callbacks (which is when the main rendering pipeline has
  /// been flushed). If a frame is in progress and post-frame
  /// callbacks haven't been executed yet, then the registered
  /// callback is still executed during the frame. Otherwise, the
  /// registered callback is executed during the next frame.
340
  ///
341 342
  /// The callbacks are executed in the order in which they have been
  /// added.
343
  void addPostFrameCallback(FrameCallback callback) {
344 345 346
    _postFrameCallbacks.add(callback);
  }

Hans Muller's avatar
Hans Muller committed
347
  /// Whether this scheduler has requested that handleBeginFrame be called soon.
348
  bool get hasScheduledFrame => _hasScheduledFrame;
349
  bool _hasScheduledFrame = false;
350

351 352
  /// Whether this scheduler is currently producing a frame in [handleBeginFrame].
  bool get isProducingFrame => _isProducingFrame;
353
  bool _isProducingFrame = false;
354

355 356
  /// Schedules a new frame using [scheduleFrame] if this object is not
  /// currently producing a frame.
357
  ///
358 359
  /// After this is called, the framework ensures that the end of the
  /// [handleBeginFrame] function will (eventually) be reached.
360
  void ensureVisualUpdate() {
361
    if (_isProducingFrame)
362
      return;
363
    scheduleFrame();
364
  }
365

366 367 368 369 370 371 372
  /// If necessary, schedules a new frame by calling
  /// [ui.window.scheduleFrame].
  ///
  /// After this is called, the engine will (eventually) call
  /// [handleBeginFrame]. (This call might be delayed, e.g. if the
  /// device's screen is turned off it will typically be delayed until
  /// the screen is on and the application is visible.)
373 374 375 376 377 378
  void scheduleFrame() {
    if (_hasScheduledFrame)
      return;
    ui.window.scheduleFrame();
    _hasScheduledFrame = true;
  }
379

380 381 382 383
  Duration _firstRawTimeStampInEpoch;
  Duration _epochStart = Duration.ZERO;
  Duration _lastRawTimeStamp = Duration.ZERO;

384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
  /// Prepares the scheduler for a non-monotonic change to how time stamps are calcuated.
  ///
  /// Callbacks received from the scheduler assume that their time stamps are
  /// monotonically increasing. The raw time stamp passed to [handleBeginFrame]
  /// is monotonic, but the scheduler might adjust those time stamps to provide
  /// [timeDilation]. Without careful handling, these adjusts could cause time
  /// to appear to run backwards.
  ///
  /// The [resetEpoch] function ensures that the time stamps are monotonic by
  /// reseting the base time stamp used for future time stamp adjustments to the
  /// current value. For example, if the [timeDilation] decreases, rather than
  /// scaling down the [Duration] since the beginning of time, [resetEpoch] will
  /// ensure that we only scale down the duration since [resetEpoch] was called.
  ///
  /// Note: Setting [timeDilation] calls [resetEpoch] automatically. You don't
  /// need to call [resetEpoch] yourself.
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
  void resetEpoch() {
    _epochStart = _adjustForEpoch(_lastRawTimeStamp);
    _firstRawTimeStampInEpoch = null;
  }

  /// Adjusts the given time stamp into the current epoch.
  ///
  /// This both offsets the time stamp to account for when the epoch started
  /// (both in raw time and in the epoch's own time line) and scales the time
  /// stamp to reflect the time dilation in the current epoch.
  ///
  /// These mechanisms together combine to ensure that the durations we give
  /// during frame callbacks are monotonically increasing.
  Duration _adjustForEpoch(Duration rawTimeStamp) {
    Duration rawDurationSinceEpoch = _firstRawTimeStampInEpoch == null ? Duration.ZERO : rawTimeStamp - _firstRawTimeStampInEpoch;
    return new Duration(microseconds: (rawDurationSinceEpoch.inMicroseconds / timeDilation).round() + _epochStart.inMicroseconds);
  }

418 419 420
  /// Called by the engine to produce a new frame.
  ///
  /// This function first calls all the callbacks registered by
421 422 423 424
  /// [scheduleFrameCallback]/[addFrameCallback], then calls all the
  /// callbacks registered by [addPersistentFrameCallback], which
  /// typically drive the rendering pipeline, and finally calls the
  /// callbacks registered by [addPostFrameCallback].
425
  void handleBeginFrame(Duration rawTimeStamp) {
426
    Timeline.startSync('Frame');
427 428 429
    _firstRawTimeStampInEpoch ??= rawTimeStamp;
    Duration timeStamp = _adjustForEpoch(rawTimeStamp);
    _lastRawTimeStamp = rawTimeStamp;
430 431 432
    assert(!_isProducingFrame);
    _isProducingFrame = true;
    _hasScheduledFrame = false;
433
    _invokeTransientFrameCallbacks(timeStamp);
434

435
    for (FrameCallback callback in _persistentCallbacks)
436
      _invokeFrameCallback(callback, timeStamp);
437

438 439
    _isProducingFrame = false;

440 441
    List<FrameCallback> localPostFrameCallbacks =
        new List<FrameCallback>.from(_postFrameCallbacks);
442
    _postFrameCallbacks.clear();
443
    for (FrameCallback callback in localPostFrameCallbacks)
444
      _invokeFrameCallback(callback, timeStamp);
445 446 447 448

    Timeline.finishSync();

    // All frame-related callbacks have been executed. Run lower-priority tasks.
449
    _runTasks();
450 451
  }

452 453
  void _invokeTransientFrameCallbacks(Duration timeStamp) {
    Timeline.startSync('Animate');
454
    assert(_isProducingFrame);
455 456 457 458 459 460 461 462 463 464
    Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
    _transientCallbacks = new Map<int, _FrameCallbackEntry>();
    callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
      if (!_removedIds.contains(id))
        _invokeFrameCallback(callbackEntry.callback, timeStamp, callbackEntry.stack);
    });
    _removedIds.clear();
    Timeline.finishSync();
  }

465
  // Calls the given [callback] with [timestamp] as argument.
466 467 468 469 470
  //
  // Wraps the callback in a try/catch and forwards any error to
  // [debugSchedulerExceptionHandler], if set. If not set, then simply prints
  // the error.
  void _invokeFrameCallback(FrameCallback callback, Duration timeStamp, [ StackTrace callbackStack ]) {
471
    assert(callback != null);
472
    assert(_FrameCallbackEntry.currentCallbackStack == null);
473
    // TODO(ianh): Consider using a Zone instead to track the current callback registration stack
474
    assert(() { _FrameCallbackEntry.currentCallbackStack = callbackStack; return true; });
475 476
    try {
      callback(timeStamp);
477
    } catch (exception, exceptionStack) {
478 479
      FlutterError.reportError(new FlutterErrorDetails(
        exception: exception,
480
        stack: exceptionStack,
481
        library: 'scheduler library',
482
        context: 'during a scheduler callback',
483
        informationCollector: (callbackStack == null) ? null : (StringBuffer information) {
484 485 486 487 488 489
          information.writeln(
            '\nThis exception was thrown in the context of a scheduler callback. '
            'When the scheduler callback was _registered_ (as opposed to when the '
            'exception was thrown), this was the stack:'
          );
          FlutterError.defaultStackFilter(callbackStack.toString().trimRight().split('\n')).forEach(information.writeln);
490
        }
491
      ));
492
    }
493 494
    assert(() { _FrameCallbackEntry.currentCallbackStack = null; return true; });
  }
495 496
}

497
/// The default [SchedulingStrategy] for [SchedulerBinding.schedulingStrategy].
498 499 500 501
///
/// If there are any frame callbacks registered, only runs tasks with
/// a [Priority] of [Priority.animation] or higher. Otherwise, runs
/// all tasks.
502
bool defaultSchedulingStrategy({ int priority, SchedulerBinding scheduler }) {
Ian Hickson's avatar
Ian Hickson committed
503
  if (scheduler.transientCallbackCount > 0)
504
    return priority >= Priority.animation.value;
Ian Hickson's avatar
Ian Hickson committed
505
  return true;
506
}