blob: 55ee5f5415cb742eb3c9e975306d76301a3f349e [file] [log] [blame]
// Copyright 2014 The Flutter 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' show Flow, Timeline;
import 'dart:ui' show AppLifecycleState, FramePhase, FrameTiming, TimingsCallback;
import 'package:collection/collection.dart' show PriorityQueue, HeapPriorityQueue;
import 'package:flutter/foundation.dart';
import 'debug.dart';
import 'priority.dart';
export 'dart:ui' show AppLifecycleState, VoidCallback;
/// Slows down animations by this factor to help in development.
double get timeDilation => _timeDilation;
double _timeDilation = 1.0;
/// Setting the time dilation automatically calls [SchedulerBinding.resetEpoch]
/// to ensure that time stamps seen by consumers of the scheduler binding are
/// always increasing.
set timeDilation(double value) {
assert(value > 0.0);
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;
}
/// Signature for frame-related callbacks 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 FrameCallback = void Function(Duration timeStamp);
/// Signature for [Scheduler.scheduleTask] callbacks.
///
/// The type argument `T` is the task's return value. Consider [void] if the
/// task does not return a value.
typedef TaskCallback<T> = T Function();
/// Signature for the [SchedulerBinding.schedulingStrategy] callback. Called
/// 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], the default [SchedulingStrategy] for [SchedulerBinding.schedulingStrategy].
typedef SchedulingStrategy = bool Function({ int priority, SchedulerBinding scheduler });
class _TaskEntry<T> {
_TaskEntry(this.task, this.priority, this.debugLabel, this.flow) {
assert(() {
debugStack = StackTrace.current;
return true;
}());
completer = Completer<T>();
}
final TaskCallback<T> task;
final int priority;
final String debugLabel;
final Flow flow;
StackTrace debugStack;
Completer<T> completer;
void run() {
if (!kReleaseMode) {
Timeline.timeSync(
debugLabel ?? 'Scheduled Task',
() {
completer.complete(task());
},
flow: flow != null ? Flow.step(flow.id) : null,
);
} else {
completer.complete(task());
}
}
}
class _FrameCallbackEntry {
_FrameCallbackEntry(this.callback, { bool rescheduling = false }) {
assert(() {
if (rescheduling) {
assert(() {
if (debugCurrentCallbackStack == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('scheduleFrameCallback called with rescheduling true, but no callback is in scope.'),
ErrorDescription(
'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.'
),
ErrorHint(
'If this is the initial registration of the callback, or if the '
'callback is asynchronous, then do not use the "rescheduling" '
'argument.'
)
]);
}
return true;
}());
debugStack = debugCurrentCallbackStack;
} else {
// TODO(ianh): trim the frames from this library, so that the call to scheduleFrameCallback is the top one
debugStack = StackTrace.current;
}
return true;
}());
}
final FrameCallback callback;
static StackTrace debugCurrentCallbackStack;
StackTrace debugStack;
}
/// The various phases that a [SchedulerBinding] goes through during
/// [SchedulerBinding.handleBeginFrame].
///
/// This is exposed by [SchedulerBinding.schedulerPhase].
///
/// The values of this enum are ordered in the same order as the phases occur,
/// so their relative index values can be compared to each other.
///
/// See also:
///
/// * [WidgetsBinding.drawFrame], which pumps the build and rendering pipeline
/// to generate a frame.
enum SchedulerPhase {
/// No frame is being processed. Tasks (scheduled by
/// [WidgetsBinding.scheduleTask]), microtasks (scheduled by
/// [scheduleMicrotask]), [Timer] callbacks, event handlers (e.g. from user
/// input), and other callbacks (e.g. from [Future]s, [Stream]s, and the like)
/// may be executing.
idle,
/// The transient callbacks (scheduled by
/// [WidgetsBinding.scheduleFrameCallback]) are currently executing.
///
/// Typically, these callbacks handle updating objects to new animation
/// states.
///
/// See [SchedulerBinding.handleBeginFrame].
transientCallbacks,
/// Microtasks scheduled during the processing of transient callbacks are
/// current executing.
///
/// This may include, for instance, callbacks from futures resolved during the
/// [transientCallbacks] phase.
midFrameMicrotasks,
/// The persistent callbacks (scheduled by
/// [WidgetsBinding.addPersistentFrameCallback]) are currently executing.
///
/// Typically, this is the build/layout/paint pipeline. See
/// [WidgetsBinding.drawFrame] and [SchedulerBinding.handleDrawFrame].
persistentCallbacks,
/// The post-frame callbacks (scheduled by
/// [WidgetsBinding.addPostFrameCallback]) are currently executing.
///
/// Typically, these callbacks handle cleanup and scheduling of work for the
/// next frame.
///
/// See [SchedulerBinding.handleDrawFrame].
postFrameCallbacks,
}
/// Scheduler for running the following:
///
/// * _Transient callbacks_, triggered by the system's [Window.onBeginFrame]
/// callback, for synchronizing the application's behavior to the system's
/// display. For example, [Ticker]s and [AnimationController]s trigger from
/// these.
///
/// * _Persistent callbacks_, triggered by the system's [Window.onDrawFrame]
/// callback, for updating the system's display after transient callbacks have
/// executed. For example, the rendering layer uses this to drive its
/// rendering pipeline.
///
/// * _Post-frame callbacks_, which are run after persistent callbacks, just
/// before returning from the [Window.onDrawFrame] callback.
///
/// * Non-rendering tasks, to be run between frames. These are given a
/// priority and are executed in priority order according to a
/// [schedulingStrategy].
mixin SchedulerBinding on BindingBase {
@override
void initInstances() {
super.initInstances();
_instance = this;
if (!kReleaseMode) {
int frameNumber = 0;
addTimingsCallback((List<FrameTiming> timings) {
for (final FrameTiming frameTiming in timings) {
frameNumber += 1;
_profileFramePostEvent(frameNumber, frameTiming);
}
});
}
}
final List<TimingsCallback> _timingsCallbacks = <TimingsCallback>[];
/// Add a [TimingsCallback] that receives [FrameTiming] sent from the engine.
///
/// This can be used, for example, to monitor the performance in release mode,
/// or to get a signal when the first frame is rasterized.
///
/// This is preferred over using [Window.onReportTimings] directly because
/// [addTimingsCallback] allows multiple callbacks.
///
/// If the same callback is added twice, it will be executed twice.
void addTimingsCallback(TimingsCallback callback) {
_timingsCallbacks.add(callback);
if (_timingsCallbacks.length == 1) {
assert(window.onReportTimings == null);
window.onReportTimings = _executeTimingsCallbacks;
}
assert(window.onReportTimings == _executeTimingsCallbacks);
}
/// Removes a callback that was earlier added by [addTimingsCallback].
void removeTimingsCallback(TimingsCallback callback) {
assert(_timingsCallbacks.contains(callback));
_timingsCallbacks.remove(callback);
if (_timingsCallbacks.isEmpty) {
window.onReportTimings = null;
}
}
void _executeTimingsCallbacks(List<FrameTiming> timings) {
final List<TimingsCallback> clonedCallbacks =
List<TimingsCallback>.from(_timingsCallbacks);
for (final TimingsCallback callback in clonedCallbacks) {
try {
if (_timingsCallbacks.contains(callback)) {
callback(timings);
}
} catch (exception, stack) {
InformationCollector collector;
assert(() {
collector = () sync* {
yield DiagnosticsProperty<TimingsCallback>(
'The TimingsCallback that gets executed was',
callback,
style: DiagnosticsTreeStyle.errorProperty,
);
};
return true;
}());
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
context: ErrorDescription('while executing callbacks for FrameTiming'),
informationCollector: collector
));
}
}
}
/// The current [SchedulerBinding], if one has been created.
static SchedulerBinding get instance => _instance;
static SchedulerBinding _instance;
@override
void initServiceExtensions() {
super.initServiceExtensions();
if (!kReleaseMode) {
registerNumericServiceExtension(
name: 'timeDilation',
getter: () async => timeDilation,
setter: (double value) async {
timeDilation = value;
},
);
}
}
/// Whether the application is visible, and if so, whether it is currently
/// interactive.
///
/// This is set by [handleAppLifecycleStateChanged] when the
/// [SystemChannels.lifecycle] notification is dispatched.
///
/// The preferred way to watch for changes to this value is using
/// [WidgetsBindingObserver.didChangeAppLifecycleState].
AppLifecycleState get lifecycleState => _lifecycleState;
AppLifecycleState _lifecycleState;
/// Called when the application lifecycle state changes.
///
/// Notifies all the observers using
/// [WidgetsBindingObserver.didChangeAppLifecycleState].
///
/// This method exposes notifications from [SystemChannels.lifecycle].
@protected
@mustCallSuper
void handleAppLifecycleStateChanged(AppLifecycleState state) {
assert(state != null);
_lifecycleState = state;
switch (state) {
case AppLifecycleState.resumed:
case AppLifecycleState.inactive:
_setFramesEnabledState(true);
break;
case AppLifecycleState.paused:
case AppLifecycleState.detached:
_setFramesEnabledState(false);
break;
}
}
/// The strategy to use when deciding whether to run a task or not.
///
/// Defaults to [defaultSchedulingStrategy].
SchedulingStrategy schedulingStrategy = defaultSchedulingStrategy;
static int _taskSorter (_TaskEntry<dynamic> e1, _TaskEntry<dynamic> e2) {
return -e1.priority.compareTo(e2.priority);
}
final PriorityQueue<_TaskEntry<dynamic>> _taskQueue = HeapPriorityQueue<_TaskEntry<dynamic>>(_taskSorter);
/// Schedules the given `task` with the given `priority` and returns a
/// [Future] that completes to the `task`'s eventual return value.
///
/// The `debugLabel` and `flow` are used to report the task to the [Timeline],
/// for use when profiling.
///
/// ## Processing model
///
/// 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.
///
/// If an animation is running, including, for instance, a [ProgressIndicator]
/// indicating that there are pending tasks, then tasks with a priority below
/// [Priority.animation] won't run (at least, not with the
/// [defaultSchedulingStrategy]; this can be configured using
/// [schedulingStrategy]).
Future<T> scheduleTask<T>(
TaskCallback<T> task,
Priority priority, {
String debugLabel,
Flow flow,
}) {
final bool isFirstTask = _taskQueue.isEmpty;
final _TaskEntry<T> entry = _TaskEntry<T>(
task,
priority.value,
debugLabel,
flow,
);
_taskQueue.add(entry);
if (isFirstTask && !locked)
_ensureEventLoopCallback();
return entry.completer.future;
}
@override
void unlocked() {
super.unlocked();
if (_taskQueue.isNotEmpty)
_ensureEventLoopCallback();
}
// Whether this scheduler already requested to be called from the event loop.
bool _hasRequestedAnEventLoopCallback = false;
// Ensures that the scheduler services a task scheduled by
// [SchedulerBinding.scheduleTask].
void _ensureEventLoopCallback() {
assert(!locked);
assert(_taskQueue.isNotEmpty);
if (_hasRequestedAnEventLoopCallback)
return;
_hasRequestedAnEventLoopCallback = true;
Timer.run(_runTasks);
}
// Scheduled by _ensureEventLoopCallback.
void _runTasks() {
_hasRequestedAnEventLoopCallback = false;
if (handleEventLoopCallback())
_ensureEventLoopCallback(); // runs next task when there's time
}
/// Execute the highest-priority task, if it is of a high enough priority.
///
/// Returns true if a task was executed and there are other tasks remaining
/// (even if they are not high-enough priority).
///
/// Returns false if no task was executed, which can occur if there are no
/// tasks scheduled, if the scheduler is [locked], or if the highest-priority
/// task is of too low a priority given the current [schedulingStrategy].
///
/// Also returns false if there are no tasks remaining.
@visibleForTesting
bool handleEventLoopCallback() {
if (_taskQueue.isEmpty || locked)
return false;
final _TaskEntry<dynamic> entry = _taskQueue.first;
if (schedulingStrategy(priority: entry.priority, scheduler: this)) {
try {
_taskQueue.removeFirst();
entry.run();
} catch (exception, exceptionStack) {
StackTrace callbackStack;
assert(() {
callbackStack = entry.debugStack;
return true;
}());
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: exceptionStack,
library: 'scheduler library',
context: ErrorDescription('during a task callback'),
informationCollector: (callbackStack == null) ? null : () sync* {
yield DiagnosticsStackTrace(
'\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',
callbackStack,
);
},
));
}
return _taskQueue.isNotEmpty;
}
return false;
}
int _nextFrameCallbackId = 0; // positive
Map<int, _FrameCallbackEntry> _transientCallbacks = <int, _FrameCallbackEntry>{};
final Set<int> _removedIds = HashSet<int>();
/// The current number of transient frame callbacks scheduled.
///
/// This is reset to zero just before all the currently scheduled
/// transient callbacks are called, at the start of a frame.
///
/// 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.
int get transientCallbackCount => _transientCallbacks.length;
/// Schedules the given transient frame callback.
///
/// Adds the given callback to the list of frame callbacks and ensures that a
/// frame is scheduled.
///
/// If this is a one-off registration, ignore the `rescheduling` argument.
///
/// If this is a callback that will be re-registered each time it fires, then
/// when you re-register 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 re-registered. This makes it
/// easier to track down the original reason that a particular callback was
/// called. If `rescheduling` is true, the call must be in the context of a
/// frame callback.
///
/// Callbacks registered with this method can be canceled using
/// [cancelFrameCallbackWithId].
int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {
scheduleFrame();
_nextFrameCallbackId += 1;
_transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);
return _nextFrameCallbackId;
}
/// Cancels the transient frame callback with the given [id].
///
/// Removes the given callback from the list of frame callbacks. If a frame
/// has been requested, this does not also cancel that request.
///
/// Transient frame callbacks are those registered using
/// [scheduleFrameCallback].
void cancelFrameCallbackWithId(int id) {
assert(id > 0);
_transientCallbacks.remove(id);
_removedIds.add(id);
}
/// Asserts that there are no registered transient callbacks; if
/// there are, prints their locations and throws an exception.
///
/// A transient frame callback is one that was registered with
/// [scheduleFrameCallback].
///
/// This is expected to be called at the end of tests (the
/// flutter_test framework does it automatically in normal cases).
///
/// Call this method when you expect there to be no transient
/// callbacks registered, in an assert statement with a message that
/// you want printed when a transient callback is registered:
///
/// ```dart
/// assert(SchedulerBinding.instance.debugAssertNoTransientCallbacks(
/// '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) {
// We cache the values so that we can produce them later
// even if the information collector is called after
// the problem has been resolved.
final int count = transientCallbackCount;
final Map<int, _FrameCallbackEntry> callbacks = Map<int, _FrameCallbackEntry>.from(_transientCallbacks);
FlutterError.reportError(FlutterErrorDetails(
exception: reason,
library: 'scheduler library',
informationCollector: () sync* {
if (count == 1) {
// TODO(jacobr): I have added an extra line break in this case.
yield ErrorDescription(
'There was one transient callback left. '
'The stack trace for when it was registered is as follows:'
);
} else {
yield ErrorDescription(
'There were $count transient callbacks left. '
'The stack traces for when they were registered are as follows:'
);
}
for (final int id in callbacks.keys) {
final _FrameCallbackEntry entry = callbacks[id];
yield DiagnosticsStackTrace('── callback $id ──', entry.debugStack, showSeparator: false);
}
},
));
}
return true;
}());
return true;
}
/// Prints the stack for where the current transient callback was registered.
///
/// A transient frame callback is one that was registered with
/// [scheduleFrameCallback].
///
/// When called in debug more and in the context of a transient callback, this
/// function prints the stack trace from where the current transient callback
/// was registered (i.e. where it first called [scheduleFrameCallback]).
///
/// When called in debug mode in other contexts, it prints a message saying
/// that this function was not called in the context a transient callback.
///
/// In release mode, this function does nothing.
///
/// To call this function, use the following code:
///
/// ```dart
/// SchedulerBinding.debugPrintTransientCallbackRegistrationStack();
/// ```
static void debugPrintTransientCallbackRegistrationStack() {
assert(() {
if (_FrameCallbackEntry.debugCurrentCallbackStack != null) {
debugPrint('When the current transient callback was registered, this was the stack:');
debugPrint(
FlutterError.defaultStackFilter(
_FrameCallbackEntry.debugCurrentCallbackStack.toString().trimRight().split('\n')
).join('\n')
);
} else {
debugPrint('No transient callback is currently executing.');
}
return true;
}());
}
final List<FrameCallback> _persistentCallbacks = <FrameCallback>[];
/// Adds a persistent frame callback.
///
/// Persistent callbacks are called after transient
/// (non-persistent) frame callbacks.
///
/// 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.
///
/// Persistent frame callbacks cannot be unregistered. Once registered, they
/// are called for every frame for the lifetime of the application.
void addPersistentFrameCallback(FrameCallback callback) {
_persistentCallbacks.add(callback);
}
final List<FrameCallback> _postFrameCallbacks = <FrameCallback>[];
/// Schedule a callback for the end of this frame.
///
/// Does *not* request a new frame.
///
/// 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.
///
/// The callbacks are executed in the order in which they have been
/// added.
///
/// Post-frame callbacks cannot be unregistered. They are called exactly once.
///
/// See also:
///
/// * [scheduleFrameCallback], which registers a callback for the start of
/// the next frame.
void addPostFrameCallback(FrameCallback callback) {
_postFrameCallbacks.add(callback);
}
Completer<void> _nextFrameCompleter;
/// Returns a Future that completes after the frame completes.
///
/// If this is called between frames, a frame is immediately scheduled if
/// necessary. If this is called during a frame, the Future completes after
/// the current frame.
///
/// If the device's screen is currently turned off, this may wait a very long
/// time, since frames are not scheduled while the device's screen is turned
/// off.
Future<void> get endOfFrame {
if (_nextFrameCompleter == null) {
if (schedulerPhase == SchedulerPhase.idle)
scheduleFrame();
_nextFrameCompleter = Completer<void>();
addPostFrameCallback((Duration timeStamp) {
_nextFrameCompleter.complete();
_nextFrameCompleter = null;
});
}
return _nextFrameCompleter.future;
}
/// Whether this scheduler has requested that [handleBeginFrame] be called soon.
bool get hasScheduledFrame => _hasScheduledFrame;
bool _hasScheduledFrame = false;
/// The phase that the scheduler is currently operating under.
SchedulerPhase get schedulerPhase => _schedulerPhase;
SchedulerPhase _schedulerPhase = SchedulerPhase.idle;
/// Whether frames are currently being scheduled when [scheduleFrame] is called.
///
/// This value depends on the value of the [lifecycleState].
bool get framesEnabled => _framesEnabled;
bool _framesEnabled = true;
void _setFramesEnabledState(bool enabled) {
if (_framesEnabled == enabled)
return;
_framesEnabled = enabled;
if (enabled)
scheduleFrame();
}
@protected
void ensureFrameCallbacksRegistered() {
window.onBeginFrame ??= _handleBeginFrame;
window.onDrawFrame ??= _handleDrawFrame;
}
/// Schedules a new frame using [scheduleFrame] if this object is not
/// currently producing a frame.
///
/// Calling this method ensures that [handleDrawFrame] will eventually be
/// called, unless it's already in progress.
///
/// This has no effect if [schedulerPhase] is
/// [SchedulerPhase.transientCallbacks] or [SchedulerPhase.midFrameMicrotasks]
/// (because a frame is already being prepared in that case), or
/// [SchedulerPhase.persistentCallbacks] (because a frame is actively being
/// rendered in that case). It will schedule a frame if the [schedulerPhase]
/// is [SchedulerPhase.idle] (in between frames) or
/// [SchedulerPhase.postFrameCallbacks] (after a frame).
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks:
return;
}
}
/// If necessary, schedules a new frame by calling
/// [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.) Calling this during a frame forces
/// another frame to be scheduled, even if the current frame has not yet
/// completed.
///
/// Scheduled frames are serviced when triggered by a "Vsync" signal provided
/// by the operating system. The "Vsync" signal, or vertical synchronization
/// signal, was historically related to the display refresh, at a time when
/// hardware physically moved a beam of electrons vertically between updates
/// of the display. The operation of contemporary hardware is somewhat more
/// subtle and complicated, but the conceptual "Vsync" refresh signal continue
/// to be used to indicate when applications should update their rendering.
///
/// To have a stack trace printed to the console any time this function
/// schedules a frame, set [debugPrintScheduleFrameStacks] to true.
///
/// See also:
///
/// * [scheduleForcedFrame], which ignores the [lifecycleState] when
/// scheduling a frame.
/// * [scheduleWarmUpFrame], which ignores the "Vsync" signal entirely and
/// triggers a frame immediately.
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled)
return;
assert(() {
if (debugPrintScheduleFrameStacks)
debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
return true;
}());
ensureFrameCallbacksRegistered();
window.scheduleFrame();
_hasScheduledFrame = true;
}
/// Schedules a new frame by calling [Window.scheduleFrame].
///
/// After this is called, the engine will call [handleBeginFrame], even if
/// frames would normally not be scheduled by [scheduleFrame] (e.g. even if
/// the device's screen is turned off).
///
/// The framework uses this to force a frame to be rendered at the correct
/// size when the phone is rotated, so that a correctly-sized rendering is
/// available when the screen is turned back on.
///
/// To have a stack trace printed to the console any time this function
/// schedules a frame, set [debugPrintScheduleFrameStacks] to true.
///
/// Prefer using [scheduleFrame] unless it is imperative that a frame be
/// scheduled immediately, since using [scheduleForceFrame] will cause
/// significantly higher battery usage when the device should be idle.
///
/// Consider using [scheduleWarmUpFrame] instead if the goal is to update the
/// rendering as soon as possible (e.g. at application startup).
void scheduleForcedFrame() {
// TODO(chunhtai): Removes the if case once the issue is fixed
// https://github.com/flutter/flutter/issues/45131
if (!framesEnabled)
return;
if (_hasScheduledFrame)
return;
assert(() {
if (debugPrintScheduleFrameStacks)
debugPrintStack(label: 'scheduleForcedFrame() called. Current phase is $schedulerPhase.');
return true;
}());
window.scheduleFrame();
_hasScheduledFrame = true;
}
bool _warmUpFrame = false;
/// Schedule a frame to run as soon as possible, rather than waiting for
/// the engine to request a frame in response to a system "Vsync" signal.
///
/// This is used during application startup so that the first frame (which is
/// likely to be quite expensive) gets a few extra milliseconds to run.
///
/// Locks events dispatching until the scheduled frame has completed.
///
/// If a frame has already been scheduled with [scheduleFrame] or
/// [scheduleForcedFrame], this call may delay that frame.
///
/// If any scheduled frame has already begun or if another
/// [scheduleWarmUpFrame] was already called, this call will be ignored.
///
/// Prefer [scheduleFrame] to update the display in normal operation.
void scheduleWarmUpFrame() {
if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle)
return;
_warmUpFrame = true;
Timeline.startSync('Warm-up frame');
final bool hadScheduledFrame = _hasScheduledFrame;
// We use timers here to ensure that microtasks flush in between.
Timer.run(() {
assert(_warmUpFrame);
handleBeginFrame(null);
});
Timer.run(() {
assert(_warmUpFrame);
handleDrawFrame();
// We call resetEpoch after this frame so that, in the hot reload case,
// the very next frame pretends to have occurred immediately after this
// warm-up frame. The warm-up frame's timestamp will typically be far in
// the past (the time of the last real frame), so if we didn't reset the
// epoch we would see a sudden jump from the old time in the warm-up frame
// to the new time in the "real" frame. The biggest problem with this is
// that implicit animations end up being triggered at the old time and
// then skipping every frame and finishing in the new time.
resetEpoch();
_warmUpFrame = false;
if (hadScheduledFrame)
scheduleFrame();
});
// Lock events so touch events etc don't insert themselves until the
// scheduled frame has finished.
lockEvents(() async {
await endOfFrame;
Timeline.finishSync();
});
}
Duration _firstRawTimeStampInEpoch;
Duration _epochStart = Duration.zero;
Duration _lastRawTimeStamp = Duration.zero;
/// Prepares the scheduler for a non-monotonic change to how time stamps are
/// calculated.
///
/// 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
/// resetting 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.
///
/// Setting [timeDilation] calls [resetEpoch] automatically. You don't need to
/// call [resetEpoch] yourself.
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) {
final Duration rawDurationSinceEpoch = _firstRawTimeStampInEpoch == null ? Duration.zero : rawTimeStamp - _firstRawTimeStampInEpoch;
return Duration(microseconds: (rawDurationSinceEpoch.inMicroseconds / timeDilation).round() + _epochStart.inMicroseconds);
}
/// The time stamp for the frame currently being processed.
///
/// This is only valid while between the start of [handleBeginFrame] and the
/// end of the corresponding [handleDrawFrame], i.e. while a frame is being
/// produced.
Duration get currentFrameTimeStamp {
assert(_currentFrameTimeStamp != null);
return _currentFrameTimeStamp;
}
Duration _currentFrameTimeStamp;
/// The raw time stamp as provided by the engine to [Window.onBeginFrame]
/// for the frame currently being processed.
///
/// Unlike [currentFrameTimeStamp], this time stamp is neither adjusted to
/// offset when the epoch started nor scaled to reflect the [timeDilation] in
/// the current epoch.
///
/// On most platforms, this is a more or less arbitrary value, and should
/// generally be ignored. On Fuchsia, this corresponds to the system-provided
/// presentation time, and can be used to ensure that animations running in
/// different processes are synchronized.
Duration get currentSystemFrameTimeStamp {
assert(_lastRawTimeStamp != null);
return _lastRawTimeStamp;
}
int _debugFrameNumber = 0;
String _debugBanner;
bool _ignoreNextEngineDrawFrame = false;
void _handleBeginFrame(Duration rawTimeStamp) {
if (_warmUpFrame) {
assert(!_ignoreNextEngineDrawFrame);
_ignoreNextEngineDrawFrame = true;
return;
}
handleBeginFrame(rawTimeStamp);
}
void _handleDrawFrame() {
if (_ignoreNextEngineDrawFrame) {
_ignoreNextEngineDrawFrame = false;
return;
}
handleDrawFrame();
}
/// Called by the engine to prepare the framework to produce a new frame.
///
/// This function calls all the transient frame callbacks registered by
/// [scheduleFrameCallback]. It then returns, any scheduled microtasks are run
/// (e.g. handlers for any [Future]s resolved by transient frame callbacks),
/// and [handleDrawFrame] is called to continue the frame.
///
/// If the given time stamp is null, the time stamp from the last frame is
/// reused.
///
/// To have a banner shown at the start of every frame in debug mode, set
/// [debugPrintBeginFrameBanner] to true. The banner will be printed to the
/// console using [debugPrint] and will contain the frame number (which
/// increments by one for each frame), and the time stamp of the frame. If the
/// given time stamp was null, then the string "warm-up frame" is shown
/// instead of the time stamp. This allows frames eagerly pushed by the
/// framework to be distinguished from those requested by the engine in
/// response to the "Vsync" signal from the operating system.
///
/// You can also show a banner at the end of every frame by setting
/// [debugPrintEndFrameBanner] to true. This allows you to distinguish log
/// statements printed during a frame from those printed between frames (e.g.
/// in response to events or timers).
void handleBeginFrame(Duration rawTimeStamp) {
Timeline.startSync('Frame', arguments: timelineWhitelistArguments);
_firstRawTimeStampInEpoch ??= rawTimeStamp;
_currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);
if (rawTimeStamp != null)
_lastRawTimeStamp = rawTimeStamp;
assert(() {
_debugFrameNumber += 1;
if (debugPrintBeginFrameBanner || debugPrintEndFrameBanner) {
final StringBuffer frameTimeStampDescription = StringBuffer();
if (rawTimeStamp != null) {
_debugDescribeTimeStamp(_currentFrameTimeStamp, frameTimeStampDescription);
} else {
frameTimeStampDescription.write('(warm-up frame)');
}
_debugBanner = '▄▄▄▄▄▄▄▄ Frame ${_debugFrameNumber.toString().padRight(7)} ${frameTimeStampDescription.toString().padLeft(18)} ▄▄▄▄▄▄▄▄';
if (debugPrintBeginFrameBanner)
debugPrint(_debugBanner);
}
return true;
}());
assert(schedulerPhase == SchedulerPhase.idle);
_hasScheduledFrame = false;
try {
// TRANSIENT FRAME CALLBACKS
Timeline.startSync('Animate', arguments: timelineWhitelistArguments);
_schedulerPhase = SchedulerPhase.transientCallbacks;
final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
_transientCallbacks = <int, _FrameCallbackEntry>{};
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id))
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
});
_removedIds.clear();
} finally {
_schedulerPhase = SchedulerPhase.midFrameMicrotasks;
}
}
/// Called by the engine to produce a new frame.
///
/// This method is called immediately after [handleBeginFrame]. It calls all
/// the callbacks registered by [addPersistentFrameCallback], which typically
/// drive the rendering pipeline, and then calls the callbacks registered by
/// [addPostFrameCallback].
///
/// See [handleBeginFrame] for a discussion about debugging hooks that may be
/// useful when working with frame callbacks.
void handleDrawFrame() {
assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
Timeline.finishSync(); // end the "Animate" phase
try {
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (final FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
// POST-FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (final FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
_schedulerPhase = SchedulerPhase.idle;
Timeline.finishSync(); // end the Frame
assert(() {
if (debugPrintEndFrameBanner)
debugPrint('▀' * _debugBanner.length);
_debugBanner = null;
return true;
}());
_currentFrameTimeStamp = null;
}
}
void _profileFramePostEvent(int frameNumber, FrameTiming frameTiming) {
postEvent('Flutter.Frame', <String, dynamic>{
'number': frameNumber,
'startTime': frameTiming.timestampInMicroseconds(FramePhase.buildStart),
'elapsed': frameTiming.totalSpan.inMicroseconds,
'build': frameTiming.buildDuration.inMicroseconds,
'raster': frameTiming.rasterDuration.inMicroseconds,
});
}
static void _debugDescribeTimeStamp(Duration timeStamp, StringBuffer buffer) {
if (timeStamp.inDays > 0)
buffer.write('${timeStamp.inDays}d ');
if (timeStamp.inHours > 0)
buffer.write('${timeStamp.inHours - timeStamp.inDays * Duration.hoursPerDay}h ');
if (timeStamp.inMinutes > 0)
buffer.write('${timeStamp.inMinutes - timeStamp.inHours * Duration.minutesPerHour}m ');
if (timeStamp.inSeconds > 0)
buffer.write('${timeStamp.inSeconds - timeStamp.inMinutes * Duration.secondsPerMinute}s ');
buffer.write('${timeStamp.inMilliseconds - timeStamp.inSeconds * Duration.millisecondsPerSecond}');
final int microseconds = timeStamp.inMicroseconds - timeStamp.inMilliseconds * Duration.microsecondsPerMillisecond;
if (microseconds > 0)
buffer.write('.${microseconds.toString().padLeft(3, "0")}');
buffer.write('ms');
}
// Calls 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, [ StackTrace callbackStack ]) {
assert(callback != null);
assert(_FrameCallbackEntry.debugCurrentCallbackStack == null);
assert(() {
_FrameCallbackEntry.debugCurrentCallbackStack = callbackStack;
return true;
}());
try {
callback(timeStamp);
} catch (exception, exceptionStack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: exceptionStack,
library: 'scheduler library',
context: ErrorDescription('during a scheduler callback'),
informationCollector: (callbackStack == null) ? null : () sync* {
yield DiagnosticsStackTrace(
'\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',
callbackStack,
);
},
));
}
assert(() {
_FrameCallbackEntry.debugCurrentCallbackStack = null;
return true;
}());
}
}
/// The default [SchedulingStrategy] for [SchedulerBinding.schedulingStrategy].
///
/// If there are any frame callbacks registered, only runs tasks with
/// a [Priority] of [Priority.animation] or higher. Otherwise, runs
/// all tasks.
bool defaultSchedulingStrategy({ int priority, SchedulerBinding scheduler }) {
if (scheduler.transientCallbackCount > 0)
return priority >= Priority.animation.value;
return true;
}