| // 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:collection/collection.dart'; | |
| import 'package:meta/meta.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'package:xml/xml.dart'; | |
| import '../framework/android_manifest_utils.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, | |
| enableHcpp: 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({bool? enableImpeller, bool forceOpenGLES = false}) { | |
| return StartupTest( | |
| '${flutterDirectory.path}/dev/benchmarks/complex_layout', | |
| enableImpeller: enableImpeller, | |
| forceOpenGLES: forceOpenGLES, | |
| ).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({bool? enableImpeller}) { | |
| return StartupTest( | |
| '${flutterDirectory.path}/examples/platform_view', | |
| reportMetrics: false, | |
| enableImpeller: enableImpeller, | |
| ).run; | |
| } | |
| TaskFunction createBasicMaterialCompileTest() { | |
| return () async { | |
| const 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; | |
| } | |
| /// Creates a task that runs the text shadow performance benchmark. | |
| TaskFunction createTextShadowPerfTest({ | |
| bool? enableImpeller, | |
| List<String> createPlatforms = const <String>[], | |
| }) { | |
| return PerfTest( | |
| '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', | |
| 'test_driver/run_app.dart', | |
| 'text_shadow_perf', | |
| testDriver: 'test_driver/text_shadow_perf_test.dart', | |
| enableImpeller: enableImpeller, | |
| createPlatforms: createPlatforms, | |
| ).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 testDirectory = '${flutterDirectory.path}/dev/benchmarks/complex_layout'; | |
| const 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 data = json.decode( | |
| file('${testOutputDirectory(testDirectory)}/scroll_smoothness_test.json') | |
| .readAsStringSync(), | |
| ) as Map<String, dynamic>; | |
| final result = <String, dynamic>{}; | |
| void addResult(dynamic data, String suffix) { | |
| assert(data is Map<String, dynamic>); | |
| if (data is Map<String, dynamic>) { | |
| const metricKeys = <String>['janky_count', 'average_abs_jerk', 'dropped_frame_count']; | |
| for (final 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({List<String> createPlatforms = const <String>[]}) { | |
| return PerfTest.e2e( | |
| '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', | |
| 'test/gradient_dynamic_perf_e2e.dart', | |
| createPlatforms: createPlatforms, | |
| ).run; | |
| } | |
| TaskFunction createGradientConsistentPerfE2ETest({ | |
| List<String> createPlatforms = const <String>[], | |
| }) { | |
| return PerfTest.e2e( | |
| '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', | |
| 'test/gradient_consistent_perf_e2e.dart', | |
| createPlatforms: createPlatforms, | |
| ).run; | |
| } | |
| TaskFunction createGradientStaticPerfE2ETest({List<String> createPlatforms = const <String>[]}) { | |
| return PerfTest.e2e( | |
| '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', | |
| 'test/gradient_static_perf_e2e.dart', | |
| createPlatforms: createPlatforms, | |
| ).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; | |
| } | |
| TaskFunction createDrawArcsAllFillStylesPerfTest() { | |
| return PerfTest( | |
| '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', | |
| 'test_driver/run_app.dart', | |
| 'draw_arcs_all_fill_styles_perf', | |
| enableImpeller: true, | |
| testDriver: 'test_driver/draw_arcs_all_fill_styles_perf_test.dart', | |
| saveTraceFile: true, | |
| ).run; | |
| } | |
| TaskFunction createDrawArcsAllStrokeStylesPerfTest() { | |
| return PerfTest( | |
| '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', | |
| 'test_driver/run_app.dart', | |
| 'draw_arcs_all_stroke_styles_perf', | |
| enableImpeller: true, | |
| testDriver: 'test_driver/draw_arcs_all_stroke_styles_perf_test.dart', | |
| saveTraceFile: true, | |
| ).run; | |
| } | |
| Map<String, dynamic> _average(List<Map<String, dynamic>> results, int iterations) { | |
| final tally = <String, dynamic>{}; | |
| for (final 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(manifestPath); | |
| if (!file.existsSync()) { | |
| throw Exception('Info.plist not found at $manifestPath'); | |
| } | |
| final String xmlStr = file.readAsStringSync(); | |
| final xmlDoc = XmlDocument.parse(xmlStr); | |
| final 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(manifestPath); | |
| if (!file.existsSync()) { | |
| throw Exception('Info.plist not found at $manifestPath'); | |
| } | |
| await exec('git', <String>['checkout', file.path]); | |
| } | |
| void _addHcppSupportToManifest(String testDirectory) { | |
| final keyPairs = <(String, String)>[('io.flutter.embedding.android.EnableHcpp', 'true')]; | |
| addMetadataToManifest(testDirectory, keyPairs); | |
| } | |
| void _addMergedPlatformThreadSupportToManifest(String testDirectory) { | |
| final 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 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 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 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(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, | |
| this.enableImpeller, | |
| this.forceOpenGLES = false, | |
| }); | |
| final String testDirectory; | |
| final bool reportMetrics; | |
| final bool enableLazyShaderMode; | |
| final bool enableHcpp; | |
| final bool? enableImpeller; | |
| final bool forceOpenGLES; | |
| 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 iterations = 5; | |
| final results = <Map<String, dynamic>>[]; | |
| if (enableLazyShaderMode) { | |
| _addLazyShaderMode(testDirectory); | |
| } | |
| if (enableHcpp) { | |
| _addHcppSupportToManifest(testDirectory); | |
| } | |
| if ((enableImpeller ?? true) && forceOpenGLES) { | |
| _addOpenGLESToManifest(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.webServer: | |
| break; | |
| case DeviceOperatingSystem.windows: | |
| await flutter( | |
| 'build', | |
| options: <String>['windows', '-v', '--profile', '--target=$target'], | |
| ); | |
| final String basename = path.basename(testDirectory); | |
| final arch = Abi.current() == Abi.windowsX64 ? 'x64' : 'arm64'; | |
| applicationBinaryPath = path.join( | |
| testDirectory, | |
| 'build', | |
| 'windows', | |
| arch, | |
| 'runner', | |
| 'Profile', | |
| '$basename.exe', | |
| ); | |
| } | |
| const maxFailures = 3; | |
| var currentFailures = 0; | |
| for (var 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(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', | |
| if (enableImpeller != null && enableImpeller!) '--enable-impeller', | |
| if (enableImpeller != null && !enableImpeller!) '--no-enable-impeller', | |
| ], | |
| environment: runEnvironment, | |
| canFail: true, | |
| ); | |
| timer.cancel(); | |
| if (result == 0) { | |
| final 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.webServer: | |
| 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 = Completer<void>(); | |
| var 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(); | |
| } | |
| }); | |
| var 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 = '', | |
| this._resultFilename, | |
| this.device, | |
| this.flutterDriveCallback, | |
| this.timeoutSeconds, | |
| this.enableImpeller, | |
| this.forceOpenGLES, | |
| this.disablePartialRepaint = false, | |
| this.enableMergedPlatformThread = false, | |
| this.enableHcpp = false, | |
| this.enableLazyShaderMode = false, | |
| this.createPlatforms = const <String>[], | |
| }); | |
| 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 this._resultFilename = 'e2e_perf_summary', | |
| this.device, | |
| this.flutterDriveCallback, | |
| this.timeoutSeconds, | |
| this.enableImpeller, | |
| this.forceOpenGLES, | |
| this.disablePartialRepaint = false, | |
| this.enableMergedPlatformThread = false, | |
| this.enableHcpp = false, | |
| this.enableLazyShaderMode = false, | |
| this.createPlatforms = const <String>[], | |
| }) : saveTraceFile = false, | |
| timelineFileName = null; | |
| /// 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 and the HCPP platform view backend. | |
| final bool enableHcpp; | |
| /// 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', '.'], | |
| ); | |
| } | |
| var changedPlist = false; | |
| var 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 (enableHcpp) { | |
| _addHcppSupportToManifest(testDirectory); | |
| } | |
| if (enableLazyShaderMode) { | |
| _addLazyShaderMode(testDirectory); | |
| } | |
| } | |
| if (disablePartialRepaint || enableMergedPlatformThread) { | |
| changedPlist = true; | |
| _updateManifestSettings( | |
| testDirectory, | |
| disablePartialRepaint: disablePartialRepaint, | |
| platformThreadMerged: enableMergedPlatformThread, | |
| ); | |
| } | |
| final 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 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.webServer: | |
| case DeviceOperatingSystem.windows: | |
| recordGPU = false; | |
| } | |
| final 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 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 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 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 kGzipCompressionLevel = '-9'; | |
| final 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 metrics = <String, dynamic>{ | |
| ...compileInitialRelease, | |
| ...compileFullRelease, | |
| ...compileInitialDebug, | |
| ...compileFullDebug, | |
| ...compileSecondDebug, | |
| }; | |
| final 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<List<double>> getMetricsFromXCResults(String xcResultsBundleName) async { | |
| return inDirectory<List<double>>(testDirectory, () async { | |
| var resultsJson = <dynamic>[]; | |
| // First, grab the id from the info.plist. | |
| final ProcessResult plistIDResult = await Process.run( | |
| workingDirectory: testDirectory, | |
| 'plutil', | |
| <String>['-extract', 'rootId.hash', 'raw', '-o', '-', '$xcResultsBundleName/Info.plist'], | |
| ); | |
| final String plistID = plistIDResult.stdout.toString().trim(); | |
| // Next, get the ActionsInvocationRecord and Extract the testsRef ID | |
| var testRefID = ''; | |
| await Process.run(workingDirectory: testDirectory, 'xcrun', <String>[ | |
| 'xcresulttool', | |
| 'get', | |
| '--path', | |
| xcResultsBundleName, | |
| '--id', | |
| plistID, | |
| '--format', | |
| 'json', | |
| '--legacy', | |
| ]).then((ProcessResult result) { | |
| final dynamic actionsInvocationRecordJSON = json.decode(result.stdout.toString()); | |
| print(result.stdout); | |
| testRefID = | |
| // ignore: avoid_dynamic_calls | |
| actionsInvocationRecordJSON['actions']['_values'][0]['actionResult']['testsRef']['id']['_value'] | |
| .toString(); | |
| }); | |
| // Next, grab the ActionTestSummary using our testRefID. | |
| var actionTestSummaryID = ''; | |
| await Process.run(workingDirectory: testDirectory, 'xcrun', <String>[ | |
| 'xcresulttool', | |
| 'get', | |
| '--path', | |
| xcResultsBundleName, | |
| '--id', | |
| testRefID, | |
| '--format', | |
| 'json', | |
| '--legacy', | |
| ]).then((ProcessResult result) { | |
| final dynamic actionTestSummaryJSON = json.decode(result.stdout.toString()); | |
| actionTestSummaryID = | |
| // ignore: avoid_dynamic_calls | |
| actionTestSummaryJSON['summaries']['_values'][0]['testableSummaries']['_values'][0]['tests']['_values'][0]['subtests']['_values'][0]['subtests']['_values'][0]['subtests']['_values'][0]['summaryRef']['id']['_value'] | |
| .toString(); | |
| }); | |
| dynamic resultMetricsJSON = ''; | |
| // Finally, grab the metrics. | |
| await Process.run(workingDirectory: testDirectory, 'xcrun', <String>[ | |
| 'xcresulttool', | |
| 'get', | |
| '--path', | |
| xcResultsBundleName, | |
| '--id', | |
| actionTestSummaryID, | |
| '--format', | |
| 'json', | |
| '--legacy', | |
| ]).then((ProcessResult result) { | |
| resultMetricsJSON = json.decode(result.stdout.toString()); | |
| resultsJson = | |
| // ignore: avoid_dynamic_calls | |
| resultMetricsJSON['performanceMetrics']['_values'][0]['measurements']['_values'] | |
| as List<dynamic>; | |
| }); | |
| final extractedLaunchTimes = <double>[]; | |
| resultsJson | |
| .map( | |
| (dynamic item) => extractedLaunchTimes.add( | |
| double.parse((item as Map<String, dynamic>)['_value'] as String), | |
| ), | |
| ) | |
| .toList(); | |
| return extractedLaunchTimes; | |
| }); | |
| } | |
| 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']); | |
| /* Compile Time */ | |
| var releaseSizeInBytes = 0; | |
| final 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); | |
| } | |
| }); | |
| /* App Size */ | |
| final 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(); | |
| /* Time to First Frame */ | |
| const resultBundleName = 'benchmarkResults.xcresult'; | |
| await Process.run(workingDirectory: testDirectory, 'rm', <String>[ | |
| '-rf', | |
| '$testDirectory/$resultBundleName', | |
| ]).then((ProcessResult results) { | |
| print(results.stdout); | |
| }); | |
| // Run the benchmarking tests, and create the xcResults file. | |
| await Process.run(workingDirectory: testDirectory, 'xcodebuild', <String>[ | |
| 'test', | |
| '-project', | |
| 'hello_world_swiftui.xcodeproj', | |
| '-scheme', | |
| 'hello_world_swiftui', | |
| '-destination', | |
| 'platform=iOS', | |
| '-only-testing:BenchmarkTests/BenchmarkTests/testTimeToFirstFrame', | |
| '-resultBundlePath', | |
| '$testDirectory/$resultBundleName', | |
| '-verbose', | |
| 'DEVELOPMENT_TEAM=$developmentTeam', | |
| 'CODE_SIGN_STYLE=$codeSignStyle', | |
| 'PROVISIONING_PROFILE_SPECIFIER=$provisioningProfile', | |
| ]); | |
| final List<double> extractedLaunchTimes = await getMetricsFromXCResults(resultBundleName); | |
| final metrics = <String, dynamic>{}; | |
| metrics.addAll(<String, dynamic>{ | |
| 'release_swiftui_compile_millis': watch.elapsedMilliseconds, | |
| 'release_swiftui_size_bytes': releaseSizeInBytes, | |
| 'time_to_first_frame': extractedLaunchTimes.average, | |
| }); | |
| return TaskResult.success(metrics); | |
| }); | |
| } | |
| Future<Map<String, dynamic>> _compileApp({required bool deleteGradleCache}) async { | |
| await flutter('clean'); | |
| if (deleteGradleCache) { | |
| final gradleCacheDir = Directory('$testDirectory/android/.gradle'); | |
| rmTree(gradleCacheDir); | |
| } | |
| final watch = Stopwatch(); | |
| int releaseSizeInBytes; | |
| final options = <String>['--release']; | |
| final 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 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 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.webServer: | |
| throw Exception('Unsupported option for Web Server 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 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 gradleCacheDir = Directory('$testDirectory/android/.gradle'); | |
| rmTree(gradleCacheDir); | |
| } | |
| final watch = Stopwatch(); | |
| final 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.webServer: | |
| throw Exception('Unsupported option for Web Server devices'); | |
| 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.webServer: | |
| case DeviceOperatingSystem.windows: | |
| throw Exception('Called ${CompileTest.getSizesFromDarwinApp} with $operatingSystem.'); | |
| } | |
| final 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 fileToMetadata = <String, _UnzipListEntry>{}; | |
| // First three lines are header, last two lines are footer. | |
| for (var i = 3; i < lines.length - 2; i++) { | |
| final 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 (var 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 startMemoryStatistics = ListStatistics(_startMemory); | |
| final endMemoryStatistics = ListStatistics(_endMemory); | |
| final diffMemoryStatistics = ListStatistics(_diffMemory); | |
| final 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'); | |
| var tapped = false; | |
| var 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 data = | |
| json.decode(file('$project/$_kJsonFileName').readAsStringSync()) as Map<String, dynamic>; | |
| final samples = (data['samples'] as Map<String, dynamic>)['data'] as List<dynamic>; | |
| var maxRss = 0; | |
| var 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', | |
| '--${_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 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 (var i = 0; i < 180; i += 1) { | |
| final 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; | |
| } |