// 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'; import 'dart:collection'; import 'dart:developer'; import 'dart:ui' as ui show window; import 'dart:ui' show VoidCallback; import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; export 'dart:ui' show VoidCallback; /// 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. typedef void FrameCallback(Duration timeStamp); 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; /// An entry in the scheduler's priority queue. /// /// Combines the task and its priority. class _TaskEntry { final VoidCallback task; final int priority; const _TaskEntry(this.task, this.priority); } 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. abstract class Scheduler extends BindingBase { /// Requires clients to use the [scheduler] singleton void initInstances() { super.initInstances(); _instance = this; ui.window.onBeginFrame = handleBeginFrame; } static Scheduler _instance; static Scheduler get instance => _instance; SchedulingStrategy schedulingStrategy = new DefaultSchedulingStrategy(); 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); /// Whether this scheduler already requested to be called from the event loop. bool _hasRequestedAnEventLoopCallback = false; /// Whether this scheduler already requested to be called at the beginning of /// the next frame. bool _hasRequestedABeginFrameCallback = false; /// Schedules the given [task] with the given [priority]. void scheduleTask(VoidCallback task, Priority priority) { bool isFirstTask = _taskQueue.isEmpty; _taskQueue.add(new _TaskEntry(task, priority._value)); if (isFirstTask) _ensureEventLoopCallback(); } /// Invoked by the system when there is time to run tasks. void handleEventLoopCallback() { _hasRequestedAnEventLoopCallback = false; _runTasks(); } void _runTasks() { if (_taskQueue.isEmpty) return; _TaskEntry entry = _taskQueue.first; if (schedulingStrategy.shouldRunTaskWithPriority(priority: entry.priority, scheduler: this)) { try { (_taskQueue.removeFirst().task)(); } finally { if (_taskQueue.isNotEmpty) _ensureEventLoopCallback(); } } else { // TODO(floitsch): we shouldn't need to request a frame. Just schedule // an event-loop callback. _ensureBeginFrameCallback(); } } int _nextFrameCallbackId = 0; // positive Map<int, FrameCallback> _transientCallbacks = <int, FrameCallback>{}; final Set<int> _removedIds = new HashSet<int>(); int get transientCallbackCount => _transientCallbacks.length; /// Schedules the given frame callback. /// /// 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) { _nextFrameCallbackId += 1; _transientCallbacks[_nextFrameCallbackId] = callback; return _nextFrameCallbackId; } /// Cancels the callback of the given [id]. /// /// 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) { assert(id > 0); _transientCallbacks.remove(id); _removedIds.add(id); } final List<FrameCallback> _persistentCallbacks = new List<FrameCallback>(); /// 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) { _persistentCallbacks.add(callback); } final List<FrameCallback> _postFrameCallbacks = new List<FrameCallback>(); /// Schedule a callback for the end of this frame. /// /// Does *not* request a new frame. /// /// 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) { _postFrameCallbacks.add(callback); } static bool get debugInFrame => _debugInFrame; static bool _debugInFrame = false; void _invokeTransientFrameCallbacks(Duration timeStamp) { Timeline.startSync('Animate'); assert(_debugInFrame); Map<int, FrameCallback> callbacks = _transientCallbacks; _transientCallbacks = new Map<int, FrameCallback>(); callbacks.forEach((int id, FrameCallback callback) { if (!_removedIds.contains(id)) invokeFrameCallback(callback, timeStamp); }); _removedIds.clear(); Timeline.finishSync(); } /// Called by the engine to produce a new frame. /// /// This function first calls all the callbacks registered by /// [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) { Timeline.startSync('Begin frame'); assert(!_debugInFrame); assert(() { _debugInFrame = true; return true; }); Duration timeStamp = new Duration( microseconds: (rawTimeStamp.inMicroseconds / timeDilation).round()); _hasRequestedABeginFrameCallback = false; _invokeTransientFrameCallbacks(timeStamp); for (FrameCallback callback in _persistentCallbacks) invokeFrameCallback(callback, timeStamp); List<FrameCallback> localPostFrameCallbacks = new List<FrameCallback>.from(_postFrameCallbacks); _postFrameCallbacks.clear(); for (FrameCallback callback in localPostFrameCallbacks) invokeFrameCallback(callback, timeStamp); assert(() { _debugInFrame = false; return true; }); Timeline.finishSync(); // All frame-related callbacks have been executed. Run lower-priority tasks. _runTasks(); } /// 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) { assert(callback != null); try { callback(timeStamp); } catch (exception, stack) { if (debugSchedulerExceptionHandler != null) { debugSchedulerExceptionHandler(exception, stack); } else { debugPrint('-- EXCEPTION CAUGHT BY SCHEDULER LIBRARY -------------------------------'); debugPrint('An exception was raised during a scheduler callback:'); debugPrint('$exception'); debugPrint('Stack trace:'); debugPrint('$stack'); debugPrint('------------------------------------------------------------------------'); } } } /// Ensures that the scheduler is woken by the event loop. void _ensureEventLoopCallback() { if (_hasRequestedAnEventLoopCallback) return; Timer.run(handleEventLoopCallback); _hasRequestedAnEventLoopCallback = true; } // TODO(floitsch): "ensureVisualUpdate" doesn't really fit into the scheduler. void ensureVisualUpdate() { _ensureBeginFrameCallback(); } /// Schedules a new frame. void _ensureBeginFrameCallback() { if (_hasRequestedABeginFrameCallback) return; ui.window.scheduleFrame(); _hasRequestedABeginFrameCallback = true; } } abstract class SchedulingStrategy { bool shouldRunTaskWithPriority({ int priority, Scheduler scheduler }); } 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). bool shouldRunTaskWithPriority({ int priority, Scheduler scheduler }) { if (scheduler.transientCallbackCount > 0) return priority >= Priority.animation._value; return true; } }