Unverified Commit 186d1e9b authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Have the framework in charge of scheduling frames. (#13344)

...instead of the engine.
parent 425bd5a8
[^═]*(this line contains the test framework's output with the clock and so forth)?
══╡ EXCEPTION CAUGHT BY SCHEDULER LIBRARY ╞═════════════════════════════════════════════════════════
The following message was thrown:
An animation is still running even after the widget tree was disposed.
There was one transient callback left. The stack trace for when it was registered is as follows:
── callback 2 ──
<<skip until matching line>>
#[0-9]+ main.+ \(.+/flutter/dev/automated_tests/flutter_test/ticker_test\.dart:[0-9]+:[0-9]+\)
<<skip until matching line>>
════════════════════════════════════════════════════════════════════════════════════════════════════
.*..:.. \+0 -1: - Does flutter_test catch leaking tickers\? \[E\]
Test failed\. See exception logs above\.
The test description was: Does flutter_test catch leaking tickers\?
*
.*..:.. \+0 -1: Some tests failed\. *
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Does flutter_test catch leaking tickers?', (WidgetTester tester) async {
new Ticker((Duration duration) { })..start();
final ByteData message = const StringCodec().encodeMessage('AppLifecycleState.paused');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
});
}
......@@ -105,8 +105,8 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
);
registerSignalServiceExtension(
name: 'debugDumpSemanticsTreeInInverseHitTestOrder',
callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest); return debugPrintDone; }
name: 'debugDumpSemanticsTreeInInverseHitTestOrder',
callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest); return debugPrintDone; }
);
}
......@@ -139,14 +139,17 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
/// Called when the system metrics change.
///
/// See [Window.onMetricsChanged].
@protected
void handleMetricsChanged() {
assert(renderView != null);
renderView.configuration = createViewConfiguration();
scheduleForcedFrame();
}
/// Called when the platform text scale factor changes.
///
/// See [Window.onTextScaleFactorChanged].
@protected
void handleTextScaleFactorChanged() { }
/// Returns a [ViewConfiguration] configured for the [RenderView] based on the
......@@ -266,26 +269,6 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
/// Schedule a frame to run as soon as possible, rather than waiting for
/// the engine to request a frame.
///
/// This is used during application startup so that the first frame (which is
/// likely to be quite expensive) gets a few extra milliseconds to run.
void scheduleWarmUpFrame() {
// We use timers here to ensure that microtasks flush in between.
//
// We call resetEpoch after this frame so that, in the hot reload case, the
// very next frame pretends to have occurred immediately after this warm-up
// frame. The warm-up frame's timestamp will typically be far in the past
// (the time of the last real frame), so if we didn't reset the epoch we
// would see a sudden jump from the old time in the warm-up frame to the new
// time in the "real" frame. The biggest problem with this is that implicit
// animations end up being triggered at the old time and then skipping every
// frame and finishing in the new time.
Timer.run(() { handleBeginFrame(null); });
Timer.run(() { handleDrawFrame(); resetEpoch(); });
}
@override
Future<Null> performReassemble() async {
await super.performReassemble();
......
......@@ -101,9 +101,21 @@ class Ticker {
/// A ticker that is [muted] can be active (see [isActive]) yet not be
/// ticking. In that case, the ticker will not call its callback, and
/// [isTicking] will be false, but time will still be progressing.
// TODO(ianh): we should teach the scheduler binding about the lifecycle events
// and then this could return an accurate view of the actual scheduler.
bool get isTicking => _future != null && !muted;
///
/// This will return false if the [Scheduler.lifecycleState] is one that
/// indicates the application is not currently visible (e.g. if the device's
/// screen is turned off).
bool get isTicking {
if (_future == null)
return false;
if (muted)
return false;
if (SchedulerBinding.instance.framesEnabled)
return true;
if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.idle)
return true; // for example, we might be in a warm-up frame or forced frame
return false;
}
/// Whether time is elapsing for this [Ticker]. Becomes true when [start] is
/// called and false when [stop] is called.
......
......@@ -8,6 +8,8 @@ import 'package:flutter/foundation.dart';
import 'platform_channel.dart';
export 'dart:typed_data' show ByteData;
/// A message encoding/decoding mechanism.
///
/// Both operations throw an exception, if conversion fails. Such situations
......
......@@ -231,7 +231,7 @@ abstract class WidgetsBindingObserver {
}
/// The glue between the widgets layer and the Flutter engine.
abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererBinding {
abstract class WidgetsBinding extends BindingBase with SchedulerBinding, GestureBinding, RendererBinding {
// This class is intended to be used as a mixin, and should not be
// extended directly.
factory WidgetsBinding._() => null;
......@@ -243,7 +243,6 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
buildOwner.onBuildScheduled = _handleBuildScheduled;
ui.window.onLocaleChanged = handleLocaleChanged;
SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
SystemChannels.system.setMessageHandler(_handleSystemMessage);
}
......@@ -369,6 +368,8 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
/// Calls [dispatchLocaleChanged] to notify the binding observers.
///
/// See [Window.onLocaleChanged].
@protected
@mustCallSuper
void handleLocaleChanged() {
dispatchLocaleChanged(ui.window.locale);
}
......@@ -379,6 +380,8 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
///
/// This is called by [handleLocaleChanged] when the [Window.onLocaleChanged]
/// notification is received.
@protected
@mustCallSuper
void dispatchLocaleChanged(Locale locale) {
for (WidgetsBindingObserver observer in _observers)
observer.didChangeLocale(locale);
......@@ -398,6 +401,7 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
///
/// This method exposes the `popRoute` notification from
/// [SystemChannels.navigation].
@protected
Future<Null> handlePopRoute() async {
for (WidgetsBindingObserver observer in new List<WidgetsBindingObserver>.from(_observers)) {
if (await observer.didPopRoute())
......@@ -416,6 +420,8 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
///
/// This method exposes the `pushRoute` notification from
/// [SystemChannels.navigation].
@protected
@mustCallSuper
Future<Null> handlePushRoute(String route) async {
for (WidgetsBindingObserver observer in new List<WidgetsBindingObserver>.from(_observers)) {
if (await observer.didPushRoute(route))
......@@ -433,35 +439,13 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
return new Future<Null>.value();
}
/// Called when the application lifecycle state changes.
///
/// Notifies all the observers using
/// [WidgetsBindingObserver.didChangeAppLifecycleState].
///
/// This method exposes notifications from [SystemChannels.lifecycle].
@override
void handleAppLifecycleStateChanged(AppLifecycleState state) {
super.handleAppLifecycleStateChanged(state);
for (WidgetsBindingObserver observer in _observers)
observer.didChangeAppLifecycleState(state);
}
Future<String> _handleLifecycleMessage(String message) async {
switch (message) {
case 'AppLifecycleState.paused':
handleAppLifecycleStateChanged(AppLifecycleState.paused);
break;
case 'AppLifecycleState.resumed':
handleAppLifecycleStateChanged(AppLifecycleState.resumed);
break;
case 'AppLifecycleState.inactive':
handleAppLifecycleStateChanged(AppLifecycleState.inactive);
break;
case 'AppLifecycleState.suspending':
handleAppLifecycleStateChanged(AppLifecycleState.suspending);
break;
}
return null;
}
/// Called when the operating system notifies the application of a memory
/// pressure situation.
///
......
......@@ -40,9 +40,11 @@ class TestServiceExtensionsBinding extends BindingBase
}
int reassembled = 0;
bool pendingReassemble = false;
@override
Future<Null> performReassemble() {
reassembled += 1;
pendingReassemble = true;
return super.performReassemble();
}
......@@ -60,6 +62,17 @@ class TestServiceExtensionsBinding extends BindingBase
ui.window.onDrawFrame();
}
@override
void scheduleForcedFrame() {
expect(true, isFalse);
}
@override
void scheduleWarmUpFrame() {
expect(pendingReassemble, isTrue);
pendingReassemble = false;
}
Future<Null> flushMicrotasks() {
final Completer<Null> completer = new Completer<Null>();
Timer.run(completer.complete);
......
......@@ -4,11 +4,12 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:test/test.dart';
import 'scheduler_tester.dart';
class TestSchedulerBinding extends BindingBase with SchedulerBinding { }
class TestSchedulerBinding extends BindingBase with ServicesBinding, SchedulerBinding { }
void main() {
final SchedulerBinding scheduler = new TestSchedulerBinding();
......
......@@ -4,9 +4,10 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:test/test.dart';
class TestSchedulerBinding extends BindingBase with SchedulerBinding { }
class TestSchedulerBinding extends BindingBase with ServicesBinding, SchedulerBinding { }
class TestStrategy {
int allowedPriority = 10000;
......@@ -32,20 +33,20 @@ void main() {
strategy.allowedPriority = 100;
for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback();
expect(scheduler.handleEventLoopCallback(), isFalse);
expect(executedTasks.isEmpty, isTrue);
strategy.allowedPriority = 50;
for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback();
expect(executedTasks.length, equals(1));
expect(scheduler.handleEventLoopCallback(), i == 0 ? isTrue : isFalse);
expect(executedTasks, hasLength(1));
expect(executedTasks.single, equals(80));
executedTasks.clear();
strategy.allowedPriority = 20;
for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback();
expect(executedTasks.length, equals(2));
expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse);
expect(executedTasks, hasLength(2));
expect(executedTasks[0], equals(23));
expect(executedTasks[1], equals(23));
executedTasks.clear();
......@@ -55,32 +56,32 @@ void main() {
scheduleAddingTask(5);
scheduleAddingTask(97);
for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback();
expect(executedTasks.length, equals(2));
expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse);
expect(executedTasks, hasLength(2));
expect(executedTasks[0], equals(99));
expect(executedTasks[1], equals(97));
executedTasks.clear();
strategy.allowedPriority = 10;
for (int i = 0; i < 3; i += 1)
scheduler.handleEventLoopCallback();
expect(executedTasks.length, equals(2));
expect(scheduler.handleEventLoopCallback(), i < 2 ? isTrue : isFalse);
expect(executedTasks, hasLength(2));
expect(executedTasks[0], equals(19));
expect(executedTasks[1], equals(11));
executedTasks.clear();
strategy.allowedPriority = 1;
for (int i = 0; i < 4; i += 1)
scheduler.handleEventLoopCallback();
expect(executedTasks.length, equals(3));
expect(scheduler.handleEventLoopCallback(), i < 3 ? isTrue : isFalse);
expect(executedTasks, hasLength(3));
expect(executedTasks[0], equals(5));
expect(executedTasks[1], equals(3));
expect(executedTasks[2], equals(2));
executedTasks.clear();
strategy.allowedPriority = 0;
scheduler.handleEventLoopCallback();
expect(executedTasks.length, equals(1));
expect(scheduler.handleEventLoopCallback(), isFalse);
expect(executedTasks, hasLength(1));
expect(executedTasks[0], equals(0));
});
}
......@@ -3,13 +3,14 @@
// found in the LICENSE file.
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Ticker mute control test', (WidgetTester tester) async {
int tickCount = 0;
void handleTick(Duration duration) {
++tickCount;
tickCount += 1;
}
final Ticker ticker = new Ticker(handleTick);
......@@ -81,4 +82,25 @@ void main() {
expect(ticker, hasOneLineDescription);
expect(ticker.toString(debugIncludeStack: true), contains('testFunction'));
});
testWidgets('Ticker stops ticking when application is paused', (WidgetTester tester) async {
int tickCount = 0;
void handleTick(Duration duration) {
tickCount += 1;
}
final Ticker ticker = new Ticker(handleTick);
ticker.start();
expect(ticker.isTicking, isTrue);
expect(ticker.isActive, isTrue);
expect(tickCount, equals(0));
final ByteData message = const StringCodec().encodeMessage('AppLifecycleState.paused');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(ticker.isTicking, isFalse);
expect(ticker.isActive, isTrue);
ticker.stop();
});
}
......@@ -84,4 +84,56 @@ void main() {
WidgetsBinding.instance.removeObserver(observer);
});
testWidgets('Application lifecycle affects frame scheduling', (WidgetTester tester) async {
ByteData message;
expect(tester.binding.hasScheduledFrame, isFalse);
message = const StringCodec().encodeMessage('AppLifecycleState.paused');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(tester.binding.hasScheduledFrame, isFalse);
message = const StringCodec().encodeMessage('AppLifecycleState.resumed');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(tester.binding.hasScheduledFrame, isTrue);
await tester.pump();
expect(tester.binding.hasScheduledFrame, isFalse);
message = const StringCodec().encodeMessage('AppLifecycleState.inactive');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(tester.binding.hasScheduledFrame, isFalse);
message = const StringCodec().encodeMessage('AppLifecycleState.suspending');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(tester.binding.hasScheduledFrame, isFalse);
message = const StringCodec().encodeMessage('AppLifecycleState.inactive');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(tester.binding.hasScheduledFrame, isTrue);
await tester.pump();
expect(tester.binding.hasScheduledFrame, isFalse);
message = const StringCodec().encodeMessage('AppLifecycleState.paused');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
expect(tester.binding.hasScheduledFrame, isFalse);
tester.binding.scheduleFrame();
expect(tester.binding.hasScheduledFrame, isFalse);
tester.binding.scheduleForcedFrame();
expect(tester.binding.hasScheduledFrame, isTrue);
await tester.pump();
expect(tester.binding.hasScheduledFrame, isFalse);
int frameCount = 0;
tester.binding.addPostFrameCallback((Duration duration) { frameCount += 1; });
expect(tester.binding.hasScheduledFrame, isFalse);
await tester.pump(const Duration(milliseconds: 1));
expect(tester.binding.hasScheduledFrame, isFalse);
expect(frameCount, 0);
tester.binding.scheduleWarmUpFrame(); // this actually tests flutter_test's implementation
expect(tester.binding.hasScheduledFrame, isFalse);
expect(frameCount, 1);
});
}
......@@ -583,6 +583,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
handleBeginFrame(null);
_fakeAsync.flushMicrotasks();
handleDrawFrame();
_fakeAsync.flushMicrotasks();
}
@override
......@@ -829,6 +830,13 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
super.scheduleFrame();
}
@override
void scheduleForcedFrame() {
if (framePolicy == LiveTestWidgetsFlutterBindingFramePolicy.benchmark)
return; // In benchmark mode, don't actually schedule any engine frames.
super.scheduleForcedFrame();
}
bool _doDrawThisFrame;
@override
......
......@@ -38,6 +38,11 @@ void main() {
return _testFile('test_async_utils_unguarded', automatedTestsDirectory, flutterTestDirectory);
});
testUsingContext('report a nice error when a Ticker is left running', () async {
Cache.flutterRoot = '../..';
return _testFile('ticker', automatedTestsDirectory, flutterTestDirectory);
});
testUsingContext('report a nice error when a pubspec.yaml is missing a flutter_test dependency', () async {
final String missingDependencyTests = fs.path.join('..', '..', 'dev', 'missing_dependency_tests');
Cache.flutterRoot = '../..';
......
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