[integration_test] add support to get timeline (#2947)

diff --git a/packages/integration_test/CHANGELOG.md b/packages/integration_test/CHANGELOG.md
index ee72930..d57819e 100644
--- a/packages/integration_test/CHANGELOG.md
+++ b/packages/integration_test/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.8.2
+
+* Add support to get timeline.
+
 ## 0.8.1
 
 * Show stack trace of widget test errors on the platform side
diff --git a/packages/integration_test/example/test_driver/example_integration_io.dart b/packages/integration_test/example/test_driver/example_integration_io.dart
index 35fc727..7ed2896 100644
--- a/packages/integration_test/example/test_driver/example_integration_io.dart
+++ b/packages/integration_test/example/test_driver/example_integration_io.dart
@@ -13,22 +13,29 @@
 import 'package:integration_test_example/main.dart' as app;
 
 void main() {
-  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+  final IntegrationTestWidgetsFlutterBinding binding =
+      IntegrationTestWidgetsFlutterBinding.ensureInitialized()
+          as IntegrationTestWidgetsFlutterBinding;
   testWidgets('verify text', (WidgetTester tester) async {
     // Build our app and trigger a frame.
     app.main();
 
-    // Trigger a frame.
-    await tester.pumpAndSettle();
+    // Trace the timeline of the following operation. The timeline result will
+    // be written to `build/integration_response_data.json` with the key
+    // `timeline`.
+    await binding.traceAction(() async {
+      // Trigger a frame.
+      await tester.pumpAndSettle();
 
-    // Verify that platform version is retrieved.
-    expect(
-      find.byWidgetPredicate(
-        (Widget widget) =>
-            widget is Text &&
-            widget.data.startsWith('Platform: ${Platform.operatingSystem}'),
-      ),
-      findsOneWidget,
-    );
+      // Verify that platform version is retrieved.
+      expect(
+        find.byWidgetPredicate(
+          (Widget widget) =>
+              widget is Text &&
+              widget.data.startsWith('Platform: ${Platform.operatingSystem}'),
+        ),
+        findsOneWidget,
+      );
+    });
   });
 }
diff --git a/packages/integration_test/lib/integration_test.dart b/packages/integration_test/lib/integration_test.dart
index 430b7ee..f6980bc 100644
--- a/packages/integration_test/lib/integration_test.dart
+++ b/packages/integration_test/lib/integration_test.dart
@@ -3,12 +3,15 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:developer' as developer;
 
 import 'package:flutter/rendering.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
+import 'package:vm_service/vm_service.dart' as vm;
+import 'package:vm_service/vm_service_io.dart' as vm_io;
 
 import 'common.dart';
 import '_extension_io.dart' if (dart.library.html) '_extension_web.dart';
@@ -191,4 +194,92 @@
     );
     results[description] ??= _success;
   }
+
+  vm.VmService _vmService;
+
+  /// Initialize the [vm.VmService] settings for the timeline.
+  @visibleForTesting
+  Future<void> enableTimeline({
+    List<String> streams = const <String>['all'],
+    @visibleForTesting vm.VmService vmService,
+  }) async {
+    assert(streams != null);
+    assert(streams.isNotEmpty);
+    if (vmService != null) {
+      _vmService = vmService;
+    }
+    if (_vmService == null) {
+      final developer.ServiceProtocolInfo info =
+          await developer.Service.getInfo();
+      assert(info.serverUri != null);
+      _vmService = await vm_io.vmServiceConnectUri(
+        'ws://localhost:${info.serverUri.port}${info.serverUri.path}ws',
+      );
+    }
+    await _vmService.setVMTimelineFlags(streams);
+  }
+
+  /// Runs [action] and returns a [vm.Timeline] trace for it.
+  ///
+  /// Waits for the `Future` returned by [action] to complete prior to stopping
+  /// the trace.
+  ///
+  /// The `streams` parameter limits the recorded timeline event streams to only
+  /// the ones listed. By default, all streams are recorded.
+  /// See `timeline_streams` in
+  /// [Dart-SDK/runtime/vm/timeline.cc](https://github.com/dart-lang/sdk/blob/master/runtime/vm/timeline.cc)
+  ///
+  /// If [retainPriorEvents] is true, retains events recorded prior to calling
+  /// [action]. Otherwise, prior events are cleared before calling [action]. By
+  /// default, prior events are cleared.
+  Future<vm.Timeline> traceTimeline(
+    Future<dynamic> action(), {
+    List<String> streams = const <String>['all'],
+    bool retainPriorEvents = false,
+  }) async {
+    await enableTimeline(streams: streams);
+    if (retainPriorEvents) {
+      await action();
+      return await _vmService.getVMTimeline();
+    }
+
+    await _vmService.clearVMTimeline();
+    final vm.Timestamp startTime = await _vmService.getVMTimelineMicros();
+    await action();
+    final vm.Timestamp endTime = await _vmService.getVMTimelineMicros();
+    return await _vmService.getVMTimeline(
+      timeOriginMicros: startTime.timestamp,
+      timeExtentMicros: endTime.timestamp,
+    );
+  }
+
+  /// This is a convenience wrap of [traceTimeline] and send the result back to
+  /// the host for the [flutter_driver] style tests.
+  ///
+  /// This records the timeline during `action` and adds the result to
+  /// [reportData] with `reportKey`. [reportData] contains the extra information
+  /// of the test other than test success/fail. It will be passed back to the
+  /// host and be processed by the [ResponseDataCallback] defined in
+  /// [integrationDriver]. By default it will be written to
+  /// `build/integration_response_data.json` with the key `timeline`.
+  ///
+  /// For tests with multiple calls of this method, `reportKey` needs to be a
+  /// unique key, otherwise the later result will override earlier one.
+  ///
+  /// The `streams` and `retainPriorEvents` parameters are passed as-is to
+  /// [traceTimeline].
+  Future<void> traceAction(
+    Future<dynamic> action(), {
+    List<String> streams = const <String>['all'],
+    bool retainPriorEvents = false,
+    String reportKey = 'timeline',
+  }) async {
+    vm.Timeline timeline = await traceTimeline(
+      action,
+      streams: streams,
+      retainPriorEvents: retainPriorEvents,
+    );
+    reportData ??= <String, dynamic>{};
+    reportData[reportKey] = timeline.toJson();
+  }
 }
diff --git a/packages/integration_test/pubspec.yaml b/packages/integration_test/pubspec.yaml
index c151414..9dd4ade 100644
--- a/packages/integration_test/pubspec.yaml
+++ b/packages/integration_test/pubspec.yaml
@@ -1,6 +1,6 @@
 name: integration_test
 description: Runs tests that use the flutter_test API as integration tests.
-version: 0.8.1
+version: 0.8.2
 homepage: https://github.com/flutter/plugins/tree/master/packages/integration_test
 
 environment:
@@ -15,9 +15,11 @@
   flutter_test:
     sdk: flutter
   path: ^1.6.4
+  vm_service: ^4.2.0
 
 dev_dependencies:
   pedantic: ^1.8.0
+  mockito: ^4.1.1
 
 flutter:
   plugin:
diff --git a/packages/integration_test/test/binding_test.dart b/packages/integration_test/test/binding_test.dart
index bad365a..ef4efc5 100644
--- a/packages/integration_test/test/binding_test.dart
+++ b/packages/integration_test/test/binding_test.dart
@@ -1,8 +1,18 @@
+import 'dart:convert';
+
 import 'package:flutter/material.dart';
 
 import 'package:integration_test/integration_test.dart';
 import 'package:integration_test/common.dart';
 import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:vm_service/vm_service.dart' as vm;
+
+vm.Timeline _ktimelines = vm.Timeline(
+  traceEvents: <vm.TimelineEvent>[],
+  timeOriginMicros: 100,
+  timeExtentMicros: 200,
+);
 
 void main() async {
   Future<Map<String, dynamic>> request;
@@ -14,10 +24,21 @@
     final IntegrationTestWidgetsFlutterBinding integrationBinding =
         binding as IntegrationTestWidgetsFlutterBinding;
 
+    MockVM mockVM;
+    List<int> clockTimes = [100, 200];
+
     setUp(() {
       request = integrationBinding.callback(<String, String>{
         'command': 'request_data',
       });
+      mockVM = MockVM();
+      when(mockVM.getVMTimeline(
+        timeOriginMicros: anyNamed('timeOriginMicros'),
+        timeExtentMicros: anyNamed('timeExtentMicros'),
+      )).thenAnswer((_) => Future.value(_ktimelines));
+      when(mockVM.getVMTimelineMicros()).thenAnswer(
+        (_) => Future.value(vm.Timestamp(timestamp: clockTimes.removeAt(0))),
+      );
     });
 
     testWidgets('Run Integration app', (WidgetTester tester) async {
@@ -53,6 +74,17 @@
       expect(widgetCenter.dx, windowCenterX);
       expect(widgetCenter.dy, windowCenterY);
     });
+
+    testWidgets('Test traceAction', (WidgetTester tester) async {
+      await integrationBinding.enableTimeline(vmService: mockVM);
+      await integrationBinding.traceAction(() async {});
+      expect(integrationBinding.reportData, isNotNull);
+      expect(integrationBinding.reportData.containsKey('timeline'), true);
+      expect(
+        json.encode(integrationBinding.reportData['timeline']),
+        json.encode(_ktimelines),
+      );
+    });
   });
 
   tearDownAll(() async {
@@ -66,3 +98,5 @@
     assert(result.data['answer'] == 42);
   });
 }
+
+class MockVM extends Mock implements vm.VmService {}