Commit 5bc8888e authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Make tests more realistic (#5762)

Previously, pumpWidget() would do a partial pump (it didn't trigger
Ticker callbacks or post-frame callbacks), and pump() would do a full
pump. This patch brings them closer together. It also makes runApp run a
full actual frame, rather than skipping the transient callback part of
the frame logic. Having "half-frames" in the system was confusing and
could lead to bugs where code expecting to run before the next layout
pass didn't because a "half-frame" ran first.

Also, make Tickers start ticking in the frame that they were started in,
if they were started during a frame. This means we no longer spin a
frame for t=0, we jump straight to the first actual frame.

Other changes in this patch:

* rename WidgetsBinding._runApp to WidgetsBinding.attachRootWidget, so
  that tests can use it to more accurately mock out runApp.

* allow loadStructuredData to return synchronously.

* make handleBeginFrame handle not being given a time stamp.

* make DataPipeImageProvider.loadAsync protected (rather than private),
  and document it. There wasn't really a reason for it to be private.

* fix ImageConfiguration.toString.

* introduce debugPrintBuildScope and debugPrintScheduleBuildForStacks,
  which can help debug problems with widgets getting marked as dirty but
  not cleaned.

* make debugPrintRebuildDirtyWidgets say "Building" the first time and
  "Rebuilding" the second, to make it clearer when a widget is first
  created. This makes debugging widget lifecycle issues much easier.

* make debugDumpApp more resilient.

* debugPrintStack now takes a label that is printed before the stack.

* improve the banner shown for debugPrintBeginFrameBanner.

* various and sundry documentation fixes
parent b71d7694
...@@ -334,7 +334,11 @@ class FlutterError extends AssertionError { ...@@ -334,7 +334,11 @@ class FlutterError extends AssertionError {
/// ///
/// The `maxFrames` argument can be given to limit the stack to the given number /// The `maxFrames` argument can be given to limit the stack to the given number
/// of lines. By default, all non-filtered stack lines are shown. /// of lines. By default, all non-filtered stack lines are shown.
void debugPrintStack({ int maxFrames }) { ///
/// The `label` argument, if present, will be printed before the stack.
void debugPrintStack({ String label, int maxFrames }) {
if (label != null)
debugPrint(label);
List<String> lines = StackTrace.current.toString().trimRight().split('\n'); List<String> lines = StackTrace.current.toString().trimRight().split('\n');
if (maxFrames != null) if (maxFrames != null)
lines = lines.take(maxFrames); lines = lines.take(maxFrames);
......
...@@ -16,7 +16,7 @@ typedef void DebugPrintCallback(String message, { int wrapWidth }); ...@@ -16,7 +16,7 @@ typedef void DebugPrintCallback(String message, { int wrapWidth });
/// ///
/// By default, this function very crudely attempts to throttle the rate at /// By default, this function very crudely attempts to throttle the rate at
/// which messages are sent to avoid data loss on Android. This means that /// which messages are sent to avoid data loss on Android. This means that
/// interleaving calls to this function (directly or indirectly via /// interleaving calls to this function (directly or indirectly via, e.g.,
/// [debugDumpRenderTree] or [debugDumpApp]) and to the Dart [print] method can /// [debugDumpRenderTree] or [debugDumpApp]) and to the Dart [print] method can
/// result in out-of-order messages in the logs. /// result in out-of-order messages in the logs.
/// ///
......
...@@ -1337,7 +1337,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { ...@@ -1337,7 +1337,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
if (owner != null) { if (owner != null) {
assert(() { assert(() {
if (debugPrintMarkNeedsLayoutStacks) if (debugPrintMarkNeedsLayoutStacks)
debugPrintStack(); debugPrintStack(label: 'markNeedsLayout() called for $this');
return true; return true;
}); });
owner._nodesNeedingLayout.add(this); owner._nodesNeedingLayout.add(this);
...@@ -1790,7 +1790,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { ...@@ -1790,7 +1790,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
if (isRepaintBoundary) { if (isRepaintBoundary) {
assert(() { assert(() {
if (debugPrintMarkNeedsPaintStacks) if (debugPrintMarkNeedsPaintStacks)
debugPrintStack(); debugPrintStack(label: 'markNeedsPaint() called for $this');
return true; return true;
}); });
// If we always have our own layer, then we can just repaint // If we always have our own layer, then we can just repaint
......
...@@ -469,46 +469,97 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -469,46 +469,97 @@ abstract class SchedulerBinding extends BindingBase {
return new Duration(microseconds: (rawDurationSinceEpoch.inMicroseconds / timeDilation).round() + _epochStart.inMicroseconds); return new Duration(microseconds: (rawDurationSinceEpoch.inMicroseconds / timeDilation).round() + _epochStart.inMicroseconds);
} }
/// The time stamp for the frame currently being processed.
///
/// This is only valid while [handleBeginFrame] is running, i.e. while a frame
/// is being produced.
// TODO(ianh): Replace this when fixing https://github.com/flutter/flutter/issues/5469
Duration get currentFrameTimeStamp {
assert(_currentFrameTimeStamp != null);
return _currentFrameTimeStamp;
}
Duration _currentFrameTimeStamp;
int _debugFrameNumber = 0;
/// Called by the engine to produce a new frame. /// Called by the engine to produce a new frame.
/// ///
/// This function first calls all the callbacks registered by /// This function first calls all the callbacks registered by
/// [scheduleFrameCallback]/[addFrameCallback], then calls all the /// [scheduleFrameCallback]/[addFrameCallback], then calls all the callbacks
/// callbacks registered by [addPersistentFrameCallback], which /// registered by [addPersistentFrameCallback], which typically drive the
/// typically drive the rendering pipeline, and finally calls the /// rendering pipeline, and finally calls the callbacks registered by
/// callbacks registered by [addPostFrameCallback]. /// [addPostFrameCallback].
///
/// If the given time stamp is null, the time stamp from the last frame is
/// reused.
///
/// To have a banner shown at the start of every frame in debug mode, set
/// [debugPrintBeginFrameBanner] to true. The banner will be printed to the
/// console using [debugPrint] and will contain the frame number (which
/// increments by one for each frame), and the time stamp of the frame. If the
/// given time stamp was null, then the string "warm-up frame" is shown
/// instead of the time stamp. This allows you to distinguish frames eagerly
/// pushed by the framework from those requested by the engine in response to
/// the vsync signal from the operating system.
///
/// You can also show a banner at the end of every frame by setting
/// [debugPrintEndFrameBanner] to true. This allows you to distinguish log
/// statements printed during a frame from those printed between frames (e.g.
/// in response to events or timers).
void handleBeginFrame(Duration rawTimeStamp) { void handleBeginFrame(Duration rawTimeStamp) {
Timeline.startSync('Frame'); Timeline.startSync('Frame');
_firstRawTimeStampInEpoch ??= rawTimeStamp; _firstRawTimeStampInEpoch ??= rawTimeStamp;
Duration timeStamp = _adjustForEpoch(rawTimeStamp); _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);
if (rawTimeStamp != null)
_lastRawTimeStamp = rawTimeStamp;
String debugBanner;
assert(() { assert(() {
_debugFrameNumber += 1;
if (debugPrintBeginFrameBanner || debugPrintEndFrameBanner) {
StringBuffer frameTimeStampDescription = new StringBuffer();
if (rawTimeStamp != null) {
_debugDescribeTimeStamp(_currentFrameTimeStamp, frameTimeStampDescription);
} else {
frameTimeStampDescription.write('(warm-up frame)');
}
debugBanner = '▄▄▄▄▄▄▄▄ Frame ${_debugFrameNumber.toString().padRight(7)} ${frameTimeStampDescription.toString().padLeft(18)} ▄▄▄▄▄▄▄▄';
if (debugPrintBeginFrameBanner) if (debugPrintBeginFrameBanner)
debugPrint('━━━━━━━┫ Begin Frame ($timeStamp) ┣━━━━━━━'); debugPrint(debugBanner);
}
return true; return true;
}); });
_lastRawTimeStamp = rawTimeStamp;
assert(!_isProducingFrame); assert(!_isProducingFrame);
_isProducingFrame = true; _isProducingFrame = true;
_hasScheduledFrame = false; _hasScheduledFrame = false;
_invokeTransientFrameCallbacks(timeStamp); try {
for (FrameCallback callback in _persistentCallbacks) // TRANSIENT FRAME CALLBACKS
_invokeFrameCallback(callback, timeStamp); _invokeTransientFrameCallbacks(_currentFrameTimeStamp);
// PERSISTENT FRAME CALLBACKS
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
_isProducingFrame = false; _isProducingFrame = false;
// POST-FRAME CALLBACKS
List<FrameCallback> localPostFrameCallbacks = List<FrameCallback> localPostFrameCallbacks =
new List<FrameCallback>.from(_postFrameCallbacks); new List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear(); _postFrameCallbacks.clear();
for (FrameCallback callback in localPostFrameCallbacks) for (FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, timeStamp); _invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
_isProducingFrame = false; // just in case we throw before setting it above
_currentFrameTimeStamp = null;
Timeline.finishSync(); Timeline.finishSync();
assert(() { assert(() {
if (debugPrintEndFrameBanner) if (debugPrintEndFrameBanner)
debugPrint('━━━━━━━┫ End of Frame ┣━━━━━━━'); debugPrint('▀' * debugBanner.length);
return true; return true;
}); });
}
// All frame-related callbacks have been executed. Run lower-priority tasks. // All frame-related callbacks have been executed. Run lower-priority tasks.
_runTasks(); _runTasks();
...@@ -527,6 +578,22 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -527,6 +578,22 @@ abstract class SchedulerBinding extends BindingBase {
Timeline.finishSync(); Timeline.finishSync();
} }
static void _debugDescribeTimeStamp(Duration timeStamp, StringBuffer buffer) {
if (timeStamp.inDays > 0)
buffer.write('${timeStamp.inDays}d ');
if (timeStamp.inHours > 0)
buffer.write('${timeStamp.inHours - timeStamp.inDays * Duration.HOURS_PER_DAY}h ');
if (timeStamp.inMinutes > 0)
buffer.write('${timeStamp.inMinutes - timeStamp.inHours * Duration.MINUTES_PER_HOUR}m ');
if (timeStamp.inSeconds > 0)
buffer.write('${timeStamp.inSeconds - timeStamp.inMinutes * Duration.SECONDS_PER_MINUTE}s ');
buffer.write('${timeStamp.inMilliseconds - timeStamp.inSeconds * Duration.MILLISECONDS_PER_SECOND}');
int microseconds = timeStamp.inMicroseconds - timeStamp.inMilliseconds * Duration.MICROSECONDS_PER_MILLISECOND;
if (microseconds > 0)
buffer.write('.${microseconds.toString().padLeft(3, "0")}');
buffer.write('ms');
}
// Calls the given [callback] with [timestamp] as argument. // Calls the given [callback] with [timestamp] as argument.
// //
// Wraps the callback in a try/catch and forwards any error to // Wraps the callback in a try/catch and forwards any error to
......
...@@ -5,10 +5,12 @@ ...@@ -5,10 +5,12 @@
/// Print a banner at the beginning of each frame. /// Print a banner at the beginning of each frame.
/// ///
/// Frames triggered by the engine and handler by the scheduler binding will /// Frames triggered by the engine and handler by the scheduler binding will
/// have a banner saying "Begin Frame" and giving the time stamp of the frame. /// have a banner giving the frame number and the time stamp of the frame.
/// ///
/// Frames triggered eagerly by the widget framework (e.g. when calling /// Frames triggered eagerly by the widget framework (e.g. when calling
/// [runApp]) will have a label saying "Begin Warm-Up Frame". /// [runApp]) will have a label saying "warm-up frame" instead of the time stamp
/// (the time stamp sent to frame callbacks in that case is the time of the last
/// frame, or 0:00 if it is the first frame).
/// ///
/// To include a banner at the end of each frame as well, to distinguish /// To include a banner at the end of each frame as well, to distinguish
/// intra-frame output from inter-frame output, set [debugPrintEndFrameBanner] /// intra-frame output from inter-frame output, set [debugPrintEndFrameBanner]
......
...@@ -40,6 +40,8 @@ class Ticker { ...@@ -40,6 +40,8 @@ class Ticker {
assert(_startTime == null); assert(_startTime == null);
_completer = new Completer<Null>(); _completer = new Completer<Null>();
_scheduleTick(); _scheduleTick();
if (SchedulerBinding.instance.isProducingFrame)
_startTime = SchedulerBinding.instance.currentFrameTimeStamp;
return _completer.future; return _completer.future;
} }
......
...@@ -155,12 +155,27 @@ abstract class CachingAssetBundle extends AssetBundle { ...@@ -155,12 +155,27 @@ abstract class CachingAssetBundle extends AssetBundle {
assert(parser != null); assert(parser != null);
if (_structuredDataCache.containsKey(key)) if (_structuredDataCache.containsKey(key))
return _structuredDataCache[key]; return _structuredDataCache[key];
final Completer<dynamic> completer = new Completer<dynamic>(); Completer<dynamic> completer;
_structuredDataCache[key] = completer.future; Future<dynamic> result;
completer.complete(loadString(key, cache: false).then/*<dynamic>*/(parser)); loadString(key, cache: false).then(parser).then((dynamic value) {
completer.future.then((dynamic value) { result = new SynchronousFuture<dynamic>(value);
_structuredDataCache[key] = new SynchronousFuture<dynamic>(value); _structuredDataCache[key] = result;
if (completer != null) {
// We already returned from the loadStructuredData function, which means
// we are in the asynchronous mode. Pass the value to the completer. The
// completer's future is what we returned.
completer.complete(value);
}
}); });
if (result != null) {
// The code above ran synchronously, and came up with an answer.
// Return the SynchronousFuture that we created above.
return result;
}
// The code above hasn't yet run its "then" handler yet. Let's prepare a
// completer for it to use when it does run.
completer = new Completer<dynamic>();
_structuredDataCache[key] = completer.future;
return completer.future; return completer.future;
} }
......
...@@ -99,26 +99,31 @@ class ImageConfiguration { ...@@ -99,26 +99,31 @@ class ImageConfiguration {
if (hasArguments) if (hasArguments)
result.write(', '); result.write(', ');
result.write('bundle: $bundle'); result.write('bundle: $bundle');
hasArguments = true;
} }
if (devicePixelRatio != null) { if (devicePixelRatio != null) {
if (hasArguments) if (hasArguments)
result.write(', '); result.write(', ');
result.write('devicePixelRatio: $devicePixelRatio'); result.write('devicePixelRatio: $devicePixelRatio');
hasArguments = true;
} }
if (locale != null) { if (locale != null) {
if (hasArguments) if (hasArguments)
result.write(', '); result.write(', ');
result.write('locale: $locale'); result.write('locale: $locale');
hasArguments = true;
} }
if (size != null) { if (size != null) {
if (hasArguments) if (hasArguments)
result.write(', '); result.write(', ');
result.write('size: $size'); result.write('size: $size');
hasArguments = true;
} }
if (platform != null) { if (platform != null) {
if (hasArguments) if (hasArguments)
result.write(', '); result.write(', ');
result.write('platform: $platform'); result.write('platform: $platform');
hasArguments = true;
} }
result.write(')'); result.write(')');
return result.toString(); return result.toString();
...@@ -219,10 +224,12 @@ abstract class DataPipeImageProvider<T> extends ImageProvider<T> { ...@@ -219,10 +224,12 @@ abstract class DataPipeImageProvider<T> extends ImageProvider<T> {
/// const constructors so that they can be used in const expressions. /// const constructors so that they can be used in const expressions.
const DataPipeImageProvider(); const DataPipeImageProvider();
/// Converts a key into an [ImageStreamCompleter], and begins fetching the
/// image using [loadAsync].
@override @override
ImageStreamCompleter load(T key) { ImageStreamCompleter load(T key) {
return new OneFrameImageStreamCompleter( return new OneFrameImageStreamCompleter(
_loadAsync(key), loadAsync(key),
informationCollector: (StringBuffer information) { informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this'); information.writeln('Image provider: $this');
information.write('Image key: $key'); information.write('Image key: $key');
...@@ -230,7 +237,12 @@ abstract class DataPipeImageProvider<T> extends ImageProvider<T> { ...@@ -230,7 +237,12 @@ abstract class DataPipeImageProvider<T> extends ImageProvider<T> {
); );
} }
Future<ImageInfo> _loadAsync(T key) async { /// Fetches the image from the data pipe, decodes it, and returns a
/// corresponding [ImageInfo] object.
///
/// This function is used by [load].
@protected
Future<ImageInfo> loadAsync(T key) async {
final mojo.MojoDataPipeConsumer dataPipe = await loadDataPipe(key); final mojo.MojoDataPipeConsumer dataPipe = await loadDataPipe(key);
if (dataPipe == null) if (dataPipe == null)
throw 'Unable to read data'; throw 'Unable to read data';
...@@ -367,7 +379,7 @@ abstract class AssetBundleImageProvider extends DataPipeImageProvider<AssetBundl ...@@ -367,7 +379,7 @@ abstract class AssetBundleImageProvider extends DataPipeImageProvider<AssetBundl
Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration); Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration);
@override @override
Future<mojo.MojoDataPipeConsumer> loadDataPipe(AssetBundleImageKey key) async { Future<mojo.MojoDataPipeConsumer> loadDataPipe(AssetBundleImageKey key) {
return key.bundle.load(key.name); return key.bundle.load(key.name);
} }
......
...@@ -110,7 +110,7 @@ class AssetImage extends AssetBundleImageProvider { ...@@ -110,7 +110,7 @@ class AssetImage extends AssetBundleImageProvider {
if (completer != null) { if (completer != null) {
// We already returned from this function, which means we are in the // We already returned from this function, which means we are in the
// asynchronous mode. Pass the value to the completer. The completer's // asynchronous mode. Pass the value to the completer. The completer's
// function is what we returned. // future is what we returned.
completer.complete(key); completer.complete(key);
} else { } else {
// We haven't yet returned, so we must have been called synchronously // We haven't yet returned, so we must have been called synchronously
......
...@@ -321,23 +321,19 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren ...@@ -321,23 +321,19 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
/// This is initialized the first time [runApp] is called. /// This is initialized the first time [runApp] is called.
Element get renderViewElement => _renderViewElement; Element get renderViewElement => _renderViewElement;
Element _renderViewElement; Element _renderViewElement;
void _runApp(Widget app) {
/// Takes a widget and attaches it to the [renderViewElement], creating it if
/// necessary.
///
/// This is called by [runApp] to configure the widget tree.
///
/// See also [RenderObjectToWidgetAdapter.attachToRenderTree].
void attachRootWidget(Widget rootWidget) {
_renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>( _renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>(
container: renderView, container: renderView,
debugShortDescription: '[root]', debugShortDescription: '[root]',
child: app child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement); ).attachToRenderTree(buildOwner, renderViewElement);
assert(() {
if (debugPrintBeginFrameBanner)
debugPrint('━━━━━━━┫ Begin Warm-Up Frame ┣━━━━━━━');
return true;
});
beginFrame();
assert(() {
if (debugPrintEndFrameBanner)
debugPrint('━━━━━━━┫ End of Warm-Up Frame ┣━━━━━━━');
return true;
});
} }
@override @override
...@@ -352,18 +348,32 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren ...@@ -352,18 +348,32 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
/// Inflate the given widget and attach it to the screen. /// Inflate the given widget and attach it to the screen.
/// ///
/// Initializes the binding using [WidgetsFlutterBinding] if necessary. /// Initializes the binding using [WidgetsFlutterBinding] if necessary.
///
/// See also:
///
/// * [WidgetsBinding.attachRootWidget], which creates the root widget for the
/// widget hierarchy.
/// * [RenderObjectToWidgetAdapter.attachToRenderTree], which creates the root
/// element for the element hierarchy.
/// * [WidgetsBinding.handleBeginFrame], which pumps the widget pipeline to
/// ensure the widget, element, and render trees are all built.
void runApp(Widget app) { void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()._runApp(app); WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..handleBeginFrame(null);
} }
/// Print a string representation of the currently running app. /// Print a string representation of the currently running app.
void debugDumpApp() { void debugDumpApp() {
assert(WidgetsBinding.instance != null); assert(WidgetsBinding.instance != null);
assert(WidgetsBinding.instance.renderViewElement != null);
String mode = 'RELEASE MODE'; String mode = 'RELEASE MODE';
assert(() { mode = 'CHECKED MODE'; return true; }); assert(() { mode = 'CHECKED MODE'; return true; });
debugPrint('${WidgetsBinding.instance.runtimeType} - $mode'); debugPrint('${WidgetsBinding.instance.runtimeType} - $mode');
if (WidgetsBinding.instance.renderViewElement != null) {
debugPrint(WidgetsBinding.instance.renderViewElement.toStringDeep()); debugPrint(WidgetsBinding.instance.renderViewElement.toStringDeep());
} else {
debugPrint('<no tree currently mounted>');
}
} }
/// A bridge from a [RenderObject] to an [Element] tree. /// A bridge from a [RenderObject] to an [Element] tree.
...@@ -406,7 +416,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi ...@@ -406,7 +416,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
/// child of [container]. /// child of [container].
/// ///
/// If `element` is null, this function will create a new element. Otherwise, /// If `element` is null, this function will create a new element. Otherwise,
/// the given element will be updated with this widget. /// the given element will have an update scheduled to switch to this widget.
/// ///
/// Used by [runApp] to bootstrap applications. /// Used by [runApp] to bootstrap applications.
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) { RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
...@@ -420,9 +430,8 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi ...@@ -420,9 +430,8 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
element.mount(null, null); element.mount(null, null);
}); });
} else { } else {
owner.buildScope(element, () { element._newWidget = this;
element.update(this); element.markNeedsBuild();
});
} }
return element; return element;
} }
...@@ -476,6 +485,20 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObje ...@@ -476,6 +485,20 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObje
_rebuild(); _rebuild();
} }
// When we are assigned a new widget, we store it here
// until we are ready to update to it.
Widget _newWidget;
@override
void performRebuild() {
assert(_newWidget != null);
final Widget newWidget = _newWidget;
_newWidget = null;
super.performRebuild();
update(newWidget);
assert(_newWidget == null);
}
void _rebuild() { void _rebuild() {
try { try {
_child = updateChild(_child, widget.child, _rootChildSlot); _child = updateChild(_child, widget.child, _rootChildSlot);
......
...@@ -9,8 +9,39 @@ import 'framework.dart'; ...@@ -9,8 +9,39 @@ import 'framework.dart';
import 'table.dart'; import 'table.dart';
/// Log the dirty widgets that are built each frame. /// Log the dirty widgets that are built each frame.
///
/// Combined with [debugPrintBuildScope] or [debugPrintBeginFrameBanner], this
/// allows you to distinguish builds triggered by the initial mounting of a
/// widget tree (e.g. in a call to [runApp]) from the regular builds triggered
/// by the pipeline (see [WidgetsBinding.beginFrame].
///
/// Combined with [debugPrintScheduleBuildForStacks], this lets you watch a
/// widget's dirty/clean lifecycle.
bool debugPrintRebuildDirtyWidgets = false; bool debugPrintRebuildDirtyWidgets = false;
/// Log all calls to [BuildOwner.buildScope].
///
/// Combined with [debugPrintScheduleBuildForStacks], this allows you to track
/// when a [State.setState] call gets serviced.
///
/// Combined with [debugPrintRebuildDirtyWidgets] or
/// [debugPrintBeginFrameBanner], this allows you to distinguish builds
/// triggered by the initial mounting of a widget tree (e.g. in a call to
/// [runApp]) from the regular builds triggered by the pipeline (see
/// [WidgetsBinding.beginFrame].
bool debugPrintBuildScope = false;
/// Log the call stacks that mark widgets as needing to be rebuilt.
///
/// This is called whenever [BuildOwner.scheduleBuildFor] adds an element to the
/// dirty list. Typically this is as a result of [Element.markNeedsBuild] being
/// called, which itself is usually a result of [State.setState] being called.
///
/// To see when a widget is rebuilt, see [debugPrintRebuildDirtyWidgets].
///
/// To see when the dirty list is flushed, see [debugPrintBuildDirtyElements].
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
/// reactivated (retaken). /// reactivated (retaken).
/// ///
......
...@@ -1419,6 +1419,8 @@ class BuildOwner { ...@@ -1419,6 +1419,8 @@ class BuildOwner {
assert(element.owner == this); assert(element.owner == this);
assert(element._inDirtyList == _dirtyElements.contains(element)); assert(element._inDirtyList == _dirtyElements.contains(element));
assert(() { assert(() {
if (debugPrintScheduleBuildForStacks)
debugPrintStack(label: 'scheduleBuildFor() called for $element${_dirtyElements.contains(element) ? " (ALREADY IN LIST)" : ""}');
if (element._inDirtyList) { if (element._inDirtyList) {
throw new FlutterError( throw new FlutterError(
'scheduleBuildFor() called for a widget for which a build was already scheduled.\n' 'scheduleBuildFor() called for a widget for which a build was already scheduled.\n'
...@@ -1426,8 +1428,13 @@ class BuildOwner { ...@@ -1426,8 +1428,13 @@ class BuildOwner {
' $element\n' ' $element\n'
'The current dirty list consists of:\n' 'The current dirty list consists of:\n'
' $_dirtyElements\n' ' $_dirtyElements\n'
'This should not be possible and probably indicates a bug in the widgets framework. ' 'This usually indicates that a widget was rebuilt outside the build phase (thus '
'Please report it: https://github.com/flutter/flutter/issues/new' 'marking the element as clean even though it is still in the dirty list). '
'This should not be possible and is probably caused by a bug in the widgets framework. '
'Please report it: https://github.com/flutter/flutter/issues/new\n'
'To debug this issue, consider setting the debugPrintScheduleBuildForStacks and '
'debugPrintBuildDirtyElements flags to true and looking for a call to scheduleBuildFor '
'for a widget that is labeled "ALREADY IN LIST".'
); );
} }
if (!element.dirty) { if (!element.dirty) {
...@@ -1450,6 +1457,11 @@ class BuildOwner { ...@@ -1450,6 +1457,11 @@ class BuildOwner {
} }
_dirtyElements.add(element); _dirtyElements.add(element);
element._inDirtyList = true; element._inDirtyList = true;
assert(() {
if (debugPrintScheduleBuildForStacks)
debugPrint('...dirty list is now: $_dirtyElements');
return true;
});
} }
int _debugStateLockLevel = 0; int _debugStateLockLevel = 0;
...@@ -1507,6 +1519,8 @@ class BuildOwner { ...@@ -1507,6 +1519,8 @@ class BuildOwner {
assert(_debugStateLockLevel >= 0); assert(_debugStateLockLevel >= 0);
assert(!_debugBuilding); assert(!_debugBuilding);
assert(() { assert(() {
if (debugPrintBuildScope)
debugPrint('buildScope called with context $context; dirty list is: $_dirtyElements');
_debugStateLockLevel += 1; _debugStateLockLevel += 1;
_debugBuilding = true; _debugBuilding = true;
return true; return true;
...@@ -1543,6 +1557,8 @@ class BuildOwner { ...@@ -1543,6 +1557,8 @@ class BuildOwner {
assert(() { assert(() {
_debugBuilding = false; _debugBuilding = false;
_debugStateLockLevel -= 1; _debugStateLockLevel -= 1;
if (debugPrintBuildScope)
debugPrint('buildScope finished');
return true; return true;
}); });
} }
...@@ -2138,6 +2154,9 @@ abstract class BuildableElement extends Element { ...@@ -2138,6 +2154,9 @@ abstract class BuildableElement extends Element {
// should be adding the element back into the list when it's reactivated. // should be adding the element back into the list when it's reactivated.
bool _inDirtyList = false; bool _inDirtyList = false;
// Whether we've already built or not. Set in [rebuild].
bool _debugBuiltOnce = false;
// We let widget authors call setState from initState, didUpdateConfig, and // We let widget authors call setState from initState, didUpdateConfig, and
// build even when state is locked because its convenient and a no-op anyway. // build even when state is locked because its convenient and a no-op anyway.
// This flag ensures that this convenience is only allowed on the element // This flag ensures that this convenience is only allowed on the element
...@@ -2194,16 +2213,22 @@ abstract class BuildableElement extends Element { ...@@ -2194,16 +2213,22 @@ abstract class BuildableElement extends Element {
owner.scheduleBuildFor(this); owner.scheduleBuildFor(this);
} }
/// Called by the binding when scheduleBuild() has been called to mark this /// Called by the binding when [BuildOwner.scheduleBuildFor] has been called
/// element dirty, and, in components, by update() when the widget has /// to mark this element dirty, and, in components, by [mount] when the
/// changed. /// element is first built and by [update] when the widget has changed.
void rebuild() { void rebuild() {
assert(_debugLifecycleState != _ElementLifecycle.initial); assert(_debugLifecycleState != _ElementLifecycle.initial);
if (!_active || !_dirty) if (!_active || !_dirty)
return; return;
assert(() { assert(() {
if (debugPrintRebuildDirtyWidgets) if (debugPrintRebuildDirtyWidgets) {
if (!_debugBuiltOnce) {
debugPrint('Building $this');
_debugBuiltOnce = true;
} else {
debugPrint('Rebuilding $this'); debugPrint('Rebuilding $this');
}
}
return true; return true;
}); });
assert(_debugLifecycleState == _ElementLifecycle.active); assert(_debugLifecycleState == _ElementLifecycle.active);
...@@ -2304,12 +2329,12 @@ abstract class ComponentElement extends BuildableElement { ...@@ -2304,12 +2329,12 @@ abstract class ComponentElement extends BuildableElement {
rebuild(); rebuild();
} }
/// Calls the build() method of the [StatelessWidget] object (for /// Calls the `build` method of the [StatelessWidget] object (for
/// stateless widgets) or the [State] object (for stateful widgets) and /// stateless widgets) or the [State] object (for stateful widgets) and
/// then updates the widget tree. /// then updates the widget tree.
/// ///
/// Called automatically during mount() to generate the first build, and by /// Called automatically during [mount] to generate the first build, and by
/// rebuild() when the element needs updating. /// [rebuild] when the element needs updating.
@override @override
void performRebuild() { void performRebuild() {
assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true)); assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
......
...@@ -40,6 +40,7 @@ void main() { ...@@ -40,6 +40,7 @@ void main() {
box = tester.renderObject(find.byType(ExpansionPanelList)); box = tester.renderObject(find.byType(ExpansionPanelList));
expect(box.size.height, equals(oldHeight)); expect(box.size.height, equals(oldHeight));
// now expand the child panel
await tester.pumpWidget( await tester.pumpWidget(
new ScrollableViewport( new ScrollableViewport(
child: new ExpansionPanelList( child: new ExpansionPanelList(
...@@ -53,14 +54,16 @@ void main() { ...@@ -53,14 +54,16 @@ void main() {
return new Text(isExpanded ? 'B' : 'A'); return new Text(isExpanded ? 'B' : 'A');
}, },
body: new SizedBox(height: 100.0), body: new SizedBox(height: 100.0),
isExpanded: true isExpanded: true // this is the addition
) )
] ]
) )
) )
); );
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
expect(find.text('A'), findsNothing); expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget); expect(find.text('B'), findsOneWidget);
box = tester.renderObject(find.byType(ExpansionPanelList)); box = tester.renderObject(find.byType(ExpansionPanelList));
......
...@@ -597,7 +597,6 @@ void main() { ...@@ -597,7 +597,6 @@ void main() {
) )
); );
await tester.pump();
expect(box.size.height, equals(300)); expect(box.size.height, equals(300));
matches(getBorderRadius(tester, 0), RadiusType.Round, RadiusType.Round); matches(getBorderRadius(tester, 0), RadiusType.Round, RadiusType.Round);
......
...@@ -62,7 +62,7 @@ class TestAssetBundle extends CachingAssetBundle { ...@@ -62,7 +62,7 @@ class TestAssetBundle extends CachingAssetBundle {
pipe = new TestMojoDataPipeConsumer(4.0); pipe = new TestMojoDataPipeConsumer(4.0);
break; break;
} }
return (new Completer<mojo.MojoDataPipeConsumer>()..complete(pipe)).future; return new SynchronousFuture<mojo.MojoDataPipeConsumer>(pipe);
} }
@override @override
...@@ -79,9 +79,21 @@ class TestAssetBundle extends CachingAssetBundle { ...@@ -79,9 +79,21 @@ class TestAssetBundle extends CachingAssetBundle {
class TestAssetImage extends AssetImage { class TestAssetImage extends AssetImage {
TestAssetImage(String name) : super(name); TestAssetImage(String name) : super(name);
@override
Future<ImageInfo> loadAsync(AssetBundleImageKey key) {
ImageInfo result;
key.bundle.load(key.name).then((mojo.MojoDataPipeConsumer dataPipe) {
decodeImage(dataPipe).then((ui.Image image) {
result = new ImageInfo(image: image, scale: getScale(key));
});
});
assert(result != null);
return new SynchronousFuture<ImageInfo>(result);
}
@override @override
Future<ui.Image> decodeImage(TestMojoDataPipeConsumer pipe) { Future<ui.Image> decodeImage(TestMojoDataPipeConsumer pipe) {
return new Future<ui.Image>.value(new TestImage(pipe.scale)); return new SynchronousFuture<ui.Image>(new TestImage(pipe.scale));
} }
} }
......
...@@ -164,7 +164,8 @@ class WidgetTester extends WidgetController implements HitTestDispatcher { ...@@ -164,7 +164,8 @@ class WidgetTester extends WidgetController implements HitTestDispatcher {
EnginePhase phase = EnginePhase.sendSemanticsTree EnginePhase phase = EnginePhase.sendSemanticsTree
]) { ]) {
return TestAsyncUtils.guard(() { return TestAsyncUtils.guard(() {
runApp(widget); binding.attachRootWidget(widget);
binding.scheduleFrame();
return binding.pump(duration, phase); return binding.pump(duration, phase);
}); });
} }
......
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