// 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/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<List<double>> getMetricsFromXCResults(String xcResultsBundleName) async {
    return inDirectory<List<double>>(testDirectory, () async {
      List<dynamic> 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
      String 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.
      String 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 List<double> 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 */
      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);
        }
      });

      /* App Size */
      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();

      /* Time to First Frame */
      const String 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 Map<String, dynamic> 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 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;
}
