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, (_) {});
});
}
......@@ -139,14 +139,17 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
/// Called when the system metrics change.
///
/// See [Window.onMetricsChanged].
@protected
void handleMetricsChanged() {
assert(renderView != null);
renderView.configuration = createViewConfiguration();
scheduleForcedFrame();
}
/// Called when the platform text scale factor changes.
///
/// See [Window.onTextScaleFactorChanged].
@protected
void handleTextScaleFactorChanged() { }
/// Returns a [ViewConfiguration] configured for the [RenderView] based on the
......@@ -266,26 +269,6 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
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
Future<Null> performReassemble() async {
await super.performReassemble();
......
......@@ -6,15 +6,16 @@ import 'dart:async';
import 'dart:collection';
import 'dart:developer';
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:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'debug.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.
double get timeDilation => _timeDilation;
......@@ -51,9 +52,16 @@ typedef void FrameCallback(Duration timeStamp);
typedef bool SchedulingStrategy({ int priority, SchedulerBinding scheduler });
class _TaskEntry {
const _TaskEntry(this.task, this.priority);
_TaskEntry(this.task, this.priority) {
assert(() {
debugStack = StackTrace.current;
return true;
}());
}
final VoidCallback task;
final int priority;
StackTrace debugStack;
}
class _FrameCallbackEntry {
......@@ -85,7 +93,6 @@ class _FrameCallbackEntry {
final FrameCallback callback;
// debug-mode fields
static StackTrace debugCurrentCallbackStack;
StackTrace debugStack;
}
......@@ -158,7 +165,7 @@ enum SchedulerPhase {
/// * Non-rendering tasks, to be run between frames. These are given a
/// priority and are executed in priority order according to a
/// [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
// extended directly.
factory SchedulerBinding._() => null;
......@@ -167,8 +174,9 @@ abstract class SchedulerBinding extends BindingBase {
void initInstances() {
super.initInstances();
_instance = this;
ui.window.onBeginFrame = handleBeginFrame;
ui.window.onDrawFrame = handleDrawFrame;
ui.window.onBeginFrame = _handleBeginFrame;
ui.window.onDrawFrame = _handleDrawFrame;
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
}
/// The current [SchedulerBinding], if one has been created.
......@@ -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.
///
/// Defaults to [defaultSchedulingStrategy].
......@@ -221,41 +282,65 @@ abstract class SchedulerBinding extends BindingBase {
// Whether this scheduler already requested to be called from the event loop.
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() {
assert(!locked);
assert(_taskQueue.isNotEmpty);
if (_hasRequestedAnEventLoopCallback)
return;
Timer.run(handleEventLoopCallback);
_hasRequestedAnEventLoopCallback = true;
Timer.run(_runTasks);
}
/// Called by the system when there is time to run tasks.
void handleEventLoopCallback() {
// Scheduled by _ensureEventLoopCallback.
void _runTasks() {
_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.
void _runTasks() {
/// 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;
return false;
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)) {
try {
(_taskQueue.removeFirst().task)();
} finally {
if (_taskQueue.isNotEmpty)
_ensureEventLoopCallback();
} catch (exception, exceptionStack) {
StackTrace callbackStack;
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 {
// TODO(floitsch): we shouldn't need to request a frame. Just schedule
// an event-loop callback.
scheduleFrame();
));
}
return _taskQueue.isNotEmpty;
}
return false;
}
int _nextFrameCallbackId = 0; // positive
......@@ -437,6 +522,11 @@ abstract class SchedulerBinding extends BindingBase {
/// 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);
}
......@@ -473,6 +563,20 @@ abstract class SchedulerBinding extends BindingBase {
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();
}
/// Schedules a new frame using [scheduleFrame] if this object is not
/// currently producing a frame.
///
......@@ -494,10 +598,25 @@ abstract class SchedulerBinding extends BindingBase {
/// 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)
if (_hasScheduledFrame || !_framesEnabled)
return;
assert(() {
if (debugPrintScheduleFrameStacks)
......@@ -508,6 +627,76 @@ abstract class SchedulerBinding extends BindingBase {
_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 _epochStart = Duration.ZERO;
Duration _lastRawTimeStamp = Duration.ZERO;
......@@ -560,6 +749,24 @@ abstract class SchedulerBinding extends BindingBase {
int _profileFrameNumber = 0;
final Stopwatch _profileFrameStopwatch = new Stopwatch();
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.
///
......@@ -576,9 +783,9 @@ abstract class SchedulerBinding extends BindingBase {
/// 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 you to distinguish frames eagerly
/// pushed by the framework from those requested by the engine in response to
/// the vsync signal from the operating system.
/// 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
......@@ -670,9 +877,6 @@ abstract class SchedulerBinding extends BindingBase {
}());
_currentFrameTimeStamp = null;
}
// All frame-related callbacks have been executed. Run lower-priority tasks.
_runTasks();
}
void _profileFramePostEvent() {
......@@ -707,7 +911,6 @@ abstract class SchedulerBinding extends BindingBase {
void _invokeFrameCallback(FrameCallback callback, Duration timeStamp, [ StackTrace callbackStack ]) {
assert(callback != 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; }());
try {
callback(timeStamp);
......
......@@ -101,9 +101,21 @@ class Ticker {
/// 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
/// [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.
bool get isTicking => _future != null && !muted;
///
/// This will return false if the [Scheduler.lifecycleState] is one that
/// 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
/// called and false when [stop] is called.
......
......@@ -8,6 +8,8 @@ import 'package:flutter/foundation.dart';
import 'platform_channel.dart';
export 'dart:typed_data' show ByteData;
/// A message encoding/decoding mechanism.
///
/// Both operations throw an exception, if conversion fails. Such situations
......
......@@ -231,7 +231,7 @@ abstract class WidgetsBindingObserver {
}
/// 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
// extended directly.
factory WidgetsBinding._() => null;
......@@ -243,7 +243,6 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
buildOwner.onBuildScheduled = _handleBuildScheduled;
ui.window.onLocaleChanged = handleLocaleChanged;
SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
SystemChannels.system.setMessageHandler(_handleSystemMessage);
}
......@@ -369,6 +368,8 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
/// Calls [dispatchLocaleChanged] to notify the binding observers.
///
/// See [Window.onLocaleChanged].
@protected
@mustCallSuper
void handleLocaleChanged() {
dispatchLocaleChanged(ui.window.locale);
}
......@@ -379,6 +380,8 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
///
/// This is called by [handleLocaleChanged] when the [Window.onLocaleChanged]
/// notification is received.
@protected
@mustCallSuper
void dispatchLocaleChanged(Locale locale) {
for (WidgetsBindingObserver observer in _observers)
observer.didChangeLocale(locale);
......@@ -398,6 +401,7 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
///
/// This method exposes the `popRoute` notification from
/// [SystemChannels.navigation].
@protected
Future<Null> handlePopRoute() async {
for (WidgetsBindingObserver observer in new List<WidgetsBindingObserver>.from(_observers)) {
if (await observer.didPopRoute())
......@@ -416,6 +420,8 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
///
/// This method exposes the `pushRoute` notification from
/// [SystemChannels.navigation].
@protected
@mustCallSuper
Future<Null> handlePushRoute(String route) async {
for (WidgetsBindingObserver observer in new List<WidgetsBindingObserver>.from(_observers)) {
if (await observer.didPushRoute(route))
......@@ -433,35 +439,13 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
return new Future<Null>.value();
}
/// Called when the application lifecycle state changes.
///
/// Notifies all the observers using
/// [WidgetsBindingObserver.didChangeAppLifecycleState].
///
/// This method exposes notifications from [SystemChannels.lifecycle].
@override
void handleAppLifecycleStateChanged(AppLifecycleState state) {
super.handleAppLifecycleStateChanged(state);
for (WidgetsBindingObserver observer in _observers)
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
/// pressure situation.
///
......
......@@ -40,9 +40,11 @@ class TestServiceExtensionsBinding extends BindingBase
}
int reassembled = 0;
bool pendingReassemble = false;
@override
Future<Null> performReassemble() {
reassembled += 1;
pendingReassemble = true;
return super.performReassemble();
}
......@@ -60,6 +62,17 @@ class TestServiceExtensionsBinding extends BindingBase
ui.window.onDrawFrame();
}
@override
void scheduleForcedFrame() {
expect(true, isFalse);
}
@override
void scheduleWarmUpFrame() {
expect(pendingReassemble, isTrue);
pendingReassemble = false;
}
Future<Null> flushMicrotasks() {
final Completer<Null> completer = new Completer<Null>();
Timer.run(completer.complete);
......
......@@ -4,11 +4,12 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:test/test.dart';
import 'scheduler_tester.dart';
class TestSchedulerBinding extends BindingBase with SchedulerBinding { }
class TestSchedulerBinding extends BindingBase with ServicesBinding, SchedulerBinding { }
void main() {
final SchedulerBinding scheduler = new TestSchedulerBinding();
......
......@@ -4,9 +4,10 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:test/test.dart';
class TestSchedulerBinding extends BindingBase with SchedulerBinding { }
class TestSchedulerBinding extends BindingBase with ServicesBinding, SchedulerBinding { }
class TestStrategy {
int allowedPriority = 10000;
......@@ -32,20 +33,20 @@ void main() {
strategy.allowedPriority = 100;
for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback();
expect(scheduler.handleEventLoopCallback(), isFalse);
expect(executedTasks.isEmpty, isTrue);
strategy.allowedPriority = 50;
for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback();
expect(executedTasks.length, equals(1));
expect(scheduler.handleEventLoopCallback(), i == 0 ? isTrue : isFalse);
expect(executedTasks, hasLength(1));
expect(executedTasks.single, equals(80));
executedTasks.clear();
strategy.allowedPriority = 20;
for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback();
expect(executedTasks.length, equals(2));
expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse);
expect(executedTasks, hasLength(2));
expect(executedTasks[0], equals(23));
expect(executedTasks[1], equals(23));
executedTasks.clear();
......@@ -55,32 +56,32 @@ void main() {
scheduleAddingTask(5);
scheduleAddingTask(97);
for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback();
expect(executedTasks.length, equals(2));
expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse);
expect(executedTasks, hasLength(2));
expect(executedTasks[0], equals(99));
expect(executedTasks[1], equals(97));
executedTasks.clear();
strategy.allowedPriority = 10;
for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback();
expect(executedTasks.length, equals(2));
expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse);
expect(executedTasks, hasLength(2));
expect(executedTasks[0], equals(19));
expect(executedTasks[1], equals(11));
executedTasks.clear();
strategy.allowedPriority = 1;
for (int i = 0; i < 4; i += 1)
scheduler.handleEventLoopCallback();
expect(executedTasks.length, equals(3));
expect(scheduler.handleEventLoopCallback(), i < 3 ? isTrue : isFalse);
expect(executedTasks, hasLength(3));
expect(executedTasks[0], equals(5));
expect(executedTasks[1], equals(3));
expect(executedTasks[2], equals(2));
executedTasks.clear();
strategy.allowedPriority = 0;
scheduler.handleEventLoopCallback();
expect(executedTasks.length, equals(1));
expect(scheduler.handleEventLoopCallback(), isFalse);
expect(executedTasks, hasLength(1));
expect(executedTasks[0], equals(0));
});
}
......@@ -3,13 +3,14 @@
// 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('Ticker mute control test', (WidgetTester tester) async {
int tickCount = 0;
void handleTick(Duration duration) {
++tickCount;
tickCount += 1;
}
final Ticker ticker = new Ticker(handleTick);
......@@ -81,4 +82,25 @@ void main() {
expect(ticker, hasOneLineDescription);
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() {
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 {
handleBeginFrame(null);
_fakeAsync.flushMicrotasks();
handleDrawFrame();
_fakeAsync.flushMicrotasks();
}
@override
......@@ -829,6 +830,13 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
super.scheduleFrame();
}
@override
void scheduleForcedFrame() {
if (framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.benchmark)
return; // In benchmark mode, don't actually schedule any engine frames.
super.scheduleForcedFrame();
}
bool _doDrawThisFrame;
@override
......
......@@ -38,6 +38,11 @@ void main() {
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 {
final String missingDependencyTests = fs.path.join('..', '..', 'dev', 'missing_dependency_tests');
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