scheduler.dart 10.8 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 8
import 'dart:developer';
import 'dart:ui' as ui;
9 10

import 'package:collection/priority_queue.dart';
Ian Hickson's avatar
Ian Hickson committed
11
import 'package:flutter/services.dart';
12

13 14 15 16 17 18 19 20 21
/// 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.
22
typedef void FrameCallback(Duration timeStamp);
23 24 25 26 27 28 29 30

typedef void SchedulerExceptionHandler(dynamic exception, StackTrace stack);
/// 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;

31 32 33
/// An entry in the scheduler's priority queue.
///
/// Combines the task and its priority.
34
class _TaskEntry {
35
  final ui.VoidCallback task;
36 37
  final int priority;

38
  const _TaskEntry(this.task, this.priority);
39 40 41 42 43 44 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
}

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
85
abstract class Scheduler extends BindingBase {
86
  /// Requires clients to use the [scheduler] singleton
Ian Hickson's avatar
Ian Hickson committed
87 88 89 90

  void initInstances() {
    super.initInstances();
    _instance = this;
91
    ui.window.onBeginFrame = handleBeginFrame;
92
  }
93

Ian Hickson's avatar
Ian Hickson committed
94 95 96
  static Scheduler _instance;
  static Scheduler get instance => _instance;

97 98
  SchedulingStrategy schedulingStrategy = new DefaultSchedulingStrategy();

Hixie's avatar
Hixie committed
99 100 101 102 103
  static int _taskSorter (_TaskEntry e1, _TaskEntry e2) {
    // Note that we inverse the priority.
    return -e1.priority.compareTo(e2.priority);
  }
  final PriorityQueue _taskQueue = new HeapPriorityQueue<_TaskEntry>(_taskSorter);
104

105 106
  /// Whether this scheduler already requested to be called from the event loop.
  bool _hasRequestedAnEventLoopCallback = false;
107

108 109 110
  /// Whether this scheduler already requested to be called at the beginning of
  /// the next frame.
  bool _hasRequestedABeginFrameCallback = false;
111 112

  /// Schedules the given [task] with the given [priority].
113
  void scheduleTask(ui.VoidCallback task, Priority priority) {
114 115
    bool isFirstTask = _taskQueue.isEmpty;
    _taskQueue.add(new _TaskEntry(task, priority._value));
116
    if (isFirstTask)
117
      _ensureEventLoopCallback();
118 119 120
  }

  /// Invoked by the system when there is time to run tasks.
121 122 123 124 125 126 127
  void handleEventLoopCallback() {
    _hasRequestedAnEventLoopCallback = false;
    _runTasks();
  }

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

144
  int _nextFrameCallbackId = 0; // positive
145
  Map<int, FrameCallback> _transientCallbacks = <int, FrameCallback>{};
146
  final Set<int> _removedIds = new HashSet<int>();
147 148 149 150

  int get transientCallbackCount => _transientCallbacks.length;

  /// Schedules the given frame callback.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
  ///
  /// 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) {
167 168 169 170 171 172
    _nextFrameCallbackId += 1;
    _transientCallbacks[_nextFrameCallbackId] = callback;
    return _nextFrameCallbackId;
  }

  /// Cancels the callback of the given [id].
173 174 175 176
  ///
  /// 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) {
177 178 179 180 181
    assert(id > 0);
    _transientCallbacks.remove(id);
    _removedIds.add(id);
  }

182
  final List<FrameCallback> _persistentCallbacks = new List<FrameCallback>();
183

184 185 186 187 188 189 190 191 192 193
  /// 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) {
194 195 196
    _persistentCallbacks.add(callback);
  }

197
  final List<FrameCallback> _postFrameCallbacks = new List<FrameCallback>();
198 199 200

  /// Schedule a callback for the end of this frame.
  ///
201
  /// Does *not* request a new frame.
202
  ///
203 204 205 206 207 208 209 210 211
  /// 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) {
212 213 214
    _postFrameCallbacks.add(callback);
  }

215
  bool _isInFrame = false;
216

217
  void _invokeTransientFrameCallbacks(Duration timeStamp) {
218
    Timeline.startSync('Animate');
219 220 221 222
    assert(_isInFrame);
    Map<int, FrameCallback> callbacks = _transientCallbacks;
    _transientCallbacks = new Map<int, FrameCallback>();
    callbacks.forEach((int id, FrameCallback callback) {
223
      if (!_removedIds.contains(id))
224
        invokeFrameCallback(callback, timeStamp);
225 226 227 228 229 230 231 232
    });
    _removedIds.clear();
    Timeline.finishSync();
  }

  /// Called by the engine to produce a new frame.
  ///
  /// This function first calls all the callbacks registered by
233 234 235 236 237
  /// [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) {
238
    Timeline.startSync('Begin frame');
239 240
    assert(!_isInFrame);
    _isInFrame = true;
241 242
    Duration timeStamp = new Duration(
        microseconds: (rawTimeStamp.inMicroseconds / timeDilation).round());
243 244
    _hasRequestedABeginFrameCallback = false;
    _invokeTransientFrameCallbacks(timeStamp);
245

246 247
    for (FrameCallback callback in _persistentCallbacks)
      invokeFrameCallback(callback, timeStamp);
248

249 250
    List<FrameCallback> localPostFrameCallbacks =
        new List<FrameCallback>.from(_postFrameCallbacks);
251
    _postFrameCallbacks.clear();
252 253
    for (FrameCallback callback in localPostFrameCallbacks)
      invokeFrameCallback(callback, timeStamp);
254

255
    _isInFrame = false;
256 257 258
    Timeline.finishSync();

    // All frame-related callbacks have been executed. Run lower-priority tasks.
259
    _runTasks();
260 261
  }

262 263 264 265 266 267
  /// 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) {
268 269 270 271 272 273 274
    assert(callback != null);
    try {
      callback(timeStamp);
    } catch (exception, stack) {
      if (debugSchedulerExceptionHandler != null) {
        debugSchedulerExceptionHandler(exception, stack);
      } else {
275 276 277 278 279 280
        debugPrint('-- EXCEPTION CAUGHT BY SCHEDULER LIBRARY -------------------------------');
        debugPrint('An exception was raised during a scheduler callback:');
        debugPrint('$exception');
        debugPrint('Stack trace:');
        debugPrint('$stack');
        debugPrint('------------------------------------------------------------------------');
281 282 283 284
      }
    }
  }

285 286 287
  /// Ensures that the scheduler is woken by the event loop.
  void _ensureEventLoopCallback() {
    if (_hasRequestedAnEventLoopCallback)
288
      return;
289 290
    Timer.run(handleEventLoopCallback);
    _hasRequestedAnEventLoopCallback = true;
291 292
  }

293
  // TODO(floitsch): "ensureVisualUpdate" doesn't really fit into the scheduler.
294
  void ensureVisualUpdate() {
295
    _ensureBeginFrameCallback();
296 297 298
  }

  /// Schedules a new frame.
299 300
  void _ensureBeginFrameCallback() {
    if (_hasRequestedABeginFrameCallback)
301
      return;
302
    ui.window.scheduleFrame();
303
    _hasRequestedABeginFrameCallback = true;
304 305 306 307
  }
}

abstract class SchedulingStrategy {
Ian Hickson's avatar
Ian Hickson committed
308
  bool shouldRunTaskWithPriority({ int priority, Scheduler scheduler });
309 310 311 312 313 314
}

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