[e2e] Add new e2e_driver for handling response data and performance watcher (#2906)
diff --git a/packages/e2e/CHANGELOG.md b/packages/e2e/CHANGELOG.md
index bc844ce..e48b95d 100644
--- a/packages/e2e/CHANGELOG.md
+++ b/packages/e2e/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.6.3
+
+* Add customizable `flutter_driver` adaptor.
+* Add utilities for tracking frame performance in an e2e test.
+
## 0.6.2+1
* Fix incorrect test results when one test passes then another fails
diff --git a/packages/e2e/example/test_driver/example_e2e_test.dart b/packages/e2e/example/test_driver/example_e2e_test.dart
index 983c386..cc3ea15 100644
--- a/packages/e2e/example/test_driver/example_e2e_test.dart
+++ b/packages/e2e/example/test_driver/example_e2e_test.dart
@@ -2,4 +2,4 @@
import 'package:e2e/e2e_driver.dart' as e2e;
-Future<void> main() async => e2e.main();
+Future<void> main() async => e2e.e2eDriver();
diff --git a/packages/e2e/lib/e2e_driver.dart b/packages/e2e/lib/e2e_driver.dart
index 2e43c5a..c33083e 100644
--- a/packages/e2e/lib/e2e_driver.dart
+++ b/packages/e2e/lib/e2e_driver.dart
@@ -1,21 +1,93 @@
+// 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';
import 'dart:io';
-import 'package:e2e/common.dart' as e2e;
import 'package:flutter_driver/flutter_driver.dart';
-Future<void> main() async {
+import 'package:e2e/common.dart' as e2e;
+import 'package:path/path.dart' as path;
+
+/// This method remains for backword compatibility.
+Future<void> main() => e2eDriver();
+
+/// Flutter Driver test output directory.
+///
+/// Tests should write any output files to this directory. Defaults to the path
+/// set in the FLUTTER_TEST_OUTPUTS_DIR environment variable, or `build` if
+/// unset.
+String testOutputsDirectory =
+ Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? 'build';
+
+/// The callback type to handle [e2e.Response.data] after the test succcess.
+typedef ResponseDataCallback = FutureOr<void> Function(Map<String, dynamic>);
+
+/// Writes a json-serializable json data to to
+/// [testOutputsDirectory]/`testOutputFilename.json`.
+///
+/// This is the default `responseDataCallback` in [e2eDriver].
+Future<void> writeResponseData(
+ Map<String, dynamic> data, {
+ String testOutputFilename = 'e2e_response_data',
+ String destinationDirectory,
+}) async {
+ assert(testOutputFilename != null);
+ destinationDirectory ??= testOutputsDirectory;
+ await fs.directory(destinationDirectory).create(recursive: true);
+ final File file = fs.file(path.join(
+ destinationDirectory,
+ '$testOutputFilename.json',
+ ));
+ final String resultString = _encodeJson(data, true);
+ await file.writeAsString(resultString);
+}
+
+/// Adaptor to run E2E test using `flutter drive`.
+///
+/// `timeout` controls the longest time waited before the test ends.
+/// It is not necessarily the execution time for the test app: the test may
+/// finish sooner than the `timeout`.
+///
+/// `responseDataCallback` is the handler for processing [e2e.Response.data].
+/// The default value is `writeResponseData`.
+///
+/// To an E2E test `<test_name>.dart` using `flutter drive`, put a file named
+/// `<test_name>_test.dart` in the app's `test_driver` directory:
+///
+/// ```dart
+/// import 'dart:async';
+///
+/// import 'package:e2e/e2e_driver.dart' as e2e;
+///
+/// Future<void> main() async => e2e.e2eDriver();
+///
+/// ```
+Future<void> e2eDriver({
+ Duration timeout = const Duration(minutes: 1),
+ ResponseDataCallback responseDataCallback = writeResponseData,
+}) async {
final FlutterDriver driver = await FlutterDriver.connect();
- final String jsonResult =
- await driver.requestData(null, timeout: const Duration(minutes: 1));
+ final String jsonResult = await driver.requestData(null, timeout: timeout);
final e2e.Response response = e2e.Response.fromJson(jsonResult);
await driver.close();
if (response.allTestsPassed) {
print('All tests passed.');
+ if (responseDataCallback != null) {
+ await responseDataCallback(response.data);
+ }
exit(0);
} else {
print('Failure Details:\n${response.formattedFailureDetails}');
exit(1);
}
}
+
+const JsonEncoder _prettyEncoder = JsonEncoder.withIndent(' ');
+
+String _encodeJson(Map<String, dynamic> jsonObject, bool pretty) {
+ return pretty ? _prettyEncoder.convert(jsonObject) : json.encode(jsonObject);
+}
diff --git a/packages/e2e/lib/e2e_perf.dart b/packages/e2e/lib/e2e_perf.dart
new file mode 100644
index 0000000..1b2ddd7
--- /dev/null
+++ b/packages/e2e/lib/e2e_perf.dart
@@ -0,0 +1,199 @@
+// 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:ui';
+
+import 'package:flutter/scheduler.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+
+import 'package:e2e/e2e.dart';
+
+/// The maximum amount of time considered safe to spend for a frame's build
+/// phase. Anything past that is in the danger of missing the frame as 60FPS.
+///
+/// Changing this doesn't re-evaluate existing summary.
+Duration kBuildBudget = const Duration(milliseconds: 16);
+// TODO(CareF): Automatically calculate the refresh budget (#61958)
+
+bool _firstRun = true;
+
+/// The warning message to show when a benchmark is performed with assert on.
+/// TODO(CareF) remove this and update pubspect when flutter/flutter#61509 is
+/// in released version.
+const String kDebugWarning = '''
+┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓
+┇ ⚠ THIS BENCHMARK IS BEING RUN IN DEBUG MODE ⚠ ┇
+┡╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┦
+│ │
+│ Numbers obtained from a benchmark while asserts are │
+│ enabled will not accurately reflect the performance │
+│ that will be experienced by end users using release ╎
+│ builds. Benchmarks should be run using this command ╎
+│ line: "flutter run --profile test.dart" or ┊
+│ or "flutter drive --profile -t test.dart". ┊
+│ ┊
+└─────────────────────────────────────────────────╌┄┈ 🐢
+''';
+
+/// watches the [FrameTiming] of `action` and report it to the e2e binding.
+Future<void> watchPerformance(
+ E2EWidgetsFlutterBinding binding,
+ Future<void> action(), {
+ String reportKey = 'performance',
+}) async {
+ assert(() {
+ if (_firstRun) {
+ debugPrint(kDebugWarning);
+ _firstRun = false;
+ }
+ return true;
+ }());
+ final List<FrameTiming> frameTimings = <FrameTiming>[];
+ final TimingsCallback watcher = frameTimings.addAll;
+ binding.addTimingsCallback(watcher);
+ await action();
+ binding.removeTimingsCallback(watcher);
+ final FrameTimingSummarizer frameTimes = FrameTimingSummarizer(frameTimings);
+ binding.reportData = <String, dynamic>{reportKey: frameTimes.summary};
+}
+
+/// This class and summarizes a list of [FrameTiming] for the performance
+/// metrics.
+class FrameTimingSummarizer {
+ /// Summarize `data` to frame build time and frame rasterizer time statistics.
+ ///
+ /// See [TimelineSummary.summaryJson] for detail.
+ factory FrameTimingSummarizer(List<FrameTiming> data) {
+ assert(data != null);
+ assert(data.isNotEmpty);
+ final List<Duration> frameBuildTime = List<Duration>.unmodifiable(
+ data.map<Duration>((FrameTiming datum) => datum.buildDuration),
+ );
+ final List<Duration> frameBuildTimeSorted =
+ List<Duration>.from(frameBuildTime)..sort();
+ final List<Duration> frameRasterizerTime = List<Duration>.unmodifiable(
+ data.map<Duration>((FrameTiming datum) => datum.rasterDuration),
+ );
+ final List<Duration> frameRasterizerTimeSorted =
+ List<Duration>.from(frameRasterizerTime)..sort();
+ final Duration Function(Duration, Duration) add =
+ (Duration a, Duration b) => a + b;
+ return FrameTimingSummarizer._(
+ frameBuildTime: frameBuildTime,
+ frameRasterizerTime: frameRasterizerTime,
+ // This avarage calculation is microsecond precision, which is fine
+ // because typical values of these times are milliseconds.
+ averageFrameBuildTime: frameBuildTime.reduce(add) ~/ data.length,
+ p90FrameBuildTime: _findPercentile(frameBuildTimeSorted, 0.90),
+ p99FrameBuildTime: _findPercentile(frameBuildTimeSorted, 0.99),
+ worstFrameBuildTime: frameBuildTimeSorted.last,
+ missedFrameBuildBudget: _countExceed(frameBuildTimeSorted, kBuildBudget),
+ averageFrameRasterizerTime:
+ frameRasterizerTime.reduce(add) ~/ data.length,
+ p90FrameRasterizerTime: _findPercentile(frameRasterizerTimeSorted, 0.90),
+ p99FrameRasterizerTime: _findPercentile(frameRasterizerTimeSorted, 0.99),
+ worstFrameRasterizerTime: frameRasterizerTimeSorted.last,
+ missedFrameRasterizerBudget:
+ _countExceed(frameRasterizerTimeSorted, kBuildBudget),
+ );
+ }
+
+ const FrameTimingSummarizer._({
+ @required this.frameBuildTime,
+ @required this.frameRasterizerTime,
+ @required this.averageFrameBuildTime,
+ @required this.p90FrameBuildTime,
+ @required this.p99FrameBuildTime,
+ @required this.worstFrameBuildTime,
+ @required this.missedFrameBuildBudget,
+ @required this.averageFrameRasterizerTime,
+ @required this.p90FrameRasterizerTime,
+ @required this.p99FrameRasterizerTime,
+ @required this.worstFrameRasterizerTime,
+ @required this.missedFrameRasterizerBudget,
+ });
+
+ /// List of frame build time in microseconds
+ final List<Duration> frameBuildTime;
+
+ /// List of frame rasterizer time in microseconds
+ final List<Duration> frameRasterizerTime;
+
+ /// The average value of [frameBuildTime] in milliseconds.
+ final Duration averageFrameBuildTime;
+
+ /// The 90-th percentile value of [frameBuildTime] in milliseconds
+ final Duration p90FrameBuildTime;
+
+ /// The 99-th percentile value of [frameBuildTime] in milliseconds
+ final Duration p99FrameBuildTime;
+
+ /// The largest value of [frameBuildTime] in milliseconds
+ final Duration worstFrameBuildTime;
+
+ /// Number of items in [frameBuildTime] that's greater than [kBuildBudget]
+ final int missedFrameBuildBudget;
+
+ /// The average value of [frameRasterizerTime] in milliseconds.
+ final Duration averageFrameRasterizerTime;
+
+ /// The 90-th percentile value of [frameRasterizerTime] in milliseconds.
+ final Duration p90FrameRasterizerTime;
+
+ /// The 99-th percentile value of [frameRasterizerTime] in milliseconds.
+ final Duration p99FrameRasterizerTime;
+
+ /// The largest value of [frameRasterizerTime] in milliseconds.
+ final Duration worstFrameRasterizerTime;
+
+ /// Number of items in [frameRasterizerTime] that's greater than [kBuildBudget]
+ final int missedFrameRasterizerBudget;
+
+ /// Convert the summary result to a json object.
+ ///
+ /// See [TimelineSummary.summaryJson] for detail.
+ Map<String, dynamic> get summary => <String, dynamic>{
+ 'average_frame_build_time_millis':
+ averageFrameBuildTime.inMicroseconds / 1E3,
+ '90th_percentile_frame_build_time_millis':
+ p90FrameBuildTime.inMicroseconds / 1E3,
+ '99th_percentile_frame_build_time_millis':
+ p99FrameBuildTime.inMicroseconds / 1E3,
+ 'worst_frame_build_time_millis':
+ worstFrameBuildTime.inMicroseconds / 1E3,
+ 'missed_frame_build_budget_count': missedFrameBuildBudget,
+ 'average_frame_rasterizer_time_millis':
+ averageFrameRasterizerTime.inMicroseconds / 1E3,
+ '90th_percentile_frame_rasterizer_time_millis':
+ p90FrameRasterizerTime.inMicroseconds / 1E3,
+ '99th_percentile_frame_rasterizer_time_millis':
+ p99FrameRasterizerTime.inMicroseconds / 1E3,
+ 'worst_frame_rasterizer_time_millis':
+ worstFrameRasterizerTime.inMicroseconds / 1E3,
+ 'missed_frame_rasterizer_budget_count': missedFrameRasterizerBudget,
+ 'frame_count': frameBuildTime.length,
+ 'frame_build_times': frameBuildTime
+ .map<int>((Duration datum) => datum.inMicroseconds)
+ .toList(),
+ 'frame_rasterizer_times': frameRasterizerTime
+ .map<int>((Duration datum) => datum.inMicroseconds)
+ .toList(),
+ };
+}
+
+// The following helper functions require data sorted
+
+// return the 100*p-th percentile of the data
+T _findPercentile<T>(List<T> data, double p) {
+ assert(p >= 0 && p <= 1);
+ return data[((data.length - 1) * p).round()];
+}
+
+// return the number of items in data that > threshold
+int _countExceed<T extends Comparable<T>>(List<T> data, T threshold) {
+ return data.length -
+ data.indexWhere((T datum) => datum.compareTo(threshold) > 0);
+}
diff --git a/packages/e2e/pubspec.yaml b/packages/e2e/pubspec.yaml
index e3f39c0..e4e476d 100644
--- a/packages/e2e/pubspec.yaml
+++ b/packages/e2e/pubspec.yaml
@@ -1,6 +1,6 @@
name: e2e
description: Runs tests that use the flutter_test API as integration tests.
-version: 0.6.2+1
+version: 0.6.3
homepage: https://github.com/flutter/plugins/tree/master/packages/e2e
environment:
@@ -14,6 +14,7 @@
sdk: flutter
flutter_test:
sdk: flutter
+ path: ^1.6.4
dev_dependencies:
pedantic: ^1.8.0
diff --git a/packages/e2e/test/frame_timing_summarizer_test.dart b/packages/e2e/test/frame_timing_summarizer_test.dart
new file mode 100644
index 0000000..f3a1285
--- /dev/null
+++ b/packages/e2e/test/frame_timing_summarizer_test.dart
@@ -0,0 +1,35 @@
+import 'dart:ui';
+
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:e2e/e2e_perf.dart';
+
+void main() {
+ test('Test FrameTimingSummarizer', () {
+ List<int> buildTimes = <int>[
+ for (int i = 1; i <= 100; i += 1) 1000 * i,
+ ];
+ buildTimes = buildTimes.reversed.toList();
+ List<int> rasterTimes = <int>[
+ for (int i = 1; i <= 100; i += 1) 1000 * i + 1000,
+ ];
+ rasterTimes = rasterTimes.reversed.toList();
+ List<FrameTiming> inputData = <FrameTiming>[
+ for (int i = 0; i < 100; i += 1)
+ FrameTiming(<int>[0, buildTimes[i], 500, rasterTimes[i]]),
+ ];
+ FrameTimingSummarizer summary = FrameTimingSummarizer(inputData);
+ expect(summary.averageFrameBuildTime.inMicroseconds, 50500);
+ expect(summary.p90FrameBuildTime.inMicroseconds, 90000);
+ expect(summary.p99FrameBuildTime.inMicroseconds, 99000);
+ expect(summary.worstFrameBuildTime.inMicroseconds, 100000);
+ expect(summary.missedFrameBuildBudget, 84);
+
+ expect(summary.averageFrameRasterizerTime.inMicroseconds, 51000);
+ expect(summary.p90FrameRasterizerTime.inMicroseconds, 90500);
+ expect(summary.p99FrameRasterizerTime.inMicroseconds, 99500);
+ expect(summary.worstFrameRasterizerTime.inMicroseconds, 100500);
+ expect(summary.missedFrameRasterizerBudget, 85);
+ expect(summary.frameBuildTime.length, 100);
+ });
+}