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 00000000000..c42f8b005b5 --- /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 main() async { + final Completer ready = new Completer(); + 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.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.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.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 ff68529f472..1729629efd8 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 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 a7880307a8f..f66573015a8 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 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', ['keyevent', 'KEYCODE_BACK']); + await receivedNextMessage; + + // Give Android time to settle (e.g. run GCs) after closing the app. + await new Future.delayed(const Duration(milliseconds: 100)); + + // Relaunch the app, wait for it to launch. + prepareForNextMessage('READY'); + final String output = await device.shellEval('am', ['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.delayed(const Duration(milliseconds: 100)); + } + await recordEnd(); + } +} Future 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 90217afb344..8c2b301e97e 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 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 16898ac77c7..f3a99aebc61 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 useMemory() async { + print('launching $project$test on device...'); + await flutter('run', options: [ + '--verbose', + '--release', + '--no-resident', + '-d', device.deviceId, + test, + ]); + await new Future.delayed(const Duration(milliseconds: 1500)); + await recordStart(); + await new Future.delayed(const Duration(milliseconds: 3000)); + await recordEnd(); + } +} Future 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 de65020a0fd..6849e03dd62 100644 --- a/dev/devicelab/lib/framework/adb.dart +++ b/dev/devicelab/lib/framework/adb.dart @@ -83,9 +83,18 @@ abstract class Device { /// Assumes the device doesn't have a secure unlock pattern. Future unlock(); + /// Emulate a tap on the touch screen. + Future tap(int x, int y); + /// Read memory statistics for a process. Future> getMemoryStats(String packageName); + /// Stream the system log from the device. + /// + /// Flutter applications' `print` statements end up in this log + /// with some prefix. + Stream get logcat; + /// Stop a process. Future stop(String packageName); } @@ -237,6 +246,11 @@ class AndroidDevice implements Device { await shellExec('input', const ['keyevent', '82']); } + @override + Future tap(int x, int y) async { + await shellExec('input', ['tap', '$x', '$y']); + } + /// Retrieves device's wakefulness state. /// /// See: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/PowerManagerInternal.java @@ -271,6 +285,63 @@ class AndroidDevice implements Device { }; } + @override + Stream get logcat { + final Completer stdoutDone = new Completer(); + final Completer stderrDone = new Completer(); + final Completer processDone = new Completer(); + final Completer abort = new Completer(); + bool aborted = false; + StreamController stream; + stream = new StreamController( + onListen: () async { + await adb(['logcat', '--clear']); + final Process process = await startProcess(adbPath, ['-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(>[ + Future.wait(>[ + 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 stop(String packageName) async { return shellExec('am', ['force-stop', packageName]); @@ -371,11 +442,21 @@ class IosDevice implements Device { @override Future unlock() async {} + @override + Future tap(int x, int y) async { + throw 'Not implemented'; + } + @override Future> getMemoryStats(String packageName) async { throw 'Not implemented'; } + @override + Stream get logcat { + throw 'Not implemented'; + } + @override Future stop(String packageName) async {} } diff --git a/dev/devicelab/lib/framework/framework.dart b/dev/devicelab/lib/framework/framework.dart index aa6c3a81410..3ad25f5118b 100644 --- a/dev/devicelab/lib/framework/framework.dart +++ b/dev/devicelab/lib/framework/framework.dart @@ -81,12 +81,15 @@ class _TaskRunner { Future 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 fc3a7779b11..3263000b70d 100644 --- a/dev/devicelab/lib/framework/utils.dart +++ b/dev/devicelab/lib/framework/utils.dart @@ -244,7 +244,7 @@ Future forceQuitRunningProcesses() async { // 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 @@ int parseServicePort(String line, { // 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 15fca3a0588..0d31ef63ff3 100644 --- a/dev/devicelab/lib/tasks/perf_tests.dart +++ b/dev/devicelab/lib/tasks/perf_tests.dart @@ -7,7 +7,7 @@ import 'dart:convert' show json; 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 @@ TaskFunction createTilesScrollPerfTest() { ).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 @@ TaskFunction createComplexLayoutCompileTest() { 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 @@ class CompileTest { static Future> 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, ['thin'], environment: { '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 { 'app_framework_uncompressed_bytes': await appFramework.length(), @@ -420,123 +389,167 @@ class CompileTest { /// 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 get receivedNextMessage => _receivedNextMessage?.future; + Completer _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(); + } + + int get iterationCount => 15; + + Device get device => _device; + Device _device; Future 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: ['get']); if (deviceOperatingSystem == DeviceOperatingSystem.ios) - await prepareProvisioningCertificates(testDirectory); + await prepareProvisioningCertificates(project); - final List runOptions = [ - '-v', - '--profile', - '--trace-startup', // wait for the first frame to render - '-d', - deviceId, - '--observatory-port', - '0', - ]; - if (testTarget != null) - runOptions.addAll(['-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 adb = device.logcat.listen( + (String data) { + if (data.contains('==== MEMORY BENCHMARK ==== $_nextMessage ====')) + _receivedNextMessage.complete(); + }, + ); - final Map startData = await device.getMemoryStats(packageName); - - final Map data = { - 'start_total_kb': startData['total_kb'], - }; - - if (testTarget != null) { - await flutter('drive', options: [ - '-v', - '-t', - testTarget, - '-d', - deviceId, - '--use-existing-app=http://localhost:$observatoryPort', - ]); - - final Map 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.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 memoryUsage = {}; + 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 launchApp() async { + prepareForNextMessage('READY'); + print('launching $project$test on device...'); + await flutter('run', options: [ + '--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 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 _startMemory = []; + final List _endMemory = []; + final List _diffMemory = []; + + Map _startMemoryUsage; + + @protected + Future recordStart() async { + assert(_startMemoryUsage == null); + print('snapshotting memory usage...'); + _startMemoryUsage = await device.getMemoryStats(package); + } + + @protected + Future recordEnd() async { + assert(_startMemoryUsage != null); + print('snapshotting memory usage...'); + final Map 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 data) { + assert(data.isNotEmpty); + assert(data.length % 2 == 1); + final List 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 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: ['get']); - - await flutter('run', options: [ - '-v', - '--profile', - '--trace-startup', // wait for the first frame to render - '-d', - deviceId, - ]); - - final Map startData = await device.getMemoryStats(packageName); - - final Map data = { - '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', ['keyevent', 'KEYCODE_BACK']); - await new Future.delayed(const Duration(milliseconds: 1000)); - final String output = await device.shellEval('am', ['start', '-n', '$packageName/$activityName']); - print(output); - if (output.contains('Error')) - return new TaskResult.failure('unable to launch activity'); - await new Future.delayed(const Duration(milliseconds: 1000)); - } - - final Map 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 asMap(String prefix) { + return { + '$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 6cc8dc8b3fa..82d6842353b 100644 --- a/dev/devicelab/test/adb_test.dart +++ b/dev/devicelab/test/adb_test.dart @@ -93,6 +93,15 @@ void main() { ]); }); }); + + group('adb', () { + test('tap', () async { + await device.tap(100, 200); + expectLog([ + cmd(command: 'input', arguments: ['tap', '100', '200']), + ]); + }); + }); }); } diff --git a/examples/flutter_gallery/lib/gallery/home.dart b/examples/flutter_gallery/lib/gallery/home.dart index 0e7ee45aec0..7ec60da9d22 100644 --- a/examples/flutter_gallery/lib/gallery/home.dart +++ b/examples/flutter_gallery/lib/gallery/home.dart @@ -363,7 +363,7 @@ class _GalleryHomeState extends State with SingleTickerProviderStat ? 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 13afb6fad5f..2f080aadd9b 100644 --- a/examples/flutter_gallery/test/live_smoketest.dart +++ b/examples/flutter_gallery/test/live_smoketest.dart @@ -44,7 +44,7 @@ Future main() async { 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 @@ Future main() async { } } -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 @@ class _LiveWidgetController { return finder; } - Future tap(Finder finder) async { - await _controller.tap(await _waitForElement(finder)); + @override + Future tap(Finder finder, { int pointer }) async { + await tap(await _waitForElement(finder), pointer: pointer); } Future 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 42b8e372e2d..00000000000 --- 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 8771a2fc369..00000000000 --- 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 00000000000..0eeb307ec3a --- /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 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 main() async { + MaterialPageRoute.debugEnableFadingRoutes = true; // ignore: deprecated_member_use + runApp(const GalleryApp()); + await endOfAnimation(); + await new Future.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 00000000000..cce1dc9eeec --- /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 endOfAnimation() async { + do { + await SchedulerBinding.instance.endOfFrame; + } while (SchedulerBinding.instance.hasScheduledFrame); +} + +Future main() async { + MaterialPageRoute.debugEnableFadingRoutes = true; // ignore: deprecated_member_use + final Completer ready = new Completer(); + 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.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.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.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 a2dc0ad653e..3626016acbf 100644 --- a/packages/flutter_driver/lib/src/driver/driver.dart +++ b/packages/flutter_driver/lib/src/driver/driver.dart @@ -447,19 +447,19 @@ class FlutterDriver { /// 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 @@ class FlutterDriver { // 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.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 78584c25933..2043ed5bd71 100644 --- a/packages/flutter_driver/lib/src/extension/extension.dart +++ b/packages/flutter_driver/lib/src/extension/extension.dart @@ -146,7 +146,7 @@ class FlutterDriverExtension { static final Logger _log = new Logger('FlutterDriverExtension'); - final WidgetController _prober = new WidgetController(WidgetsBinding.instance); + final WidgetController _prober = new LiveWidgetController(WidgetsBinding.instance); final Map _commandHandlers = {}; final Map _commandDeserializers = {}; final Map _finders = {}; diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart index e93bba422c2..9baea2cb9fa 100644 --- a/packages/flutter_test/lib/src/controller.dart +++ b/packages/flutter_test/lib/src/controller.dart @@ -15,8 +15,12 @@ import 'test_pointer.dart'; /// 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 @@ class WidgetController { /// 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 pump(Duration duration) => new Future.value(null); + /// + /// See also [SchedulerBinding.endOfFrame], which returns a future that could + /// be appropriate to return in the implementation of this method. + Future 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 @@ class WidgetController { /// 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 pump(Duration duration) async { + if (duration != null) + await new Future.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 2357f2219e5..81849e16acc 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -361,11 +361,11 @@ Future _buildGradleProjectV2( 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 6eceade9334..2105aa53da8 100644 --- a/packages/flutter_tools/lib/src/base/process.dart +++ b/packages/flutter_tools/lib/src/base/process.dart @@ -302,10 +302,11 @@ String runSync(List cmd, { void _traceCommand(List 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 cmd, {