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, (_) {});
|
||||||
|
});
|
||||||
|
}
|
@ -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
|
|||||||
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';
|
|||||||
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);
|
|||||||
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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
/// * 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 {
|
|||||||
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 {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
|||||||
// 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() {
|
|
||||||
_hasRequestedAnEventLoopCallback = false;
|
|
||||||
_runTasks();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called when the system wakes up and at the end of each frame.
|
|
||||||
void _runTasks() {
|
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)
|
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 {
|
));
|
||||||
// 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
|
int _nextFrameCallbackId = 0; // positive
|
||||||
@ -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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
_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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
}());
|
}());
|
||||||
_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 {
|
|||||||
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 {
|
|||||||
/// 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';
|
|||||||
|
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
|||||||
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
|
|||||||
/// 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
|
|||||||
///
|
///
|
||||||
/// 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
|
|||||||
///
|
///
|
||||||
/// 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
|
|||||||
///
|
///
|
||||||
/// 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
|
|||||||
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
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
|||||||
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 @@
|
|||||||
|
|
||||||
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 @@
|
|||||||
|
|
||||||
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() {
|
|||||||
|
|
||||||
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() {
|
|||||||
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 @@
|
|||||||
// 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() {
|
|||||||
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() {
|
|||||||
|
|
||||||
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 {
|
|||||||
handleBeginFrame(null);
|
handleBeginFrame(null);
|
||||||
_fakeAsync.flushMicrotasks();
|
_fakeAsync.flushMicrotasks();
|
||||||
handleDrawFrame();
|
handleDrawFrame();
|
||||||
|
_fakeAsync.flushMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -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() {
|
|||||||
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 = '../..';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user