flutter/dev/devicelab/lib/tasks/perf_tests.dart
Chris Bracken 09d4dabd6d
iOS: Update minimum iOS version to 13.0 (#167737)
This updates the Flutter minimum iOS version from 12.0 to 13.0, adds a
migrator for existing apps, and updates our own examples, tests, and
benchmark apps to 13.0. A follow-up patch will drop iOS 13 `@available`
checks in the embedder.

This is required in order to use Swift in the embedder and not need to
bundle the Swift runtime libs in every app that uses Flutter. Swift
stable ABI

As of March 2025, usage of iOS is well below 1%, see example public
usage data here:
https://telemetrydeck.com/survey/apple/iOS/majorSystemVersions/

This patch makes the following changes:
1. Updates ios_deployment_target from 12.0 to 13.0.
2. Changes templates to `IPHONEOS_DEPLOYMENT_TARGET`,
`MinimumOSVersion`, and Podfile `platform :ios` to 12.0.
3. Adds migrator for Podfile part to migrate `platform :ios, '11.0'` ->
`platform :ios, '12.0'`
4. Compiles with `-miphoneos-version-min=12.0`
5. Runs the migrator on all example apps and integration tests.
6. Updates examples, tests to iOS 13 deployment target

It also updates `verify_exported.dart`:
* iOS 13 introduces stricter separation of const and non-const global
symbols. Previously, these were declared in the Mach-O `__DATA` section
which may be mapped read-write, but now they're in a dedicated
`__DATA_CONST` section which is mapped read-only. This adds
`(__DATA_CONST,__const)` to the allowlist with the same enforcement on
exported symbol naming as before.

See also (ios_deployment_target):
* https://github.com/flutter/buildroot/pull/808
* https://github.com/flutter/buildroot/pull/574

See also (template, migrator):
* https://github.com/flutter/flutter/pull/62902
* https://github.com/flutter/flutter/pull/85174
* https://github.com/flutter/flutter/pull/101963
* https://github.com/flutter/flutter/pull/140478

Issue: https://github.com/flutter/flutter/issues/167735

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-04-24 20:15:13 +00:00

2391 lines
80 KiB
Dart

// 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:async';
import 'dart:convert' show LineSplitter, json, utf8;
import 'dart:ffi' show Abi;
import 'dart:io';
import 'dart:math' as math;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:xml/xml.dart';
import '../framework/devices.dart';
import '../framework/framework.dart';
import '../framework/host_agent.dart';
import '../framework/task_result.dart';
import '../framework/utils.dart';
/// Must match flutter_driver/lib/src/common.dart.
///
/// Redefined here to avoid taking a dependency on flutter_driver.
String testOutputDirectory(String testDirectory) {
return Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? '$testDirectory/build';
}
TaskFunction createComplexLayoutScrollPerfTest({
bool measureCpuGpu = true,
bool badScroll = false,
bool? enableImpeller,
bool forceOpenGLES = false,
}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/complex_layout',
badScroll ? 'test_driver/scroll_perf_bad.dart' : 'test_driver/scroll_perf.dart',
'complex_layout_scroll_perf',
measureCpuGpu: measureCpuGpu,
enableImpeller: enableImpeller,
forceOpenGLES: forceOpenGLES,
).run;
}
TaskFunction createTilesScrollPerfTest({bool? enableImpeller}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/complex_layout',
'test_driver/scroll_perf.dart',
'tiles_scroll_perf',
enableImpeller: enableImpeller,
).run;
}
TaskFunction createUiKitViewScrollPerfTest({bool? enableImpeller}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/platform_views_layout',
'test_driver/uikit_view_scroll_perf.dart',
'platform_views_scroll_perf',
testDriver: 'test_driver/scroll_perf_test.dart',
needsFullTimeline: false,
enableImpeller: enableImpeller,
enableMergedPlatformThread: true,
).run;
}
TaskFunction createUiKitViewScrollPerfAdBannersTest({bool? enableImpeller}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/platform_views_layout',
'test_driver/uikit_view_scroll_perf_ad_banners.dart',
'platform_views_scroll_perf_ad_banners',
testDriver: 'test_driver/scroll_perf_ad_banners_test.dart',
needsFullTimeline: false,
enableImpeller: enableImpeller,
).run;
}
TaskFunction createUiKitViewScrollPerfBottomAdBannerTest({bool? enableImpeller}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/platform_views_layout',
'test_driver/uikit_view_scroll_perf_bottom_ad_banner.dart',
'platform_views_scroll_perf_bottom_ad_banner',
testDriver: 'test_driver/scroll_perf_bottom_ad_banner_test.dart',
needsFullTimeline: false,
enableImpeller: enableImpeller,
).run;
}
TaskFunction createUiKitViewScrollPerfNonIntersectingTest({bool? enableImpeller}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/platform_views_layout',
'test_driver/uikit_view_scroll_perf_non_intersecting.dart',
'platform_views_scroll_perf_non_intersecting',
testDriver: 'test_driver/scroll_perf_non_intersecting_test.dart',
needsFullTimeline: false,
enableImpeller: enableImpeller,
).run;
}
TaskFunction createAndroidTextureScrollPerfTest({bool? enableImpeller}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/platform_views_layout',
'test_driver/android_view_scroll_perf.dart',
'platform_views_scroll_perf',
testDriver: 'test_driver/scroll_perf_test.dart',
needsFullTimeline: false,
enableImpeller: enableImpeller,
enableMergedPlatformThread: true,
).run;
}
TaskFunction createAndroidHCPPScrollPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/platform_views_layout',
'test_driver/scroll_perf_hcpp.dart',
'platform_views_hcpp_scroll_perf',
testDriver: 'test_driver/scroll_perf_hcpp_test.dart',
needsFullTimeline: false,
enableImpeller: true,
enableSurfaceControl: true,
enableMergedPlatformThread: true,
).run;
}
TaskFunction createAndroidViewScrollPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/platform_views_layout_hybrid_composition',
'test_driver/android_view_scroll_perf.dart',
'platform_views_scroll_perf_hybrid_composition',
testDriver: 'test_driver/scroll_perf_test.dart',
enableMergedPlatformThread: true,
).run;
}
TaskFunction createHomeScrollPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/integration_tests/flutter_gallery',
'test_driver/scroll_perf.dart',
'home_scroll_perf',
).run;
}
TaskFunction createCullOpacityPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'cull_opacity_perf',
testDriver: 'test_driver/cull_opacity_perf_test.dart',
).run;
}
TaskFunction createCullOpacityPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/cull_opacity_perf_e2e.dart',
).run;
}
TaskFunction createCubicBezierPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'cubic_bezier_perf',
testDriver: 'test_driver/cubic_bezier_perf_test.dart',
).run;
}
TaskFunction createCubicBezierPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/cubic_bezier_perf_e2e.dart',
).run;
}
TaskFunction createBackdropFilterPerfTest({bool measureCpuGpu = true, bool? enableImpeller}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'backdrop_filter_perf',
measureCpuGpu: measureCpuGpu,
testDriver: 'test_driver/backdrop_filter_perf_test.dart',
saveTraceFile: true,
enableImpeller: enableImpeller,
disablePartialRepaint: true,
).run;
}
TaskFunction createAnimationWithMicrotasksPerfTest({bool measureCpuGpu = true}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'animation_with_microtasks_perf',
measureCpuGpu: measureCpuGpu,
testDriver: 'test_driver/animation_with_microtasks_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createBackdropFilterPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/backdrop_filter_perf_e2e.dart',
).run;
}
TaskFunction createPostBackdropFilterPerfTest({bool measureCpuGpu = true}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'post_backdrop_filter_perf',
measureCpuGpu: measureCpuGpu,
testDriver: 'test_driver/post_backdrop_filter_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createSimpleAnimationPerfTest({bool measureCpuGpu = true, bool? enableImpeller}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'simple_animation_perf',
measureCpuGpu: measureCpuGpu,
testDriver: 'test_driver/simple_animation_perf_test.dart',
saveTraceFile: true,
enableImpeller: enableImpeller,
).run;
}
TaskFunction createAnimatedPlaceholderPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/animated_placeholder_perf_e2e.dart',
).run;
}
TaskFunction createPictureCachePerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'picture_cache_perf',
testDriver: 'test_driver/picture_cache_perf_test.dart',
).run;
}
TaskFunction createPictureCachePerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/picture_cache_perf_e2e.dart',
).run;
}
TaskFunction createPictureCacheComplexityScoringPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'picture_cache_complexity_scoring_perf',
testDriver: 'test_driver/picture_cache_complexity_scoring_perf_test.dart',
).run;
}
TaskFunction createOpenPayScrollPerfTest({bool measureCpuGpu = true}) {
return PerfTest(
openpayDirectory.path,
'test_driver/scroll_perf.dart',
'openpay_scroll_perf',
measureCpuGpu: measureCpuGpu,
testDriver: 'test_driver/scroll_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createFlutterGalleryStartupTest({
String target = 'lib/main.dart',
Map<String, String>? runEnvironment,
bool enableLazyShaderMode = false,
bool enableHCPP = false,
}) {
return StartupTest(
'${flutterDirectory.path}/dev/integration_tests/flutter_gallery',
target: target,
runEnvironment: runEnvironment,
enableLazyShaderMode: enableLazyShaderMode,
enableHCPP: enableHCPP,
).run;
}
TaskFunction createComplexLayoutStartupTest() {
return StartupTest('${flutterDirectory.path}/dev/benchmarks/complex_layout').run;
}
TaskFunction createFlutterGalleryCompileTest() {
return CompileTest('${flutterDirectory.path}/dev/integration_tests/flutter_gallery').run;
}
TaskFunction createHelloWorldCompileTest() {
return CompileTest(
'${flutterDirectory.path}/examples/hello_world',
reportPackageContentSizes: true,
).run;
}
TaskFunction createImitationGameSwiftUITest() {
return CompileTest(
'${flutterDirectory.path}/dev/benchmarks/imitation_game_swiftui',
reportPackageContentSizes: true,
).runSwiftUIApp;
}
TaskFunction createImitationGameFlutterTest() {
flutter(
'create',
options: <String>[
'--platforms=ios',
'${flutterDirectory.path}/dev/benchmarks/imitation_game_flutter',
'--no-overwrite',
],
);
return CompileTest(
'${flutterDirectory.path}/dev/benchmarks/imitation_game_flutter',
reportPackageContentSizes: true,
).run;
}
TaskFunction createWebCompileTest() {
return const WebCompileTest().run;
}
TaskFunction createFlutterViewStartupTest() {
return StartupTest('${flutterDirectory.path}/examples/flutter_view', reportMetrics: false).run;
}
TaskFunction createPlatformViewStartupTest() {
return StartupTest('${flutterDirectory.path}/examples/platform_view', reportMetrics: false).run;
}
TaskFunction createBasicMaterialCompileTest() {
return () async {
const String sampleAppName = 'sample_flutter_app';
final Directory sampleDir = dir('${Directory.systemTemp.path}/$sampleAppName');
rmTree(sampleDir);
await inDirectory<void>(Directory.systemTemp, () async {
await flutter('create', options: <String>['--template=app', sampleAppName]);
});
if (!sampleDir.existsSync()) {
throw 'Failed to create default Flutter app in ${sampleDir.path}';
}
return CompileTest(sampleDir.path).run();
};
}
TaskFunction createTextfieldPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'textfield_perf',
testDriver: 'test_driver/textfield_perf_test.dart',
).run;
}
TaskFunction createTextfieldPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/textfield_perf_e2e.dart',
).run;
}
TaskFunction createVeryLongPictureScrollingPerfE2ETest({required bool enableImpeller}) {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/very_long_picture_scrolling_perf_e2e.dart',
enableImpeller: enableImpeller,
).run;
}
TaskFunction createSlidersPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'sliders_perf',
testDriver: 'test_driver/sliders_perf_test.dart',
).run;
}
TaskFunction createFullscreenTextfieldPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'fullscreen_textfield_perf',
testDriver: 'test_driver/fullscreen_textfield_perf_test.dart',
).run;
}
TaskFunction createFullscreenTextfieldPerfE2ETest({bool? enableImpeller}) {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/fullscreen_textfield_perf_e2e.dart',
enableImpeller: enableImpeller,
).run;
}
TaskFunction createClipperCachePerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/clipper_cache_perf_e2e.dart',
).run;
}
TaskFunction createColorFilterAndFadePerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'color_filter_and_fade_perf',
testDriver: 'test_driver/color_filter_and_fade_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createColorFilterAndFadePerfE2ETest({bool? enableImpeller}) {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/color_filter_and_fade_perf_e2e.dart',
enableImpeller: enableImpeller,
).run;
}
TaskFunction createColorFilterCachePerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/color_filter_cache_perf_e2e.dart',
).run;
}
TaskFunction createColorFilterWithUnstableChildPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/color_filter_with_unstable_child_perf_e2e.dart',
).run;
}
TaskFunction createShaderMaskCachePerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/shader_mask_cache_perf_e2e.dart',
).run;
}
TaskFunction createFadingChildAnimationPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'fading_child_animation_perf',
testDriver: 'test_driver/fading_child_animation_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createImageFilteredTransformAnimationPerfTest({bool? enableImpeller}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'imagefiltered_transform_animation_perf',
testDriver: 'test_driver/imagefiltered_transform_animation_perf_test.dart',
saveTraceFile: true,
enableImpeller: enableImpeller,
).run;
}
TaskFunction createsMultiWidgetConstructPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/multi_widget_construction_perf_e2e.dart',
).run;
}
TaskFunction createListTextLayoutPerfE2ETest({bool? enableImpeller}) {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/list_text_layout_perf_e2e.dart',
enableImpeller: enableImpeller,
).run;
}
TaskFunction createsScrollSmoothnessPerfTest() {
final String testDirectory = '${flutterDirectory.path}/dev/benchmarks/complex_layout';
const String testTarget = 'test/measure_scroll_smoothness.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,
'-d',
deviceId,
],
);
final Map<String, dynamic> data =
json.decode(
file(
'${testOutputDirectory(testDirectory)}/scroll_smoothness_test.json',
).readAsStringSync(),
)
as Map<String, dynamic>;
final Map<String, dynamic> result = <String, dynamic>{};
void addResult(dynamic data, String suffix) {
assert(data is Map<String, dynamic>);
if (data is Map<String, dynamic>) {
const List<String> metricKeys = <String>[
'janky_count',
'average_abs_jerk',
'dropped_frame_count',
];
for (final String key in metricKeys) {
result[key + suffix] = data[key];
}
}
}
addResult(data['resample on with 90Hz input'], '_with_resampler_90Hz');
addResult(data['resample on with 59Hz input'], '_with_resampler_59Hz');
addResult(data['resample off with 90Hz input'], '_without_resampler_90Hz');
addResult(data['resample off with 59Hz input'], '_without_resampler_59Hz');
return TaskResult.success(result, benchmarkScoreKeys: result.keys.toList());
});
};
}
TaskFunction createOpacityPeepholeOneRectPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/opacity_peephole_one_rect_perf_e2e.dart',
).run;
}
TaskFunction createOpacityPeepholeColOfRowsPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/opacity_peephole_col_of_rows_perf_e2e.dart',
).run;
}
TaskFunction createOpacityPeepholeOpacityOfGridPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/opacity_peephole_opacity_of_grid_perf_e2e.dart',
).run;
}
TaskFunction createOpacityPeepholeGridOfOpacityPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/opacity_peephole_grid_of_opacity_perf_e2e.dart',
).run;
}
TaskFunction createOpacityPeepholeFadeTransitionTextPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/opacity_peephole_fade_transition_text_perf_e2e.dart',
).run;
}
TaskFunction createOpacityPeepholeGridOfAlphaSaveLayersPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/opacity_peephole_grid_of_alpha_savelayers_perf_e2e.dart',
).run;
}
TaskFunction createOpacityPeepholeColOfAlphaSaveLayerRowsPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/opacity_peephole_col_of_alpha_savelayer_rows_perf_e2e.dart',
).run;
}
TaskFunction createGradientDynamicPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/gradient_dynamic_perf_e2e.dart',
).run;
}
TaskFunction createGradientConsistentPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/gradient_consistent_perf_e2e.dart',
).run;
}
TaskFunction createGradientStaticPerfE2ETest() {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/gradient_static_perf_e2e.dart',
).run;
}
TaskFunction createAnimatedAdvancedBlendPerfTest({bool? enableImpeller, bool? forceOpenGLES}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'animated_advanced_blend_perf',
enableImpeller: enableImpeller,
forceOpenGLES: forceOpenGLES,
testDriver: 'test_driver/animated_advanced_blend_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createRRectBlurPerfTest({bool? enableImpeller, bool? forceOpenGLES}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'rrect_blur_perf',
enableImpeller: enableImpeller,
forceOpenGLES: forceOpenGLES,
testDriver: 'test_driver/rrect_blur_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createAnimatedBlurBackropFilterPerfTest({bool? enableImpeller, bool? forceOpenGLES}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'animated_blur_backdrop_filter_perf',
enableImpeller: enableImpeller,
forceOpenGLES: forceOpenGLES,
testDriver: 'test_driver/animated_blur_backdrop_filter_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createDrawPointsPerfTest({bool? enableImpeller}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'draw_points_perf',
enableImpeller: enableImpeller,
testDriver: 'test_driver/draw_points_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createDrawAtlasPerfTest({bool? forceOpenGLES}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'draw_atlas_perf',
enableImpeller: true,
testDriver: 'test_driver/draw_atlas_perf_test.dart',
saveTraceFile: true,
forceOpenGLES: forceOpenGLES,
).run;
}
TaskFunction createDrawVerticesPerfTest({bool? forceOpenGLES}) {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'draw_vertices_perf',
enableImpeller: true,
testDriver: 'test_driver/draw_vertices_perf_test.dart',
saveTraceFile: true,
forceOpenGLES: forceOpenGLES,
).run;
}
TaskFunction createPathTessellationStaticPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'tessellation_perf_static',
enableImpeller: true,
testDriver: 'test_driver/path_tessellation_static_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createPathTessellationDynamicPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'tessellation_perf_dynamic',
enableImpeller: true,
testDriver: 'test_driver/path_tessellation_dynamic_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createPathStrokeTessellationStaticPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'stroke_tessellation_perf_static',
enableImpeller: true,
testDriver: 'test_driver/path_stroke_tessellation_static_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createPathStrokeTessellationDynamicPerfTest() {
return PerfTest(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test_driver/run_app.dart',
'stroke_tessellation_perf_dynamic',
enableImpeller: true,
testDriver: 'test_driver/path_stroke_tessellation_dynamic_perf_test.dart',
saveTraceFile: true,
).run;
}
TaskFunction createAnimatedComplexOpacityPerfE2ETest({bool? enableImpeller}) {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/animated_complex_opacity_perf_e2e.dart',
enableImpeller: enableImpeller,
).run;
}
TaskFunction createAnimatedComplexImageFilteredPerfE2ETest({bool? enableImpeller}) {
return PerfTest.e2e(
'${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
'test/animated_complex_image_filtered_perf_e2e.dart',
enableImpeller: enableImpeller,
).run;
}
Map<String, dynamic> _average(List<Map<String, dynamic>> results, int iterations) {
final Map<String, dynamic> tally = <String, dynamic>{};
for (final Map<String, dynamic> item in results) {
item.forEach((String key, dynamic value) {
if (tally.containsKey(key)) {
tally[key] = (tally[key] as int) + (value as int);
} else {
tally[key] = value;
}
});
}
tally.forEach((String key, dynamic value) {
tally[key] = (value as int) ~/ iterations;
});
return tally;
}
/// Opens the file at testDirectory + 'ios/Runner/Info.plist'
/// and adds additional manifest settings.
void _updateManifestSettings(
String testDirectory, {
required bool disablePartialRepaint,
required bool platformThreadMerged,
}) {
final String manifestPath = path.join(testDirectory, 'ios', 'Runner', 'Info.plist');
final File file = File(manifestPath);
if (!file.existsSync()) {
throw Exception('Info.plist not found at $manifestPath');
}
final String xmlStr = file.readAsStringSync();
final XmlDocument xmlDoc = XmlDocument.parse(xmlStr);
final List<(String, String)> keyPairs = <(String, String)>[
if (disablePartialRepaint) ('FLTDisablePartialRepaint', disablePartialRepaint.toString()),
if (platformThreadMerged) ('FLTEnableMergedPlatformUIThread', platformThreadMerged.toString()),
];
final XmlElement applicationNode = xmlDoc.findAllElements('dict').first;
// Check if the meta-data node already exists.
for (final (String key, String value) in keyPairs) {
applicationNode.children.add(
XmlElement(XmlName('key'), <XmlAttribute>[], <XmlNode>[XmlText(key)], false),
);
applicationNode.children.add(XmlElement(XmlName(value)));
}
file.writeAsStringSync(xmlDoc.toXmlString(pretty: true, indent: ' '));
}
Future<void> _resetPlist(String testDirectory) async {
final String manifestPath = path.join(testDirectory, 'ios', 'Runner', 'Info.plist');
final File file = File(manifestPath);
if (!file.existsSync()) {
throw Exception('Info.plist not found at $manifestPath');
}
await exec('git', <String>['checkout', file.path]);
}
void _addMetadataToManifest(String testDirectory, List<(String, String)> keyPairs) {
final String manifestPath = path.join(
testDirectory,
'android',
'app',
'src',
'main',
'AndroidManifest.xml',
);
final File file = File(manifestPath);
if (!file.existsSync()) {
throw Exception('AndroidManifest.xml not found at $manifestPath');
}
final String xmlStr = file.readAsStringSync();
final XmlDocument xmlDoc = XmlDocument.parse(xmlStr);
final XmlElement applicationNode = xmlDoc.findAllElements('application').first;
// Check if the meta-data node already exists.
for (final (String key, String value) in keyPairs) {
final Iterable<XmlElement> existingMetaData = applicationNode
.findAllElements('meta-data')
.where((XmlElement node) => node.getAttribute('android:name') == key);
if (existingMetaData.isNotEmpty) {
final XmlElement existingEntry = existingMetaData.first;
existingEntry.setAttribute('android:value', value);
} else {
final XmlElement metaData = XmlElement(XmlName('meta-data'), <XmlAttribute>[
XmlAttribute(XmlName('android:name'), key),
XmlAttribute(XmlName('android:value'), value),
]);
applicationNode.children.add(metaData);
}
}
file.writeAsStringSync(xmlDoc.toXmlString(pretty: true, indent: ' '));
}
void _addSurfaceControlSupportToManifest(String testDirectory) {
final List<(String, String)> keyPairs = <(String, String)>[
('io.flutter.embedding.android.EnableSurfaceControl', 'true'),
];
_addMetadataToManifest(testDirectory, keyPairs);
}
void _addMergedPlatformThreadSupportToManifest(String testDirectory) {
final List<(String, String)> keyPairs = <(String, String)>[
('io.flutter.embedding.android.EnableMergedPlatformUIThread', 'true'),
];
_addMetadataToManifest(testDirectory, keyPairs);
}
/// Opens the file at testDirectory + 'android/app/src/main/AndroidManifest.xml'
/// <meta-data
/// android:name="io.flutter.embedding.android.EnableVulkanGPUTracing"
/// android:value="true" />
void _addVulkanGPUTracingToManifest(String testDirectory) {
final List<(String, String)> keyPairs = <(String, String)>[
('io.flutter.embedding.android.EnableVulkanGPUTracing', 'true'),
];
_addMetadataToManifest(testDirectory, keyPairs);
}
/// Opens the file at testDirectory + 'android/app/src/main/AndroidManifest.xml'
/// <meta-data
/// android:name="io.flutter.embedding.android.ImpellerShaderMode"
/// android:value="lazy" />
void _addLazyShaderMode(String testDirectory) {
final List<(String, String)> keyPairs = <(String, String)>[
('io.flutter.embedding.android.ImpellerLazyShaderInitialization', 'true'),
];
_addMetadataToManifest(testDirectory, keyPairs);
}
/// Opens the file at testDirectory + 'android/app/src/main/AndroidManifest.xml'
/// and adds the following entry to the application.
/// <meta-data
/// android:name="io.flutter.embedding.android.ImpellerBackend"
/// android:value="opengles" />
/// <meta-data
/// android:name="io.flutter.embedding.android.EnableOpenGLGPUTracing"
/// android:value="true" />
void _addOpenGLESToManifest(String testDirectory) {
final List<(String, String)> keyPairs = <(String, String)>[
('io.flutter.embedding.android.ImpellerBackend', 'opengles'),
('io.flutter.embedding.android.EnableOpenGLGPUTracing', 'true'),
];
_addMetadataToManifest(testDirectory, keyPairs);
}
Future<void> _resetManifest(String testDirectory) async {
final String manifestPath = path.join(
testDirectory,
'android',
'app',
'src',
'main',
'AndroidManifest.xml',
);
final File file = File(manifestPath);
if (!file.existsSync()) {
throw Exception('AndroidManifest.xml not found at $manifestPath');
}
await exec('git', <String>['checkout', file.path]);
}
/// Measure application startup performance.
class StartupTest {
const StartupTest(
this.testDirectory, {
this.reportMetrics = true,
this.target = 'lib/main.dart',
this.runEnvironment,
this.enableLazyShaderMode = false,
this.enableHCPP = false,
});
final String testDirectory;
final bool reportMetrics;
final bool enableLazyShaderMode;
final bool enableHCPP;
final String target;
final Map<String, String>? runEnvironment;
Future<TaskResult> run() async {
return inDirectory<TaskResult>(testDirectory, () async {
final Device device = await devices.workingDevice;
await device.unlock();
const int iterations = 5;
final List<Map<String, dynamic>> results = <Map<String, dynamic>>[];
if (enableLazyShaderMode) {
_addLazyShaderMode(testDirectory);
}
if (enableHCPP) {
_addSurfaceControlSupportToManifest(testDirectory);
}
try {
section('Building application');
String? applicationBinaryPath;
switch (deviceOperatingSystem) {
case DeviceOperatingSystem.android:
await flutter(
'build',
options: <String>[
'apk',
'-v',
'--profile',
'--target-platform=android-arm,android-arm64',
'--target=$target',
],
);
applicationBinaryPath = '$testDirectory/build/app/outputs/flutter-apk/app-profile.apk';
case DeviceOperatingSystem.androidArm:
await flutter(
'build',
options: <String>[
'apk',
'-v',
'--profile',
'--target-platform=android-arm',
'--target=$target',
],
);
applicationBinaryPath = '$testDirectory/build/app/outputs/flutter-apk/app-profile.apk';
case DeviceOperatingSystem.androidArm64:
await flutter(
'build',
options: <String>[
'apk',
'-v',
'--profile',
'--target-platform=android-arm64',
'--target=$target',
],
);
applicationBinaryPath = '$testDirectory/build/app/outputs/flutter-apk/app-profile.apk';
case DeviceOperatingSystem.fake:
case DeviceOperatingSystem.fuchsia:
case DeviceOperatingSystem.linux:
break;
case DeviceOperatingSystem.ios:
case DeviceOperatingSystem.macos:
await flutter(
'build',
options: <String>[
if (deviceOperatingSystem == DeviceOperatingSystem.ios) 'ios' else 'macos',
'-v',
'--profile',
'--target=$target',
if (deviceOperatingSystem == DeviceOperatingSystem.ios) '--no-publish-port',
],
);
final String buildRoot = path.join(testDirectory, 'build');
applicationBinaryPath = _findDarwinAppInBuildDirectory(buildRoot);
case DeviceOperatingSystem.windows:
await flutter(
'build',
options: <String>['windows', '-v', '--profile', '--target=$target'],
);
final String basename = path.basename(testDirectory);
final String arch = Abi.current() == Abi.windowsX64 ? 'x64' : 'arm64';
applicationBinaryPath = path.join(
testDirectory,
'build',
'windows',
arch,
'runner',
'Profile',
'$basename.exe',
);
}
const int maxFailures = 3;
int currentFailures = 0;
for (int i = 0; i < iterations; i += 1) {
// Startup should not take more than a few minutes. After 10 minutes,
// take a screenshot to help debug.
final Timer timer = Timer(const Duration(minutes: 10), () async {
print('Startup not completed within 10 minutes. Taking a screenshot...');
await _flutterScreenshot(
device.deviceId,
'screenshot_startup_${DateTime.now().toLocal().toIso8601String()}.png',
);
});
final int result = await flutter(
'run',
options: <String>[
'--no-android-gradle-daemon',
'--no-publish-port',
'--verbose',
'--profile',
'--trace-startup',
'--target=$target',
'-d',
device.deviceId,
if (applicationBinaryPath != null) '--use-application-binary=$applicationBinaryPath',
],
environment: runEnvironment,
canFail: true,
);
timer.cancel();
if (result == 0) {
final Map<String, dynamic> data =
json.decode(
file(
'${testOutputDirectory(testDirectory)}/start_up_info.json',
).readAsStringSync(),
)
as Map<String, dynamic>;
results.add(data);
} else {
currentFailures += 1;
await _flutterScreenshot(
device.deviceId,
'screenshot_startup_failure_$currentFailures.png',
);
i -= 1;
if (currentFailures == maxFailures) {
return TaskResult.failure('Application failed to start $maxFailures times');
}
}
await device.uninstallApp();
}
final Map<String, dynamic> averageResults = _average(results, iterations);
if (!reportMetrics) {
return TaskResult.success(averageResults);
}
return TaskResult.success(
averageResults,
benchmarkScoreKeys: <String>[
'timeToFirstFrameMicros',
'timeToFirstFrameRasterizedMicros',
],
);
} finally {
await _resetManifest(testDirectory);
}
});
}
Future<void> _flutterScreenshot(String deviceId, String screenshotName) async {
if (hostAgent.dumpDirectory != null) {
await flutter(
'screenshot',
options: <String>[
'-d',
deviceId,
'--out',
hostAgent.dumpDirectory!.childFile(screenshotName).path,
],
canFail: true,
);
}
}
}
/// A one-off test to verify that devtools starts in profile mode.
class DevtoolsStartupTest {
const DevtoolsStartupTest(this.testDirectory);
final String testDirectory;
Future<TaskResult> run() async {
return inDirectory<TaskResult>(testDirectory, () async {
final Device device = await devices.workingDevice;
section('Building application');
String? applicationBinaryPath;
switch (deviceOperatingSystem) {
case DeviceOperatingSystem.android:
await flutter(
'build',
options: <String>[
'apk',
'-v',
'--profile',
'--target-platform=android-arm,android-arm64',
],
);
applicationBinaryPath = '$testDirectory/build/app/outputs/flutter-apk/app-profile.apk';
case DeviceOperatingSystem.androidArm:
await flutter(
'build',
options: <String>['apk', '-v', '--profile', '--target-platform=android-arm'],
);
applicationBinaryPath = '$testDirectory/build/app/outputs/flutter-apk/app-profile.apk';
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';
case DeviceOperatingSystem.ios:
await flutter('build', options: <String>['ios', '-v', '--profile']);
applicationBinaryPath = _findDarwinAppInBuildDirectory(
'$testDirectory/build/ios/iphoneos',
);
case DeviceOperatingSystem.fake:
case DeviceOperatingSystem.fuchsia:
case DeviceOperatingSystem.linux:
case DeviceOperatingSystem.macos:
case DeviceOperatingSystem.windows:
break;
}
final Process process = await startFlutter(
'run',
options: <String>[
'--no-android-gradle-daemon',
'--no-publish-port',
'--verbose',
'--profile',
'-d',
device.deviceId,
if (applicationBinaryPath != null) '--use-application-binary=$applicationBinaryPath',
],
);
final Completer<void> completer = Completer<void>();
bool sawLine = false;
process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((String line) {
print('[STDOUT]: $line');
// Wait for devtools output.
if (line.contains('The Flutter DevTools debugger and profiler')) {
sawLine = true;
completer.complete();
}
});
bool didExit = false;
unawaited(
process.exitCode.whenComplete(() {
didExit = true;
}),
);
await Future.any(<Future<void>>[
completer.future,
Future<void>.delayed(const Duration(minutes: 5)),
process.exitCode,
]);
if (!didExit) {
process.stdin.writeln('q');
await process.exitCode;
}
await device.uninstallApp();
if (sawLine) {
return TaskResult.success(null, benchmarkScoreKeys: <String>[]);
}
return TaskResult.failure(
'Did not see line "The Flutter DevTools debugger and profiler" in output',
);
});
}
}
/// A callback function to be used to mock the flutter drive command in PerfTests.
///
/// The `options` contains all the arguments in the `flutter drive` command in PerfTests.
typedef FlutterDriveCallback = void Function(List<String> options);
/// Measures application runtime performance, specifically per-frame
/// performance.
class PerfTest {
const PerfTest(
this.testDirectory,
this.testTarget,
this.timelineFileName, {
this.measureCpuGpu = true,
this.measureMemory = true,
this.measureTotalGCTime = true,
this.saveTraceFile = false,
this.testDriver,
this.needsFullTimeline = true,
this.benchmarkScoreKeys,
this.dartDefine = '',
String? resultFilename,
this.device,
this.flutterDriveCallback,
this.timeoutSeconds,
this.enableImpeller,
this.forceOpenGLES,
this.disablePartialRepaint = false,
this.enableMergedPlatformThread = false,
this.enableSurfaceControl = false,
this.enableLazyShaderMode = false,
this.createPlatforms = const <String>[],
}) : _resultFilename = resultFilename;
const PerfTest.e2e(
this.testDirectory,
this.testTarget, {
this.measureCpuGpu = false,
this.measureMemory = false,
this.measureTotalGCTime = false,
this.testDriver = 'test_driver/e2e_test.dart',
this.needsFullTimeline = false,
this.benchmarkScoreKeys = _kCommonScoreKeys,
this.dartDefine = '',
String resultFilename = 'e2e_perf_summary',
this.device,
this.flutterDriveCallback,
this.timeoutSeconds,
this.enableImpeller,
this.forceOpenGLES,
this.disablePartialRepaint = false,
this.enableMergedPlatformThread = false,
this.enableSurfaceControl = false,
this.enableLazyShaderMode = false,
this.createPlatforms = const <String>[],
}) : saveTraceFile = false,
timelineFileName = null,
_resultFilename = resultFilename;
/// The directory where the app under test is defined.
final String testDirectory;
/// The main entry-point file of the application, as run on the device.
final String testTarget;
// The prefix name of the filename such as `<timelineFileName>.timeline_summary.json`.
final String? timelineFileName;
String get traceFilename => '$timelineFileName.timeline';
String get resultFilename => _resultFilename ?? '$timelineFileName.timeline_summary';
final String? _resultFilename;
/// The test file to run on the host.
final String? testDriver;
/// Whether to collect CPU and GPU metrics.
final bool measureCpuGpu;
/// Whether to collect memory metrics.
final bool measureMemory;
/// Whether to summarize total GC time on the UI thread from the timeline.
final bool measureTotalGCTime;
/// Whether to collect full timeline, meaning if `--trace-startup` flag is needed.
final bool needsFullTimeline;
/// Whether to save the trace timeline file `*.timeline.json`.
final bool saveTraceFile;
/// The device to test on.
///
/// If null, the device is selected depending on the current environment.
final Device? device;
/// The function called instead of the actually `flutter drive`.
///
/// If it is not `null`, `flutter drive` will not happen in the PerfTests.
final FlutterDriveCallback? flutterDriveCallback;
/// Whether the perf test should enable Impeller.
final bool? enableImpeller;
/// Whether the perf test force Impeller's OpenGLES backend.
final bool? forceOpenGLES;
/// Whether partial repaint functionality should be disabled (iOS only).
final bool disablePartialRepaint;
/// Whether the UI thread should be the platform thread.
final bool enableMergedPlatformThread;
/// Whether to enable SurfaceControl swapchain.
final bool enableSurfaceControl;
/// Whether to defer construction of all PSO objects in the Impeller backend.
final bool enableLazyShaderMode;
/// Number of seconds to time out the test after, allowing debug callbacks to run.
final int? timeoutSeconds;
/// The keys of the values that need to be reported.
///
/// If it's `null`, then report:
/// ```Dart
/// <String>[
/// 'average_frame_build_time_millis',
/// 'worst_frame_build_time_millis',
/// '90th_percentile_frame_build_time_millis',
/// '99th_percentile_frame_build_time_millis',
/// 'average_frame_rasterizer_time_millis',
/// 'worst_frame_rasterizer_time_millis',
/// '90th_percentile_frame_rasterizer_time_millis',
/// '99th_percentile_frame_rasterizer_time_millis',
/// 'average_vsync_transitions_missed',
/// '90th_percentile_vsync_transitions_missed',
/// '99th_percentile_vsync_transitions_missed',
/// if (measureCpuGpu) 'average_cpu_usage',
/// if (measureCpuGpu) 'average_gpu_usage',
/// ]
/// ```
final List<String>? benchmarkScoreKeys;
/// Additional flags for `--dart-define` to control the test
final String dartDefine;
/// Additional platforms to create with `flutter create` before running
/// the test.
final List<String> createPlatforms;
Future<TaskResult> run() {
return internalRun();
}
@protected
Future<TaskResult> internalRun({String? existingApp}) {
return inDirectory<TaskResult>(testDirectory, () async {
late Device selectedDevice;
selectedDevice = device ?? await devices.workingDevice;
await selectedDevice.unlock();
await selectedDevice.toggleFixedPerformanceMode(true);
final String deviceId = selectedDevice.deviceId;
final String? localEngine = localEngineFromEnv;
final String? localEngineHost = localEngineHostFromEnv;
final String? localEngineSrcPath = localEngineSrcPathFromEnv;
if (createPlatforms.isNotEmpty) {
// Ensure that the platform-specific manifests are freshly created and
// do not contain any settings from previous runs.
await exec('git', <String>['clean', '-f', testDirectory]);
await flutter(
'create',
options: <String>['--platforms', createPlatforms.join(','), '--no-overwrite', '.'],
);
}
bool changedPlist = false;
bool changedManifest = false;
Future<void> resetManifest() async {
if (!changedManifest) {
return;
}
try {
await _resetManifest(testDirectory);
} catch (err) {
print('Caught exception while trying to reset AndroidManifest: $err');
}
}
Future<void> resetPlist() async {
if (!changedPlist) {
return;
}
try {
await _resetPlist(testDirectory);
} catch (err) {
print('Caught exception while trying to reset Info.plist: $err');
}
}
try {
if (enableImpeller ?? false) {
changedManifest = true;
_addVulkanGPUTracingToManifest(testDirectory);
if (forceOpenGLES ?? false) {
_addOpenGLESToManifest(testDirectory);
}
if (enableMergedPlatformThread) {
_addMergedPlatformThreadSupportToManifest(testDirectory);
}
if (enableSurfaceControl) {
_addSurfaceControlSupportToManifest(testDirectory);
}
if (enableLazyShaderMode) {
_addLazyShaderMode(testDirectory);
}
}
if (disablePartialRepaint || enableMergedPlatformThread) {
changedPlist = true;
_updateManifestSettings(
testDirectory,
disablePartialRepaint: disablePartialRepaint,
platformThreadMerged: enableMergedPlatformThread,
);
}
final List<String> options = <String>[
if (localEngine != null) ...<String>['--local-engine', localEngine],
if (localEngineHost != null) ...<String>['--local-engine-host', localEngineHost],
if (localEngineSrcPath != null) ...<String>[
'--local-engine-src-path',
localEngineSrcPath,
],
'--no-android-gradle-daemon',
'-v',
'--verbose-system-logs',
'--profile',
if (timeoutSeconds != null) ...<String>['--timeout', timeoutSeconds.toString()],
if (needsFullTimeline) '--trace-startup', // Enables "endless" timeline event buffering.
'-t', testTarget,
if (testDriver != null) ...<String>['--driver', testDriver!],
if (existingApp != null) ...<String>['--use-existing-app', existingApp],
if (dartDefine.isNotEmpty) ...<String>['--dart-define', dartDefine],
if (enableImpeller != null && enableImpeller!) '--enable-impeller',
if (enableImpeller != null && !enableImpeller!) '--no-enable-impeller',
'-d',
deviceId,
];
if (flutterDriveCallback != null) {
flutterDriveCallback!(options);
} else {
await flutter('drive', options: options);
}
} finally {
await resetManifest();
await resetPlist();
await selectedDevice.toggleFixedPerformanceMode(false);
}
final Map<String, dynamic> data =
json.decode(
file(
'${testOutputDirectory(testDirectory)}/$resultFilename.json',
).readAsStringSync(),
)
as Map<String, dynamic>;
if (data['frame_count'] as int < 5) {
return TaskResult.failure(
'Timeline contains too few frames: ${data['frame_count']}. Possibly '
'trace events are not being captured.',
);
}
final bool recordGPU;
switch (deviceOperatingSystem) {
case DeviceOperatingSystem.ios:
recordGPU = true;
case DeviceOperatingSystem.android:
case DeviceOperatingSystem.androidArm:
case DeviceOperatingSystem.androidArm64:
recordGPU = true;
case DeviceOperatingSystem.fake:
case DeviceOperatingSystem.fuchsia:
case DeviceOperatingSystem.linux:
case DeviceOperatingSystem.macos:
case DeviceOperatingSystem.windows:
recordGPU = false;
}
final bool isAndroid = deviceOperatingSystem == DeviceOperatingSystem.android;
return TaskResult.success(
data,
detailFiles: <String>[
if (saveTraceFile) '${testOutputDirectory(testDirectory)}/$traceFilename.json',
],
benchmarkScoreKeys:
benchmarkScoreKeys ??
<String>[
..._kCommonScoreKeys,
'average_vsync_transitions_missed',
'90th_percentile_vsync_transitions_missed',
'99th_percentile_vsync_transitions_missed',
if (measureCpuGpu && !isAndroid) ...<String>[
// See https://github.com/flutter/flutter/issues/68888
if (data['average_cpu_usage'] != null) 'average_cpu_usage',
if (data['average_gpu_usage'] != null) 'average_gpu_usage',
],
if (measureMemory && !isAndroid) ...<String>[
// See https://github.com/flutter/flutter/issues/68888
if (data['average_memory_usage'] != null) 'average_memory_usage',
if (data['90th_percentile_memory_usage'] != null) '90th_percentile_memory_usage',
if (data['99th_percentile_memory_usage'] != null) '99th_percentile_memory_usage',
],
if (measureTotalGCTime) 'total_ui_gc_time',
if (data['30hz_frame_percentage'] != null) '30hz_frame_percentage',
if (data['60hz_frame_percentage'] != null) '60hz_frame_percentage',
if (data['80hz_frame_percentage'] != null) '80hz_frame_percentage',
if (data['90hz_frame_percentage'] != null) '90hz_frame_percentage',
if (data['120hz_frame_percentage'] != null) '120hz_frame_percentage',
if (data['illegal_refresh_rate_frame_count'] != null)
'illegal_refresh_rate_frame_count',
if (recordGPU) ...<String>[
// GPU Frame Time.
if (data['average_gpu_frame_time'] != null) 'average_gpu_frame_time',
if (data['90th_percentile_gpu_frame_time'] != null)
'90th_percentile_gpu_frame_time',
if (data['99th_percentile_gpu_frame_time'] != null)
'99th_percentile_gpu_frame_time',
if (data['worst_gpu_frame_time'] != null) 'worst_gpu_frame_time',
// GPU Memory.
if (data['average_gpu_memory_mb'] != null) 'average_gpu_memory_mb',
if (data['90th_percentile_gpu_memory_mb'] != null) '90th_percentile_gpu_memory_mb',
if (data['99th_percentile_gpu_memory_mb'] != null) '99th_percentile_gpu_memory_mb',
if (data['worst_gpu_memory_mb'] != null) 'worst_gpu_memory_mb',
],
],
);
});
}
}
const List<String> _kCommonScoreKeys = <String>[
'average_frame_build_time_millis',
'worst_frame_build_time_millis',
'90th_percentile_frame_build_time_millis',
'99th_percentile_frame_build_time_millis',
'average_frame_rasterizer_time_millis',
'worst_frame_rasterizer_time_millis',
'90th_percentile_frame_rasterizer_time_millis',
'99th_percentile_frame_rasterizer_time_millis',
'old_gen_gc_count',
];
/// Measures how long it takes to compile a Flutter app to JavaScript and how
/// big the compiled code is.
class WebCompileTest {
const WebCompileTest();
Future<TaskResult> run() async {
final Map<String, Object> metrics = <String, Object>{};
metrics.addAll(
await runSingleBuildTest(
directory: '${flutterDirectory.path}/examples/hello_world',
metric: 'hello_world',
),
);
metrics.addAll(
await runSingleBuildTest(
directory: '${flutterDirectory.path}/dev/integration_tests/flutter_gallery',
metric: 'flutter_gallery',
),
);
const String sampleAppName = 'sample_flutter_app';
final Directory sampleDir = dir('${Directory.systemTemp.path}/$sampleAppName');
rmTree(sampleDir);
await inDirectory<void>(Directory.systemTemp, () async {
await flutter('create', options: <String>['--template=app', sampleAppName]);
});
metrics.addAll(
await runSingleBuildTest(directory: sampleDir.path, metric: 'basic_material_app'),
);
return TaskResult.success(metrics, benchmarkScoreKeys: metrics.keys.toList());
}
/// Run a single web compile test and return its metrics.
///
/// Run a single web compile test for the app under [directory], and store
/// its metrics with prefix [metric].
static Future<Map<String, int>> runSingleBuildTest({
required String directory,
required String metric,
bool measureBuildTime = false,
}) {
return inDirectory<Map<String, int>>(directory, () async {
final Map<String, int> metrics = <String, int>{};
await flutter('clean');
await flutter('packages', options: <String>['get']);
final Stopwatch? watch = measureBuildTime ? Stopwatch() : null;
watch?.start();
await evalFlutter(
'build',
options: <String>['web', '-v', '--release', '--no-pub', '--no-web-resources-cdn'],
);
watch?.stop();
final String buildDir = path.join(directory, 'build', 'web');
metrics.addAll(
await getSize(
directories: <String, String>{'web_build_dir': buildDir},
files: <String, String>{
'dart2js': path.join(buildDir, 'main.dart.js'),
'canvaskit_wasm': path.join(buildDir, 'canvaskit', 'canvaskit.wasm'),
'canvaskit_js': path.join(buildDir, 'canvaskit', 'canvaskit.js'),
'flutter_js': path.join(buildDir, 'flutter.js'),
},
metric: metric,
),
);
if (measureBuildTime) {
metrics['${metric}_dart2js_millis'] = watch!.elapsedMilliseconds;
}
return metrics;
});
}
/// Obtains the size and gzipped size of both [dartBundleFile] and [buildDir].
static Future<Map<String, int>> getSize({
/// Mapping of metric key name to file system path for directories to measure
Map<String, String> directories = const <String, String>{},
/// Mapping of metric key name to file system path for files to measure
Map<String, String> files = const <String, String>{},
required String metric,
}) async {
const String kGzipCompressionLevel = '-9';
final Map<String, int> sizeMetrics = <String, int>{};
final Directory tempDir = Directory.systemTemp.createTempSync('perf_tests_gzips');
try {
for (final MapEntry<String, String> entry in files.entries) {
final String key = entry.key;
final String filePath = entry.value;
sizeMetrics['${metric}_${key}_uncompressed_bytes'] = File(filePath).lengthSync();
await Process.run('gzip', <String>['--keep', kGzipCompressionLevel, filePath]);
// gzip does not provide a CLI option to specify an output file, so
// instead just move the output file to the temp dir
final File compressedFile = File(
'$filePath.gz',
).renameSync(path.join(tempDir.absolute.path, '$key.gz'));
sizeMetrics['${metric}_${key}_compressed_bytes'] = compressedFile.lengthSync();
}
for (final MapEntry<String, String> entry in directories.entries) {
final String key = entry.key;
final String dirPath = entry.value;
final String tarball = path.join(tempDir.absolute.path, '$key.tar');
await Process.run('tar', <String>['--create', '--verbose', '--file=$tarball', dirPath]);
sizeMetrics['${metric}_${key}_uncompressed_bytes'] = File(tarball).lengthSync();
// get size of compressed build directory
await Process.run('gzip', <String>['--keep', kGzipCompressionLevel, tarball]);
sizeMetrics['${metric}_${key}_compressed_bytes'] = File('$tarball.gz').lengthSync();
}
} finally {
try {
tempDir.deleteSync(recursive: true);
} on FileSystemException {
print('Failed to delete ${tempDir.path}.');
}
}
return sizeMetrics;
}
}
/// Measures how long it takes to compile a Flutter app and how big the compiled
/// code is.
class CompileTest {
const CompileTest(this.testDirectory, {this.reportPackageContentSizes = false});
final String testDirectory;
final bool reportPackageContentSizes;
Future<TaskResult> run() async {
return inDirectory<TaskResult>(testDirectory, () async {
await flutter('packages', options: <String>['get']);
// "initial" compile required downloading and creating the `android/.gradle` directory while "full"
// compiles only run `flutter clean` between runs.
final Map<String, dynamic> compileInitialRelease = await _compileApp(deleteGradleCache: true);
final Map<String, dynamic> compileFullRelease = await _compileApp(deleteGradleCache: false);
final Map<String, dynamic> compileInitialDebug = await _compileDebug(
clean: true,
deleteGradleCache: true,
metricKey: 'debug_initial_compile_millis',
);
final Map<String, dynamic> compileFullDebug = await _compileDebug(
clean: true,
deleteGradleCache: false,
metricKey: 'debug_full_compile_millis',
);
// Build again without cleaning, should be faster.
final Map<String, dynamic> compileSecondDebug = await _compileDebug(
clean: false,
deleteGradleCache: false,
metricKey: 'debug_second_compile_millis',
);
final Map<String, dynamic> metrics = <String, dynamic>{
...compileInitialRelease,
...compileFullRelease,
...compileInitialDebug,
...compileFullDebug,
...compileSecondDebug,
};
final File mainDart = File('$testDirectory/lib/main.dart');
if (mainDart.existsSync()) {
final List<int> bytes = mainDart.readAsBytesSync();
// "Touch" the file
mainDart.writeAsStringSync(' ', mode: FileMode.append, flush: true);
// Build after "edit" without clean should be faster than first build
final Map<String, dynamic> compileAfterEditDebug = await _compileDebug(
clean: false,
deleteGradleCache: false,
metricKey: 'debug_compile_after_edit_millis',
);
metrics.addAll(compileAfterEditDebug);
// Revert the changes
mainDart.writeAsBytesSync(bytes, flush: true);
}
return TaskResult.success(metrics, benchmarkScoreKeys: metrics.keys.toList());
});
}
Future<TaskResult> runSwiftUIApp() async {
return inDirectory<TaskResult>(testDirectory, () async {
final Map<String, String> environment = Platform.environment;
final String developmentTeam = environment['FLUTTER_XCODE_DEVELOPMENT_TEAM'] ?? 'S8QB4VV633';
final String? codeSignStyle = environment['FLUTTER_XCODE_CODE_SIGN_STYLE'];
final String? provisioningProfile =
environment['FLUTTER_XCODE_PROVISIONING_PROFILE_SPECIFIER'];
await Process.run('xcodebuild', <String>['clean', '-allTargets']);
int releaseSizeInBytes = 0;
final Stopwatch watch = Stopwatch();
watch.start();
await Process.run(workingDirectory: testDirectory, 'xcodebuild', <String>[
'-allowProvisioningUpdates',
'-scheme',
'hello_world_swiftui',
'-target',
'hello_world_swiftui',
'-sdk',
'iphoneos',
'-configuration',
'Release',
'-archivePath',
'$testDirectory/hello_world_swiftui',
'archive',
'DEVELOPMENT_TEAM=$developmentTeam',
'CODE_SIGN_STYLE=$codeSignStyle',
'PROVISIONING_PROFILE_SPECIFIER=$provisioningProfile',
]).then((ProcessResult results) {
watch.stop();
print(results.stdout);
if (results.exitCode != 0) {
print(results.stderr);
}
});
final String appPath =
'$testDirectory/hello_world_swiftui.xcarchive/Products/Applications/hello_world_swiftui.app';
// Zip up the .app file to get an approximation of the .ipa size.
await exec('tar', <String>['-zcf', 'app.tar.gz', appPath]);
releaseSizeInBytes = await file('$testDirectory/app.tar.gz').length();
final Map<String, dynamic> metrics = <String, dynamic>{};
metrics.addAll(<String, dynamic>{
'release_swiftui_compile_millis': watch.elapsedMilliseconds,
'release_swiftui_size_bytes': releaseSizeInBytes,
});
return TaskResult.success(metrics);
});
}
Future<Map<String, dynamic>> _compileApp({required bool deleteGradleCache}) async {
await flutter('clean');
if (deleteGradleCache) {
final Directory gradleCacheDir = Directory('$testDirectory/android/.gradle');
rmTree(gradleCacheDir);
}
final Stopwatch watch = Stopwatch();
int releaseSizeInBytes;
final List<String> options = <String>['--release'];
final Map<String, dynamic> metrics = <String, dynamic>{};
switch (deviceOperatingSystem) {
case DeviceOperatingSystem.ios:
case DeviceOperatingSystem.macos:
unawaited(stderr.flush());
late final String deviceId;
if (deviceOperatingSystem == DeviceOperatingSystem.ios) {
deviceId = 'ios';
} else if (deviceOperatingSystem == DeviceOperatingSystem.macos) {
deviceId = 'macos';
} else {
throw Exception('Attempted to run darwin compile workflow with $deviceOperatingSystem');
}
options.insert(0, deviceId);
options.add('--tree-shake-icons');
options.add('--split-debug-info=infos/');
watch.start();
await flutter('build', options: options);
watch.stop();
final Directory buildDirectory = dir(path.join(cwd, 'build'));
final String? appPath = _findDarwinAppInBuildDirectory(buildDirectory.path);
if (appPath == null) {
throw 'Failed to find app bundle in ${buildDirectory.path}';
}
// Validate changes in Dart snapshot format and data layout do not
// change compression size. This also simulates the size of an IPA on iOS.
await exec('tar', <String>['-zcf', 'build/app.tar.gz', appPath]);
releaseSizeInBytes = await file('$cwd/build/app.tar.gz').length();
if (reportPackageContentSizes) {
final Map<String, Object> sizeMetrics = await getSizesFromDarwinApp(
appPath: appPath,
operatingSystem: deviceOperatingSystem,
);
metrics.addAll(sizeMetrics);
}
case DeviceOperatingSystem.android:
case DeviceOperatingSystem.androidArm:
options.insert(0, 'apk');
options.add('--target-platform=android-arm');
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));
}
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));
}
case DeviceOperatingSystem.fake:
throw Exception('Unsupported option for fake devices');
case DeviceOperatingSystem.fuchsia:
throw Exception('Unsupported option for Fuchsia devices');
case DeviceOperatingSystem.linux:
throw Exception('Unsupported option for Linux devices');
case DeviceOperatingSystem.windows:
unawaited(stderr.flush());
options.insert(0, 'windows');
options.add('--tree-shake-icons');
options.add('--split-debug-info=infos/');
watch.start();
await flutter('build', options: options);
watch.stop();
final String basename = path.basename(cwd);
final String arch = Abi.current() == Abi.windowsX64 ? 'x64' : 'arm64';
final String exePath = path.join(
cwd,
'build',
'windows',
arch,
'runner',
'release',
'$basename.exe',
);
final File exe = file(exePath);
// On Windows, we do not produce a single installation package file,
// rather a directory containing an .exe and .dll files.
// The release size is set to the size of the produced .exe file
releaseSizeInBytes = exe.lengthSync();
}
metrics.addAll(<String, dynamic>{
'release_${deleteGradleCache ? 'initial' : 'full'}_compile_millis': watch.elapsedMilliseconds,
'release_size_bytes': releaseSizeInBytes,
});
return metrics;
}
Future<Map<String, dynamic>> _compileDebug({
required bool deleteGradleCache,
required bool clean,
required String metricKey,
}) async {
if (clean) {
await flutter('clean');
}
if (deleteGradleCache) {
final Directory gradleCacheDir = Directory('$testDirectory/android/.gradle');
rmTree(gradleCacheDir);
}
final Stopwatch watch = Stopwatch();
final List<String> options = <String>['--debug'];
switch (deviceOperatingSystem) {
case DeviceOperatingSystem.ios:
options.insert(0, 'ios');
case DeviceOperatingSystem.android:
case DeviceOperatingSystem.androidArm:
options.insert(0, 'apk');
options.add('--target-platform=android-arm');
case DeviceOperatingSystem.androidArm64:
options.insert(0, 'apk');
options.add('--target-platform=android-arm64');
case DeviceOperatingSystem.fake:
throw Exception('Unsupported option for fake devices');
case DeviceOperatingSystem.fuchsia:
throw Exception('Unsupported option for Fuchsia devices');
case DeviceOperatingSystem.linux:
throw Exception('Unsupported option for Linux devices');
case DeviceOperatingSystem.macos:
unawaited(stderr.flush());
options.insert(0, 'macos');
case DeviceOperatingSystem.windows:
unawaited(stderr.flush());
options.insert(0, 'windows');
}
watch.start();
await flutter('build', options: options);
watch.stop();
return <String, dynamic>{metricKey: watch.elapsedMilliseconds};
}
static Future<Map<String, Object>> getSizesFromDarwinApp({
required String appPath,
required DeviceOperatingSystem operatingSystem,
}) async {
late final File flutterFramework;
late final String frameworkDirectory;
switch (deviceOperatingSystem) {
case DeviceOperatingSystem.ios:
frameworkDirectory = path.join(appPath, 'Frameworks');
flutterFramework = File(path.join(frameworkDirectory, 'Flutter.framework', 'Flutter'));
case DeviceOperatingSystem.macos:
frameworkDirectory = path.join(appPath, 'Contents', 'Frameworks');
flutterFramework = File(
path.join(frameworkDirectory, 'FlutterMacOS.framework', 'FlutterMacOS'),
); // https://github.com/flutter/flutter/issues/70413
case DeviceOperatingSystem.android:
case DeviceOperatingSystem.androidArm:
case DeviceOperatingSystem.androidArm64:
case DeviceOperatingSystem.fake:
case DeviceOperatingSystem.fuchsia:
case DeviceOperatingSystem.linux:
case DeviceOperatingSystem.windows:
throw Exception('Called ${CompileTest.getSizesFromDarwinApp} with $operatingSystem.');
}
final File appFramework = File(path.join(frameworkDirectory, 'App.framework', 'App'));
return <String, Object>{
'app_framework_uncompressed_bytes': await appFramework.length(),
'flutter_framework_uncompressed_bytes': await flutterFramework.length(),
};
}
static Future<Map<String, dynamic>> getSizesFromApk(String apkPath) async {
final String output = await eval('unzip', <String>['-v', apkPath]);
final List<String> lines = output.split('\n');
final Map<String, _UnzipListEntry> fileToMetadata = <String, _UnzipListEntry>{};
// First three lines are header, last two lines are footer.
for (int i = 3; i < lines.length - 2; i++) {
final _UnzipListEntry entry = _UnzipListEntry.fromLine(lines[i]);
fileToMetadata[entry.path] = entry;
}
final _UnzipListEntry libflutter = fileToMetadata['lib/armeabi-v7a/libflutter.so']!;
final _UnzipListEntry libapp = fileToMetadata['lib/armeabi-v7a/libapp.so']!;
final _UnzipListEntry license = fileToMetadata['assets/flutter_assets/NOTICES.Z']!;
return <String, dynamic>{
'libflutter_uncompressed_bytes': libflutter.uncompressedSize,
'libflutter_compressed_bytes': libflutter.compressedSize,
'libapp_uncompressed_bytes': libapp.uncompressedSize,
'libapp_compressed_bytes': libapp.compressedSize,
'license_uncompressed_bytes': license.uncompressedSize,
'license_compressed_bytes': license.compressedSize,
};
}
}
/// Measure application memory usage.
class MemoryTest {
MemoryTest(this.project, this.test, this.package, {this.requiresTapToStart = false});
final String project;
final String test;
final String package;
final bool requiresTapToStart;
/// 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 = Completer<void>();
}
int get iterationCount => 10;
Device? get device => _device;
Device? _device;
Future<TaskResult> run() {
return inDirectory<TaskResult>(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();
await flutter('packages', options: <String>['get']);
final StreamSubscription<String> adb = device!.logcat.listen((String data) {
if (data.contains('==== MEMORY BENCHMARK ==== $_nextMessage ====')) {
_receivedNextMessage?.complete();
}
});
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 Future<void>.delayed(const Duration(milliseconds: 10));
}
await adb.cancel();
await device!.uninstallApp();
final ListStatistics startMemoryStatistics = ListStatistics(_startMemory);
final ListStatistics endMemoryStatistics = ListStatistics(_endMemory);
final ListStatistics diffMemoryStatistics = ListStatistics(_diffMemory);
final Map<String, dynamic> memoryUsage = <String, dynamic>{
...startMemoryStatistics.asMap('start'),
...endMemoryStatistics.asMap('end'),
...diffMemoryStatistics.asMap('diff'),
};
_device = null;
_startMemory.clear();
_endMemory.clear();
_diffMemory.clear();
return 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;
}
/// Taps the application and looks for acknowledgement.
///
/// This is used by several tests to ensure scrolling gestures are installed.
Future<void> tapNotification() async {
// Keep "tapping" the device till it responds with the string we expect,
// or throw an error instead of tying up the infrastructure for 30 minutes.
prepareForNextMessage('TAPPED');
bool tapped = false;
int tapCount = 0;
await Future.any(<Future<void>>[
() async {
while (true) {
if (tapped) {
break;
}
tapCount += 1;
print('tapping device... [$tapCount]');
await device!.tap(100, 100);
await Future<void>.delayed(const Duration(milliseconds: 100));
}
}(),
() async {
print('awaiting "tapped" message... (timeout: 10 seconds)');
try {
await receivedNextMessage?.timeout(const Duration(seconds: 10));
} finally {
tapped = true;
}
}(),
]);
}
/// To change the behavior 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();
if (requiresTapToStart) {
await tapNotification();
}
prepareForNextMessage('DONE');
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'] as int);
_endMemory.add(endMemoryUsage['total_kb'] as int);
_diffMemory.add((endMemoryUsage['total_kb'] as int) - (_startMemoryUsage!['total_kb'] as int));
}
}
class DevToolsMemoryTest {
DevToolsMemoryTest(this.project, this.driverTest);
final String project;
final String driverTest;
Future<TaskResult> run() {
return inDirectory<TaskResult>(project, () async {
_device = await devices.workingDevice;
await _device.unlock();
await flutter(
'drive',
driveWithDds: true,
options: <String>[
'-d',
_device.deviceId,
'--profile',
'--profile-memory',
_kJsonFileName,
'--no-publish-port',
'-v',
driverTest,
],
);
final Map<String, dynamic> data =
json.decode(file('$project/$_kJsonFileName').readAsStringSync()) as Map<String, dynamic>;
final List<dynamic> samples =
(data['samples'] as Map<String, dynamic>)['data'] as List<dynamic>;
int maxRss = 0;
int maxAdbTotal = 0;
for (final Map<String, dynamic> sample in samples.cast<Map<String, dynamic>>()) {
if (sample['rss'] != null) {
maxRss = math.max(maxRss, sample['rss'] as int);
}
if (sample['adb_memoryInfo'] != null) {
maxAdbTotal = math.max(
maxAdbTotal,
(sample['adb_memoryInfo'] as Map<String, dynamic>)['Total'] as int,
);
}
}
return TaskResult.success(
<String, dynamic>{'maxRss': maxRss, 'maxAdbTotal': maxAdbTotal},
benchmarkScoreKeys: <String>['maxRss', 'maxAdbTotal'],
);
});
}
late Device _device;
static const String _kJsonFileName = 'devtools_memory.json';
}
enum ReportedDurationTestFlavor { debug, profile, release }
String _reportedDurationTestToString(ReportedDurationTestFlavor flavor) {
return switch (flavor) {
ReportedDurationTestFlavor.debug => 'debug',
ReportedDurationTestFlavor.profile => 'profile',
ReportedDurationTestFlavor.release => 'release',
};
}
class ReportedDurationTest {
ReportedDurationTest(this.flavor, this.project, this.test, this.package, this.durationPattern);
final ReportedDurationTestFlavor flavor;
final String project;
final String test;
final String package;
final RegExp durationPattern;
final Completer<int> durationCompleter = Completer<int>();
int get iterationCount => 10;
Device? get device => _device;
Device? _device;
Future<TaskResult> run() {
return inDirectory<TaskResult>(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();
await flutter('packages', options: <String>['get']);
final StreamSubscription<String> adb = device!.logcat.listen((String data) {
if (durationPattern.hasMatch(data)) {
durationCompleter.complete(int.parse(durationPattern.firstMatch(data)!.group(1)!));
}
});
print('launching $project$test on device...');
await flutter(
'run',
options: <String>[
'--verbose',
'--no-publish-port',
'--no-fast-start',
'--${_reportedDurationTestToString(flavor)}',
'--no-resident',
'-d',
device!.deviceId,
test,
],
);
final int duration = await durationCompleter.future;
print('terminating...');
await device!.stop(package);
await adb.cancel();
_device = null;
final Map<String, dynamic> reportedDuration = <String, dynamic>{'duration': duration};
_device = null;
return TaskResult.success(
reportedDuration,
benchmarkScoreKeys: reportedDuration.keys.toList(),
);
});
}
}
/// Holds simple statistics of an odd-lengthed list of integers.
class ListStatistics {
factory ListStatistics(Iterable<int> data) {
assert(data.isNotEmpty);
assert(data.length.isOdd);
final List<int> sortedData = data.toList()..sort();
return ListStatistics._(
sortedData.first,
sortedData.last,
sortedData[(sortedData.length - 1) ~/ 2],
);
}
const ListStatistics._(this.min, this.max, this.median);
final int min;
final int max;
final int median;
Map<String, int> asMap(String prefix) {
return <String, int>{'$prefix-min': min, '$prefix-max': max, '$prefix-median': median};
}
}
class _UnzipListEntry {
factory _UnzipListEntry.fromLine(String line) {
final List<String> data = line.trim().split(RegExp(r'\s+'));
assert(data.length == 8);
return _UnzipListEntry._(
uncompressedSize: int.parse(data[0]),
compressedSize: int.parse(data[2]),
path: data[7],
);
}
_UnzipListEntry._({
required this.uncompressedSize,
required this.compressedSize,
required this.path,
}) : assert(compressedSize <= uncompressedSize);
final int uncompressedSize;
final int compressedSize;
final String path;
}
/// Wait for up to 1 hour for the file to appear.
Future<File> waitForFile(String path) async {
for (int i = 0; i < 180; i += 1) {
final File file = File(path);
print('looking for ${file.path}');
if (file.existsSync()) {
return file;
}
await Future<void>.delayed(const Duration(seconds: 20));
}
throw StateError('Did not find vmservice out file after 1 hour');
}
String? _findDarwinAppInBuildDirectory(String searchDirectory) {
for (final FileSystemEntity entity in Directory(searchDirectory).listSync(recursive: true)) {
if (entity.path.endsWith('.app')) {
return entity.path;
}
}
return null;
}