Commit e8c46927 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Flush microtasks after transient callbacks are run. (#9702)

This splits the frame pipeline into two, beginFrame and drawFrame.

As part of making this change I added some debugging hooks that helped
debug the issues that came up:

 * I added debugPrintScheduleFrameStacks which prints a stack whenever
   a frame is actually scheduled, so you can see why frames are being
   scheduled.

 * I added some toString output to EditableText and RawKeyboardListener.

 * I added a scheduler_tester.dart library for scheduler library tests.

 * I changed the test framework to flush microtasks before pumping.

 * Some asserts that had the old string literal form were replaced by
   asserts with messages.

I also fixed a few subtle bugs that this uncovered:

 * setState() now calls `ensureVisualUpdate`, rather than
   `scheduleFrame`. This means that calling it from an
   AnimationController callback does not actually schedule an extra
   redundant frame as it used to.

 * I corrected some documentation.
parent 89856c0e
......@@ -24,6 +24,11 @@ class BenchmarkingBinding extends LiveTestWidgetsFlutterBinding {
void handleBeginFrame(Duration rawTimeStamp) {
stopwatch.start();
super.handleBeginFrame(rawTimeStamp);
}
@override
void handleDrawFrame() {
super.handleDrawFrame();
stopwatch.stop();
}
}
......
......@@ -167,12 +167,12 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
}
void _handlePersistentFrameCallback(Duration timeStamp) {
beginFrame();
drawFrame();
}
/// Pump the rendering pipeline to generate a frame.
///
/// This method is called by [handleBeginFrame], which itself is called
/// This method is called by [handleDrawFrame], which itself is called
/// automatically by the engine when when it is time to lay out and paint a
/// frame.
///
......@@ -185,42 +185,49 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
/// driving [AnimationController] objects, which means all of the active
/// [Animation] objects tick at this point.
///
/// [handleBeginFrame] then invokes all the persistent frame callbacks, of which
/// the most notable is this method, [beginFrame], which proceeds as follows:
/// 2. Microtasks: After [handleBeginFrame] returns, any microtasks that got
/// scheduled by transient frame callbacks get to run. This typically includes
/// callbacks for futures from [Ticker]s and [AnimationController]s that
/// completed this frame.
///
/// 2. The layout phase: All the dirty [RenderObject]s in the system are laid
/// After [handleBeginFrame], [handleDrawFrame], which is registered with
/// [ui.window.onDrawFrame], is called, which invokes all the persistent frame
/// callbacks, of which the most notable is this method, [drawFrame], which
/// proceeds as follows:
///
/// 3. The layout phase: All the dirty [RenderObject]s in the system are laid
/// out (see [RenderObject.performLayout]). See [RenderObject.markNeedsLayout]
/// for further details on marking an object dirty for layout.
///
/// 3. The compositing bits phase: The compositing bits on any dirty
/// 4. The compositing bits phase: The compositing bits on any dirty
/// [RenderObject] objects are updated. See
/// [RenderObject.markNeedsCompositingBitsUpdate].
///
/// 4. The paint phase: All the dirty [RenderObject]s in the system are
/// 5. The paint phase: All the dirty [RenderObject]s in the system are
/// repainted (see [RenderObject.paint]). This generates the [Layer] tree. See
/// [RenderObject.markNeedsPaint] for further details on marking an object
/// dirty for paint.
///
/// 5. The compositing phase: The layer tree is turned into a [ui.Scene] and
/// 6. The compositing phase: The layer tree is turned into a [ui.Scene] and
/// sent to the GPU.
///
/// 6. The semantics phase: All the dirty [RenderObject]s in the system have
/// 7. The semantics phase: All the dirty [RenderObject]s in the system have
/// their semantics updated (see [RenderObject.SemanticsAnnotator]). This
/// generates the [SemanticsNode] tree. See
/// [RenderObject.markNeedsSemanticsUpdate] for further details on marking an
/// object dirty for semantics.
///
/// For more details on steps 2-6, see [PipelineOwner].
/// For more details on steps 3-7, see [PipelineOwner].
///
/// 7. The finalization phase: After [beginFrame] returns, [handleBeginFrame]
/// then invokes post-frame callbacks (registered with [addPostFrameCallback].
/// 8. The finalization phase: After [drawFrame] returns, [handleDrawFrame]
/// then invokes post-frame callbacks (registered with [addPostFrameCallback]).
///
/// Some bindings (for example, the [WidgetsBinding]) add extra steps to this
/// list (for example, see [WidgetsBinding.beginFrame]).
/// list (for example, see [WidgetsBinding.drawFrame]).
//
// When editing the above, also update widgets/binding.dart's copy.
@protected
void beginFrame() {
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
......@@ -229,6 +236,17 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
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.
Timer.run(() { handleBeginFrame(null); });
Timer.run(() { handleDrawFrame(); });
}
@override
Future<Null> reassembleApplication() async {
await super.reassembleApplication();
......@@ -238,7 +256,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
} finally {
Timeline.finishSync();
}
handleBeginFrame(null);
scheduleWarmUpFrame();
await endOfFrame;
}
......
......@@ -110,14 +110,24 @@ enum SchedulerPhase {
/// The transient callbacks (scheduled by
/// [WidgetsBinding.scheduleFrameCallback]) are currently executing.
///
/// Typically, these callbacks handle updating objects to new animation states.
/// Typically, these callbacks handle updating objects to new animation
/// states.
///
/// See [handleBeginFrame].
transientCallbacks,
/// Microtasks scheduled during the processing of transient callbacks are
/// current executing.
///
/// This may include, for instance, callbacks from futures resulted during the
/// [transientCallbacks] phase.
midFrameMicrotasks,
/// The persistent callbacks (scheduled by
/// [WidgetsBinding.addPersistentFrameCallback]) are currently executing.
///
/// Typically, this is the build/layout/paint pipeline. See
/// [WidgetsBinding.beginFrame].
/// [WidgetsBinding.drawFrame] and [handleDrawFrame].
persistentCallbacks,
/// The post-frame callbacks (scheduled by
......@@ -125,15 +135,25 @@ enum SchedulerPhase {
///
/// Typically, these callbacks handle cleanup and scheduling of work for the
/// next frame.
///
/// See [handleDrawFrame].
postFrameCallbacks,
}
/// Scheduler for running the following:
///
/// * _Frame callbacks_, triggered by the system's
/// [ui.window.onBeginFrame] callback, for synchronizing the
/// application's behavior to the system's display. For example, the
/// rendering layer uses this to drive its rendering pipeline.
/// * _Transient callbacks_, triggered by the system's [ui.window.onBeginFrame]
/// callback, for synchronizing the application's behavior to the system's
/// display. For example, [Ticker]s and [AnimationController]s trigger from
/// these.
///
/// * _Persistent callbacks_, triggered by the system's [ui.window.onDrawFrame]
/// callback, for updating the system's display after transient callbacks have
/// executed. For example, the rendering layer uses this to drive its
/// rendering pipeline.
///
/// * _Post-frame callbacks_, which are run after persistent callbacks, just
/// before returning from the [ui.window.onDrawFrame] callback.
///
/// * Non-rendering tasks, to be run between frames. These are given a
/// priority and are executed in priority order according to a
......@@ -145,6 +165,7 @@ abstract class SchedulerBinding extends BindingBase {
super.initInstances();
_instance = this;
ui.window.onBeginFrame = handleBeginFrame;
ui.window.onDrawFrame = handleDrawFrame;
}
/// The current [SchedulerBinding], if one has been created.
......@@ -434,7 +455,7 @@ abstract class SchedulerBinding extends BindingBase {
return _nextFrameCompleter.future;
}
/// Whether this scheduler has requested that handleBeginFrame be called soon.
/// Whether this scheduler has requested that [handleBeginFrame] be called soon.
bool get hasScheduledFrame => _hasScheduledFrame;
bool _hasScheduledFrame = false;
......@@ -457,12 +478,22 @@ abstract class SchedulerBinding extends BindingBase {
/// [ui.window.scheduleFrame].
///
/// After this is called, the engine will (eventually) call
/// [handleBeginFrame]. (This call might be delayed, e.g. if the
/// device's screen is turned off it will typically be delayed until
/// the screen is on and the application is visible.)
/// [handleBeginFrame]. (This call might be delayed, e.g. if the device's
/// screen is turned off it will typically be delayed until the screen is on
/// and the application is visible.) Calling this during a frame forces
/// another frame to be scheduled, even if the current frame has not yet
/// completed.
///
/// To have a stack trace printed to the console any time this function
/// schedules a frame, set [debugPrintScheduleFrameStacks] to true.
void scheduleFrame() {
if (_hasScheduledFrame)
return;
assert(() {
if (debugPrintScheduleFrameStacks)
debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
return true;
});
ui.window.scheduleFrame();
_hasScheduledFrame = true;
}
......@@ -517,14 +548,14 @@ abstract class SchedulerBinding extends BindingBase {
Duration _currentFrameTimeStamp;
int _debugFrameNumber = 0;
String _debugBanner;
/// Called by the engine to produce a new frame.
/// Called by the engine to prepare the framework to produce a new frame.
///
/// This function first calls all the callbacks registered by
/// [scheduleFrameCallback], then calls all the callbacks
/// registered by [addPersistentFrameCallback], which typically drive the
/// rendering pipeline, and finally calls the callbacks registered by
/// [addPostFrameCallback].
/// This function calls all the transient frame callbacks registered by
/// [scheduleFrameCallback]. It then returns, any scheduled microtasks are run
/// (e.g. handlers for any [Future]s resolved by transient frame callbacks),
/// and [handleDrawFrame] is called to continue the frame.
///
/// If the given time stamp is null, the time stamp from the last frame is
/// reused.
......@@ -549,7 +580,6 @@ abstract class SchedulerBinding extends BindingBase {
if (rawTimeStamp != null)
_lastRawTimeStamp = rawTimeStamp;
String debugBanner;
assert(() {
_debugFrameNumber += 1;
if (debugPrintBeginFrameBanner || debugPrintEndFrameBanner) {
......@@ -559,9 +589,9 @@ abstract class SchedulerBinding extends BindingBase {
} else {
frameTimeStampDescription.write('(warm-up frame)');
}
debugBanner = '▄▄▄▄▄▄▄▄ Frame ${_debugFrameNumber.toString().padRight(7)} ${frameTimeStampDescription.toString().padLeft(18)} ▄▄▄▄▄▄▄▄';
_debugBanner = '▄▄▄▄▄▄▄▄ Frame ${_debugFrameNumber.toString().padRight(7)} ${frameTimeStampDescription.toString().padLeft(18)} ▄▄▄▄▄▄▄▄';
if (debugPrintBeginFrameBanner)
debugPrint(debugBanner);
debugPrint(_debugBanner);
}
return true;
});
......@@ -569,11 +599,34 @@ abstract class SchedulerBinding extends BindingBase {
assert(schedulerPhase == SchedulerPhase.idle);
_hasScheduledFrame = false;
try {
// TRANSIENT FRAME CALLBACKS
Timeline.startSync('Animate');
_schedulerPhase = SchedulerPhase.transientCallbacks;
_invokeTransientFrameCallbacks(_currentFrameTimeStamp);
final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
_transientCallbacks = <int, _FrameCallbackEntry>{};
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id))
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
});
_removedIds.clear();
} finally {
_schedulerPhase = SchedulerPhase.midFrameMicrotasks;
}
}
/// Called by the engine to produce a new frame.
///
/// This method is called immediately after [handleBeginFrame]. It calls all
/// the callbacks registered by [addPersistentFrameCallback], which typically
/// drive the rendering pipeline, and then calls the callbacks registered by
/// [addPostFrameCallback].
///
/// See [handleBeginFrame] for a discussion about debugging hooks that may be
/// useful when working with frame callbacks.
void handleDrawFrame() {
assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
Timeline.finishSync(); // end the "Animate" phase
try {
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (FrameCallback callback in _persistentCallbacks)
......@@ -586,14 +639,14 @@ abstract class SchedulerBinding extends BindingBase {
_postFrameCallbacks.clear();
for (FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
_schedulerPhase = SchedulerPhase.idle;
_currentFrameTimeStamp = null;
Timeline.finishSync();
assert(() {
if (debugPrintEndFrameBanner)
debugPrint('▀' * debugBanner.length);
debugPrint('▀' * _debugBanner.length);
_debugBanner = null;
return true;
});
}
......@@ -602,19 +655,6 @@ abstract class SchedulerBinding extends BindingBase {
_runTasks();
}
void _invokeTransientFrameCallbacks(Duration timeStamp) {
Timeline.startSync('Animate');
assert(schedulerPhase == SchedulerPhase.transientCallbacks);
final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
_transientCallbacks = <int, _FrameCallbackEntry>{};
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id))
_invokeFrameCallback(callbackEntry.callback, timeStamp, callbackEntry.debugStack);
});
_removedIds.clear();
Timeline.finishSync();
}
static void _debugDescribeTimeStamp(Duration timeStamp, StringBuffer buffer) {
if (timeStamp.inDays > 0)
buffer.write('${timeStamp.inDays}d ');
......
......@@ -30,6 +30,17 @@ bool debugPrintBeginFrameBanner = false;
/// determining if code is running during a frame or between frames.
bool debugPrintEndFrameBanner = false;
/// Log the call stacks that cause a frame to be scheduled.
///
/// This is called whenever [Scheduler.scheduleFrame] schedules a frame. This
/// can happen for various reasons, e.g. when a [Ticker] or
/// [AnimationController] is started, or when [RenderObject.markNeedsLayout] is
/// called, or when [State.setState] is called.
///
/// To get a stack specifically when widgets are scheduled to be built, see
/// [debugPrintScheduleBuildForStacks].
bool debugPrintScheduleFrameStacks = false;
/// Returns true if none of the scheduler library debug variables have been changed.
///
/// This function is used by the test framework to ensure that debug variables
......
......@@ -272,7 +272,7 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
}
return true;
});
scheduleFrame();
ensureVisualUpdate();
}
/// Whether we are currently in a frame. This is used to verify
......@@ -286,7 +286,7 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
/// Pump the build and rendering pipeline to generate a frame.
///
/// This method is called by [handleBeginFrame], which itself is called
/// This method is called by [handleDrawFrame], which itself is called
/// automatically by the engine when when it is time to lay out and paint a
/// frame.
///
......@@ -299,50 +299,57 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
/// driving [AnimationController] objects, which means all of the active
/// [Animation] objects tick at this point.
///
/// [handleBeginFrame] then invokes all the persistent frame callbacks, of which
/// the most notable is this method, [beginFrame], which proceeds as follows:
/// 2. Microtasks: After [handleBeginFrame] returns, any microtasks that got
/// scheduled by transient frame callbacks get to run. This typically includes
/// callbacks for futures from [Ticker]s and [AnimationController]s that
/// completed this frame.
///
/// 2. The build phase: All the dirty [Element]s in the widget tree are
/// After [handleBeginFrame], [handleDrawFrame], which is registered with
/// [ui.window.onDrawFrame], is called, which invokes all the persistent frame
/// callbacks, of which the most notable is this method, [drawFrame], which
/// proceeds as follows:
///
/// 3. The build phase: All the dirty [Element]s in the widget tree are
/// rebuilt (see [State.build]). See [State.setState] for further details on
/// marking a widget dirty for building. See [BuildOwner] for more information
/// on this step.
///
/// 3. The layout phase: All the dirty [RenderObject]s in the system are laid
/// 4. The layout phase: All the dirty [RenderObject]s in the system are laid
/// out (see [RenderObject.performLayout]). See [RenderObject.markNeedsLayout]
/// for further details on marking an object dirty for layout.
///
/// 4. The compositing bits phase: The compositing bits on any dirty
/// 5. The compositing bits phase: The compositing bits on any dirty
/// [RenderObject] objects are updated. See
/// [RenderObject.markNeedsCompositingBitsUpdate].
///
/// 5. The paint phase: All the dirty [RenderObject]s in the system are
/// 6. The paint phase: All the dirty [RenderObject]s in the system are
/// repainted (see [RenderObject.paint]). This generates the [Layer] tree. See
/// [RenderObject.markNeedsPaint] for further details on marking an object
/// dirty for paint.
///
/// 6. The compositing phase: The layer tree is turned into a [ui.Scene] and
/// 7. The compositing phase: The layer tree is turned into a [ui.Scene] and
/// sent to the GPU.
///
/// 7. The semantics phase: All the dirty [RenderObject]s in the system have
/// 8. The semantics phase: All the dirty [RenderObject]s in the system have
/// their semantics updated (see [RenderObject.SemanticsAnnotator]). This
/// generates the [SemanticsNode] tree. See
/// [RenderObject.markNeedsSemanticsUpdate] for further details on marking an
/// object dirty for semantics.
///
/// For more details on steps 3-7, see [PipelineOwner].
/// For more details on steps 4-8, see [PipelineOwner].
///
/// 8. The finalization phase in the widgets layer: The widgets tree is
/// 9. The finalization phase in the widgets layer: The widgets tree is
/// finalized. This causes [State.dispose] to be invoked on any objects that
/// were removed from the widgets tree this frame. See
/// [BuildOwner.finalizeTree] for more details.
///
/// 9. The finalization phase in the scheduler layer: After [beginFrame]
/// returns, [handleBeginFrame] then invokes post-frame callbacks (registered
/// with [addPostFrameCallback].
/// 10. The finalization phase in the scheduler layer: After [drawFrame]
/// returns, [handleDrawFrame] then invokes post-frame callbacks (registered
/// with [addPostFrameCallback]).
//
// When editing the above, also update rendering/binding.dart's copy.
@override
void beginFrame() {
void drawFrame() {
assert(!debugBuildingDirtyElements);
assert(() {
debugBuildingDirtyElements = true;
......@@ -351,7 +358,7 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
try {
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.beginFrame();
super.drawFrame();
buildOwner.finalizeTree();
} finally {
assert(() {
......@@ -429,7 +436,7 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..handleBeginFrame(null);
..scheduleWarmUpFrame();
}
/// Print a string representation of the currently running app.
......
......@@ -45,6 +45,8 @@ bool debugPrintBuildScope = false;
/// To see when a widget is rebuilt, see [debugPrintRebuildDirtyWidgets].
///
/// To see when the dirty list is flushed, see [debugPrintBuildDirtyElements].
///
/// To see when a frame is scheduled, see [debugPrintScheduleFrameStacks].
bool debugPrintScheduleBuildForStacks = false;
/// Log when widgets with global keys are deactivated and log when they are
......
......@@ -210,6 +210,26 @@ class EditableText extends StatefulWidget {
@override
EditableTextState createState() => new EditableTextState();
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('controller: $controller');
description.add('focusNode: $focusNode');
if (obscureText != false)
description.add('obscureText: $obscureText');
description.add('$style');
if (textAlign != null)
description.add('$textAlign');
if (textScaleFactor != null)
description.add('textScaleFactor: $textScaleFactor');
if (maxLines != 1)
description.add('maxLines: $maxLines');
if (autofocus != false)
description.add('autofocus: $autofocus');
if (keyboardType != null)
description.add('keyboardType: $keyboardType');
}
}
/// State for a [EditableText].
......
......@@ -48,6 +48,12 @@ class RawKeyboardListener extends StatefulWidget {
@override
_RawKeyboardListenerState createState() => new _RawKeyboardListenerState();
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('focusNode: $focusNode');
}
}
class _RawKeyboardListenerState extends State<RawKeyboardListener> {
......
......@@ -2,14 +2,20 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart';
import '../scheduler/scheduler_tester.dart';
void main() {
setUp(() {
WidgetsFlutterBinding.ensureInitialized();
WidgetsBinding.instance.resetEpoch();
ui.window.onBeginFrame = null;
ui.window.onDrawFrame = null;
});
test('Can set value during status callback', () {
......@@ -34,10 +40,10 @@ void main() {
controller.forward();
expect(didComplete, isFalse);
expect(didDismiss, isFalse);
WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 1));
tick(const Duration(seconds: 1));
expect(didComplete, isFalse);
expect(didDismiss, isFalse);
WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 2));
tick(const Duration(seconds: 2));
expect(didComplete, isTrue);
expect(didDismiss, isTrue);
......@@ -89,16 +95,16 @@ void main() {
controller.reverse();
log.clear();
WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 10));
tick(const Duration(seconds: 10));
expect(log, equals(<AnimationStatus>[]));
expect(valueLog, equals(<AnimationStatus>[]));
WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 20));
tick(const Duration(seconds: 20));
expect(log, equals(<AnimationStatus>[]));
expect(valueLog, equals(<AnimationStatus>[]));
WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 30));
tick(const Duration(seconds: 30));
expect(log, equals(<AnimationStatus>[]));
expect(valueLog, equals(<AnimationStatus>[]));
WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 40));
tick(const Duration(seconds: 40));
expect(log, equals(<AnimationStatus>[]));
expect(valueLog, equals(<AnimationStatus>[]));
......@@ -157,8 +163,8 @@ void main() {
);
controller.fling();
WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 1));
WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 2));
tick(const Duration(seconds: 1));
tick(const Duration(seconds: 2));
expect(controller.value, 1.0);
controller.stop();
......@@ -170,12 +176,12 @@ void main() {
);
largeRangeController.fling();
WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 3));
WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 4));
tick(const Duration(seconds: 3));
tick(const Duration(seconds: 4));
expect(largeRangeController.value, 45.0);
largeRangeController.fling(velocity: -1.0);
WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 5));
WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 6));
tick(const Duration(seconds: 5));
tick(const Duration(seconds: 6));
expect(largeRangeController.value, -30.0);
largeRangeController.stop();
});
......@@ -186,9 +192,9 @@ void main() {
vsync: const TestVSync(),
);
controller.forward();
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 30));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 40));
tick(const Duration(milliseconds: 20));
tick(const Duration(milliseconds: 30));
tick(const Duration(milliseconds: 40));
expect(controller.lastElapsedDuration, equals(const Duration(milliseconds: 20)));
controller.stop();
});
......@@ -200,14 +206,14 @@ void main() {
);
expect(controller, hasOneLineDescription);
controller.forward();
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 10));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20));
tick(const Duration(milliseconds: 10));
tick(const Duration(milliseconds: 20));
expect(controller, hasOneLineDescription);
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 30));
tick(const Duration(milliseconds: 30));
expect(controller, hasOneLineDescription);
controller.reverse();
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 40));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 50));
tick(const Duration(milliseconds: 40));
tick(const Duration(milliseconds: 50));
expect(controller, hasOneLineDescription);
controller.stop();
});
......@@ -220,36 +226,36 @@ void main() {
// mid-flight
controller.forward();
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 0));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 500));
tick(const Duration(milliseconds: 0));
tick(const Duration(milliseconds: 500));
expect(controller.velocity, inInclusiveRange(0.9, 1.1));
// edges
controller.forward();
expect(controller.velocity, inInclusiveRange(0.4, 0.6));
WidgetsBinding.instance.handleBeginFrame(Duration.ZERO);
tick(Duration.ZERO);
expect(controller.velocity, inInclusiveRange(0.4, 0.6));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 5));
tick(const Duration(milliseconds: 5));
expect(controller.velocity, inInclusiveRange(0.9, 1.1));
controller.forward(from: 0.5);
expect(controller.velocity, inInclusiveRange(0.4, 0.6));
WidgetsBinding.instance.handleBeginFrame(Duration.ZERO);
tick(Duration.ZERO);
expect(controller.velocity, inInclusiveRange(0.4, 0.6));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 5));
tick(const Duration(milliseconds: 5));
expect(controller.velocity, inInclusiveRange(0.9, 1.1));
// stopped
controller.forward(from: 1.0);
expect(controller.velocity, 0.0);
WidgetsBinding.instance.handleBeginFrame(Duration.ZERO);
tick(Duration.ZERO);
expect(controller.velocity, 0.0);
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 500));
tick(const Duration(milliseconds: 500));
expect(controller.velocity, 0.0);
controller.forward();
WidgetsBinding.instance.handleBeginFrame(Duration.ZERO);
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 1000));
tick(Duration.ZERO);
tick(const Duration(milliseconds: 1000));
expect(controller.velocity, 0.0);
controller.stop();
......
......@@ -4,6 +4,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
void main() {
......@@ -25,10 +26,10 @@ void main() {
log.add('a'); // t=0
await controller1.forward(); // starts at t=0 again
log.add('b'); // wants to end at t=100 but missed frames until t=150
await controller2.forward(); // starts at t=200
log.add('c'); // wants to end at t=800 but missed frames until t=850
await controller3.forward(); // starts at t=1200
log.add('d'); // wants to end at t=1500 but missed frames until t=1600
await controller2.forward(); // starts at t=150
log.add('c'); // wants to end at t=750 but missed frames until t=799
await controller3.forward(); // starts at t=799
log.add('d'); // wants to end at t=1099 but missed frames until t=1200
}
log.add('start');
runTest().then((Null value) {
......@@ -47,11 +48,11 @@ void main() {
await tester.pump(const Duration(milliseconds: 400)); // t=600
expect(log, <String>['start', 'a', 'b']);
await tester.pump(const Duration(milliseconds: 199)); // t=799
expect(log, <String>['start', 'a', 'b']);
expect(log, <String>['start', 'a', 'b', 'c']);
await tester.pump(const Duration(milliseconds: 51)); // t=850
expect(log, <String>['start', 'a', 'b', 'c']);
await tester.pump(const Duration(milliseconds: 400)); // t=1200
expect(log, <String>['start', 'a', 'b', 'c']);
expect(log, <String>['start', 'a', 'b', 'c', 'd', 'end']);
await tester.pump(const Duration(milliseconds: 400)); // t=1600
expect(log, <String>['start', 'a', 'b', 'c', 'd', 'end']);
});
......@@ -74,10 +75,10 @@ void main() {
log.add('a'); // t=0
await controller1.forward().orCancel; // starts at t=0 again
log.add('b'); // wants to end at t=100 but missed frames until t=150
await controller2.forward().orCancel; // starts at t=200
log.add('c'); // wants to end at t=800 but missed frames until t=850
await controller3.forward().orCancel; // starts at t=1200
log.add('d'); // wants to end at t=1500 but missed frames until t=1600
await controller2.forward().orCancel; // starts at t=150
log.add('c'); // wants to end at t=750 but missed frames until t=799
await controller3.forward().orCancel; // starts at t=799
log.add('d'); // wants to end at t=1099 but missed frames until t=1200
}
log.add('start');
runTest().then((Null value) {
......@@ -96,11 +97,11 @@ void main() {
await tester.pump(const Duration(milliseconds: 400)); // t=600
expect(log, <String>['start', 'a', 'b']);
await tester.pump(const Duration(milliseconds: 199)); // t=799
expect(log, <String>['start', 'a', 'b']);
expect(log, <String>['start', 'a', 'b', 'c']);
await tester.pump(const Duration(milliseconds: 51)); // t=850
expect(log, <String>['start', 'a', 'b', 'c']);
await tester.pump(const Duration(milliseconds: 400)); // t=1200
expect(log, <String>['start', 'a', 'b', 'c']);
expect(log, <String>['start', 'a', 'b', 'c', 'd', 'end']);
await tester.pump(const Duration(milliseconds: 400)); // t=1600
expect(log, <String>['start', 'a', 'b', 'c', 'd', 'end']);
});
......
......@@ -50,10 +50,13 @@ class TestServiceExtensionsBinding extends BindingBase
void scheduleFrame() {
frameScheduled = true;
}
void doFrame() {
Future<Null> doFrame() async {
frameScheduled = false;
if (ui.window.onBeginFrame != null)
ui.window.onBeginFrame(Duration.ZERO);
await flushMicrotasks();
if (ui.window.onDrawFrame != null)
ui.window.onDrawFrame();
}
Future<Null> flushMicrotasks() {
......@@ -72,7 +75,7 @@ Future<Map<String, String>> hasReassemble(Future<Map<String, String>> pendingRes
await binding.flushMicrotasks();
expect(binding.frameScheduled, isTrue);
expect(completed, isFalse);
binding.doFrame();
await binding.doFrame();
await binding.flushMicrotasks();
expect(completed, isTrue);
expect(binding.frameScheduled, isFalse);
......@@ -85,7 +88,7 @@ void main() {
test('Service extensions - pretest', () async {
binding = new TestServiceExtensionsBinding();
expect(binding.frameScheduled, isTrue);
binding.doFrame(); // initial frame scheduled by creating the binding
await binding.doFrame(); // initial frame scheduled by creating the binding
expect(binding.frameScheduled, isFalse);
expect(debugPrint, equals(debugPrintThrottled));
......@@ -134,6 +137,7 @@ void main() {
test('Service extensions - debugDumpRenderTree', () async {
Map<String, String> result;
await binding.doFrame();
result = await binding.testExtension('debugDumpRenderTree', <String, String>{});
expect(result, <String, String>{});
expect(console, <Matcher>[
......@@ -165,7 +169,7 @@ void main() {
await binding.flushMicrotasks();
expect(binding.frameScheduled, isTrue);
expect(completed, isFalse);
binding.doFrame();
await binding.doFrame();
await binding.flushMicrotasks();
expect(completed, isTrue);
expect(binding.frameScheduled, isFalse);
......@@ -179,7 +183,7 @@ void main() {
pendingResult = binding.testExtension('debugPaint', <String, String>{ 'enabled': 'false' });
await binding.flushMicrotasks();
expect(binding.frameScheduled, isTrue);
binding.doFrame();
await binding.doFrame();
expect(binding.frameScheduled, isFalse);
result = await pendingResult;
expect(result, <String, String>{ 'enabled': 'false' });
......@@ -294,7 +298,7 @@ void main() {
await binding.flushMicrotasks();
expect(completed, false);
expect(binding.frameScheduled, isTrue);
binding.doFrame();
await binding.doFrame();
await binding.flushMicrotasks();
expect(completed, true);
expect(binding.frameScheduled, isFalse);
......@@ -319,7 +323,7 @@ void main() {
await binding.flushMicrotasks();
expect(binding.frameScheduled, isTrue);
expect(completed, false);
binding.doFrame();
await binding.doFrame();
await binding.flushMicrotasks();
expect(completed, true);
expect(binding.frameScheduled, isFalse);
......
......@@ -15,7 +15,7 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
EnginePhase phase = EnginePhase.composite;
@override
void beginFrame() {
void drawFrame() {
assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead');
pipelineOwner.flushLayout();
if (phase == EnginePhase.layout)
......@@ -82,7 +82,7 @@ void pumpFrame({ EnginePhase phase: EnginePhase.layout }) {
assert(renderer.renderView != null);
assert(renderer.renderView.child != null); // call layout() first!
renderer.phase = phase;
renderer.beginFrame();
renderer.drawFrame();
}
class TestCallbackPainter extends CustomPainter {
......
......@@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:test/test.dart';
import 'scheduler_tester.dart';
class TestSchedulerBinding extends BindingBase with SchedulerBinding { }
void main() {
......@@ -39,7 +41,7 @@ void main() {
scheduler.scheduleFrameCallback(firstCallback);
secondId = scheduler.scheduleFrameCallback(secondCallback);
scheduler.handleBeginFrame(const Duration(milliseconds: 16));
tick(const Duration(milliseconds: 16));
expect(firstCallbackRan, isTrue);
expect(secondCallbackRan, isFalse);
......@@ -47,7 +49,7 @@ void main() {
firstCallbackRan = false;
secondCallbackRan = false;
scheduler.handleBeginFrame(const Duration(milliseconds: 32));
tick(const Duration(milliseconds: 32));
expect(firstCallbackRan, isFalse);
expect(secondCallbackRan, isFalse);
......
// Copyright 2017 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';
@Deprecated('animation_tester is not compatible with dart:async')
class Future { } // so that people can't import us and dart:async
void tick(Duration duration) {
// We don't bother running microtasks between these two calls
// because we don't use Futures in these tests and so don't care.
SchedulerBinding.instance.handleBeginFrame(duration);
SchedulerBinding.instance.handleDrawFrame();
}
......@@ -144,7 +144,7 @@ void main() {
duration: const Duration(milliseconds: 100),
);
// Item's 0, 1, 2 at 0, 100, 200. All heights 100.
// Items 0, 1, 2 at 0, 100, 200. All heights 100.
expect(itemTop(0), 0.0);
expect(itemBottom(0), 100.0);
expect(itemTop(1), 100.0);
......@@ -154,7 +154,7 @@ void main() {
// Newly removed item 0's height should animate from 100 to 0 over 100ms
// Item's 0, 1, 2 at 0, 50, 150. Item 0's height is 50.
// Items 0, 1, 2 at 0, 50, 150. Item 0's height is 50.
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(itemTop(0), 0.0);
......@@ -164,10 +164,8 @@ void main() {
expect(itemTop(2), 150.0);
expect(itemBottom(2), 250.0);
// Item's 0, 1, 2 at 0, 0, 0. Item 0's height is 0.
// Items 1, 2 at 0, 100.
await tester.pumpAndSettle();
expect(itemTop(0), 0.0);
expect(itemBottom(0), 0.0);
expect(itemTop(1), 0.0);
expect(itemBottom(1), 100.0);
expect(itemTop(2), 100.0);
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -10,7 +12,8 @@ void sendFakeKeyEvent(Map<String, dynamic> data) {
BinaryMessages.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data),
(_) {});
(ByteData data) { },
);
}
void main() {
......@@ -25,7 +28,6 @@ void main() {
final List<RawKeyEvent> events = <RawKeyEvent>[];
final FocusNode focusNode = new FocusNode();
tester.binding.focusManager.rootScope.requestFocus(focusNode);
await tester.pumpWidget(new RawKeyboardListener(
focusNode: focusNode,
......@@ -33,6 +35,9 @@ void main() {
child: new Container(),
));
tester.binding.focusManager.rootScope.requestFocus(focusNode);
await tester.idle();
sendFakeKeyEvent(<String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
......@@ -40,7 +45,6 @@ void main() {
'codePoint': 0x64,
'modifiers': 0x08,
});
await tester.idle();
expect(events.length, 1);
......@@ -60,7 +64,6 @@ void main() {
final List<RawKeyEvent> events = <RawKeyEvent>[];
final FocusNode focusNode = new FocusNode();
tester.binding.focusManager.rootScope.requestFocus(focusNode);
await tester.pumpWidget(new RawKeyboardListener(
focusNode: focusNode,
......@@ -68,6 +71,9 @@ void main() {
child: new Container(),
));
tester.binding.focusManager.rootScope.requestFocus(focusNode);
await tester.idle();
sendFakeKeyEvent(<String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
......@@ -75,7 +81,6 @@ void main() {
'codePoint': 0x64,
'modifiers': 0x08,
});
await tester.idle();
expect(events.length, 1);
......
......@@ -28,7 +28,7 @@ import 'test_text_input.dart';
/// Phases that can be reached by [WidgetTester.pumpWidget] and
/// [TestWidgetsFlutterBinding.pump].
///
/// See [WidgetsBinding.beginFrame] for a more detailed description of some of
/// See [WidgetsBinding.drawFrame] for a more detailed description of some of
/// these phases.
enum EnginePhase {
/// The build phase in the widgets library. See [BuildOwner.buildScope].
......@@ -490,6 +490,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
void initInstances() {
super.initInstances();
ui.window.onBeginFrame = null;
ui.window.onDrawFrame = null;
}
FakeAsync _fakeAsync;
......@@ -519,15 +520,27 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
_fakeAsync.elapse(duration);
_phase = newPhase;
if (hasScheduledFrame) {
_fakeAsync.flushMicrotasks();
handleBeginFrame(new Duration(
milliseconds: _clock.now().millisecondsSinceEpoch,
));
_fakeAsync.flushMicrotasks();
handleDrawFrame();
}
_fakeAsync.flushMicrotasks();
return new Future<Null>.value();
});
}
@override
void scheduleWarmUpFrame() {
// We override the default version of this so that the application-startup warm-up frame
// does not schedule timers which we might never get around to running.
handleBeginFrame(null);
_fakeAsync.flushMicrotasks();
handleDrawFrame();
}
@override
Future<Null> idle() {
final Future<Null> result = super.idle();
......@@ -537,9 +550,9 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
EnginePhase _phase = EnginePhase.sendSemanticsTree;
// Cloned from RendererBinding.beginFrame() but with early-exit semantics.
// Cloned from RendererBinding.drawFrame() but with early-exit semantics.
@override
void beginFrame() {
void drawFrame() {
assert(inTest);
try {
debugBuildingDirtyElements = true;
......@@ -599,14 +612,14 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
@override
void _verifyInvariants() {
super._verifyInvariants();
assert(() {
'A periodic Timer is still running even after the widget tree was disposed.';
return _fakeAsync.periodicTimerCount == 0;
});
assert(() {
'A Timer is still pending even after the widget tree was disposed.';
return _fakeAsync.nonPeriodicTimerCount == 0;
});
assert(
_fakeAsync.periodicTimerCount == 0,
'A periodic Timer is still running even after the widget tree was disposed.'
);
assert(
_fakeAsync.nonPeriodicTimerCount == 0,
'A Timer is still pending even after the widget tree was disposed.'
);
assert(_fakeAsync.microtaskCount == 0); // Shouldn't be possible.
}
......@@ -740,12 +753,19 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
/// ```
LiveTestWidgetsFlutterBindingFramePolicy framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fadePointers;
bool _doDrawThisFrame;
@override
void handleBeginFrame(Duration rawTimeStamp) {
assert(_doDrawThisFrame == null);
if (_expectingFrame ||
(framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.fullyLive) ||
(framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.fadePointers && _viewNeedsPaint))
(framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.fadePointers && _viewNeedsPaint)) {
_doDrawThisFrame = true;
super.handleBeginFrame(rawTimeStamp);
} else {
_doDrawThisFrame = false;
}
_viewNeedsPaint = false;
if (_expectingFrame) { // set during pump
assert(_pendingFrame != null);
......@@ -757,6 +777,14 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
}
}
@override
void handleDrawFrame() {
assert(_doDrawThisFrame != null);
if (_doDrawThisFrame)
super.handleDrawFrame();
_doDrawThisFrame = null;
}
@override
void initRenderView() {
assert(renderView == null);
......
......@@ -210,10 +210,9 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
}
/// Repeatedly calls [pump] with the given `duration` until there are no
/// longer any transient callbacks scheduled. This will call [pump] at least
/// once, even if no transient callbacks are scheduled when the function is
/// called, in case there are dirty widgets to rebuild which will themselves
/// register new transient callbacks.
/// longer any frames scheduled. This will call [pump] at least once, even if
/// no frames are scheduled when the function is called, to flush any pending
/// microtasks which may themselves schedule a frame.
///
/// This essentially waits for all animations to have completed.
///
......@@ -251,13 +250,26 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
throw new FlutterError('pumpAndSettle timed out');
await binding.pump(duration, phase);
count += 1;
} while (hasRunningAnimations);
} while (binding.hasScheduledFrame);
}).then<int>((Null _) => count);
}
/// Whether ther are any any transient callbacks scheduled.
/// Whether there are any any transient callbacks scheduled.
///
/// This essentially checks whether all animations have completed.
///
/// See also:
///
/// * [pumpAndSettle], which essentially calls [pump] until there are no
/// scheduled frames.
///
/// * [SchedulerBinding.transientCallbackCount], which is the value on which
/// this is based.
///
/// * [SchedulerBinding.hasScheduledFrame], which is true whenever a frame is
/// pending. [SchedulerBinding.hasScheduledFrame] is made true when a
/// widget calls [State.setState], even if there are no transient callbacks
/// scheduled. This is what [pumpAndSettle] uses.
bool get hasRunningAnimations => binding.transientCallbackCount > 0;
@override
......
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