Unverified Commit 9d9109aa authored by chunhtai's avatar chunhtai Committed by GitHub

add a benchmark test for stack size (#75039)

parent 24d8dbb1
......@@ -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';
......@@ -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);
},
),
],
),
);
......
// 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<ffi.Void> Function(
ffi.Pointer<ffi.Void>, ffi.IntPtr, ffi.Int32, ffi.Int32, ffi.Int32, ffi.IntPtr);
typedef DartMmap = ffi.Pointer<ffi.Void> Function(
ffi.Pointer<ffi.Void>, int, int, int, int, int);
final DartMmap mmap = ffi.DynamicLibrary.process().lookupFunction<CMmap, DartMmap>('mmap');
// c interop function:
// int mprotect(void* addr, size_t len, int prot);
typedef CMprotect = ffi.Int32 Function(ffi.Pointer<ffi.Void>, ffi.IntPtr, ffi.Int32);
typedef DartMprotect = int Function(ffi.Pointer<ffi.Void>, int, int);
final DartMprotect mprotect = ffi.DynamicLibrary.process()
.lookupFunction<CMprotect, DartMprotect>('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', <String>['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<ffi.Void> 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<ffi.Uint8>().asTypedList(kMemorySize).setAll(
kMemoryStartingIndex,
<int>[
// "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<ffi.NativeFunction<ffi.IntPtr Function()>>()
.asFunction<int Function()>();
}();
class StackSizePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
child: Column(
children: <Widget>[
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<String>(kStackSizeKey),
);
}
}
// 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(<String, dynamic>{
'stack_size': stackSizeInBytes,
}));
}, timeout: const Timeout(kTimeout));
}
String _encodeJson(Map<String, dynamic> jsonObject) {
return _prettyEncoder.convert(jsonObject);
}
......@@ -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<void> Function(FlutterDriver driver);
Future<void> 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<void>.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<void> driverOps(FlutterDriver driver),
Future<void> setupOps(FlutterDriver driver),
}) {
test(testName, () 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<void>.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<void>.delayed(pageDelay);
}
Timeline timeline;
await runDriverTestForRoute(routeName, (FlutterDriver driver) async {
if (pageDelay != null) {
// Wait for the page to load
await Future<void>.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<void> durationFuture = Future<void>.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);
......
// 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<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.androidArm64;
await task(createStackSizeTest());
}
......@@ -52,7 +52,7 @@ String _findMatchId(List<String> 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<bool> _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<bool> isArm64() async {
final String cpuInfo = await shellEval('getprop', const <String>['ro.product.cpu.abi']);
return cpuInfo.contains('arm64');
}
Future<void> _updateDeviceInfo() async {
String info;
try {
......
......@@ -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<TaskResult>(testDirectory, () async {
final Device device = await devices.workingDevice;
await device.unlock();
final String deviceId = device.deviceId;
await flutter('packages', options: <String>['get']);
await flutter('drive', options: <String>[
'--no-android-gradle-daemon',
'-v',
'--verbose-system-logs',
'--profile',
'-t', testTarget,
'--driver', testDriver,
'-d',
deviceId,
]);
final Map<String, dynamic> data = json.decode(
file('$testDirectory/build/stack_size.json').readAsStringSync(),
) as Map<String, dynamic>;
final Map<String, dynamic> result = <String, dynamic>{
'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: <String>[
'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: <String>[
'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:
......
......@@ -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(<CommandArgs>[
cmd(command: 'getprop', arguments: <String>['ro.bootimage.build.fingerprint', ';', 'getprop', 'ro.build.version.release', ';', 'getprop', 'ro.build.version.sdk'], environment: null),
cmd(command: 'getprop', arguments: <String>['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<String> shellEval(String command, List<String> arguments, { Map<String, String> environment, bool silent = false }) async {
commandLog.add(CommandArgs(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment