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