diff --git a/dev/benchmarks/macrobenchmarks/lib/common.dart b/dev/benchmarks/macrobenchmarks/lib/common.dart index 1df42bb3b1..61c21dbc1d 100644 --- a/dev/benchmarks/macrobenchmarks/lib/common.dart +++ b/dev/benchmarks/macrobenchmarks/lib/common.dart @@ -19,5 +19,8 @@ const String kImageFilteredTransformAnimationRouteName = '/imagefiltered_transfo const String kMultiWidgetConstructionRouteName = '/multi_widget_construction'; const String kHeavyGridViewRouteName = '/heavy_gridview'; const String kSimpleScrollRouteName = '/simple_scroll'; +const String kStackSizeRouteName = '/stack_size'; const String kScrollableName = '/macrobenchmark_listview'; + +const String kStackSizeKey = 'stack_size'; diff --git a/dev/benchmarks/macrobenchmarks/lib/main.dart b/dev/benchmarks/macrobenchmarks/lib/main.dart index 16d9023808..b23bbe6719 100644 --- a/dev/benchmarks/macrobenchmarks/lib/main.dart +++ b/dev/benchmarks/macrobenchmarks/lib/main.dart @@ -21,6 +21,7 @@ import 'src/picture_cache.dart'; import 'src/post_backdrop_filter.dart'; import 'src/simple_animation.dart'; import 'src/simple_scroll.dart'; +import 'src/stack_size.dart'; import 'src/text.dart'; const String kMacrobenchmarks = 'Macrobenchmarks'; @@ -54,6 +55,7 @@ class MacrobenchmarksApp extends StatelessWidget { kMultiWidgetConstructionRouteName: (BuildContext context) => const MultiWidgetConstructTable(10, 20), kHeavyGridViewRouteName: (BuildContext context) => HeavyGridViewPage(), kSimpleScrollRouteName: (BuildContext context) => SimpleScroll(), + kStackSizeRouteName: (BuildContext context) => StackSizePage(), }, ); } @@ -181,6 +183,13 @@ class HomePage extends StatelessWidget { Navigator.pushNamed(context, kLargeImageChangerRouteName); }, ), + ElevatedButton( + key: const Key(kStackSizeRouteName), + child: const Text('Stack Size'), + onPressed: () { + Navigator.pushNamed(context, kStackSizeRouteName); + }, + ), ], ), ); diff --git a/dev/benchmarks/macrobenchmarks/lib/src/stack_size.dart b/dev/benchmarks/macrobenchmarks/lib/src/stack_size.dart new file mode 100644 index 0000000000..0bb4853fb4 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/stack_size.dart @@ -0,0 +1,122 @@ +// Copyright 2014 The Flutter 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:ffi' as ffi; +import 'dart:io' as io; + +import 'package:flutter/material.dart'; + +import '../common.dart'; + +typedef GetStackPointerCallback = int Function(); + +// c interop function: +// void* mmap(void* addr, size_t len, int prot, int flags, int fd, off_t offset); +typedef CMmap = ffi.Pointer Function( + ffi.Pointer, ffi.IntPtr, ffi.Int32, ffi.Int32, ffi.Int32, ffi.IntPtr); +typedef DartMmap = ffi.Pointer Function( + ffi.Pointer, int, int, int, int, int); +final DartMmap mmap = ffi.DynamicLibrary.process().lookupFunction('mmap'); + +// c interop function: +// int mprotect(void* addr, size_t len, int prot); +typedef CMprotect = ffi.Int32 Function(ffi.Pointer, ffi.IntPtr, ffi.Int32); +typedef DartMprotect = int Function(ffi.Pointer, int, int); +final DartMprotect mprotect = ffi.DynamicLibrary.process() + .lookupFunction('mprotect'); + +const int kProtRead = 1; +const int kProtWrite = 2; +const int kProtExec = 4; + +const int kMapPrivate = 0x02; +const int kMapJit = 0x0; +const int kMapAnon = 0x20; + +const int kMemorySize = 16; +const int kInvalidFileDescriptor = -1; +const int kkFileMappingOffset = 0; + +const int kMemoryStartingIndex = 0; + +const int kExitCodeSuccess = 0; + +final GetStackPointerCallback getStackPointer = () { + // Makes sure we are running on an Android arm64 device. + if (!io.Platform.isAndroid) + throw 'This benchmark test can only be run on Android arm64 devices.'; + final io.ProcessResult result = io.Process.runSync('getprop', ['ro.product.cpu.abi']); + if (result.exitCode != 0) + throw 'Failed to retrieve CPU information.'; + if (!result.stdout.toString().contains('arm64')) + throw 'This benchmark test can only be run on Android arm64 devices.'; + + // Creates a block of memory to store the assembly code. + final ffi.Pointer region = mmap(ffi.nullptr, kMemorySize, kProtRead | kProtWrite, + kMapPrivate | kMapAnon | kMapJit, kInvalidFileDescriptor, kkFileMappingOffset); + if (region == ffi.nullptr) { + throw 'Failed to acquire memory for the test.'; + } + + // Writes the assembly code into the memory block. This assembly code returns + // the memory address of the stack pointer. + region.cast().asTypedList(kMemorySize).setAll( + kMemoryStartingIndex, + [ + // "mov x0, sp" in machine code: E0030091. + 0xe0, 0x03, 0x00, 0x91, + // "ret" in machine code: C0035FD6. + 0xc0, 0x03, 0x5f, 0xd6 + ] + ); + + // Makes sure the memory block is executable. + if (mprotect(region, kMemorySize, kProtRead | kProtExec) != kExitCodeSuccess) { + throw 'Failed to write executable code to the memory.'; + } + return region + .cast>() + .asFunction(); +}(); + +class StackSizePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Material( + child: Column( + children: [ + Container( + width: 200, + height: 100, + child: ParentWidget(), + ), + ], + ), + ); + } +} + +class ParentWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + final int myStackSize = getStackPointer(); + return ChildWidget(parentStackSize: myStackSize); + } +} + +class ChildWidget extends StatelessWidget { + const ChildWidget({this.parentStackSize, Key key}) : super(key: key); + final int parentStackSize; + + @override + Widget build(BuildContext context) { + final int myStackSize = getStackPointer(); + // Captures the stack size difference between parent widget and child widget + // during the rendering pipeline, i.e. one layer of stateless widget. + return Text( + '${parentStackSize - myStackSize}', + key: const ValueKey(kStackSizeKey), + ); + } +} diff --git a/dev/benchmarks/macrobenchmarks/test_driver/stack_size_perf_test.dart b/dev/benchmarks/macrobenchmarks/test_driver/stack_size_perf_test.dart new file mode 100644 index 0000000000..e1c592136d --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test_driver/stack_size_perf_test.dart @@ -0,0 +1,39 @@ +// Copyright 2014 The Flutter 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:convert' show JsonEncoder; + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:file/file.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; + +import 'package:macrobenchmarks/common.dart'; + +import 'util.dart'; + +const JsonEncoder _prettyEncoder = JsonEncoder.withIndent(' '); + +void main() { + test('stack_size', () async { + int stackSizeInBytes; + await runDriverTestForRoute(kStackSizeRouteName, (FlutterDriver driver) async { + final String stackSize = await driver.getText(find.byValueKey(kStackSizeKey)); + expect(stackSize.isNotEmpty, isTrue); + stackSizeInBytes = int.parse(stackSize); + }); + + expect(stackSizeInBytes > 0, isTrue); + + await fs.directory(testOutputsDirectory).create(recursive: true); + final File file = fs.file(path.join(testOutputsDirectory, 'stack_size.json')); + await file.writeAsString(_encodeJson({ + 'stack_size': stackSizeInBytes, + })); + }, timeout: const Timeout(kTimeout)); +} + +String _encodeJson(Map jsonObject) { + return _prettyEncoder.convert(jsonObject); +} diff --git a/dev/benchmarks/macrobenchmarks/test_driver/util.dart b/dev/benchmarks/macrobenchmarks/test_driver/util.dart index 341dbc5e31..6643936923 100644 --- a/dev/benchmarks/macrobenchmarks/test_driver/util.dart +++ b/dev/benchmarks/macrobenchmarks/test_driver/util.dart @@ -7,51 +7,64 @@ import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; import 'package:macrobenchmarks/common.dart'; +const Duration kTimeout = Duration(seconds: 30); + +typedef DriverTestCallBack = Future Function(FlutterDriver driver); + +Future runDriverTestForRoute(String routeName, DriverTestCallBack body) async { + final FlutterDriver driver = await FlutterDriver.connect(); + + // The slight initial delay avoids starting the timing during a + // period of increased load on the device. Without this delay, the + // benchmark has greater noise. + // See: https://github.com/flutter/flutter/issues/19434 + await Future.delayed(const Duration(milliseconds: 250)); + + await driver.forceGC(); + + final SerializableFinder scrollable = find.byValueKey(kScrollableName); + expect(scrollable, isNotNull); + final SerializableFinder button = find.byValueKey(kStackSizeRouteName); + expect(button, isNotNull); + await driver.scrollUntilVisible(scrollable, button, dyScroll: -50.0); + await driver.tap(button); + + await body(driver); + + driver.close(); +} + void macroPerfTest( String testName, String routeName, { Duration pageDelay, Duration duration = const Duration(seconds: 3), - Duration timeout = const Duration(seconds: 30), + Duration timeout = kTimeout, Future driverOps(FlutterDriver driver), Future setupOps(FlutterDriver driver), }) { test(testName, () async { - final FlutterDriver driver = await FlutterDriver.connect(); + Timeline timeline; + await runDriverTestForRoute(routeName, (FlutterDriver driver) async { + if (pageDelay != null) { + // Wait for the page to load + await Future.delayed(pageDelay); + } - // The slight initial delay avoids starting the timing during a - // period of increased load on the device. Without this delay, the - // benchmark has greater noise. - // See: https://github.com/flutter/flutter/issues/19434 - await Future.delayed(const Duration(milliseconds: 250)); - - await driver.forceGC(); - - final SerializableFinder scrollable = find.byValueKey(kScrollableName); - expect(scrollable, isNotNull); - final SerializableFinder button = find.byValueKey(routeName); - expect(button, isNotNull); - await driver.scrollUntilVisible(scrollable, button, dyScroll: -50.0); - await driver.tap(button); - - if (pageDelay != null) { - // Wait for the page to load - await Future.delayed(pageDelay); - } - - if (setupOps != null) { + if (setupOps != null) { await setupOps(driver); - } + } - final Timeline timeline = await driver.traceAction(() async { + timeline = await driver.traceAction(() async { final Future durationFuture = Future.delayed(duration); if (driverOps != null) { await driverOps(driver); } - await durationFuture; + await durationFuture; + }); }); - driver.close(); + expect(timeline, isNotNull); final TimelineSummary summary = TimelineSummary.summarize(timeline); await summary.writeSummaryToFile(testName, pretty: true); diff --git a/dev/devicelab/bin/tasks/android_stack_size_test.dart b/dev/devicelab/bin/tasks/android_stack_size_test.dart new file mode 100644 index 0000000000..9610a9347c --- /dev/null +++ b/dev/devicelab/bin/tasks/android_stack_size_test.dart @@ -0,0 +1,12 @@ +// Copyright 2014 The Flutter 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_devicelab/tasks/perf_tests.dart'; +import 'package:flutter_devicelab/framework/adb.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; + +Future main() async { + deviceOperatingSystem = DeviceOperatingSystem.androidArm64; + await task(createStackSizeTest()); +} diff --git a/dev/devicelab/lib/framework/adb.dart b/dev/devicelab/lib/framework/adb.dart index f81ec93b0f..a1412053fb 100644 --- a/dev/devicelab/lib/framework/adb.dart +++ b/dev/devicelab/lib/framework/adb.dart @@ -52,7 +52,7 @@ String _findMatchId(List idList, String idPattern) { DeviceDiscovery get devices => DeviceDiscovery(); /// Device operating system the test is configured to test. -enum DeviceOperatingSystem { android, ios, fuchsia, fake } +enum DeviceOperatingSystem { android, androidArm64 ,ios, fuchsia, fake } /// Device OS to test on. DeviceOperatingSystem deviceOperatingSystem = DeviceOperatingSystem.android; @@ -63,6 +63,8 @@ abstract class DeviceDiscovery { switch (deviceOperatingSystem) { case DeviceOperatingSystem.android: return AndroidDeviceDiscovery(); + case DeviceOperatingSystem.androidArm64: + return AndroidDeviceDiscovery(cpu: _AndroidCPU.arm64); case DeviceOperatingSystem.ios: return IosDeviceDiscovery(); case DeviceOperatingSystem.fuchsia: @@ -155,12 +157,18 @@ abstract class Device { } } +enum _AndroidCPU { + arm64, +} + class AndroidDeviceDiscovery implements DeviceDiscovery { - factory AndroidDeviceDiscovery() { - return _instance ??= AndroidDeviceDiscovery._(); + factory AndroidDeviceDiscovery({_AndroidCPU cpu}) { + return _instance ??= AndroidDeviceDiscovery._(cpu); } - AndroidDeviceDiscovery._(); + AndroidDeviceDiscovery._(this.cpu); + + final _AndroidCPU cpu; // Parses information about a device. Example: // @@ -185,6 +193,16 @@ class AndroidDeviceDiscovery implements DeviceDiscovery { return _workingDevice; } + Future _matchesCPURequirement(AndroidDevice device) async { + if (cpu == null) + return true; + switch (cpu) { + case _AndroidCPU.arm64: + return device.isArm64(); + } + return true; + } + /// Picks a random Android device out of connected devices and sets it as /// [workingDevice]. @override @@ -196,8 +214,22 @@ class AndroidDeviceDiscovery implements DeviceDiscovery { if (allDevices.isEmpty) throw const DeviceException('No Android devices detected'); - // TODO(yjbanov): filter out and warn about those with low battery level - _workingDevice = allDevices[math.Random().nextInt(allDevices.length)]; + if (cpu != null) { + for (final AndroidDevice device in allDevices) { + if (await _matchesCPURequirement(device)) { + _workingDevice = device; + break; + } + } + + } else { + // TODO(yjbanov): filter out and warn about those with low battery level + _workingDevice = allDevices[math.Random().nextInt(allDevices.length)]; + } + + if (_workingDevice == null) + throw const DeviceException('Cannot find a suitable Android device'); + print('Device chosen: $_workingDevice'); } @@ -206,6 +238,11 @@ class AndroidDeviceDiscovery implements DeviceDiscovery { final String matchedId = _findMatchId(await discoverDevices(), deviceId); if (matchedId != null) { _workingDevice = AndroidDevice(deviceId: matchedId); + if (cpu != null) { + if (!await _matchesCPURequirement(_workingDevice)) { + throw DeviceException('The selected device $matchedId does not match the cpu requirement'); + } + } print('Choose device by ID: $matchedId'); return; } @@ -444,6 +481,11 @@ class AndroidDevice extends Device { return wakefulness; } + Future isArm64() async { + final String cpuInfo = await shellEval('getprop', const ['ro.product.cpu.abi']); + return cpuInfo.contains('arm64'); + } + Future _updateDeviceInfo() async { String info; try { diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart index 6697c83a9b..a92e5ad60f 100644 --- a/dev/devicelab/lib/tasks/perf_tests.dart +++ b/dev/devicelab/lib/tasks/perf_tests.dart @@ -271,6 +271,43 @@ TaskFunction createTextfieldPerfE2ETest() { ).run; } +TaskFunction createStackSizeTest() { + final String testDirectory = + '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks'; + const String testTarget = 'test_driver/run_app.dart'; + const String testDriver = 'test_driver/stack_size_perf_test.dart'; + return () { + return inDirectory(testDirectory, () async { + final Device device = await devices.workingDevice; + await device.unlock(); + final String deviceId = device.deviceId; + await flutter('packages', options: ['get']); + + await flutter('drive', options: [ + '--no-android-gradle-daemon', + '-v', + '--verbose-system-logs', + '--profile', + '-t', testTarget, + '--driver', testDriver, + '-d', + deviceId, + ]); + final Map data = json.decode( + file('$testDirectory/build/stack_size.json').readAsStringSync(), + ) as Map; + + final Map result = { + 'stack_size_per_nesting_level': data['stack_size'], + }; + return TaskResult.success( + result, + benchmarkScoreKeys: result.keys.toList(), + ); + }); + }; +} + TaskFunction createFullscreenTextfieldPerfTest() { return PerfTest( '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', @@ -466,6 +503,15 @@ class StartupTest { ]); applicationBinaryPath = '$testDirectory/build/app/outputs/flutter-apk/app-profile.apk'; break; + case DeviceOperatingSystem.androidArm64: + await flutter('build', options: [ + 'apk', + '-v', + '--profile', + '--target-platform=android-arm64', + ]); + applicationBinaryPath = '$testDirectory/build/app/outputs/flutter-apk/app-profile.apk'; + break; case DeviceOperatingSystem.ios: await flutter('build', options: [ 'ios', @@ -998,6 +1044,20 @@ class CompileTest { if (reportPackageContentSizes) metrics.addAll(await getSizesFromApk(apkPath)); break; + case DeviceOperatingSystem.androidArm64: + options.insert(0, 'apk'); + options.add('--target-platform=android-arm64'); + options.add('--tree-shake-icons'); + options.add('--split-debug-info=infos/'); + watch.start(); + await flutter('build', options: options); + watch.stop(); + final String apkPath = '$cwd/build/app/outputs/flutter-apk/app-release.apk'; + final File apk = file(apkPath); + releaseSizeInBytes = apk.lengthSync(); + if (reportPackageContentSizes) + metrics.addAll(await getSizesFromApk(apkPath)); + break; case DeviceOperatingSystem.fuchsia: throw Exception('Unsupported option for Fuchsia devices'); case DeviceOperatingSystem.fake: @@ -1024,6 +1084,10 @@ class CompileTest { options.insert(0, 'apk'); options.add('--target-platform=android-arm'); break; + case DeviceOperatingSystem.androidArm64: + options.insert(0, 'apk'); + options.add('--target-platform=android-arm64'); + break; case DeviceOperatingSystem.fuchsia: throw Exception('Unsupported option for Fuchsia devices'); case DeviceOperatingSystem.fake: diff --git a/dev/devicelab/test/adb_test.dart b/dev/devicelab/test/adb_test.dart index 068cd6b4c6..81c13984d4 100644 --- a/dev/devicelab/test/adb_test.dart +++ b/dev/devicelab/test/adb_test.dart @@ -22,6 +22,18 @@ void main() { tearDown(() { }); + group('cpu check', () { + test('arm64', () async { + FakeDevice.pretendArm64(); + final AndroidDevice androidDevice = device as AndroidDevice; + expect(await androidDevice.isArm64(), isTrue); + expectLog([ + cmd(command: 'getprop', arguments: ['ro.bootimage.build.fingerprint', ';', 'getprop', 'ro.build.version.release', ';', 'getprop', 'ro.build.version.sdk'], environment: null), + cmd(command: 'getprop', arguments: ['ro.product.cpu.abi'], environment: null), + ]); + }); + }); + group('isAwake/isAsleep', () { test('reads Awake', () async { FakeDevice.pretendAwake(); @@ -187,6 +199,12 @@ class FakeDevice extends AndroidDevice { '''; } + static void pretendArm64() { + output = ''' + arm64 + '''; + } + @override Future shellEval(String command, List arguments, { Map environment, bool silent = false }) async { commandLog.add(CommandArgs(