scheduler.dart 10.9 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';
Ian Hickson's avatar
Ian Hickson committed
12
import 'package:flutter/services.dart';
13

14 15
export 'dart:ui' show VoidCallback;

16 17 18 19 20 21 22 23 24
/// Slows down animations by this factor to help in development.
double timeDilation = 1.0;

/// 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.
25
typedef void FrameCallback(Duration timeStamp);
26 27

typedef void SchedulerExceptionHandler(dynamic exception, StackTrace stack);
Ian Hickson's avatar
Ian Hickson committed
28 29 30

typedef bool SchedulingStrategy({ int priority, Scheduler scheduler });

31 32 33 34 35 36
/// This callback is invoked whenever an exception is caught by the scheduler.
/// The 'exception' argument contains the object that was thrown, and the
/// 'stack' argument contains the stack trace. If the callback is set, it is
/// invoked instead of printing the information to the console.
SchedulerExceptionHandler debugSchedulerExceptionHandler;

37 38 39
/// An entry in the scheduler's priority queue.
///
/// Combines the task and its priority.
40
class _TaskEntry {
41
  final VoidCallback task;
42 43
  final int priority;

44
  const _TaskEntry(this.task, this.priority);
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
}

class Priority {
  static const Priority idle = const Priority._(0);
  static const Priority animation = const Priority._(100000);
  static const Priority touch = const Priority._(200000);

  /// Relative priorities are clamped by this offset.
  ///
  /// It is still possible to have priorities that are offset by more than this
  /// amount by repeatedly taking relative offsets, but that's generally
  /// discouraged.
  static const int kMaxOffset = 10000;

  const Priority._(this._value);

  int get value => _value;
  final int _value;

  /// Returns a priority relative to this priority.
  ///
  /// A positive [offset] indicates a higher priority.
  ///
  /// The parameter [offset] is clamped to +/-[kMaxOffset].
  Priority operator +(int offset) {
    if (offset.abs() > kMaxOffset) {
      // Clamp the input offset.
      offset = kMaxOffset * offset.sign;
    }
    return new Priority._(_value + offset);
  }

  /// Returns a priority relative to this priority.
  ///
  /// A positive offset indicates a lower priority.
  ///
  /// The parameter [offset] is clamped to +/-[kMaxOffset].
  Priority operator -(int offset) => this + (-offset);
}

/// Scheduler running tasks with specific priorities.
///
/// Combines the task's priority with remaining time in a frame to decide when
/// the task should be run.
///
/// Tasks always run in the idle time after a frame has been committed.
Ian Hickson's avatar
Ian Hickson committed
91
abstract class Scheduler extends BindingBase {
92
  /// Requires clients to use the [scheduler] singleton
Ian Hickson's avatar
Ian Hickson committed
93

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

Ian Hickson's avatar
Ian Hickson committed
101 102 103
  static Scheduler _instance;
  static Scheduler get instance => _instance;

Ian Hickson's avatar
Ian Hickson committed
104
  SchedulingStrategy schedulingStrategy = defaultSchedulingStrategy;
105

Hixie's avatar
Hixie committed
106 107 108 109
  static int _taskSorter (_TaskEntry e1, _TaskEntry e2) {
    // Note that we inverse the priority.
    return -e1.priority.compareTo(e2.priority);
  }
110
  final PriorityQueue<_TaskEntry> _taskQueue = new HeapPriorityQueue<_TaskEntry>(_taskSorter);
111

112 113
  /// Whether this scheduler already requested to be called from the event loop.
  bool _hasRequestedAnEventLoopCallback = false;
114

115 116 117
  /// Whether this scheduler already requested to be called at the beginning of
  /// the next frame.
  bool _hasRequestedABeginFrameCallback = false;
118 119

  /// Schedules the given [task] with the given [priority].
120
  void scheduleTask(VoidCallback task, Priority priority) {
121 122
    bool isFirstTask = _taskQueue.isEmpty;
    _taskQueue.add(new _TaskEntry(task, priority._value));
123
    if (isFirstTask)
124
      _ensureEventLoopCallback();
125 126 127
  }

  /// Invoked by the system when there is time to run tasks.
128 129 130 131 132 133 134
  void handleEventLoopCallback() {
    _hasRequestedAnEventLoopCallback = false;
    _runTasks();
  }

  void _runTasks() {
    if (_taskQueue.isEmpty)
135
      return;
136
    _TaskEntry entry = _taskQueue.first;
Ian Hickson's avatar
Ian Hickson committed
137
    if (schedulingStrategy(priority: entry.priority, scheduler: this)) {
138
      try {
139
        (_taskQueue.removeFirst().task)();
140
      } finally {
141 142
        if (_taskQueue.isNotEmpty)
          _ensureEventLoopCallback();
143 144
      }
    } else {
145 146 147
      // TODO(floitsch): we shouldn't need to request a frame. Just schedule
      // an event-loop callback.
      _ensureBeginFrameCallback();
148 149 150
    }
  }

151
  int _nextFrameCallbackId = 0; // positive
152
  Map<int, FrameCallback> _transientCallbacks = <int, FrameCallback>{};
153
  final Set<int> _removedIds = new HashSet<int>();
154 155 156 157

  int get transientCallbackCount => _transientCallbacks.length;

  /// Schedules the given frame callback.
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
  ///
  /// Adds the given callback to the list of frame-callbacks and ensures that a
  /// frame is scheduled.
  int scheduleFrameCallback(FrameCallback callback) {
    _ensureBeginFrameCallback();
    return addFrameCallback(callback);
  }

  /// Adds a frame callback.
  ///
  /// Frame callbacks are executed at the beginning of a frame (see
  /// [handleBeginFrame]).
  ///
  /// The registered callbacks are executed in the order in which they have been
  /// registered.
  int addFrameCallback(FrameCallback callback) {
174 175 176 177 178 179
    _nextFrameCallbackId += 1;
    _transientCallbacks[_nextFrameCallbackId] = callback;
    return _nextFrameCallbackId;
  }

  /// Cancels the callback of the given [id].
180 181 182 183
  ///
  /// Removes the given callback from the list of frame callbacks. If a frame
  /// has been requested does *not* cancel that request.
  void cancelFrameCallbackWithId(int id) {
184 185 186 187 188
    assert(id > 0);
    _transientCallbacks.remove(id);
    _removedIds.add(id);
  }

189
  final List<FrameCallback> _persistentCallbacks = new List<FrameCallback>();
190

191 192 193 194 195 196 197 198 199 200
  /// Adds a persistent frame callback.
  ///
  /// Persistent callbacks are invoked after transient (non-persistent) frame
  /// callbacks.
  ///
  /// Does *not* request a new frame. Conceptually, persistent
  /// frame-callbacks are thus observers of begin-frame events. Since they are
  /// executed after the transient frame-callbacks they can drive the rendering
  /// pipeline.
  void addPersistentFrameCallback(FrameCallback callback) {
201 202 203
    _persistentCallbacks.add(callback);
  }

204
  final List<FrameCallback> _postFrameCallbacks = new List<FrameCallback>();
205 206 207

  /// Schedule a callback for the end of this frame.
  ///
208
  /// Does *not* request a new frame.
209
  ///
210 211 212 213 214 215 216 217 218
  /// The callback is run just after the persistent frame-callbacks (which is
  /// when the main rendering pipeline has been flushed). If a frame is
  /// in progress, but 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.
  ///
  /// The registered callbacks are executed in the order in which they have been
  /// registered.
  void addPostFrameCallback(FrameCallback callback) {
219 220 221
    _postFrameCallbacks.add(callback);
  }

Hixie's avatar
Hixie committed
222 223
  static bool get debugInFrame => _debugInFrame;
  static bool _debugInFrame = false;
224

225
  void _invokeTransientFrameCallbacks(Duration timeStamp) {
226
    Timeline.startSync('Animate');
Hixie's avatar
Hixie committed
227
    assert(_debugInFrame);
228 229 230
    Map<int, FrameCallback> callbacks = _transientCallbacks;
    _transientCallbacks = new Map<int, FrameCallback>();
    callbacks.forEach((int id, FrameCallback callback) {
231
      if (!_removedIds.contains(id))
232
        invokeFrameCallback(callback, timeStamp);
233 234 235 236 237 238 239 240
    });
    _removedIds.clear();
    Timeline.finishSync();
  }

  /// Called by the engine to produce a new frame.
  ///
  /// This function first calls all the callbacks registered by
241 242 243 244 245
  /// [scheduleFrameCallback]/[addFrameCallback], then calls all the callbacks
  /// registered by [addPersistentFrameCallback], which typically drive the
  /// rendering pipeline, and finally calls the callbacks registered by
  /// [addPostFrameCallback].
  void handleBeginFrame(Duration rawTimeStamp) {
246
    Timeline.startSync('Begin frame');
Hixie's avatar
Hixie committed
247 248
    assert(!_debugInFrame);
    assert(() { _debugInFrame = true; return true; });
249 250
    Duration timeStamp = new Duration(
        microseconds: (rawTimeStamp.inMicroseconds / timeDilation).round());
251 252
    _hasRequestedABeginFrameCallback = false;
    _invokeTransientFrameCallbacks(timeStamp);
253

254 255
    for (FrameCallback callback in _persistentCallbacks)
      invokeFrameCallback(callback, timeStamp);
256

257 258
    List<FrameCallback> localPostFrameCallbacks =
        new List<FrameCallback>.from(_postFrameCallbacks);
259
    _postFrameCallbacks.clear();
260 261
    for (FrameCallback callback in localPostFrameCallbacks)
      invokeFrameCallback(callback, timeStamp);
262

Hixie's avatar
Hixie committed
263
    assert(() { _debugInFrame = false; return true; });
264 265 266
    Timeline.finishSync();

    // All frame-related callbacks have been executed. Run lower-priority tasks.
267
    _runTasks();
268 269
  }

270 271 272 273 274 275
  /// Invokes the given [callback] with [timestamp] as argument.
  ///
  /// 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) {
276 277 278 279 280 281 282
    assert(callback != null);
    try {
      callback(timeStamp);
    } catch (exception, stack) {
      if (debugSchedulerExceptionHandler != null) {
        debugSchedulerExceptionHandler(exception, stack);
      } else {
283 284 285 286 287 288
        debugPrint('-- EXCEPTION CAUGHT BY SCHEDULER LIBRARY -------------------------------');
        debugPrint('An exception was raised during a scheduler callback:');
        debugPrint('$exception');
        debugPrint('Stack trace:');
        debugPrint('$stack');
        debugPrint('------------------------------------------------------------------------');
289 290 291 292
      }
    }
  }

293 294 295
  /// Ensures that the scheduler is woken by the event loop.
  void _ensureEventLoopCallback() {
    if (_hasRequestedAnEventLoopCallback)
296
      return;
297 298
    Timer.run(handleEventLoopCallback);
    _hasRequestedAnEventLoopCallback = true;
299 300
  }

301
  // TODO(floitsch): "ensureVisualUpdate" doesn't really fit into the scheduler.
302
  void ensureVisualUpdate() {
303
    _ensureBeginFrameCallback();
304 305 306
  }

  /// Schedules a new frame.
307 308
  void _ensureBeginFrameCallback() {
    if (_hasRequestedABeginFrameCallback)
309
      return;
310
    ui.window.scheduleFrame();
311
    _hasRequestedABeginFrameCallback = true;
312 313 314
  }
}

Ian Hickson's avatar
Ian Hickson committed
315 316 317 318 319 320
// 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).
bool defaultSchedulingStrategy({ int priority, Scheduler scheduler }) {
  if (scheduler.transientCallbackCount > 0)
    return priority >= Priority.animation._value;
  return true;
321
}