Unverified Commit 186d1e9b authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Have the framework in charge of scheduling frames. (#13344)

...instead of the engine.
parent 425bd5a8
[^═]*(this line contains the test framework's output with the clock and so forth)?
══╡ EXCEPTION CAUGHT BY SCHEDULER LIBRARY ╞═════════════════════════════════════════════════════════
The following message was thrown:
An animation is still running even after the widget tree was disposed.
There was one transient callback left. The stack trace for when it was registered is as follows:
── callback 2 ──
<<skip until matching line>>
#[0-9]+ main.+ \(.+/flutter/dev/automated_tests/flutter_test/ticker_test\.dart:[0-9]+:[0-9]+\)
<<skip until matching line>>
════════════════════════════════════════════════════════════════════════════════════════════════════
.*..:.. \+0 -1: - Does flutter_test catch leaking tickers\? \[E\]
Test failed\. See exception logs above\.
The test description was: Does flutter_test catch leaking tickers\?
*
.*..:.. \+0 -1: Some tests failed\. *
// Copyright 2016 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 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Does flutter_test catch leaking tickers?', (WidgetTester tester) async {
new Ticker((Duration duration) { })..start();
final ByteData message = const StringCodec().encodeMessage('AppLifecycleState.paused');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
});
}
...@@ -105,8 +105,8 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul ...@@ -105,8 +105,8 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
); );
registerSignalServiceExtension( registerSignalServiceExtension(
name: 'debugDumpSemanticsTreeInInverseHitTestOrder', name: 'debugDumpSemanticsTreeInInverseHitTestOrder',
callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest); return debugPrintDone; } callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest); return debugPrintDone; }
); );
} }
...@@ -139,14 +139,17 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul ...@@ -139,14 +139,17 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
/// Called when the system metrics change. /// Called when the system metrics change.
/// ///
/// See [Window.onMetricsChanged]. /// See [Window.onMetricsChanged].
@protected
void handleMetricsChanged() { void handleMetricsChanged() {
assert(renderView != null); assert(renderView != null);
renderView.configuration = createViewConfiguration(); renderView.configuration = createViewConfiguration();
scheduleForcedFrame();
} }
/// Called when the platform text scale factor changes. /// Called when the platform text scale factor changes.
/// ///
/// See [Window.onTextScaleFactorChanged]. /// See [Window.onTextScaleFactorChanged].
@protected
void handleTextScaleFactorChanged() { } void handleTextScaleFactorChanged() { }
/// Returns a [ViewConfiguration] configured for the [RenderView] based on the /// Returns a [ViewConfiguration] configured for the [RenderView] based on the
...@@ -266,26 +269,6 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul ...@@ -266,26 +269,6 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
} }
/// Schedule a frame to run as soon as possible, rather than waiting for
/// the engine to request a frame.
///
/// 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.
void scheduleWarmUpFrame() {
// We use timers here to ensure that microtasks flush in between.
//
// 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.
Timer.run(() { handleBeginFrame(null); });
Timer.run(() { handleDrawFrame(); resetEpoch(); });
}
@override @override
Future<Null> performReassemble() async { Future<Null> performReassemble() async {
await super.performReassemble(); await super.performReassemble();
......
...@@ -6,15 +6,16 @@ import 'dart:async'; ...@@ -6,15 +6,16 @@ import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:developer'; import 'dart:developer';
import 'dart:ui' as ui show window; import 'dart:ui' as ui show window;
import 'dart:ui' show VoidCallback; import 'dart:ui' show AppLifecycleState, VoidCallback;
import 'package:collection/collection.dart' show PriorityQueue, HeapPriorityQueue; import 'package:collection/collection.dart' show PriorityQueue, HeapPriorityQueue;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'debug.dart'; import 'debug.dart';
import 'priority.dart'; import 'priority.dart';
export 'dart:ui' show VoidCallback; export 'dart:ui' show AppLifecycleState, VoidCallback;
/// Slows down animations by this factor to help in development. /// Slows down animations by this factor to help in development.
double get timeDilation => _timeDilation; double get timeDilation => _timeDilation;
...@@ -51,9 +52,16 @@ typedef void FrameCallback(Duration timeStamp); ...@@ -51,9 +52,16 @@ typedef void FrameCallback(Duration timeStamp);
typedef bool SchedulingStrategy({ int priority, SchedulerBinding scheduler }); typedef bool SchedulingStrategy({ int priority, SchedulerBinding scheduler });
class _TaskEntry { class _TaskEntry {
const _TaskEntry(this.task, this.priority); _TaskEntry(this.task, this.priority) {
assert(() {
debugStack = StackTrace.current;
return true;
}());
}
final VoidCallback task; final VoidCallback task;
final int priority; final int priority;
StackTrace debugStack;
} }
class _FrameCallbackEntry { class _FrameCallbackEntry {
...@@ -85,7 +93,6 @@ class _FrameCallbackEntry { ...@@ -85,7 +93,6 @@ class _FrameCallbackEntry {
final FrameCallback callback; final FrameCallback callback;
// debug-mode fields
static StackTrace debugCurrentCallbackStack; static StackTrace debugCurrentCallbackStack;
StackTrace debugStack; StackTrace debugStack;
} }
...@@ -158,7 +165,7 @@ enum SchedulerPhase { ...@@ -158,7 +165,7 @@ enum SchedulerPhase {
/// * Non-rendering tasks, to be run between frames. These are given a /// * Non-rendering tasks, to be run between frames. These are given a
/// priority and are executed in priority order according to a /// priority and are executed in priority order according to a
/// [schedulingStrategy]. /// [schedulingStrategy].
abstract class SchedulerBinding extends BindingBase { abstract class SchedulerBinding extends BindingBase with ServicesBinding {
// This class is intended to be used as a mixin, and should not be // This class is intended to be used as a mixin, and should not be
// extended directly. // extended directly.
factory SchedulerBinding._() => null; factory SchedulerBinding._() => null;
...@@ -167,8 +174,9 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -167,8 +174,9 @@ abstract class SchedulerBinding extends BindingBase {
void initInstances() { void initInstances() {
super.initInstances(); super.initInstances();
_instance = this; _instance = this;
ui.window.onBeginFrame = handleBeginFrame; ui.window.onBeginFrame = _handleBeginFrame;
ui.window.onDrawFrame = handleDrawFrame; ui.window.onDrawFrame = _handleDrawFrame;
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
} }
/// The current [SchedulerBinding], if one has been created. /// The current [SchedulerBinding], if one has been created.
...@@ -187,6 +195,59 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -187,6 +195,59 @@ abstract class SchedulerBinding extends BindingBase {
); );
} }
/// 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.suspending:
_setFramesEnabledState(false);
break;
}
}
Future<String> _handleLifecycleMessage(String message) {
handleAppLifecycleStateChanged(_parseAppLifecycleMessage(message));
return null;
}
static AppLifecycleState _parseAppLifecycleMessage(String message) {
switch (message) {
case 'AppLifecycleState.paused':
return AppLifecycleState.paused;
case 'AppLifecycleState.resumed':
return AppLifecycleState.resumed;
case 'AppLifecycleState.inactive':
return AppLifecycleState.inactive;
case 'AppLifecycleState.suspending':
return AppLifecycleState.suspending;
}
return null;
}
/// The strategy to use when deciding whether to run a task or not. /// The strategy to use when deciding whether to run a task or not.
/// ///
/// Defaults to [defaultSchedulingStrategy]. /// Defaults to [defaultSchedulingStrategy].
...@@ -221,41 +282,65 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -221,41 +282,65 @@ abstract class SchedulerBinding extends BindingBase {
// Whether this scheduler already requested to be called from the event loop. // Whether this scheduler already requested to be called from the event loop.
bool _hasRequestedAnEventLoopCallback = false; bool _hasRequestedAnEventLoopCallback = false;
// Ensures that the scheduler is awakened by the event loop. // Ensures that the scheduler services a task scheduled by [scheduleTask].
void _ensureEventLoopCallback() { void _ensureEventLoopCallback() {
assert(!locked); assert(!locked);
assert(_taskQueue.isNotEmpty);
if (_hasRequestedAnEventLoopCallback) if (_hasRequestedAnEventLoopCallback)
return; return;
Timer.run(handleEventLoopCallback);
_hasRequestedAnEventLoopCallback = true; _hasRequestedAnEventLoopCallback = true;
Timer.run(_runTasks);
} }
/// Called by the system when there is time to run tasks. // Scheduled by _ensureEventLoopCallback.
void handleEventLoopCallback() { void _runTasks() {
_hasRequestedAnEventLoopCallback = false; _hasRequestedAnEventLoopCallback = false;
_runTasks(); if (handleEventLoopCallback())
_ensureEventLoopCallback(); // runs next task when there's time
} }
// Called when the system wakes up and at the end of each frame. /// Execute the highest-priority task, if it is of a high enough priority.
void _runTasks() { ///
/// 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) if (_taskQueue.isEmpty || locked)
return; return false;
final _TaskEntry entry = _taskQueue.first; final _TaskEntry entry = _taskQueue.first;
// 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).
if (schedulingStrategy(priority: entry.priority, scheduler: this)) { if (schedulingStrategy(priority: entry.priority, scheduler: this)) {
try { try {
(_taskQueue.removeFirst().task)(); (_taskQueue.removeFirst().task)();
} finally { } catch (exception, exceptionStack) {
if (_taskQueue.isNotEmpty) StackTrace callbackStack;
_ensureEventLoopCallback(); assert(() {
callbackStack = entry.debugStack;
return true;
}());
FlutterError.reportError(new FlutterErrorDetails(
exception: exception,
stack: exceptionStack,
library: 'scheduler library',
context: 'during a task callback',
informationCollector: (callbackStack == null) ? null : (StringBuffer information) {
information.writeln(
'\nThis exception was thrown in the context of a task callback. '
'When the task 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);
}
));
} }
} else { return _taskQueue.isNotEmpty;
// TODO(floitsch): we shouldn't need to request a frame. Just schedule
// an event-loop callback.
scheduleFrame();
} }
return false;
} }
int _nextFrameCallbackId = 0; // positive int _nextFrameCallbackId = 0; // positive
...@@ -437,6 +522,11 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -437,6 +522,11 @@ abstract class SchedulerBinding extends BindingBase {
/// added. /// added.
/// ///
/// Post-frame callbacks cannot be unregistered. They are called exactly once. /// 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) { void addPostFrameCallback(FrameCallback callback) {
_postFrameCallbacks.add(callback); _postFrameCallbacks.add(callback);
} }
...@@ -473,6 +563,20 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -473,6 +563,20 @@ abstract class SchedulerBinding extends BindingBase {
SchedulerPhase get schedulerPhase => _schedulerPhase; SchedulerPhase get schedulerPhase => _schedulerPhase;
SchedulerPhase _schedulerPhase = SchedulerPhase.idle; 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();
}
/// Schedules a new frame using [scheduleFrame] if this object is not /// Schedules a new frame using [scheduleFrame] if this object is not
/// currently producing a frame. /// currently producing a frame.
/// ///
...@@ -494,10 +598,25 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -494,10 +598,25 @@ abstract class SchedulerBinding extends BindingBase {
/// another frame to be scheduled, even if the current frame has not yet /// another frame to be scheduled, even if the current frame has not yet
/// completed. /// 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 /// To have a stack trace printed to the console any time this function
/// schedules a frame, set [debugPrintScheduleFrameStacks] to true. /// 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() { void scheduleFrame() {
if (_hasScheduledFrame) if (_hasScheduledFrame || !_framesEnabled)
return; return;
assert(() { assert(() {
if (debugPrintScheduleFrameStacks) if (debugPrintScheduleFrameStacks)
...@@ -508,6 +627,76 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -508,6 +627,76 @@ abstract class SchedulerBinding extends BindingBase {
_hasScheduledFrame = true; _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() {
if (_hasScheduledFrame)
return;
assert(() {
if (debugPrintScheduleFrameStacks)
debugPrintStack(label: 'scheduleForcedFrame() called. Current phase is $schedulerPhase.');
return true;
}());
ui.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.
///
/// If a frame has already been scheduled with [scheduleFrame] or
/// [scheduleForcedFrame], this call may delay that frame.
///
/// Prefer [scheduleFrame] to update the display in normal operation.
void scheduleWarmUpFrame() {
assert(!_warmUpFrame);
final bool hadScheduledFrame = _hasScheduledFrame;
_warmUpFrame = true;
// 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();
});
}
Duration _firstRawTimeStampInEpoch; Duration _firstRawTimeStampInEpoch;
Duration _epochStart = Duration.ZERO; Duration _epochStart = Duration.ZERO;
Duration _lastRawTimeStamp = Duration.ZERO; Duration _lastRawTimeStamp = Duration.ZERO;
...@@ -560,6 +749,24 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -560,6 +749,24 @@ abstract class SchedulerBinding extends BindingBase {
int _profileFrameNumber = 0; int _profileFrameNumber = 0;
final Stopwatch _profileFrameStopwatch = new Stopwatch(); final Stopwatch _profileFrameStopwatch = new Stopwatch();
String _debugBanner; 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. /// Called by the engine to prepare the framework to produce a new frame.
/// ///
...@@ -576,9 +783,9 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -576,9 +783,9 @@ abstract class SchedulerBinding extends BindingBase {
/// console using [debugPrint] and will contain the frame number (which /// 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 /// 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 /// given time stamp was null, then the string "warm-up frame" is shown
/// instead of the time stamp. This allows you to distinguish frames eagerly /// instead of the time stamp. This allows frames eagerly pushed by the
/// pushed by the framework from those requested by the engine in response to /// framework to be distinguished from those requested by the engine in
/// the vsync signal from the operating system. /// response to the "Vsync" signal from the operating system.
/// ///
/// You can also show a banner at the end of every frame by setting /// You can also show a banner at the end of every frame by setting
/// [debugPrintEndFrameBanner] to true. This allows you to distinguish log /// [debugPrintEndFrameBanner] to true. This allows you to distinguish log
...@@ -670,9 +877,6 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -670,9 +877,6 @@ abstract class SchedulerBinding extends BindingBase {
}()); }());
_currentFrameTimeStamp = null; _currentFrameTimeStamp = null;
} }
// All frame-related callbacks have been executed. Run lower-priority tasks.
_runTasks();
} }
void _profileFramePostEvent() { void _profileFramePostEvent() {
...@@ -707,7 +911,6 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -707,7 +911,6 @@ abstract class SchedulerBinding extends BindingBase {
void _invokeFrameCallback(FrameCallback callback, Duration timeStamp, [ StackTrace callbackStack ]) { void _invokeFrameCallback(FrameCallback callback, Duration timeStamp, [ StackTrace callbackStack ]) {
assert(callback != null); assert(callback != null);
assert(_FrameCallbackEntry.debugCurrentCallbackStack == null); assert(_FrameCallbackEntry.debugCurrentCallbackStack == null);
// TODO(ianh): Consider using a Zone instead to track the current callback registration stack
assert(() { _FrameCallbackEntry.debugCurrentCallbackStack = callbackStack; return true; }()); assert(() { _FrameCallbackEntry.debugCurrentCallbackStack = callbackStack; return true; }());
try { try {
callback(timeStamp); callback(timeStamp);
......
...@@ -101,9 +101,21 @@ class Ticker { ...@@ -101,9 +101,21 @@ class Ticker {
/// A ticker that is [muted] can be active (see [isActive]) yet not be /// A ticker that is [muted] can be active (see [isActive]) yet not be
/// ticking. In that case, the ticker will not call its callback, and /// ticking. In that case, the ticker will not call its callback, and
/// [isTicking] will be false, but time will still be progressing. /// [isTicking] will be false, but time will still be progressing.
// TODO(ianh): we should teach the scheduler binding about the lifecycle events ///
// and then this could return an accurate view of the actual scheduler. /// This will return false if the [Scheduler.lifecycleState] is one that
bool get isTicking => _future != null && !muted; /// indicates the application is not currently visible (e.g. if the device's
/// screen is turned off).
bool get isTicking {
if (_future == null)
return false;
if (muted)
return false;
if (SchedulerBinding.instance.framesEnabled)
return true;
if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.idle)
return true; // for example, we might be in a warm-up frame or forced frame
return false;
}
/// Whether time is elapsing for this [Ticker]. Becomes true when [start] is /// Whether time is elapsing for this [Ticker]. Becomes true when [start] is
/// called and false when [stop] is called. /// called and false when [stop] is called.
......
...@@ -8,6 +8,8 @@ import 'package:flutter/foundation.dart'; ...@@ -8,6 +8,8 @@ import 'package:flutter/foundation.dart';
import 'platform_channel.dart'; import 'platform_channel.dart';
export 'dart:typed_data' show ByteData;
/// A message encoding/decoding mechanism. /// A message encoding/decoding mechanism.
/// ///
/// Both operations throw an exception, if conversion fails. Such situations /// Both operations throw an exception, if conversion fails. Such situations
......
...@@ -231,7 +231,7 @@ abstract class WidgetsBindingObserver { ...@@ -231,7 +231,7 @@ abstract class WidgetsBindingObserver {
} }
/// The glue between the widgets layer and the Flutter engine. /// The glue between the widgets layer and the Flutter engine.
abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererBinding { abstract class WidgetsBinding extends BindingBase with SchedulerBinding, GestureBinding, RendererBinding {
// This class is intended to be used as a mixin, and should not be // This class is intended to be used as a mixin, and should not be
// extended directly. // extended directly.
factory WidgetsBinding._() => null; factory WidgetsBinding._() => null;
...@@ -243,7 +243,6 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB ...@@ -243,7 +243,6 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
buildOwner.onBuildScheduled = _handleBuildScheduled; buildOwner.onBuildScheduled = _handleBuildScheduled;
ui.window.onLocaleChanged = handleLocaleChanged; ui.window.onLocaleChanged = handleLocaleChanged;
SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation); SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
SystemChannels.system.setMessageHandler(_handleSystemMessage); SystemChannels.system.setMessageHandler(_handleSystemMessage);
} }
...@@ -369,6 +368,8 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB ...@@ -369,6 +368,8 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
/// Calls [dispatchLocaleChanged] to notify the binding observers. /// Calls [dispatchLocaleChanged] to notify the binding observers.
/// ///
/// See [Window.onLocaleChanged]. /// See [Window.onLocaleChanged].
@protected
@mustCallSuper
void handleLocaleChanged() { void handleLocaleChanged() {
dispatchLocaleChanged(ui.window.locale); dispatchLocaleChanged(ui.window.locale);
} }
...@@ -379,6 +380,8 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB ...@@ -379,6 +380,8 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
/// ///
/// This is called by [handleLocaleChanged] when the [Window.onLocaleChanged] /// This is called by [handleLocaleChanged] when the [Window.onLocaleChanged]
/// notification is received. /// notification is received.
@protected
@mustCallSuper
void dispatchLocaleChanged(Locale locale) { void dispatchLocaleChanged(Locale locale) {
for (WidgetsBindingObserver observer in _observers) for (WidgetsBindingObserver observer in _observers)
observer.didChangeLocale(locale); observer.didChangeLocale(locale);
...@@ -398,6 +401,7 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB ...@@ -398,6 +401,7 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
/// ///
/// This method exposes the `popRoute` notification from /// This method exposes the `popRoute` notification from
/// [SystemChannels.navigation]. /// [SystemChannels.navigation].
@protected
Future<Null> handlePopRoute() async { Future<Null> handlePopRoute() async {
for (WidgetsBindingObserver observer in new List<WidgetsBindingObserver>.from(_observers)) { for (WidgetsBindingObserver observer in new List<WidgetsBindingObserver>.from(_observers)) {
if (await observer.didPopRoute()) if (await observer.didPopRoute())
...@@ -416,6 +420,8 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB ...@@ -416,6 +420,8 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
/// ///
/// This method exposes the `pushRoute` notification from /// This method exposes the `pushRoute` notification from
/// [SystemChannels.navigation]. /// [SystemChannels.navigation].
@protected
@mustCallSuper
Future<Null> handlePushRoute(String route) async { Future<Null> handlePushRoute(String route) async {
for (WidgetsBindingObserver observer in new List<WidgetsBindingObserver>.from(_observers)) { for (WidgetsBindingObserver observer in new List<WidgetsBindingObserver>.from(_observers)) {
if (await observer.didPushRoute(route)) if (await observer.didPushRoute(route))
...@@ -433,35 +439,13 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB ...@@ -433,35 +439,13 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
return new Future<Null>.value(); return new Future<Null>.value();
} }
/// Called when the application lifecycle state changes. @override
///
/// Notifies all the observers using
/// [WidgetsBindingObserver.didChangeAppLifecycleState].
///
/// This method exposes notifications from [SystemChannels.lifecycle].
void handleAppLifecycleStateChanged(AppLifecycleState state) { void handleAppLifecycleStateChanged(AppLifecycleState state) {
super.handleAppLifecycleStateChanged(state);
for (WidgetsBindingObserver observer in _observers) for (WidgetsBindingObserver observer in _observers)
observer.didChangeAppLifecycleState(state); observer.didChangeAppLifecycleState(state);
} }
Future<String> _handleLifecycleMessage(String message) async {
switch (message) {
case 'AppLifecycleState.paused':
handleAppLifecycleStateChanged(AppLifecycleState.paused);
break;
case 'AppLifecycleState.resumed':
handleAppLifecycleStateChanged(AppLifecycleState.resumed);
break;
case 'AppLifecycleState.inactive':
handleAppLifecycleStateChanged(AppLifecycleState.inactive);
break;
case 'AppLifecycleState.suspending':
handleAppLifecycleStateChanged(AppLifecycleState.suspending);
break;
}
return null;
}
/// Called when the operating system notifies the application of a memory /// Called when the operating system notifies the application of a memory
/// pressure situation. /// pressure situation.
/// ///
......
...@@ -40,9 +40,11 @@ class TestServiceExtensionsBinding extends BindingBase ...@@ -40,9 +40,11 @@ class TestServiceExtensionsBinding extends BindingBase
} }
int reassembled = 0; int reassembled = 0;
bool pendingReassemble = false;
@override @override
Future<Null> performReassemble() { Future<Null> performReassemble() {
reassembled += 1; reassembled += 1;
pendingReassemble = true;
return super.performReassemble(); return super.performReassemble();
} }
...@@ -60,6 +62,17 @@ class TestServiceExtensionsBinding extends BindingBase ...@@ -60,6 +62,17 @@ class TestServiceExtensionsBinding extends BindingBase
ui.window.onDrawFrame(); ui.window.onDrawFrame();
} }
@override
void scheduleForcedFrame() {
expect(true, isFalse);
}
@override
void scheduleWarmUpFrame() {
expect(pendingReassemble, isTrue);
pendingReassemble = false;
}
Future<Null> flushMicrotasks() { Future<Null> flushMicrotasks() {
final Completer<Null> completer = new Completer<Null>(); final Completer<Null> completer = new Completer<Null>();
Timer.run(completer.complete); Timer.run(completer.complete);
......
...@@ -4,11 +4,12 @@ ...@@ -4,11 +4,12 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'scheduler_tester.dart'; import 'scheduler_tester.dart';
class TestSchedulerBinding extends BindingBase with SchedulerBinding { } class TestSchedulerBinding extends BindingBase with ServicesBinding, SchedulerBinding { }
void main() { void main() {
final SchedulerBinding scheduler = new TestSchedulerBinding(); final SchedulerBinding scheduler = new TestSchedulerBinding();
......
...@@ -4,9 +4,10 @@ ...@@ -4,9 +4,10 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
class TestSchedulerBinding extends BindingBase with SchedulerBinding { } class TestSchedulerBinding extends BindingBase with ServicesBinding, SchedulerBinding { }
class TestStrategy { class TestStrategy {
int allowedPriority = 10000; int allowedPriority = 10000;
...@@ -32,20 +33,20 @@ void main() { ...@@ -32,20 +33,20 @@ void main() {
strategy.allowedPriority = 100; strategy.allowedPriority = 100;
for (int i = 0; i < 3; i += 1) for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback(); expect(scheduler.handleEventLoopCallback(), isFalse);
expect(executedTasks.isEmpty, isTrue); expect(executedTasks.isEmpty, isTrue);
strategy.allowedPriority = 50; strategy.allowedPriority = 50;
for (int i = 0; i < 3; i += 1) for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback(); expect(scheduler.handleEventLoopCallback(), i == 0 ? isTrue : isFalse);
expect(executedTasks.length, equals(1)); expect(executedTasks, hasLength(1));
expect(executedTasks.single, equals(80)); expect(executedTasks.single, equals(80));
executedTasks.clear(); executedTasks.clear();
strategy.allowedPriority = 20; strategy.allowedPriority = 20;
for (int i = 0; i < 3; i += 1) for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback(); expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse);
expect(executedTasks.length, equals(2)); expect(executedTasks, hasLength(2));
expect(executedTasks[0], equals(23)); expect(executedTasks[0], equals(23));
expect(executedTasks[1], equals(23)); expect(executedTasks[1], equals(23));
executedTasks.clear(); executedTasks.clear();
...@@ -55,32 +56,32 @@ void main() { ...@@ -55,32 +56,32 @@ void main() {
scheduleAddingTask(5); scheduleAddingTask(5);
scheduleAddingTask(97); scheduleAddingTask(97);
for (int i = 0; i < 3; i += 1) for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback(); expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse);
expect(executedTasks.length, equals(2)); expect(executedTasks, hasLength(2));
expect(executedTasks[0], equals(99)); expect(executedTasks[0], equals(99));
expect(executedTasks[1], equals(97)); expect(executedTasks[1], equals(97));
executedTasks.clear(); executedTasks.clear();
strategy.allowedPriority = 10; strategy.allowedPriority = 10;
for (int i = 0; i < 3; i += 1) for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback(); expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse);
expect(executedTasks.length, equals(2)); expect(executedTasks, hasLength(2));
expect(executedTasks[0], equals(19)); expect(executedTasks[0], equals(19));
expect(executedTasks[1], equals(11)); expect(executedTasks[1], equals(11));
executedTasks.clear(); executedTasks.clear();
strategy.allowedPriority = 1; strategy.allowedPriority = 1;
for (int i = 0; i < 4; i += 1) for (int i = 0; i < 4; i += 1)
scheduler.handleEventLoopCallback(); expect(scheduler.handleEventLoopCallback(), i < 3 ? isTrue : isFalse);
expect(executedTasks.length, equals(3)); expect(executedTasks, hasLength(3));
expect(executedTasks[0], equals(5)); expect(executedTasks[0], equals(5));
expect(executedTasks[1], equals(3)); expect(executedTasks[1], equals(3));
expect(executedTasks[2], equals(2)); expect(executedTasks[2], equals(2));
executedTasks.clear(); executedTasks.clear();
strategy.allowedPriority = 0; strategy.allowedPriority = 0;
scheduler.handleEventLoopCallback(); expect(scheduler.handleEventLoopCallback(), isFalse);
expect(executedTasks.length, equals(1)); expect(executedTasks, hasLength(1));
expect(executedTasks[0], equals(0)); expect(executedTasks[0], equals(0));
}); });
} }
...@@ -3,13 +3,14 @@ ...@@ -3,13 +3,14 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
testWidgets('Ticker mute control test', (WidgetTester tester) async { testWidgets('Ticker mute control test', (WidgetTester tester) async {
int tickCount = 0; int tickCount = 0;
void handleTick(Duration duration) { void handleTick(Duration duration) {
++tickCount; tickCount += 1;
} }
final Ticker ticker = new Ticker(handleTick); final Ticker ticker = new Ticker(handleTick);
...@@ -81,4 +82,25 @@ void main() { ...@@ -81,4 +82,25 @@ void main() {
expect(ticker, hasOneLineDescription); expect(ticker, hasOneLineDescription);
expect(ticker.toString(debugIncludeStack: true), contains('testFunction')); expect(ticker.toString(debugIncludeStack: true), contains('testFunction'));
}); });
testWidgets('Ticker stops ticking when application is paused', (WidgetTester tester) async {
int tickCount = 0;
void handleTick(Duration duration) {
tickCount += 1;
}
final Ticker ticker = new Ticker(handleTick);
ticker.start();
expect(ticker.isTicking, isTrue);
expect(ticker.isActive, isTrue);
expect(tickCount, equals(0));
final ByteData message = const StringCodec().encodeMessage('AppLifecycleState.paused');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(ticker.isTicking, isFalse);
expect(ticker.isActive, isTrue);
ticker.stop();
});
} }
...@@ -84,4 +84,56 @@ void main() { ...@@ -84,4 +84,56 @@ void main() {
WidgetsBinding.instance.removeObserver(observer); WidgetsBinding.instance.removeObserver(observer);
}); });
testWidgets('Application lifecycle affects frame scheduling', (WidgetTester tester) async {
ByteData message;
expect(tester.binding.hasScheduledFrame, isFalse);
message = const StringCodec().encodeMessage('AppLifecycleState.paused');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(tester.binding.hasScheduledFrame, isFalse);
message = const StringCodec().encodeMessage('AppLifecycleState.resumed');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(tester.binding.hasScheduledFrame, isTrue);
await tester.pump();
expect(tester.binding.hasScheduledFrame, isFalse);
message = const StringCodec().encodeMessage('AppLifecycleState.inactive');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(tester.binding.hasScheduledFrame, isFalse);
message = const StringCodec().encodeMessage('AppLifecycleState.suspending');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(tester.binding.hasScheduledFrame, isFalse);
message = const StringCodec().encodeMessage('AppLifecycleState.inactive');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(tester.binding.hasScheduledFrame, isTrue);
await tester.pump();
expect(tester.binding.hasScheduledFrame, isFalse);
message = const StringCodec().encodeMessage('AppLifecycleState.paused');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(tester.binding.hasScheduledFrame, isFalse);
tester.binding.scheduleFrame();
expect(tester.binding.hasScheduledFrame, isFalse);
tester.binding.scheduleForcedFrame();
expect(tester.binding.hasScheduledFrame, isTrue);
await tester.pump();
expect(tester.binding.hasScheduledFrame, isFalse);
int frameCount = 0;
tester.binding.addPostFrameCallback((Duration duration) { frameCount += 1; });
expect(tester.binding.hasScheduledFrame, isFalse);
await tester.pump(const Duration(milliseconds: 1));
expect(tester.binding.hasScheduledFrame, isFalse);
expect(frameCount, 0);
tester.binding.scheduleWarmUpFrame(); // this actually tests flutter_test's implementation
expect(tester.binding.hasScheduledFrame, isFalse);
expect(frameCount, 1);
});
} }
...@@ -583,6 +583,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -583,6 +583,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
handleBeginFrame(null); handleBeginFrame(null);
_fakeAsync.flushMicrotasks(); _fakeAsync.flushMicrotasks();
handleDrawFrame(); handleDrawFrame();
_fakeAsync.flushMicrotasks();
} }
@override @override
...@@ -829,6 +830,13 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -829,6 +830,13 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
super.scheduleFrame(); super.scheduleFrame();
} }
@override
void scheduleForcedFrame() {
if (framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.benchmark)
return; // In benchmark mode, don't actually schedule any engine frames.
super.scheduleForcedFrame();
}
bool _doDrawThisFrame; bool _doDrawThisFrame;
@override @override
......
...@@ -38,6 +38,11 @@ void main() { ...@@ -38,6 +38,11 @@ void main() {
return _testFile('test_async_utils_unguarded', automatedTestsDirectory, flutterTestDirectory); return _testFile('test_async_utils_unguarded', automatedTestsDirectory, flutterTestDirectory);
}); });
testUsingContext('report a nice error when a Ticker is left running', () async {
Cache.flutterRoot = '../..';
return _testFile('ticker', automatedTestsDirectory, flutterTestDirectory);
});
testUsingContext('report a nice error when a pubspec.yaml is missing a flutter_test dependency', () async { testUsingContext('report a nice error when a pubspec.yaml is missing a flutter_test dependency', () async {
final String missingDependencyTests = fs.path.join('..', '..', 'dev', 'missing_dependency_tests'); final String missingDependencyTests = fs.path.join('..', '..', 'dev', 'missing_dependency_tests');
Cache.flutterRoot = '../..'; Cache.flutterRoot = '../..';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment