Reduce noise on the memory benchmarks (#19630) (#20163)
- Check memory usage in release builds, not profile.
- Use multiple runs and average the results.
diff --git a/dev/benchmarks/complex_layout/test_memory/scroll_perf.dart b/dev/benchmarks/complex_layout/test_memory/scroll_perf.dart
new file mode 100644
index 0000000..c42f8b0
--- /dev/null
+++ b/dev/benchmarks/complex_layout/test_memory/scroll_perf.dart
@@ -0,0 +1,72 @@
+// 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 'dart:async';
+
+import 'package:complex_layout/main.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+/// The speed, in pixels per second, that the drag gestures should end with.
+const double speed = 1500.0;
+
+/// The number of down drags and the number of up drags. The total number of
+/// gestures is twice this number.
+const int maxIterations = 4;
+
+/// The time that is allowed between gestures for the fling effect to settle.
+const Duration pauses = Duration(milliseconds: 500);
+
+Future<void> main() async {
+ final Completer<void> ready = new Completer<void>();
+ runApp(new GestureDetector(
+ onTap: () {
+ debugPrint('Received tap.');
+ ready.complete();
+ },
+ behavior: HitTestBehavior.opaque,
+ child: new IgnorePointer(
+ ignoring: true,
+ child: new ComplexLayoutApp(),
+ ),
+ ));
+ await SchedulerBinding.instance.endOfFrame;
+
+ /// Wait 50ms to allow the GPU thread to actually put up the frame. (The
+ /// endOfFrame future ends when we send the data to the engine, before the GPU
+ /// thread has had a chance to rasterize, etc.)
+ await new Future<Null>.delayed(const Duration(milliseconds: 50));
+ debugPrint('==== MEMORY BENCHMARK ==== READY ====');
+
+ await ready.future; // waits for tap sent by devicelab task
+ debugPrint('Continuing...');
+
+ // remove onTap handler, enable pointer events for app
+ runApp(new GestureDetector(
+ child: new IgnorePointer(
+ ignoring: false,
+ child: new ComplexLayoutApp(),
+ ),
+ ));
+ await SchedulerBinding.instance.endOfFrame;
+
+ final WidgetController controller = new LiveWidgetController(WidgetsBinding.instance);
+
+ // Scroll down
+ for (int iteration = 0; iteration < maxIterations; iteration += 1) {
+ debugPrint('Scroll down... $iteration/$maxIterations');
+ await controller.fling(find.byType(ListView), const Offset(0.0, -700.0), speed);
+ await new Future<Null>.delayed(pauses);
+ }
+
+ // Scroll up
+ for (int iteration = 0; iteration < maxIterations; iteration += 1) {
+ debugPrint('Scroll up... $iteration/$maxIterations');
+ await controller.fling(find.byType(ListView), const Offset(0.0, 300.0), speed);
+ await new Future<Null>.delayed(pauses);
+ }
+
+ debugPrint('==== MEMORY BENCHMARK ==== DONE ====');
+}
diff --git a/dev/devicelab/bin/tasks/complex_layout_scroll_perf__memory.dart b/dev/devicelab/bin/tasks/complex_layout_scroll_perf__memory.dart
index ff68529..1729629 100644
--- a/dev/devicelab/bin/tasks/complex_layout_scroll_perf__memory.dart
+++ b/dev/devicelab/bin/tasks/complex_layout_scroll_perf__memory.dart
@@ -4,11 +4,16 @@
import 'dart:async';
-import 'package:flutter_devicelab/tasks/perf_tests.dart';
import 'package:flutter_devicelab/framework/adb.dart';
import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/framework/utils.dart';
+import 'package:flutter_devicelab/tasks/perf_tests.dart';
Future<Null> main() async {
deviceOperatingSystem = DeviceOperatingSystem.android;
- await task(createComplexLayoutScrollMemoryTest());
+ await task(new MemoryTest(
+ '${flutterDirectory.path}/dev/benchmarks/complex_layout',
+ 'test_memory/scroll_perf.dart',
+ 'com.yourcompany.complexLayout',
+ ).run);
}
diff --git a/dev/devicelab/bin/tasks/flutter_gallery__back_button_memory.dart b/dev/devicelab/bin/tasks/flutter_gallery__back_button_memory.dart
index a788030..f665730 100644
--- a/dev/devicelab/bin/tasks/flutter_gallery__back_button_memory.dart
+++ b/dev/devicelab/bin/tasks/flutter_gallery__back_button_memory.dart
@@ -2,11 +2,57 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+/// Measure application memory usage after pausing and resuming the app
+/// with the Android back button.
+
import 'dart:async';
-import 'package:flutter_devicelab/tasks/perf_tests.dart';
+import 'package:flutter_devicelab/framework/adb.dart';
import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/framework/utils.dart';
+import 'package:flutter_devicelab/tasks/perf_tests.dart';
+
+const String packageName = 'io.flutter.demo.gallery';
+const String activityName = 'io.flutter.demo.gallery.MainActivity';
+
+class BackButtonMemoryTest extends MemoryTest {
+ BackButtonMemoryTest() : super('${flutterDirectory.path}/examples/flutter_gallery', 'test_memory/back_button.dart', packageName);
+
+ @override
+ AndroidDevice get device => super.device;
+
+ /// Perform a series of back button suspend and resume cycles.
+ @override
+ Future<void> useMemory() async {
+ await launchApp();
+ await recordStart();
+ for (int iteration = 0; iteration < 10; iteration += 1) {
+ print('back/forward iteration $iteration');
+
+ // Push back button, wait for it to be seen by the Flutter app.
+ prepareForNextMessage('AppLifecycleState.paused');
+ await device.shellExec('input', <String>['keyevent', 'KEYCODE_BACK']);
+ await receivedNextMessage;
+
+ // Give Android time to settle (e.g. run GCs) after closing the app.
+ await new Future<Null>.delayed(const Duration(milliseconds: 100));
+
+ // Relaunch the app, wait for it to launch.
+ prepareForNextMessage('READY');
+ final String output = await device.shellEval('am', <String>['start', '-n', '$packageName/$activityName']);
+ print('adb shell am start: $output');
+ if (output.contains('Error'))
+ fail('unable to launch activity');
+ await receivedNextMessage;
+
+ // Wait for the Flutter app to settle (e.g. run GCs).
+ await new Future<Null>.delayed(const Duration(milliseconds: 100));
+ }
+ await recordEnd();
+ }
+}
Future<Null> main() async {
- await task(createGalleryBackButtonMemoryTest());
+ deviceOperatingSystem = DeviceOperatingSystem.android;
+ await task(new BackButtonMemoryTest().run);
}
diff --git a/dev/devicelab/bin/tasks/flutter_gallery__memory_nav.dart b/dev/devicelab/bin/tasks/flutter_gallery__memory_nav.dart
index 90217af..8c2b301 100644
--- a/dev/devicelab/bin/tasks/flutter_gallery__memory_nav.dart
+++ b/dev/devicelab/bin/tasks/flutter_gallery__memory_nav.dart
@@ -4,9 +4,14 @@
import 'dart:async';
-import 'package:flutter_devicelab/tasks/perf_tests.dart';
import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/framework/utils.dart';
+import 'package:flutter_devicelab/tasks/perf_tests.dart';
Future<Null> main() async {
- await task(createGalleryNavigationMemoryTest());
+ await task(new MemoryTest(
+ '${flutterDirectory.path}/examples/flutter_gallery',
+ 'test_memory/memory_nav.dart',
+ 'io.flutter.demo.gallery',
+ ).run);
}
diff --git a/dev/devicelab/bin/tasks/hello_world__memory.dart b/dev/devicelab/bin/tasks/hello_world__memory.dart
index 16898ac..f3a99ae 100644
--- a/dev/devicelab/bin/tasks/hello_world__memory.dart
+++ b/dev/devicelab/bin/tasks/hello_world__memory.dart
@@ -4,9 +4,36 @@
import 'dart:async';
-import 'package:flutter_devicelab/tasks/perf_tests.dart';
import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/framework/utils.dart';
+import 'package:flutter_devicelab/tasks/perf_tests.dart';
+
+class HelloWorldMemoryTest extends MemoryTest {
+ HelloWorldMemoryTest() : super(
+ '${flutterDirectory.path}/examples/hello_world',
+ 'lib/main.dart',
+ 'io.flutter.examples.hello_world',
+ );
+
+ /// Launch an app with no instrumentation and measure its memory usage after
+ /// 1.5s and 3.0s.
+ @override
+ Future<void> useMemory() async {
+ print('launching $project$test on device...');
+ await flutter('run', options: <String>[
+ '--verbose',
+ '--release',
+ '--no-resident',
+ '-d', device.deviceId,
+ test,
+ ]);
+ await new Future<Null>.delayed(const Duration(milliseconds: 1500));
+ await recordStart();
+ await new Future<Null>.delayed(const Duration(milliseconds: 3000));
+ await recordEnd();
+ }
+}
Future<Null> main() async {
- await task(createHelloWorldMemoryTest());
+ await task(new HelloWorldMemoryTest().run);
}
diff --git a/dev/devicelab/lib/framework/adb.dart b/dev/devicelab/lib/framework/adb.dart
index de65020..6849e03 100644
--- a/dev/devicelab/lib/framework/adb.dart
+++ b/dev/devicelab/lib/framework/adb.dart
@@ -83,9 +83,18 @@
/// Assumes the device doesn't have a secure unlock pattern.
Future<Null> unlock();
+ /// Emulate a tap on the touch screen.
+ Future<Null> tap(int x, int y);
+
/// Read memory statistics for a process.
Future<Map<String, dynamic>> getMemoryStats(String packageName);
+ /// Stream the system log from the device.
+ ///
+ /// Flutter applications' `print` statements end up in this log
+ /// with some prefix.
+ Stream<String> get logcat;
+
/// Stop a process.
Future<Null> stop(String packageName);
}
@@ -237,6 +246,11 @@
await shellExec('input', const <String>['keyevent', '82']);
}
+ @override
+ Future<Null> tap(int x, int y) async {
+ await shellExec('input', <String>['tap', '$x', '$y']);
+ }
+
/// Retrieves device's wakefulness state.
///
/// See: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/PowerManagerInternal.java
@@ -272,6 +286,63 @@
}
@override
+ Stream<String> get logcat {
+ final Completer<void> stdoutDone = new Completer<void>();
+ final Completer<void> stderrDone = new Completer<void>();
+ final Completer<void> processDone = new Completer<void>();
+ final Completer<void> abort = new Completer<void>();
+ bool aborted = false;
+ StreamController<String> stream;
+ stream = new StreamController<String>(
+ onListen: () async {
+ await adb(<String>['logcat', '--clear']);
+ final Process process = await startProcess(adbPath, <String>['-s', deviceId, 'logcat']);
+ process.stdout
+ .transform(utf8.decoder)
+ .transform(const LineSplitter())
+ .listen((String line) {
+ print('adb logcat: $line');
+ stream.sink.add(line);
+ }, onDone: () { stdoutDone.complete(); });
+ process.stderr
+ .transform(utf8.decoder)
+ .transform(const LineSplitter())
+ .listen((String line) {
+ print('adb logcat stderr: $line');
+ }, onDone: () { stderrDone.complete(); });
+ process.exitCode.then((int exitCode) {
+ print('adb logcat process terminated with exit code $exitCode');
+ if (!aborted) {
+ stream.addError(BuildFailedError('adb logcat failed with exit code $exitCode.'));
+ processDone.complete();
+ }
+ });
+ await Future.any<dynamic>(<Future<dynamic>>[
+ Future.wait<void>(<Future<void>>[
+ stdoutDone.future,
+ stderrDone.future,
+ processDone.future,
+ ]),
+ abort.future,
+ ]);
+ aborted = true;
+ print('terminating adb logcat');
+ process.kill();
+ print('closing logcat stream');
+ await stream.close();
+ },
+ onCancel: () {
+ if (!aborted) {
+ print('adb logcat aborted');
+ aborted = true;
+ abort.complete();
+ }
+ },
+ );
+ return stream.stream;
+ }
+
+ @override
Future<Null> stop(String packageName) async {
return shellExec('am', <String>['force-stop', packageName]);
}
@@ -372,11 +443,21 @@
Future<Null> unlock() async {}
@override
+ Future<Null> tap(int x, int y) async {
+ throw 'Not implemented';
+ }
+
+ @override
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
throw 'Not implemented';
}
@override
+ Stream<String> get logcat {
+ throw 'Not implemented';
+ }
+
+ @override
Future<Null> stop(String packageName) async {}
}
diff --git a/dev/devicelab/lib/framework/framework.dart b/dev/devicelab/lib/framework/framework.dart
index aa6c3a8..3ad25f5 100644
--- a/dev/devicelab/lib/framework/framework.dart
+++ b/dev/devicelab/lib/framework/framework.dart
@@ -81,12 +81,15 @@
Future<TaskResult> run(Duration taskTimeout) async {
try {
_taskStarted = true;
+ print('Running task.');
final TaskResult result = await _performTask().timeout(taskTimeout);
_completer.complete(result);
return result;
} on TimeoutException catch (_) {
+ print('Task timed out after $taskTimeout.');
return new TaskResult.failure('Task timed out after $taskTimeout');
} finally {
+ print('Cleaning up after task...');
await forceQuitRunningProcesses();
_closeKeepAlivePort();
}
diff --git a/dev/devicelab/lib/framework/utils.dart b/dev/devicelab/lib/framework/utils.dart
index fc3a777..3263000 100644
--- a/dev/devicelab/lib/framework/utils.dart
+++ b/dev/devicelab/lib/framework/utils.dart
@@ -244,7 +244,7 @@
// Whatever's left, kill it.
for (ProcessInfo p in _runningProcesses) {
- print('Force quitting process:\n$p');
+ print('Force-quitting process:\n$p');
if (!p.process.kill()) {
print('Failed to force quit process');
}
@@ -528,8 +528,6 @@
// e.g. "An Observatory debugger and profiler on ... is available at: http://127.0.0.1:8100/"
final RegExp pattern = new RegExp('$prefix(\\S+:(\\d+)/\\S*)\$', multiLine: multiLine);
final Match match = pattern.firstMatch(line);
- print(pattern);
- print(match);
return match == null ? null : int.parse(match.group(2));
}
diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart
index 15fca3a..0d31ef6 100644
--- a/dev/devicelab/lib/tasks/perf_tests.dart
+++ b/dev/devicelab/lib/tasks/perf_tests.dart
@@ -7,7 +7,7 @@
import 'dart:io';
import 'package:meta/meta.dart';
-import 'package:path/path.dart' as p;
+import 'package:path/path.dart' as path;
import '../framework/adb.dart';
import '../framework/framework.dart';
@@ -30,14 +30,6 @@
).run;
}
-TaskFunction createComplexLayoutScrollMemoryTest() {
- return new MemoryTest(
- '${flutterDirectory.path}/dev/benchmarks/complex_layout',
- 'com.yourcompany.complexLayout',
- testTarget: 'test_driver/scroll_perf.dart',
- ).run;
-}
-
TaskFunction createFlutterGalleryStartupTest() {
return new StartupTest(
'${flutterDirectory.path}/examples/flutter_gallery',
@@ -62,29 +54,6 @@
return new CompileTest('${flutterDirectory.path}/dev/benchmarks/complex_layout').run;
}
-TaskFunction createHelloWorldMemoryTest() {
- return new MemoryTest(
- '${flutterDirectory.path}/examples/hello_world',
- 'io.flutter.examples.hello_world',
- ).run;
-}
-
-TaskFunction createGalleryNavigationMemoryTest() {
- return new MemoryTest(
- '${flutterDirectory.path}/examples/flutter_gallery',
- 'io.flutter.demo.gallery',
- testTarget: 'test_driver/memory_nav.dart',
- ).run;
-}
-
-TaskFunction createGalleryBackButtonMemoryTest() {
- return new AndroidBackButtonMemoryTest(
- '${flutterDirectory.path}/examples/flutter_gallery',
- 'io.flutter.demo.gallery',
- 'io.flutter.demo.gallery.MainActivity',
- ).run;
-}
-
TaskFunction createFlutterViewStartupTest() {
return new StartupTest(
'${flutterDirectory.path}/examples/flutter_view',
@@ -366,15 +335,15 @@
static Future<Map<String, dynamic>> getSizesFromIosApp(String appPath) async {
// Thin the binary to only contain one architecture.
- final String xcodeBackend = p.join(flutterDirectory.path, 'packages', 'flutter_tools', 'bin', 'xcode_backend.sh');
+ final String xcodeBackend = path.join(flutterDirectory.path, 'packages', 'flutter_tools', 'bin', 'xcode_backend.sh');
await exec(xcodeBackend, <String>['thin'], environment: <String, String>{
'ARCHS': 'arm64',
- 'WRAPPER_NAME': p.basename(appPath),
- 'TARGET_BUILD_DIR': p.dirname(appPath),
+ 'WRAPPER_NAME': path.basename(appPath),
+ 'TARGET_BUILD_DIR': path.dirname(appPath),
});
- final File appFramework = new File(p.join(appPath, 'Frameworks', 'App.framework', 'App'));
- final File flutterFramework = new File(p.join(appPath, 'Frameworks', 'Flutter.framework', 'Flutter'));
+ final File appFramework = new File(path.join(appPath, 'Frameworks', 'App.framework', 'App'));
+ final File flutterFramework = new File(path.join(appPath, 'Frameworks', 'Flutter.framework', 'Flutter'));
return <String, dynamic>{
'app_framework_uncompressed_bytes': await appFramework.length(),
@@ -420,123 +389,167 @@
/// Measure application memory usage.
class MemoryTest {
- const MemoryTest(this.testDirectory, this.packageName, { this.testTarget });
+ MemoryTest(this.project, this.test, this.package);
- final String testDirectory;
- final String packageName;
+ final String project;
+ final String test;
+ final String package;
- /// Path to a flutter driver script that will run after starting the app.
- ///
- /// If not specified, then the test will start the app, gather statistics, and then exit.
- final String testTarget;
+ /// Completes when the log line specified in the last call to
+ /// [prepareForNextMessage] is seen by `adb logcat`.
+ Future<void> get receivedNextMessage => _receivedNextMessage?.future;
+ Completer<void> _receivedNextMessage;
+ String _nextMessage;
+
+ /// Prepares the [receivedNextMessage] future such that it will complete
+ /// when `adb logcat` sees a log line with the given `message`.
+ void prepareForNextMessage(String message) {
+ _nextMessage = message;
+ _receivedNextMessage = new Completer<void>();
+ }
+
+ int get iterationCount => 15;
+
+ Device get device => _device;
+ Device _device;
Future<TaskResult> run() {
- return inDirectory(testDirectory, () async {
- final Device device = await devices.workingDevice;
+ return inDirectory(project, () async {
+ // This test currently only works on Android, because device.logcat,
+ // device.getMemoryStats, etc, aren't implemented for iOS.
+
+ _device = await devices.workingDevice;
await device.unlock();
- final String deviceId = device.deviceId;
await flutter('packages', options: <String>['get']);
if (deviceOperatingSystem == DeviceOperatingSystem.ios)
- await prepareProvisioningCertificates(testDirectory);
+ await prepareProvisioningCertificates(project);
- final List<String> runOptions = <String>[
- '-v',
- '--profile',
- '--trace-startup', // wait for the first frame to render
- '-d',
- deviceId,
- '--observatory-port',
- '0',
- ];
- if (testTarget != null)
- runOptions.addAll(<String>['-t', testTarget]);
- final String output = await evalFlutter('run', options: runOptions);
- final int observatoryPort = parseServicePort(output, prefix: 'Successfully connected to service protocol: ', multiLine: true);
- if (observatoryPort == null)
- throw new Exception('Could not find observatory port in "flutter run" output.');
+ final StreamSubscription<String> adb = device.logcat.listen(
+ (String data) {
+ if (data.contains('==== MEMORY BENCHMARK ==== $_nextMessage ===='))
+ _receivedNextMessage.complete();
+ },
+ );
- final Map<String, dynamic> startData = await device.getMemoryStats(packageName);
-
- final Map<String, dynamic> data = <String, dynamic>{
- 'start_total_kb': startData['total_kb'],
- };
-
- if (testTarget != null) {
- await flutter('drive', options: <String>[
- '-v',
- '-t',
- testTarget,
- '-d',
- deviceId,
- '--use-existing-app=http://localhost:$observatoryPort',
- ]);
-
- final Map<String, dynamic> endData = await device.getMemoryStats(packageName);
- data['end_total_kb'] = endData['total_kb'];
- data['diff_total_kb'] = endData['total_kb'] - startData['total_kb'];
+ for (int iteration = 0; iteration < iterationCount; iteration += 1) {
+ print('running memory test iteration $iteration...');
+ _startMemoryUsage = null;
+ await useMemory();
+ assert(_startMemoryUsage != null);
+ assert(_startMemory.length == iteration + 1);
+ assert(_endMemory.length == iteration + 1);
+ assert(_diffMemory.length == iteration + 1);
+ print('terminating...');
+ await device.stop(package);
+ await new Future<void>.delayed(const Duration(milliseconds: 10));
}
- await device.stop(packageName);
+ await adb.cancel();
- return new TaskResult.success(data, benchmarkScoreKeys: data.keys.toList());
+ final ListStatistics startMemoryStatistics = new ListStatistics(_startMemory);
+ final ListStatistics endMemoryStatistics = new ListStatistics(_endMemory);
+ final ListStatistics diffMemoryStatistics = new ListStatistics(_diffMemory);
+
+ final Map<String, dynamic> memoryUsage = <String, dynamic>{};
+ memoryUsage.addAll(startMemoryStatistics.asMap('start'));
+ memoryUsage.addAll(endMemoryStatistics.asMap('end'));
+ memoryUsage.addAll(diffMemoryStatistics.asMap('diff'));
+
+ _device = null;
+ _startMemory.clear();
+ _endMemory.clear();
+ _diffMemory.clear();
+
+ return new TaskResult.success(memoryUsage, benchmarkScoreKeys: memoryUsage.keys.toList());
});
}
+
+ /// Starts the app specified by [test] on the [device].
+ ///
+ /// The [run] method will terminate it by its package name ([package]).
+ Future<void> launchApp() async {
+ prepareForNextMessage('READY');
+ print('launching $project$test on device...');
+ await flutter('run', options: <String>[
+ '--verbose',
+ '--release',
+ '--no-resident',
+ '-d', device.deviceId,
+ test,
+ ]);
+ print('awaiting "ready" message...');
+ await receivedNextMessage;
+ }
+
+ /// To change the behaviour of the test, override this.
+ ///
+ /// Make sure to call recordStart() and recordEnd() once each in that order.
+ ///
+ /// By default it just launches the app, records memory usage, taps the device,
+ /// awaits a DONE notification, and records memory usage again.
+ Future<void> useMemory() async {
+ await launchApp();
+ await recordStart();
+
+ prepareForNextMessage('DONE');
+ print('tapping device...');
+ await device.tap(100, 100);
+ print('awaiting "done" message...');
+ await receivedNextMessage;
+
+ await recordEnd();
+ }
+
+ final List<int> _startMemory = <int>[];
+ final List<int> _endMemory = <int>[];
+ final List<int> _diffMemory = <int>[];
+
+ Map<String, dynamic> _startMemoryUsage;
+
+ @protected
+ Future<void> recordStart() async {
+ assert(_startMemoryUsage == null);
+ print('snapshotting memory usage...');
+ _startMemoryUsage = await device.getMemoryStats(package);
+ }
+
+ @protected
+ Future<void> recordEnd() async {
+ assert(_startMemoryUsage != null);
+ print('snapshotting memory usage...');
+ final Map<String, dynamic> endMemoryUsage = await device.getMemoryStats(package);
+ _startMemory.add(_startMemoryUsage['total_kb']);
+ _endMemory.add(endMemoryUsage['total_kb']);
+ _diffMemory.add(endMemoryUsage['total_kb'] - _startMemoryUsage['total_kb']);
+ }
}
-/// Measure application memory usage after pausing and resuming the app
-/// with the Android back button.
-class AndroidBackButtonMemoryTest {
- const AndroidBackButtonMemoryTest(this.testDirectory, this.packageName, this.activityName);
+/// Holds simple statistics of an odd-lengthed list of integers.
+class ListStatistics {
+ factory ListStatistics(Iterable<int> data) {
+ assert(data.isNotEmpty);
+ assert(data.length % 2 == 1);
+ final List<int> sortedData = data.toList()..sort();
+ return new ListStatistics._(
+ sortedData.first,
+ sortedData.last,
+ sortedData[(sortedData.length - 1) ~/ 2],
+ );
+ }
- final String testDirectory;
- final String packageName;
- final String activityName;
+ const ListStatistics._(this.min, this.max, this.median);
- Future<TaskResult> run() {
- return inDirectory(testDirectory, () async {
- if (deviceOperatingSystem != DeviceOperatingSystem.android) {
- throw 'This test is only supported on Android';
- }
+ final int min;
+ final int max;
+ final int median;
- final AndroidDevice device = await devices.workingDevice;
- await device.unlock();
- final String deviceId = device.deviceId;
- await flutter('packages', options: <String>['get']);
-
- await flutter('run', options: <String>[
- '-v',
- '--profile',
- '--trace-startup', // wait for the first frame to render
- '-d',
- deviceId,
- ]);
-
- final Map<String, dynamic> startData = await device.getMemoryStats(packageName);
-
- final Map<String, dynamic> data = <String, dynamic>{
- 'start_total_kb': startData['total_kb'],
- };
-
- // Perform a series of back button suspend and resume cycles.
- for (int i = 0; i < 10; i++) {
- await device.shellExec('input', <String>['keyevent', 'KEYCODE_BACK']);
- await new Future<Null>.delayed(const Duration(milliseconds: 1000));
- final String output = await device.shellEval('am', <String>['start', '-n', '$packageName/$activityName']);
- print(output);
- if (output.contains('Error'))
- return new TaskResult.failure('unable to launch activity');
- await new Future<Null>.delayed(const Duration(milliseconds: 1000));
- }
-
- final Map<String, dynamic> endData = await device.getMemoryStats(packageName);
- data['end_total_kb'] = endData['total_kb'];
- data['diff_total_kb'] = endData['total_kb'] - startData['total_kb'];
-
- await device.stop(packageName);
-
- return new TaskResult.success(data, benchmarkScoreKeys: data.keys.toList());
- });
+ Map<String, int> asMap(String prefix) {
+ return <String, int>{
+ '$prefix-min': min,
+ '$prefix-max': max,
+ '$prefix-median': median,
+ };
}
}
diff --git a/dev/devicelab/test/adb_test.dart b/dev/devicelab/test/adb_test.dart
index 6cc8dc8..82d6842 100644
--- a/dev/devicelab/test/adb_test.dart
+++ b/dev/devicelab/test/adb_test.dart
@@ -93,6 +93,15 @@
]);
});
});
+
+ group('adb', () {
+ test('tap', () async {
+ await device.tap(100, 200);
+ expectLog(<CommandArgs>[
+ cmd(command: 'input', arguments: <String>['tap', '100', '200']),
+ ]);
+ });
+ });
});
}
diff --git a/examples/flutter_gallery/lib/gallery/home.dart b/examples/flutter_gallery/lib/gallery/home.dart
index 0e7ee45..7ec60da 100644
--- a/examples/flutter_gallery/lib/gallery/home.dart
+++ b/examples/flutter_gallery/lib/gallery/home.dart
@@ -363,7 +363,7 @@
? const Text('Flutter gallery')
: new Text(_category.name),
),
- frontHeading: widget.testMode ? null: new Container(height: 24.0),
+ frontHeading: widget.testMode ? null : new Container(height: 24.0),
frontLayer: new AnimatedSwitcher(
duration: _kFrontLayerSwitchDuration,
switchOutCurve: switchOutCurve,
diff --git a/examples/flutter_gallery/test/live_smoketest.dart b/examples/flutter_gallery/test/live_smoketest.dart
index 13afb6f..2f080aa 100644
--- a/examples/flutter_gallery/test/live_smoketest.dart
+++ b/examples/flutter_gallery/test/live_smoketest.dart
@@ -44,7 +44,7 @@
fail('Unrecognized demo names in _kSkippedDemoTitles: $_kSkippedDemoTitles');
runApp(const GalleryApp(testMode: true));
- final _LiveWidgetController controller = new _LiveWidgetController();
+ final _LiveWidgetController controller = new _LiveWidgetController(WidgetsBinding.instance);
for (GalleryDemoCategory category in kAllGalleryDemoCategories) {
await controller.tap(find.text(category.name));
for (GalleryDemo demo in kGalleryCategoryToDemos[category]) {
@@ -73,9 +73,8 @@
}
}
-class _LiveWidgetController {
-
- final WidgetController _controller = new WidgetController(WidgetsBinding.instance);
+class _LiveWidgetController extends LiveWidgetController {
+ _LiveWidgetController(WidgetsBinding binding) : super(binding);
/// With [frameSync] enabled, Flutter Driver will wait to perform an action
/// until there are no pending frames in the app under test.
@@ -107,8 +106,9 @@
return finder;
}
- Future<Null> tap(Finder finder) async {
- await _controller.tap(await _waitForElement(finder));
+ @override
+ Future<Null> tap(Finder finder, { int pointer }) async {
+ await tap(await _waitForElement(finder), pointer: pointer);
}
Future<Null> scrollIntoView(Finder finder, {double alignment}) async {
diff --git a/examples/flutter_gallery/test_driver/memory_nav.dart b/examples/flutter_gallery/test_driver/memory_nav.dart
deleted file mode 100644
index 42b8e37..0000000
--- a/examples/flutter_gallery/test_driver/memory_nav.dart
+++ /dev/null
@@ -1,15 +0,0 @@
-// 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_driver/driver_extension.dart';
-import 'package:flutter_gallery/gallery/app.dart' show GalleryApp;
-import 'package:flutter/material.dart';
-
-void main() {
- enableFlutterDriverExtension();
- // As in lib/main.dart: overriding https://github.com/flutter/flutter/issues/13736
- // for better visual effect at the cost of performance.
- MaterialPageRoute.debugEnableFadingRoutes = true; // ignore: deprecated_member_use
- runApp(const GalleryApp(testMode: true));
-}
diff --git a/examples/flutter_gallery/test_driver/memory_nav_test.dart b/examples/flutter_gallery/test_driver/memory_nav_test.dart
deleted file mode 100644
index 8771a2f..0000000
--- a/examples/flutter_gallery/test_driver/memory_nav_test.dart
+++ /dev/null
@@ -1,32 +0,0 @@
-import 'package:flutter_driver/flutter_driver.dart';
-import 'package:test/test.dart';
-
-void main() {
- group('flutter gallery transitions', () {
- FlutterDriver driver;
- setUpAll(() async {
- driver = await FlutterDriver.connect();
- });
-
- tearDownAll(() async {
- if (driver != null)
- await driver.close();
- });
-
- test('navigation', () async {
- await driver.tap(find.text('Material'));
-
- final SerializableFinder demoList = find.byValueKey('GalleryDemoList');
- final SerializableFinder demoItem = find.text('Text fields');
- await driver.scrollUntilVisible(demoList, demoItem,
- dyScroll: -300.0,
- alignment: 0.5,
- timeout: const Duration(minutes: 1),
- );
- for (int i = 0; i < 15; i++) {
- await driver.tap(demoItem);
- await driver.tap(find.byTooltip('Back'));
- }
- });
- });
-}
diff --git a/examples/flutter_gallery/test_memory/back_button.dart b/examples/flutter_gallery/test_memory/back_button.dart
new file mode 100644
index 0000000..0eeb307
--- /dev/null
+++ b/examples/flutter_gallery/test_memory/back_button.dart
@@ -0,0 +1,37 @@
+// 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.
+
+// See //dev/devicelab/bin/tasks/flutter_gallery__memory_nav.dart
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter_gallery/gallery/app.dart' show GalleryApp;
+import 'package:flutter_test/flutter_test.dart';
+
+Future<void> endOfAnimation() async {
+ do {
+ await SchedulerBinding.instance.endOfFrame;
+ } while (SchedulerBinding.instance.hasScheduledFrame);
+}
+
+int iteration = 0;
+
+class LifecycleObserver extends WidgetsBindingObserver {
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ debugPrint('==== MEMORY BENCHMARK ==== $state ====');
+ debugPrint('This was lifecycle event number $iteration in this instance');
+ }
+}
+
+Future<void> main() async {
+ MaterialPageRoute.debugEnableFadingRoutes = true; // ignore: deprecated_member_use
+ runApp(const GalleryApp());
+ await endOfAnimation();
+ await new Future<Null>.delayed(const Duration(milliseconds: 50));
+ debugPrint('==== MEMORY BENCHMARK ==== READY ====');
+ WidgetsBinding.instance.addObserver(new LifecycleObserver());
+}
diff --git a/examples/flutter_gallery/test_memory/memory_nav.dart b/examples/flutter_gallery/test_memory/memory_nav.dart
new file mode 100644
index 0000000..cce1dc9
--- /dev/null
+++ b/examples/flutter_gallery/test_memory/memory_nav.dart
@@ -0,0 +1,72 @@
+// 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.
+
+// See //dev/devicelab/bin/tasks/flutter_gallery__memory_nav.dart
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter_gallery/gallery/app.dart' show GalleryApp;
+import 'package:flutter_test/flutter_test.dart';
+
+Future<void> endOfAnimation() async {
+ do {
+ await SchedulerBinding.instance.endOfFrame;
+ } while (SchedulerBinding.instance.hasScheduledFrame);
+}
+
+Future<void> main() async {
+ MaterialPageRoute.debugEnableFadingRoutes = true; // ignore: deprecated_member_use
+ final Completer<void> ready = new Completer<void>();
+ runApp(new GestureDetector(
+ onTap: () {
+ debugPrint('Received tap.');
+ ready.complete();
+ },
+ behavior: HitTestBehavior.opaque,
+ child: const IgnorePointer(
+ ignoring: true,
+ child: GalleryApp(testMode: true),
+ ),
+ ));
+ await SchedulerBinding.instance.endOfFrame;
+ await new Future<Null>.delayed(const Duration(milliseconds: 50));
+ debugPrint('==== MEMORY BENCHMARK ==== READY ====');
+
+ await ready.future;
+ debugPrint('Continuing...');
+
+ // remove onTap handler, enable pointer events for app
+ runApp(new GestureDetector(
+ child: const IgnorePointer(
+ ignoring: false,
+ child: GalleryApp(testMode: true),
+ ),
+ ));
+ await SchedulerBinding.instance.endOfFrame;
+
+ final WidgetController controller = new LiveWidgetController(WidgetsBinding.instance);
+
+ debugPrint('Navigating...');
+ await controller.tap(find.text('Material'));
+ await new Future<Null>.delayed(const Duration(milliseconds: 150));
+ final Finder demoList = find.byKey(const Key('GalleryDemoList'));
+ final Finder demoItem = find.text('Text fields');
+ do {
+ await controller.drag(demoList, const Offset(0.0, -300.0));
+ await new Future<Null>.delayed(const Duration(milliseconds: 20));
+ } while (!demoItem.precache());
+
+ for (int iteration = 0; iteration < 15; iteration += 1) {
+ debugPrint('Tapping... (iteration $iteration)');
+ await controller.tap(demoItem);
+ await endOfAnimation();
+ debugPrint('Backing out...');
+ await controller.tap(find.byTooltip('Back').last);
+ await endOfAnimation();
+ }
+
+ debugPrint('==== MEMORY BENCHMARK ==== DONE ====');
+}
diff --git a/packages/flutter_driver/lib/src/driver/driver.dart b/packages/flutter_driver/lib/src/driver/driver.dart
index a2dc0ad..3626016 100644
--- a/packages/flutter_driver/lib/src/driver/driver.dart
+++ b/packages/flutter_driver/lib/src/driver/driver.dart
@@ -447,19 +447,19 @@
/// ensure the item's final position matches [alignment].
///
/// The [scrollable] must locate the scrolling widget that contains [item].
- /// Typically `find.byType('ListView') or `find.byType('CustomScrollView')`.
+ /// Typically `find.byType('ListView')` or `find.byType('CustomScrollView')`.
///
- /// Atleast one of [dxScroll] and [dyScroll] must be non-zero.
+ /// At least one of [dxScroll] and [dyScroll] must be non-zero.
///
/// If [item] is below the currently visible items, then specify a negative
/// value for [dyScroll] that's a small enough increment to expose [item]
/// without potentially scrolling it up and completely out of view. Similarly
- /// if [item] is above, then specify a positve value for [dyScroll].
+ /// if [item] is above, then specify a positive value for [dyScroll].
///
- /// If [item] is to the right of the the currently visible items, then
+ /// If [item] is to the right of the currently visible items, then
/// specify a negative value for [dxScroll] that's a small enough increment to
/// expose [item] without potentially scrolling it up and completely out of
- /// view. Similarly if [item] is to the left, then specify a positve value
+ /// view. Similarly if [item] is to the left, then specify a positive value
/// for [dyScroll].
///
/// The [timeout] value should be long enough to accommodate as many scrolls
@@ -483,7 +483,7 @@
// the chance to complete if the item is already onscreen; if not, scroll
// repeatedly until we either find the item or time out.
bool isVisible = false;
- waitFor(item, timeout: timeout).then((Null _) { isVisible = true; });
+ waitFor(item, timeout: timeout).then((Null value) { isVisible = true; });
await new Future<Null>.delayed(const Duration(milliseconds: 500));
while (!isVisible) {
await scroll(scrollable, dxScroll, dyScroll, const Duration(milliseconds: 100));
diff --git a/packages/flutter_driver/lib/src/extension/extension.dart b/packages/flutter_driver/lib/src/extension/extension.dart
index 78584c2..2043ed5 100644
--- a/packages/flutter_driver/lib/src/extension/extension.dart
+++ b/packages/flutter_driver/lib/src/extension/extension.dart
@@ -146,7 +146,7 @@
static final Logger _log = new Logger('FlutterDriverExtension');
- final WidgetController _prober = new WidgetController(WidgetsBinding.instance);
+ final WidgetController _prober = new LiveWidgetController(WidgetsBinding.instance);
final Map<String, CommandHandlerCallback> _commandHandlers = <String, CommandHandlerCallback>{};
final Map<String, CommandDeserializerCallback> _commandDeserializers = <String, CommandDeserializerCallback>{};
final Map<String, FinderConstructor> _finders = <String, FinderConstructor>{};
diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart
index e93bba4..9baea2c 100644
--- a/packages/flutter_test/lib/src/controller.dart
+++ b/packages/flutter_test/lib/src/controller.dart
@@ -15,8 +15,12 @@
/// Class that programmatically interacts with widgets.
///
-/// For a variant of this class suited specifically for unit tests, see [WidgetTester].
-class WidgetController {
+/// For a variant of this class suited specifically for unit tests, see
+/// [WidgetTester]. For one suitable for live tests on a device, consider
+/// [LiveWidgetController].
+///
+/// Concrete subclasses must implement the [pump] method.
+abstract class WidgetController {
/// Creates a widget controller that uses the given binding.
WidgetController(this.binding);
@@ -392,10 +396,11 @@
/// This is invoked by [flingFrom], for instance, so that the sequence of
/// pointer events occurs over time.
///
- /// The default implementation does nothing.
- ///
/// The [WidgetTester] subclass implements this by deferring to the [binding].
- Future<Null> pump(Duration duration) => new Future<Null>.value(null);
+ ///
+ /// See also [SchedulerBinding.endOfFrame], which returns a future that could
+ /// be appropriate to return in the implementation of this method.
+ Future<Null> pump(Duration duration);
/// Attempts to drag the given widget by the given offset, by
/// starting a drag in the middle of the widget.
@@ -516,3 +521,20 @@
/// the widget's render object has been laid out at least once.
Rect getRect(Finder finder) => getTopLeft(finder) & getSize(finder);
}
+
+/// Variant of [WidgetController] that can be used in tests running
+/// on a device.
+///
+/// This is used, for instance, by [FlutterDriver].
+class LiveWidgetController extends WidgetController {
+ /// Creates a widget controller that uses the given binding.
+ LiveWidgetController(WidgetsBinding binding) : super(binding);
+
+ @override
+ Future<Null> pump(Duration duration) async {
+ if (duration != null)
+ await new Future<void>.delayed(duration);
+ binding.scheduleFrame();
+ await binding.endOfFrame;
+ }
+}
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 2357f22..81849e1 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -361,11 +361,11 @@
command.add(assembleTask);
final int exitCode = await runCommandAndStreamOutput(
- command,
- workingDirectory: flutterProject.android.directory.path,
- allowReentrantFlutter: true,
- environment: _gradleEnv,
- filter: logger.isVerbose ? null : ndkMessageFilter,
+ command,
+ workingDirectory: flutterProject.android.directory.path,
+ allowReentrantFlutter: true,
+ environment: _gradleEnv,
+ filter: logger.isVerbose ? null : ndkMessageFilter,
);
status.stop();
diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart
index 6eceade..2105aa5 100644
--- a/packages/flutter_tools/lib/src/base/process.dart
+++ b/packages/flutter_tools/lib/src/base/process.dart
@@ -302,10 +302,11 @@
void _traceCommand(List<String> args, { String workingDirectory }) {
final String argsText = args.join(' ');
- if (workingDirectory == null)
- printTrace(argsText);
- else
- printTrace('[$workingDirectory${fs.path.separator}] $argsText');
+ if (workingDirectory == null) {
+ printTrace('executing: $argsText');
+ } else {
+ printTrace('executing: [$workingDirectory${fs.path.separator}] $argsText');
+ }
}
String _runWithLoggingSync(List<String> cmd, {