perf_tests.dart 69.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:convert' show LineSplitter, json, utf8;
7
import 'dart:io';
8
import 'dart:math' as math;
9

10 11
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
12
import 'package:xml/xml.dart';
13

14 15 16 17 18
import '../framework/devices.dart';
import '../framework/framework.dart';
import '../framework/host_agent.dart';
import '../framework/task_result.dart';
import '../framework/utils.dart';
19

20 21 22
/// Must match flutter_driver/lib/src/common.dart.
///
/// Redefined here to avoid taking a dependency on flutter_driver.
Dan Field's avatar
Dan Field committed
23 24 25
String _testOutputDirectory(String testDirectory) {
  return Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? '$testDirectory/build';
}
26

27 28 29
TaskFunction createComplexLayoutScrollPerfTest({
  bool measureCpuGpu = true,
  bool badScroll = false,
30
  bool? enableImpeller,
31
}) {
32
  return PerfTest(
33
    '${flutterDirectory.path}/dev/benchmarks/complex_layout',
34 35 36
    badScroll
      ? 'test_driver/scroll_perf_bad.dart'
      : 'test_driver/scroll_perf.dart',
37
    'complex_layout_scroll_perf',
38
    measureCpuGpu: measureCpuGpu,
39
    enableImpeller: enableImpeller,
40
  ).run;
41 42
}

43
TaskFunction createTilesScrollPerfTest({bool? enableImpeller}) {
44
  return PerfTest(
45 46 47
    '${flutterDirectory.path}/dev/benchmarks/complex_layout',
    'test_driver/scroll_perf.dart',
    'tiles_scroll_perf',
48
    enableImpeller: enableImpeller,
49 50 51
  ).run;
}

52
TaskFunction createUiKitViewScrollPerfTest({bool? enableImpeller}) {
53 54
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/platform_views_layout',
55 56 57
    'test_driver/uikit_view_scroll_perf.dart',
    'platform_views_scroll_perf',
    testDriver: 'test_driver/scroll_perf_test.dart',
58
    needsFullTimeline: false,
59
    enableImpeller: enableImpeller,
60 61 62
  ).run;
}

63
TaskFunction createUiKitViewScrollPerfNonIntersectingTest({bool? enableImpeller}) {
64 65 66 67 68 69 70 71 72 73
  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;
}

74
TaskFunction createAndroidTextureScrollPerfTest({bool? enableImpeller}) {
75 76
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/platform_views_layout',
77
    'test_driver/android_view_scroll_perf.dart',
78
    'platform_views_scroll_perf',
79
    testDriver: 'test_driver/scroll_perf_test.dart',
80 81
    needsFullTimeline: false,
    enableImpeller: enableImpeller,
82 83 84 85 86
  ).run;
}

TaskFunction createAndroidViewScrollPerfTest() {
  return PerfTest(
87
    '${flutterDirectory.path}/dev/benchmarks/platform_views_layout_hybrid_composition',
88
    'test_driver/android_view_scroll_perf.dart',
89
    'platform_views_scroll_perf_hybrid_composition',
90
    testDriver: 'test_driver/scroll_perf_test.dart',
91 92 93
  ).run;
}

94 95
TaskFunction createHomeScrollPerfTest() {
  return PerfTest(
96
    '${flutterDirectory.path}/dev/integration_tests/flutter_gallery',
97 98 99 100 101
    'test_driver/scroll_perf.dart',
    'home_scroll_perf',
  ).run;
}

102 103 104
TaskFunction createCullOpacityPerfTest() {
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
105
    'test_driver/run_app.dart',
106
    'cull_opacity_perf',
107
    testDriver: 'test_driver/cull_opacity_perf_test.dart',
108 109 110
  ).run;
}

111
TaskFunction createCullOpacityPerfE2ETest() {
112
  return PerfTest.e2e(
113 114 115 116 117
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/cull_opacity_perf_e2e.dart',
  ).run;
}

118 119 120
TaskFunction createCubicBezierPerfTest() {
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
121
    'test_driver/run_app.dart',
122
    'cubic_bezier_perf',
123
    testDriver: 'test_driver/cubic_bezier_perf_test.dart',
124
  ).run;
125 126
}

127 128 129 130 131 132 133
TaskFunction createCubicBezierPerfE2ETest() {
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/cubic_bezier_perf_e2e.dart',
  ).run;
}

134 135
TaskFunction createBackdropFilterPerfTest({
    bool measureCpuGpu = true,
136
    bool? enableImpeller,
137
}) {
138 139
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
140
    'test_driver/run_app.dart',
141
    'backdrop_filter_perf',
142
    measureCpuGpu: measureCpuGpu,
143
    testDriver: 'test_driver/backdrop_filter_perf_test.dart',
144
    saveTraceFile: true,
145
    enableImpeller: enableImpeller,
146 147 148
  ).run;
}

149 150 151 152 153 154 155 156 157 158 159
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;
}

160 161 162 163 164 165 166
TaskFunction createBackdropFilterPerfE2ETest() {
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/backdrop_filter_perf_e2e.dart',
  ).run;
}

167
TaskFunction createPostBackdropFilterPerfTest({bool measureCpuGpu = true}) {
168 169
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
170
    'test_driver/run_app.dart',
171
    'post_backdrop_filter_perf',
172
    measureCpuGpu: measureCpuGpu,
173
    testDriver: 'test_driver/post_backdrop_filter_perf_test.dart',
174
    saveTraceFile: true,
175 176 177
  ).run;
}

178 179
TaskFunction createSimpleAnimationPerfTest({
  bool measureCpuGpu = true,
180
  bool? enableImpeller,
181
}) {
182 183
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
184
    'test_driver/run_app.dart',
185
    'simple_animation_perf',
186
    measureCpuGpu: measureCpuGpu,
187
    testDriver: 'test_driver/simple_animation_perf_test.dart',
188
    saveTraceFile: true,
189
    enableImpeller: enableImpeller,
190
  ).run;
191 192
}

193 194 195 196 197 198 199
TaskFunction createAnimatedPlaceholderPerfE2ETest() {
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/animated_placeholder_perf_e2e.dart',
  ).run;
}

200 201 202
TaskFunction createPictureCachePerfTest() {
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
203
    'test_driver/run_app.dart',
204
    'picture_cache_perf',
205
    testDriver: 'test_driver/picture_cache_perf_test.dart',
206 207 208
  ).run;
}

209
TaskFunction createPictureCachePerfE2ETest() {
210
  return PerfTest.e2e(
211 212 213 214 215
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/picture_cache_perf_e2e.dart',
  ).run;
}

216 217 218 219 220 221 222 223 224
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;
}

225 226 227 228 229 230 231 232 233 234 235
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;
}

236
TaskFunction createFlutterGalleryStartupTest({String target = 'lib/main.dart', Map<String, String>? runEnvironment}) {
237
  return StartupTest(
238
    '${flutterDirectory.path}/dev/integration_tests/flutter_gallery',
239
    target: target,
240
    runEnvironment: runEnvironment,
241
  ).run;
242 243
}

244
TaskFunction createComplexLayoutStartupTest() {
245
  return StartupTest(
246
    '${flutterDirectory.path}/dev/benchmarks/complex_layout',
247
  ).run;
248 249
}

250
TaskFunction createFlutterGalleryCompileTest() {
251
  return CompileTest('${flutterDirectory.path}/dev/integration_tests/flutter_gallery').run;
252 253
}

254
TaskFunction createHelloWorldCompileTest() {
255
  return CompileTest('${flutterDirectory.path}/examples/hello_world', reportPackageContentSizes: true).run;
256 257
}

258 259 260 261
TaskFunction createWebCompileTest() {
  return const WebCompileTest().run;
}

262
TaskFunction createFlutterViewStartupTest() {
263
  return StartupTest(
264 265
      '${flutterDirectory.path}/examples/flutter_view',
      reportMetrics: false,
266 267 268
  ).run;
}

269
TaskFunction createPlatformViewStartupTest() {
270
  return StartupTest(
271 272 273 274 275
    '${flutterDirectory.path}/examples/platform_view',
    reportMetrics: false,
  ).run;
}

276 277 278 279 280
TaskFunction createBasicMaterialCompileTest() {
  return () async {
    const String sampleAppName = 'sample_flutter_app';
    final Directory sampleDir = dir('${Directory.systemTemp.path}/$sampleAppName');

281
    rmTree(sampleDir);
282

283
    await inDirectory<void>(Directory.systemTemp, () async {
284
      await flutter('create', options: <String>['--template=app', sampleAppName]);
285 286
    });

287
    if (!sampleDir.existsSync()) {
288
      throw 'Failed to create default Flutter app in ${sampleDir.path}';
289
    }
290

291
    return CompileTest(sampleDir.path).run();
292
  };
293 294
}

295 296 297
TaskFunction createTextfieldPerfTest() {
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
298
    'test_driver/run_app.dart',
299
    'textfield_perf',
300
    testDriver: 'test_driver/textfield_perf_test.dart',
301 302 303
  ).run;
}

304 305 306 307 308 309 310
TaskFunction createTextfieldPerfE2ETest() {
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/textfield_perf_e2e.dart',
  ).run;
}

311 312 313 314 315 316 317 318 319
TaskFunction createSlidersPerfTest() {
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test_driver/run_app.dart',
    'sliders_perf',
    testDriver: 'test_driver/sliders_perf_test.dart',
  ).run;
}

320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
TaskFunction createStackSizeTest() {
  final String testDirectory =
      '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks';
  const String testTarget = 'test_driver/run_app.dart';
  const String testDriver = 'test_driver/stack_size_perf_test.dart';
  return () {
    return inDirectory<TaskResult>(testDirectory, () async {
      final Device device = await devices.workingDevice;
      await device.unlock();
      final String deviceId = device.deviceId;
      await flutter('packages', options: <String>['get']);

      await flutter('drive', options: <String>[
        '--no-android-gradle-daemon',
        '-v',
        '--verbose-system-logs',
        '--profile',
        '-t', testTarget,
        '--driver', testDriver,
        '-d',
        deviceId,
      ]);
      final Map<String, dynamic> data = json.decode(
Dan Field's avatar
Dan Field committed
343
        file('${_testOutputDirectory(testDirectory)}/stack_size.json').readAsStringSync(),
344 345 346 347 348 349 350 351 352 353 354 355 356
      ) as Map<String, dynamic>;

      final Map<String, dynamic> result = <String, dynamic>{
        'stack_size_per_nesting_level': data['stack_size'],
      };
      return TaskResult.success(
        result,
        benchmarkScoreKeys: result.keys.toList(),
      );
    });
  };
}

357 358 359 360 361 362 363 364 365
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;
}

366
TaskFunction createFullscreenTextfieldPerfE2ETest({
367
  bool? enableImpeller,
368
}) {
369 370 371
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/fullscreen_textfield_perf_e2e.dart',
372
    enableImpeller: enableImpeller,
373 374 375
  ).run;
}

376 377 378 379 380 381 382
TaskFunction createClipperCachePerfE2ETest() {
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/clipper_cache_perf_e2e.dart',
  ).run;
}

383 384 385
TaskFunction createColorFilterAndFadePerfTest() {
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
386
    'test_driver/run_app.dart',
387
    'color_filter_and_fade_perf',
388
    testDriver: 'test_driver/color_filter_and_fade_perf_test.dart',
389
    saveTraceFile: true,
390 391 392
  ).run;
}

393
TaskFunction createColorFilterAndFadePerfE2ETest({bool? enableImpeller}) {
394 395 396
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/color_filter_and_fade_perf_e2e.dart',
397
    enableImpeller: enableImpeller,
398 399 400
  ).run;
}

401 402 403 404 405 406 407
TaskFunction createColorFilterCachePerfE2ETest() {
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/color_filter_cache_perf_e2e.dart',
  ).run;
}

408 409 410 411 412 413 414
TaskFunction createColorFilterWithUnstableChildPerfE2ETest() {
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/color_filter_with_unstable_child_perf_e2e.dart',
  ).run;
}

415 416 417 418 419 420 421
TaskFunction createRasterCacheUseMemoryPerfE2ETest() {
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/raster_cache_use_memory_perf_e2e.dart',
  ).run;
}

422 423 424 425 426 427 428
TaskFunction createShaderMaskCachePerfE2ETest() {
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/shader_mask_cache_perf_e2e.dart',
  ).run;
}

429 430 431
TaskFunction createFadingChildAnimationPerfTest() {
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
432
    'test_driver/run_app.dart',
433
    'fading_child_animation_perf',
434
    testDriver: 'test_driver/fading_child_animation_perf_test.dart',
435
    saveTraceFile: true,
436 437 438
  ).run;
}

439
TaskFunction createImageFilteredTransformAnimationPerfTest({
440
  bool? enableImpeller,
441
}) {
442 443
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
444
    'test_driver/run_app.dart',
445
    'imagefiltered_transform_animation_perf',
446
    testDriver: 'test_driver/imagefiltered_transform_animation_perf_test.dart',
447
    saveTraceFile: true,
448
    enableImpeller: enableImpeller,
449 450 451
  ).run;
}

452
TaskFunction createsMultiWidgetConstructPerfE2ETest() {
453
  return PerfTest.e2e(
454 455 456 457 458
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/multi_widget_construction_perf_e2e.dart',
  ).run;
}

459
TaskFunction createListTextLayoutPerfE2ETest({bool? enableImpeller}) {
460 461 462 463 464 465 466
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/list_text_layout_perf_e2e.dart',
    enableImpeller: enableImpeller,
  ).run;
}

Yuqian Li's avatar
Yuqian Li committed
467 468 469 470 471 472 473 474 475 476 477 478
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>[
479
        '--no-android-gradle-daemon',
Yuqian Li's avatar
Yuqian Li committed
480 481 482 483 484 485 486 487
        '-v',
        '--verbose-system-logs',
        '--profile',
        '-t', testTarget,
        '-d',
        deviceId,
      ]);
      final Map<String, dynamic> data = json.decode(
Dan Field's avatar
Dan Field committed
488
        file('${_testOutputDirectory(testDirectory)}/scroll_smoothness_test.json').readAsStringSync(),
Yuqian Li's avatar
Yuqian Li committed
489 490 491 492 493
      ) as Map<String, dynamic>;

      final Map<String, dynamic> result = <String, dynamic>{};
      void addResult(dynamic data, String suffix) {
        assert(data is Map<String, dynamic>);
494 495 496 497 498 499 500 501 502
        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];
          }
Yuqian Li's avatar
Yuqian Li committed
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517
        }
      }
      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(),
      );
    });
  };
}

518 519 520 521 522 523 524 525 526 527 528 529
TaskFunction createFramePolicyIntegrationTest() {
  final String testDirectory =
      '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks';
  const String testTarget = 'test/frame_policy.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>[
530
        '--no-android-gradle-daemon',
531 532 533 534 535 536 537 538
        '-v',
        '--verbose-system-logs',
        '--profile',
        '-t', testTarget,
        '-d',
        deviceId,
      ]);
      final Map<String, dynamic> data = json.decode(
Dan Field's avatar
Dan Field committed
539
        file('${_testOutputDirectory(testDirectory)}/frame_policy_event_delay.json').readAsStringSync(),
540 541 542
      ) as Map<String, dynamic>;
      final Map<String, dynamic> fullLiveData = data['fullyLive'] as Map<String, dynamic>;
      final Map<String, dynamic> benchmarkLiveData = data['benchmarkLive'] as Map<String, dynamic>;
543
      final Map<String, dynamic> dataFormatted = <String, dynamic>{
544 545 546 547 548 549 550 551 552 553 554
        'average_delay_fullyLive_millis':
          fullLiveData['average_delay_millis'],
        'average_delay_benchmarkLive_millis':
          benchmarkLiveData['average_delay_millis'],
        '90th_percentile_delay_fullyLive_millis':
          fullLiveData['90th_percentile_delay_millis'],
        '90th_percentile_delay_benchmarkLive_millis':
          benchmarkLiveData['90th_percentile_delay_millis'],
      };

      return TaskResult.success(
555 556
        dataFormatted,
        benchmarkScoreKeys: dataFormatted.keys.toList(),
557 558 559 560 561
      );
    });
  };
}

562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596
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;
}

597 598 599 600 601 602 603 604 605 606 607 608 609 610
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;
}

611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631
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;
}

632 633 634 635 636 637 638 639 640 641 642 643 644 645 646
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;
}

647
TaskFunction createAnimatedBlurBackropFilterPerfTest({
648
  bool? enableImpeller,
649
  bool? forceOpenGLES,
650 651 652 653 654 655
}) {
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test_driver/run_app.dart',
    'animated_blur_backdrop_filter_perf',
    enableImpeller: enableImpeller,
656
    forceOpenGLES: forceOpenGLES,
657 658 659 660 661
    testDriver: 'test_driver/animated_blur_backdrop_filter_perf_test.dart',
    saveTraceFile: true,
  ).run;
}

662 663 664 665 666 667 668 669 670 671 672 673 674
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;
}

675 676 677 678 679 680 681 682
TaskFunction createDrawAtlasPerfTest({
  bool? forceOpenGLES,
}) {
  return PerfTest(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test_driver/run_app.dart',
    'draw_atlas_perf',
    enableImpeller: true,
683
    testDriver: 'test_driver/draw_atlas_perf_test.dart',
684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724
    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;
}

725
TaskFunction createAnimatedComplexOpacityPerfE2ETest({
726
  bool? enableImpeller,
727
}) {
728 729 730
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/animated_complex_opacity_perf_e2e.dart',
731
    enableImpeller: enableImpeller,
732 733 734
  ).run;
}

735
TaskFunction createAnimatedComplexImageFilteredPerfE2ETest({
736
  bool? enableImpeller,
737 738 739 740 741 742 743 744 745
}) {
  return PerfTest.e2e(
    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
    'test/animated_complex_image_filtered_perf_e2e.dart',
    enableImpeller: enableImpeller,
  ).run;
}


746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762
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;
}

763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819
/// 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" />
void _addOpenGLESToManifest(String testDirectory) {
  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);
  const String key = 'io.flutter.embedding.android.ImpellerBackend';
  const String value = 'opengles';

  final XmlElement applicationNode =
      xmlDoc.findAllElements('application').first;

  // Check if the meta-data node already exists.
  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: '    '));
}

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]);
}

820 821
/// Measure application startup performance.
class StartupTest {
822 823 824 825 826 827
  const StartupTest(
    this.testDirectory, {
    this.reportMetrics = true,
    this.target = 'lib/main.dart',
    this.runEnvironment,
  });
828 829

  final String testDirectory;
830
  final bool reportMetrics;
831
  final String target;
832
  final Map<String, String>? runEnvironment;
833

834
  Future<TaskResult> run() async {
835
    return inDirectory<TaskResult>(testDirectory, () async {
836
      final Device device = await devices.workingDevice;
837
      await device.unlock();
838
      const int iterations = 5;
839
      final List<Map<String, dynamic>> results = <Map<String, dynamic>>[];
840 841

      section('Building application');
842
      String? applicationBinaryPath;
843 844 845 846 847 848 849
      switch (deviceOperatingSystem) {
        case DeviceOperatingSystem.android:
          await flutter('build', options: <String>[
            'apk',
            '-v',
            '--profile',
            '--target-platform=android-arm,android-arm64',
850
            '--target=$target',
851 852
          ]);
          applicationBinaryPath = '$testDirectory/build/app/outputs/flutter-apk/app-profile.apk';
853 854 855 856 857 858
        case DeviceOperatingSystem.androidArm:
          await flutter('build', options: <String>[
            'apk',
            '-v',
            '--profile',
            '--target-platform=android-arm',
859
            '--target=$target',
860 861
          ]);
          applicationBinaryPath = '$testDirectory/build/app/outputs/flutter-apk/app-profile.apk';
862 863 864 865 866 867
        case DeviceOperatingSystem.androidArm64:
          await flutter('build', options: <String>[
            'apk',
            '-v',
            '--profile',
            '--target-platform=android-arm64',
868
            '--target=$target',
869 870
          ]);
          applicationBinaryPath = '$testDirectory/build/app/outputs/flutter-apk/app-profile.apk';
871 872
        case DeviceOperatingSystem.fake:
        case DeviceOperatingSystem.fuchsia:
873
        case DeviceOperatingSystem.linux:
874
          break;
875
        case DeviceOperatingSystem.ios:
876
        case DeviceOperatingSystem.macos:
877
          await flutter('build', options: <String>[
878
            if (deviceOperatingSystem == DeviceOperatingSystem.ios) 'ios' else 'macos',
879 880
             '-v',
            '--profile',
881
            '--target=$target',
882
          ]);
883 884
          final String buildRoot = path.join(testDirectory, 'build');
          applicationBinaryPath = _findDarwinAppInBuildDirectory(buildRoot);
885
        case DeviceOperatingSystem.windows:
886 887 888 889 890 891 892 893 894 895 896 897 898 899 900
          await flutter('build', options: <String>[
            'windows',
            '-v',
            '--profile',
            '--target=$target',
          ]);
          final String basename = path.basename(testDirectory);
          applicationBinaryPath = path.join(
            testDirectory,
            'build',
            'windows',
            'runner',
            'Profile',
            '$basename.exe'
          );
901 902
      }

903 904
      const int maxFailures = 3;
      int currentFailures = 0;
905
      for (int i = 0; i < iterations; i += 1) {
906 907 908 909 910 911 912 913 914
        // 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',
          );
        });
915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934
        final int result = await flutter(
          'run',
          options: <String>[
            '--no-android-gradle-daemon',
            '--no-publish-port',
            '--verbose',
            '--profile',
            '--trace-startup',
            // TODO(vashworth): Remove once done debugging https://github.com/flutter/flutter/issues/129836
            if (device is IosDevice)
              '--verbose-system-logs',
            '--target=$target',
            '-d',
            device.deviceId,
            if (applicationBinaryPath != null)
              '--use-application-binary=$applicationBinaryPath',
          ],
          environment: runEnvironment,
          canFail: true,
        );
935
        timer.cancel();
936 937
        if (result == 0) {
          final Map<String, dynamic> data = json.decode(
Dan Field's avatar
Dan Field committed
938
            file('${_testOutputDirectory(testDirectory)}/start_up_info.json').readAsStringSync(),
939 940 941 942
          ) as Map<String, dynamic>;
          results.add(data);
        } else {
          currentFailures += 1;
943 944 945 946
          await _flutterScreenshot(
            device.deviceId,
            'screenshot_startup_failure_$currentFailures.png',
          );
947 948 949 950 951
          i -= 1;
          if (currentFailures == maxFailures) {
            return TaskResult.failure('Application failed to start $maxFailures times');
          }
        }
952 953 954 955 956 957

        await flutter('install', options: <String>[
          '--uninstall-only',
          '-d',
          device.deviceId,
        ]);
958 959 960
      }

      final Map<String, dynamic> averageResults = _average(results, iterations);
961

962
      if (!reportMetrics) {
963
        return TaskResult.success(averageResults);
964
      }
965

966
      return TaskResult.success(averageResults, benchmarkScoreKeys: <String>[
967
        'timeToFirstFrameMicros',
968
        'timeToFirstFrameRasterizedMicros',
969 970 971
      ]);
    });
  }
972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988

  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,
      );
    }
  }
989 990
}

991 992 993 994 995 996 997 998 999 1000 1001
/// 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');
1002
      String? applicationBinaryPath;
1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033
      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',
          ]);
1034
          applicationBinaryPath = _findDarwinAppInBuildDirectory('$testDirectory/build/ios/iphoneos');
1035
        case DeviceOperatingSystem.fake:
1036
        case DeviceOperatingSystem.fuchsia:
1037
        case DeviceOperatingSystem.linux:
1038 1039
        case DeviceOperatingSystem.macos:
        case DeviceOperatingSystem.windows:
1040 1041 1042
          break;
      }

1043
      final Process process = await startFlutter(
1044
        'run',
1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055
        options: <String>[
          '--no-android-gradle-daemon',
          '--no-publish-port',
          '--verbose',
          '--profile',
          '-d',
          device.deviceId,
          if (applicationBinaryPath != null)
            '--use-application-binary=$applicationBinaryPath',
       ],
      );
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084
      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 flutter('install', options: <String>[
        '--uninstall-only',
        '-d',
        device.deviceId,
      ]);

1085
      if (sawLine) {
1086
        return TaskResult.success(null, benchmarkScoreKeys: <String>[]);
1087
      }
1088 1089 1090 1091 1092
      return TaskResult.failure('Did not see line "The Flutter DevTools debugger and profiler" in output');
    });
  }
}

1093 1094 1095 1096 1097
/// 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);

1098 1099 1100
/// Measures application runtime performance, specifically per-frame
/// performance.
class PerfTest {
1101
  const PerfTest(
1102 1103 1104
    this.testDirectory,
    this.testTarget,
    this.timelineFileName, {
1105
    this.measureCpuGpu = true,
1106
    this.measureMemory = true,
1107
    this.measureTotalGCTime = true,
1108
    this.saveTraceFile = false,
1109
    this.testDriver,
1110 1111
    this.needsFullTimeline = true,
    this.benchmarkScoreKeys,
1112
    this.dartDefine = '',
1113
    String? resultFilename,
1114 1115
    this.device,
    this.flutterDriveCallback,
1116
    this.timeoutSeconds,
1117
    this.enableImpeller,
1118
    this.forceOpenGLES,
1119 1120 1121 1122 1123
  }): _resultFilename = resultFilename;

  const PerfTest.e2e(
    this.testDirectory,
    this.testTarget, {
1124 1125
    this.measureCpuGpu = false,
    this.measureMemory = false,
1126
    this.measureTotalGCTime = false,
1127 1128 1129 1130 1131
    this.testDriver =  'test_driver/e2e_test.dart',
    this.needsFullTimeline = false,
    this.benchmarkScoreKeys = _kCommonScoreKeys,
    this.dartDefine = '',
    String resultFilename = 'e2e_perf_summary',
1132 1133
    this.device,
    this.flutterDriveCallback,
1134
    this.timeoutSeconds,
1135
    this.enableImpeller,
1136
    this.forceOpenGLES,
1137
  }) : saveTraceFile = false, timelineFileName = null, _resultFilename = resultFilename;
1138 1139

  /// The directory where the app under test is defined.
1140
  final String testDirectory;
1141
  /// The main entry-point file of the application, as run on the device.
1142
  final String testTarget;
1143
  // The prefix name of the filename such as `<timelineFileName>.timeline_summary.json`.
1144
  final String? timelineFileName;
1145
  String get traceFilename => '$timelineFileName.timeline';
1146
  String get resultFilename => _resultFilename ?? '$timelineFileName.timeline_summary';
1147
  final String? _resultFilename;
1148
  /// The test file to run on the host.
1149
  final String? testDriver;
1150
  /// Whether to collect CPU and GPU metrics.
1151
  final bool measureCpuGpu;
1152 1153
  /// Whether to collect memory metrics.
  final bool measureMemory;
1154 1155
  /// Whether to summarize total GC time on the UI thread from the timeline.
  final bool measureTotalGCTime;
1156 1157
  /// Whether to collect full timeline, meaning if `--trace-startup` flag is needed.
  final bool needsFullTimeline;
1158 1159
  /// Whether to save the trace timeline file `*.timeline.json`.
  final bool saveTraceFile;
1160 1161 1162 1163 1164 1165 1166 1167 1168
  /// 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;
1169

1170
  /// Whether the perf test should enable Impeller.
1171
  final bool? enableImpeller;
1172

1173 1174 1175
  /// Whether the perf test force Impeller's OpenGLES backend.
  final bool? forceOpenGLES;

1176 1177 1178
  /// Number of seconds to time out the test after, allowing debug callbacks to run.
  final int? timeoutSeconds;

1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194
  /// 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',
1195 1196
  ///   if (measureCpuGpu) 'average_cpu_usage',
  ///   if (measureCpuGpu) 'average_gpu_usage',
1197 1198
  /// ]
  /// ```
1199
  final List<String>? benchmarkScoreKeys;
1200

1201 1202 1203
  /// Additional flags for `--dart-define` to control the test
  final String dartDefine;

1204
  Future<TaskResult> run() {
1205 1206 1207 1208 1209
    return internalRun();
  }

  @protected
  Future<TaskResult> internalRun({
1210
      String? existingApp,
1211
  }) {
1212
    return inDirectory<TaskResult>(testDirectory, () async {
1213 1214 1215 1216 1217 1218 1219 1220
      late Device selectedDevice;
      if (device != null) {
        selectedDevice = device!;
      } else {
        selectedDevice = await devices.workingDevice;
      }
      await selectedDevice.unlock();
      final String deviceId = selectedDevice.deviceId;
1221
      final String? localEngine = localEngineFromEnv;
1222
      final String? localEngineHost = localEngineHostFromEnv;
1223
      final String? localEngineSrcPath = localEngineSrcPathFromEnv;
1224

1225 1226 1227 1228 1229 1230 1231 1232 1233 1234
      Future<void> Function()? manifestReset;
      if (forceOpenGLES ?? false) {
        assert(enableImpeller!);
        _addOpenGLESToManifest(testDirectory);
        manifestReset = () => _resetManifest(testDirectory);
      }

      try {
        final List<String> options = <String>[
          if (localEngine != null) ...<String>['--local-engine', localEngine],
1235 1236 1237 1238
          if (localEngineHost != null) ...<String>[
            '--local-engine-host',
            localEngineHost
          ],
1239 1240 1241 1242 1243 1244 1245 1246 1247 1248
          if (localEngineSrcPath != null) ...<String>[
            '--local-engine-src-path',
            localEngineSrcPath
          ],
          '--no-dds',
          '--no-android-gradle-daemon',
          '-v',
          '--verbose-system-logs',
          '--profile',
          if (timeoutSeconds != null) ...<String>[
1249 1250 1251
            '--timeout',
            timeoutSeconds.toString(),
          ],
1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275
          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 {
        if (manifestReset != null) {
          await manifestReset();
        }
1276
      }
1277

1278
      final Map<String, dynamic> data = json.decode(
Dan Field's avatar
Dan Field committed
1279
        file('${_testOutputDirectory(testDirectory)}/$resultFilename.json').readAsStringSync(),
1280
      ) as Map<String, dynamic>;
1281

1282
      if (data['frame_count'] as int < 5) {
1283
        return TaskResult.failure(
1284 1285 1286 1287 1288
          'Timeline contains too few frames: ${data['frame_count']}. Possibly '
          'trace events are not being captured.',
        );
      }

1289 1290 1291
      // TODO(liyuqian): Remove isAndroid restriction once
      // https://github.com/flutter/flutter/issues/61567 is fixed.
      final bool isAndroid = deviceOperatingSystem == DeviceOperatingSystem.android;
1292 1293
      return TaskResult.success(
        data,
1294 1295
        detailFiles: <String>[
          if (saveTraceFile)
Dan Field's avatar
Dan Field committed
1296
            '${_testOutputDirectory(testDirectory)}/$traceFilename.json',
1297
        ],
1298
        benchmarkScoreKeys: benchmarkScoreKeys ?? <String>[
1299
          ..._kCommonScoreKeys,
1300 1301 1302
          'average_vsync_transitions_missed',
          '90th_percentile_vsync_transitions_missed',
          '99th_percentile_vsync_transitions_missed',
1303
          if (measureCpuGpu && !isAndroid) ...<String>[
1304 1305 1306
            // 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',
1307 1308
          ],
          if (measureMemory && !isAndroid) ...<String>[
1309 1310
            // See https://github.com/flutter/flutter/issues/68888
            if (data['average_memory_usage'] != null) 'average_memory_usage',
1311 1312
            if (data['90th_percentile_memory_usage'] != null) '90th_percentile_memory_usage',
            if (data['99th_percentile_memory_usage'] != null) '99th_percentile_memory_usage',
1313
          ],
1314
          if (measureTotalGCTime) 'total_ui_gc_time',
1315 1316 1317 1318 1319 1320
          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',
1321 1322
        ],
      );
1323 1324 1325 1326
    });
  }
}

1327 1328 1329 1330 1331 1332 1333 1334 1335
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',
1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351
  'average_layer_cache_count',
  '90th_percentile_layer_cache_count',
  '99th_percentile_layer_cache_count',
  'worst_layer_cache_count',
  'average_layer_cache_memory',
  '90th_percentile_layer_cache_memory',
  '99th_percentile_layer_cache_memory',
  'worst_layer_cache_memory',
  'average_picture_cache_count',
  '90th_percentile_picture_cache_count',
  '99th_percentile_picture_cache_count',
  'worst_picture_cache_count',
  'average_picture_cache_memory',
  '90th_percentile_picture_cache_memory',
  '99th_percentile_picture_cache_memory',
  'worst_picture_cache_memory',
1352 1353
  'new_gen_gc_count',
  'old_gen_gc_count',
1354
];
1355

1356 1357 1358 1359 1360 1361 1362
/// 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>{};
1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379

    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 {
1380
      await flutter('create', options: <String>['--template=app', sampleAppName]);
1381
    });
1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394

    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].
1395 1396 1397 1398 1399
  static Future<Map<String, int>> runSingleBuildTest({
    required String directory,
    required String metric,
    bool measureBuildTime = false,
  }) {
1400 1401 1402
    return inDirectory<Map<String, int>>(directory, () async {
      final Map<String, int> metrics = <String, int>{};

1403
      await flutter('clean');
1404
      await flutter('packages', options: <String>['get']);
1405
      final Stopwatch? watch = measureBuildTime ? Stopwatch() : null;
1406
      watch?.start();
1407 1408 1409 1410 1411
      await evalFlutter('build', options: <String>[
        'web',
        '-v',
        '--release',
        '--no-pub',
1412
        '--no-web-resources-cdn',
1413
      ]);
1414
      watch?.stop();
1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425
      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,
      ));
1426

1427
      if (measureBuildTime) {
1428
        metrics['${metric}_dart2js_millis'] = watch!.elapsedMilliseconds;
1429
      }
1430

1431
      return metrics;
1432 1433 1434
    });
  }

1435 1436 1437 1438 1439 1440 1441 1442 1443
  /// 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';
1444 1445
    final Map<String, int> sizeMetrics = <String, int>{};

1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459
    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();
      }
1460

1461 1462 1463
      for (final MapEntry<String, String> entry in directories.entries) {
        final String key = entry.key;
        final String dirPath = entry.value;
1464

1465 1466 1467 1468 1469 1470 1471 1472
        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();
1473

1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485
        // 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;
1486 1487 1488
  }
}

1489
/// Measures how long it takes to compile a Flutter app and how big the compiled
1490
/// code is.
1491
class CompileTest {
1492
  const CompileTest(this.testDirectory, { this.reportPackageContentSizes = false });
1493 1494

  final String testDirectory;
1495
  final bool reportPackageContentSizes;
1496

1497
  Future<TaskResult> run() async {
1498
    return inDirectory<TaskResult>(testDirectory, () async {
1499
      await flutter('packages', options: <String>['get']);
1500

1501 1502 1503 1504 1505
      // "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(
1506
        clean: true,
1507 1508 1509 1510 1511 1512
        deleteGradleCache: true,
        metricKey: 'debug_initial_compile_millis',
      );
      final Map<String, dynamic> compileFullDebug = await _compileDebug(
        clean: true,
        deleteGradleCache: false,
1513 1514 1515 1516 1517
        metricKey: 'debug_full_compile_millis',
      );
      // Build again without cleaning, should be faster.
      final Map<String, dynamic> compileSecondDebug = await _compileDebug(
        clean: false,
1518
        deleteGradleCache: false,
1519 1520 1521
        metricKey: 'debug_second_compile_millis',
      );

1522
      final Map<String, dynamic> metrics = <String, dynamic>{
1523 1524 1525 1526
        ...compileInitialRelease,
        ...compileFullRelease,
        ...compileInitialDebug,
        ...compileFullDebug,
1527
        ...compileSecondDebug,
1528
      };
1529

1530 1531 1532 1533 1534 1535 1536 1537
      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,
1538
          deleteGradleCache: false,
1539 1540 1541 1542 1543 1544 1545
          metricKey: 'debug_compile_after_edit_millis',
        );
        metrics.addAll(compileAfterEditDebug);
        // Revert the changes
        mainDart.writeAsBytesSync(bytes, flush: true);
      }

1546
      return TaskResult.success(metrics, benchmarkScoreKeys: metrics.keys.toList());
1547 1548
    });
  }
1549

1550
  Future<Map<String, dynamic>> _compileApp({required bool deleteGradleCache}) async {
1551
    await flutter('clean');
1552 1553 1554 1555
    if (deleteGradleCache) {
      final Directory gradleCacheDir = Directory('$testDirectory/android/.gradle');
      rmTree(gradleCacheDir);
    }
1556
    final Stopwatch watch = Stopwatch();
1557 1558
    int releaseSizeInBytes;
    final List<String> options = <String>['--release'];
1559 1560
    final Map<String, dynamic> metrics = <String, dynamic>{};

Ian Hickson's avatar
Ian Hickson committed
1561 1562
    switch (deviceOperatingSystem) {
      case DeviceOperatingSystem.ios:
1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574
      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);
1575 1576
        options.add('--tree-shake-icons');
        options.add('--split-debug-info=infos/');
Ian Hickson's avatar
Ian Hickson committed
1577 1578 1579
        watch.start();
        await flutter('build', options: options);
        watch.stop();
1580 1581 1582 1583
        final Directory buildDirectory = dir(path.join(
          cwd,
          'build',
        ));
1584
        final String? appPath =
1585
            _findDarwinAppInBuildDirectory(buildDirectory.path);
1586
        if (appPath == null) {
1587
          throw 'Failed to find app bundle in ${buildDirectory.path}';
1588
        }
1589 1590 1591 1592
        // 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();
1593
        if (reportPackageContentSizes) {
1594
          final Map<String, Object> sizeMetrics = await getSizesFromDarwinApp(
1595 1596
            appPath: appPath,
            operatingSystem: deviceOperatingSystem,
1597 1598
          );
          metrics.addAll(sizeMetrics);
1599
        }
Ian Hickson's avatar
Ian Hickson committed
1600
      case DeviceOperatingSystem.android:
1601
      case DeviceOperatingSystem.androidArm:
Ian Hickson's avatar
Ian Hickson committed
1602
        options.insert(0, 'apk');
1603
        options.add('--target-platform=android-arm');
1604
        options.add('--tree-shake-icons');
1605
        options.add('--split-debug-info=infos/');
Ian Hickson's avatar
Ian Hickson committed
1606 1607 1608
        watch.start();
        await flutter('build', options: options);
        watch.stop();
1609 1610
        final String apkPath = '$cwd/build/app/outputs/flutter-apk/app-release.apk';
        final File apk = file(apkPath);
1611
        releaseSizeInBytes = apk.lengthSync();
1612
        if (reportPackageContentSizes) {
1613
          metrics.addAll(await getSizesFromApk(apkPath));
1614
        }
1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625
      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();
1626
        if (reportPackageContentSizes) {
1627
          metrics.addAll(await getSizesFromApk(apkPath));
1628
        }
1629 1630
      case DeviceOperatingSystem.fake:
        throw Exception('Unsupported option for fake devices');
1631 1632
      case DeviceOperatingSystem.fuchsia:
        throw Exception('Unsupported option for Fuchsia devices');
1633 1634
      case DeviceOperatingSystem.linux:
        throw Exception('Unsupported option for Linux devices');
1635
      case DeviceOperatingSystem.windows:
1636 1637 1638 1639 1640 1641 1642
        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();
1643 1644
        final String basename = path.basename(cwd);
        final String exePath = path.join(
1645 1646 1647 1648 1649
          cwd,
          'build',
          'windows',
          'runner',
          'release',
1650 1651
          '$basename.exe');
        final File exe = file(exePath);
1652
        // On Windows, we do not produce a single installation package file,
1653 1654 1655
        // 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();
1656
    }
1657

1658
    metrics.addAll(<String, dynamic>{
1659
      'release_${deleteGradleCache ? 'initial' : 'full'}_compile_millis': watch.elapsedMilliseconds,
1660
      'release_size_bytes': releaseSizeInBytes,
1661 1662 1663
    });

    return metrics;
1664 1665
  }

1666 1667
  Future<Map<String, dynamic>> _compileDebug({
    required bool deleteGradleCache,
1668 1669
    required bool clean,
    required String metricKey,
1670 1671 1672 1673
  }) async {
    if (clean) {
      await flutter('clean');
    }
1674 1675 1676 1677
    if (deleteGradleCache) {
      final Directory gradleCacheDir = Directory('$testDirectory/android/.gradle');
      rmTree(gradleCacheDir);
    }
1678
    final Stopwatch watch = Stopwatch();
Ian Hickson's avatar
Ian Hickson committed
1679 1680 1681 1682 1683
    final List<String> options = <String>['--debug'];
    switch (deviceOperatingSystem) {
      case DeviceOperatingSystem.ios:
        options.insert(0, 'ios');
      case DeviceOperatingSystem.android:
1684
      case DeviceOperatingSystem.androidArm:
Ian Hickson's avatar
Ian Hickson committed
1685
        options.insert(0, 'apk');
1686
        options.add('--target-platform=android-arm');
1687 1688 1689
      case DeviceOperatingSystem.androidArm64:
        options.insert(0, 'apk');
        options.add('--target-platform=android-arm64');
1690 1691
      case DeviceOperatingSystem.fake:
        throw Exception('Unsupported option for fake devices');
1692 1693
      case DeviceOperatingSystem.fuchsia:
        throw Exception('Unsupported option for Fuchsia devices');
1694 1695
      case DeviceOperatingSystem.linux:
        throw Exception('Unsupported option for Linux devices');
1696
      case DeviceOperatingSystem.macos:
1697 1698
        unawaited(stderr.flush());
        options.insert(0, 'macos');
1699
      case DeviceOperatingSystem.windows:
1700 1701
        unawaited(stderr.flush());
        options.insert(0, 'windows');
1702
    }
Ian Hickson's avatar
Ian Hickson committed
1703 1704 1705
    watch.start();
    await flutter('build', options: options);
    watch.stop();
1706 1707

    return <String, dynamic>{
1708
      metricKey: watch.elapsedMilliseconds,
1709 1710 1711
    };
  }

1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744
  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:
1745
      case DeviceOperatingSystem.linux:
1746 1747 1748
      case DeviceOperatingSystem.windows:
        throw Exception('Called ${CompileTest.getSizesFromDarwinApp} with $operatingSystem.');
    }
1749

1750 1751 1752 1753 1754
    final File appFramework = File(path.join(
      frameworkDirectory,
      'App.framework',
      'App',
    ));
1755

1756
    return <String, Object>{
1757 1758 1759 1760 1761
      'app_framework_uncompressed_bytes': await appFramework.length(),
      'flutter_framework_uncompressed_bytes': await flutterFramework.length(),
    };
  }

1762 1763 1764 1765 1766 1767 1768
  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++) {
1769
      final _UnzipListEntry entry = _UnzipListEntry.fromLine(lines[i]);
1770 1771 1772
      fileToMetadata[entry.path] = entry;
    }

1773 1774 1775
    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']!;
1776 1777 1778 1779

    return <String, dynamic>{
      'libflutter_uncompressed_bytes': libflutter.uncompressedSize,
      'libflutter_compressed_bytes': libflutter.compressedSize,
1780 1781
      'libapp_uncompressed_bytes': libapp.uncompressedSize,
      'libapp_compressed_bytes': libapp.compressedSize,
1782 1783
      'license_uncompressed_bytes': license.uncompressedSize,
      'license_compressed_bytes': license.compressedSize,
1784 1785
    };
  }
1786
}
1787

1788
/// Measure application memory usage.
1789
class MemoryTest {
1790 1791 1792 1793 1794 1795 1796 1797
  MemoryTest(this.project, this.test, this.package);

  final String project;
  final String test;
  final String package;

  /// Completes when the log line specified in the last call to
  /// [prepareForNextMessage] is seen by `adb logcat`.
1798 1799 1800
  Future<void>? get receivedNextMessage => _receivedNextMessage?.future;
  Completer<void>? _receivedNextMessage;
  String? _nextMessage;
1801 1802 1803 1804 1805

  /// 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;
1806
    _receivedNextMessage = Completer<void>();
1807
  }
1808

1809
  int get iterationCount => 10;
1810

1811 1812
  Device? get device => _device;
  Device? _device;
1813

1814
  Future<TaskResult> run() {
1815
    return inDirectory<TaskResult>(project, () async {
1816 1817 1818 1819
      // This test currently only works on Android, because device.logcat,
      // device.getMemoryStats, etc, aren't implemented for iOS.

      _device = await devices.workingDevice;
1820
      await device!.unlock();
1821 1822
      await flutter('packages', options: <String>['get']);

1823
      final StreamSubscription<String> adb = device!.logcat.listen(
1824
        (String data) {
1825
          if (data.contains('==== MEMORY BENCHMARK ==== $_nextMessage ====')) {
1826
            _receivedNextMessage?.complete();
1827
          }
1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839
        },
      );

      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...');
1840
        await device!.stop(package);
1841
        await Future<void>.delayed(const Duration(milliseconds: 10));
1842
      }
1843

1844
      await adb.cancel();
1845
      await flutter('install', options: <String>['--uninstall-only', '-d', device!.deviceId]);
1846

1847 1848 1849
      final ListStatistics startMemoryStatistics = ListStatistics(_startMemory);
      final ListStatistics endMemoryStatistics = ListStatistics(_endMemory);
      final ListStatistics diffMemoryStatistics = ListStatistics(_diffMemory);
1850

1851 1852 1853 1854 1855
      final Map<String, dynamic> memoryUsage = <String, dynamic>{
        ...startMemoryStatistics.asMap('start'),
        ...endMemoryStatistics.asMap('end'),
        ...diffMemoryStatistics.asMap('diff'),
      };
1856

1857 1858 1859 1860 1861
      _device = null;
      _startMemory.clear();
      _endMemory.clear();
      _diffMemory.clear();

1862
      return TaskResult.success(memoryUsage, benchmarkScoreKeys: memoryUsage.keys.toList());
1863 1864
    });
  }
1865

1866 1867 1868 1869 1870 1871 1872 1873 1874 1875
  /// 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',
1876
      '-d', device!.deviceId,
1877 1878 1879 1880 1881
      test,
    ]);
    print('awaiting "ready" message...');
    await receivedNextMessage;
  }
1882

1883
  /// To change the behavior of the test, override this.
1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894
  ///
  /// 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();

    prepareForNextMessage('DONE');
    print('tapping device...');
1895
    await device!.tap(100, 100);
1896 1897 1898 1899 1900
    print('awaiting "done" message...');
    await receivedNextMessage;

    await recordEnd();
  }
1901

1902 1903 1904
  final List<int> _startMemory = <int>[];
  final List<int> _endMemory = <int>[];
  final List<int> _diffMemory = <int>[];
1905

1906
  Map<String, dynamic>? _startMemoryUsage;
1907

1908 1909 1910 1911
  @protected
  Future<void> recordStart() async {
    assert(_startMemoryUsage == null);
    print('snapshotting memory usage...');
1912
    _startMemoryUsage = await device!.getMemoryStats(package);
1913
  }
1914

1915 1916 1917 1918
  @protected
  Future<void> recordEnd() async {
    assert(_startMemoryUsage != null);
    print('snapshotting memory usage...');
1919 1920
    final Map<String, dynamic> endMemoryUsage = await device!.getMemoryStats(package);
    _startMemory.add(_startMemoryUsage!['total_kb'] as int);
1921
    _endMemory.add(endMemoryUsage['total_kb'] as int);
1922
    _diffMemory.add((endMemoryUsage['total_kb'] as int) - (_startMemoryUsage!['total_kb'] as int));
1923 1924
  }
}
1925

1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941
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',
        options: <String>[
          '-d', _device.deviceId,
          '--profile',
1942 1943 1944
          '--profile-memory', _kJsonFileName,
          '--no-publish-port',
          '-v',
1945 1946 1947 1948 1949 1950 1951
          driverTest,
        ],
      );

      final Map<String, dynamic> data = json.decode(
        file('$project/$_kJsonFileName').readAsStringSync(),
      ) as Map<String, dynamic>;
1952
      final List<dynamic> samples = (data['samples'] as Map<String, dynamic>)['data'] as List<dynamic>;
1953 1954
      int maxRss = 0;
      int maxAdbTotal = 0;
1955
      for (final Map<String, dynamic> sample in samples.cast<Map<String, dynamic>>()) {
1956 1957 1958
        if (sample['rss'] != null) {
          maxRss = math.max(maxRss, sample['rss'] as int);
        }
1959
        if (sample['adb_memoryInfo'] != null) {
1960
          maxAdbTotal = math.max(maxAdbTotal, (sample['adb_memoryInfo'] as Map<String, dynamic>)['Total'] as int);
1961 1962
        }
      }
1963

1964
      return TaskResult.success(
1965 1966
        <String, dynamic>{'maxRss': maxRss, 'maxAdbTotal': maxAdbTotal},
        benchmarkScoreKeys: <String>['maxRss', 'maxAdbTotal'],
1967 1968 1969 1970
      );
    });
  }

1971
  late Device _device;
1972 1973 1974 1975

  static const String _kJsonFileName = 'devtools_memory.json';
}

1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990
enum ReportedDurationTestFlavor {
  debug, profile, release
}

String _reportedDurationTestToString(ReportedDurationTestFlavor flavor) {
  switch (flavor) {
    case ReportedDurationTestFlavor.debug:
      return 'debug';
    case ReportedDurationTestFlavor.profile:
      return 'profile';
    case ReportedDurationTestFlavor.release:
      return 'release';
  }
}

1991
class ReportedDurationTest {
1992
  ReportedDurationTest(this.flavor, this.project, this.test, this.package, this.durationPattern);
1993

1994
  final ReportedDurationTestFlavor flavor;
1995 1996 1997 1998 1999 2000 2001 2002 2003
  final String project;
  final String test;
  final String package;
  final RegExp durationPattern;

  final Completer<int> durationCompleter = Completer<int>();

  int get iterationCount => 10;

2004 2005
  Device? get device => _device;
  Device? _device;
2006 2007 2008 2009 2010 2011 2012

  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;
2013
      await device!.unlock();
2014 2015
      await flutter('packages', options: <String>['get']);

2016
      final StreamSubscription<String> adb = device!.logcat.listen(
2017
        (String data) {
2018
          if (durationPattern.hasMatch(data)) {
2019
            durationCompleter.complete(int.parse(durationPattern.firstMatch(data)!.group(1)!));
2020
          }
2021 2022 2023 2024 2025
        },
      );
      print('launching $project$test on device...');
      await flutter('run', options: <String>[
        '--verbose',
2026
        '--no-publish-port',
2027
        '--no-fast-start',
2028
        '--${_reportedDurationTestToString(flavor)}',
2029
        '--no-resident',
2030
        '-d', device!.deviceId,
2031 2032 2033 2034 2035
        test,
      ]);

      final int duration = await durationCompleter.future;
      print('terminating...');
2036
      await device!.stop(package);
2037 2038 2039 2040 2041
      await adb.cancel();

      _device = null;

      final Map<String, dynamic> reportedDuration = <String, dynamic>{
2042
        'duration': duration,
2043 2044 2045 2046 2047 2048 2049 2050
      };
      _device = null;

      return TaskResult.success(reportedDuration, benchmarkScoreKeys: reportedDuration.keys.toList());
    });
  }
}

2051 2052 2053 2054
/// Holds simple statistics of an odd-lengthed list of integers.
class ListStatistics {
  factory ListStatistics(Iterable<int> data) {
    assert(data.isNotEmpty);
2055
    assert(data.length.isOdd);
2056
    final List<int> sortedData = data.toList()..sort();
2057
    return ListStatistics._(
2058 2059 2060 2061 2062
      sortedData.first,
      sortedData.last,
      sortedData[(sortedData.length - 1) ~/ 2],
    );
  }
2063

2064
  const ListStatistics._(this.min, this.max, this.median);
2065

2066 2067 2068 2069 2070 2071 2072 2073 2074 2075
  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,
    };
2076 2077
  }
}
2078 2079 2080

class _UnzipListEntry {
  factory _UnzipListEntry.fromLine(String line) {
2081
    final List<String> data = line.trim().split(RegExp(r'\s+'));
2082
    assert(data.length == 8);
2083
    return _UnzipListEntry._(
2084 2085 2086 2087 2088 2089 2090
      uncompressedSize:  int.parse(data[0]),
      compressedSize: int.parse(data[2]),
      path: data[7],
    );
  }

  _UnzipListEntry._({
2091 2092 2093
    required this.uncompressedSize,
    required this.compressedSize,
    required this.path,
2094
  }) : assert(compressedSize <= uncompressedSize);
2095 2096 2097 2098 2099

  final int uncompressedSize;
  final int compressedSize;
  final String path;
}
2100

2101
/// Wait for up to 1 hour for the file to appear.
2102
Future<File> waitForFile(String path) async {
2103
  for (int i = 0; i < 180; i += 1) {
2104 2105 2106 2107 2108 2109 2110
    final File file = File(path);
    print('looking for ${file.path}');
    if (file.existsSync()) {
      return file;
    }
    await Future<void>.delayed(const Duration(seconds: 20));
  }
2111
  throw StateError('Did not find vmservice out file after 1 hour');
2112 2113
}

2114 2115 2116
String? _findDarwinAppInBuildDirectory(String searchDirectory) {
  for (final FileSystemEntity entity in Directory(searchDirectory)
    .listSync(recursive: true)) {
2117 2118 2119 2120 2121 2122
    if (entity.path.endsWith('.app')) {
      return entity.path;
    }
  }
  return null;
}