Have the framework in charge of scheduling frames. (#13344)
...instead of the engine.
This commit is contained in:
parent
425bd5a821
commit
186d1e9b0d
16
dev/automated_tests/flutter_test/ticker_expectation.txt
Normal file
16
dev/automated_tests/flutter_test/ticker_expectation.txt
Normal file
@ -0,0 +1,16 @@
|
||||
[^═]*(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\. *
|
16
dev/automated_tests/flutter_test/ticker_test.dart
Normal file
16
dev/automated_tests/flutter_test/ticker_test.dart
Normal file
@ -0,0 +1,16 @@
|
||||
// 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
|
||||
);
|
||||
|
||||
registerSignalServiceExtension(
|
||||
name: 'debugDumpSemanticsTreeInInverseHitTestOrder',
|
||||
callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest); return debugPrintDone; }
|
||||
name: 'debugDumpSemanticsTreeInInverseHitTestOrder',
|
||||
callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest); return debugPrintDone; }
|
||||
);
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
_hasRequestedAnEventLoopCallback = false;
|
||||
_runTasks();
|
||||
}
|
||||
|
||||
// Called when the system wakes up and at the end of each frame.
|
||||
// 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;
|
||||
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 = '../..';
|
||||
|
Loading…
x
Reference in New Issue
Block a user