[flutter_driver] make timeline request in chunks (#58430)

Work-around large timeline data killing devicelab machines by requesting data in 1 second intervals and combining at the end. Non-breaking change to the driver API.
diff --git a/packages/flutter_driver/lib/src/driver/vmservice_driver.dart b/packages/flutter_driver/lib/src/driver/vmservice_driver.dart
index 10859f5..9ce1509 100644
--- a/packages/flutter_driver/lib/src/driver/vmservice_driver.dart
+++ b/packages/flutter_driver/lib/src/driver/vmservice_driver.dart
@@ -370,6 +370,10 @@
         : const <Map<String, dynamic>>[];
   }
 
+  Future<Map<String, Object>> _getVMTimelineMicros() async {
+    return await _peer.sendRequest('getVMTimelineMicros') as Map<String, dynamic>;
+  }
+
   @override
   Future<void> startTracing({
     List<TimelineStream> streams = const <TimelineStream>[TimelineStream.all],
@@ -397,15 +401,43 @@
   @override
   Future<Timeline> stopTracingAndDownloadTimeline({
     Duration timeout = kUnusuallyLongTimeout,
+    int startTime,
+    int endTime,
   }) async {
     assert(timeout != null);
+    assert((startTime == null && endTime == null) ||
+           (startTime != null && endTime != null));
+
     try {
       await _warnIfSlow<void>(
         future: _peer.sendRequest(_setVMTimelineFlagsMethodName, <String, String>{'recordedStreams': '[]'}),
         timeout: timeout,
         message: 'VM is taking an unusually long time to respond to being told to stop tracing...',
       );
-      return Timeline.fromJson(await _peer.sendRequest(_getVMTimelineMethodName) as Map<String, dynamic>);
+      if (startTime == null) {
+        return Timeline.fromJson(await _peer.sendRequest(_getVMTimelineMethodName) as Map<String, dynamic>);
+      }
+      const int kSecondInMicros = 1000000;
+      int currentStart = startTime;
+      int currentEnd = startTime + kSecondInMicros; // 1 second of timeline
+      final List<Map<String, Object>> chunks = <Map<String, Object>>[];
+      do {
+        final Map<String, Object> chunk = await _peer.sendRequest(_getVMTimelineMethodName, <String, Object>{
+          'timeOriginMicros': currentStart,
+          // The range is inclusive, avoid double counting on the chance something
+          // aligns on the boundary.
+          'timeExtentMicros': kSecondInMicros - 1,
+        }) as Map<String, dynamic>;
+        chunks.add(chunk);
+        currentStart = currentEnd;
+        currentEnd += kSecondInMicros;
+      } while (currentStart < endTime);
+      return Timeline.fromJson(<String, Object>{
+        'traceEvents': <Object> [
+          for (Map<String, Object> chunk in chunks)
+            ...chunk['traceEvents'] as List<Object>,
+        ],
+      });
     } catch (error, stackTrace) {
       throw DriverError(
         'Failed to stop tracing due to remote error',
@@ -434,13 +466,19 @@
     if (!retainPriorEvents) {
       await clearTimeline();
     }
+    final Map<String, Object> startTimestamp = await _getVMTimelineMicros();
     await startTracing(streams: streams);
     await action();
+    final Map<String, Object> endTimestamp = await _getVMTimelineMicros();
 
     if (!(await _isPrecompiledMode())) {
       _log(_kDebugWarning);
     }
-    return stopTracingAndDownloadTimeline();
+
+    return stopTracingAndDownloadTimeline(
+      startTime: startTimestamp['timestamp'] as int,
+      endTime: endTimestamp['timestamp'] as int,
+    );
   }
 
   @override
diff --git a/packages/flutter_driver/test/flutter_driver_test.dart b/packages/flutter_driver/test/flutter_driver_test.dart
index b0d66d4..2809d24 100644
--- a/packages/flutter_driver/test/flutter_driver_test.dart
+++ b/packages/flutter_driver/test/flutter_driver_test.dart
@@ -538,6 +538,12 @@
             return null;
           });
 
+        when(mockPeer.sendRequest('getVMTimelineMicros'))
+          .thenAnswer((Invocation invocation) async {
+            log.add('getVMTimelineMicros');
+            return <String, Object>{};
+          });
+
         when(mockPeer.sendRequest('setVMTimelineFlags', argThat(equals(<String, dynamic>{'recordedStreams': '[all]'}))))
           .thenAnswer((Invocation invocation) async {
             log.add('startTracing');
@@ -568,8 +574,10 @@
         }, retainPriorEvents: true);
 
         expect(log, const <String>[
+          'getVMTimelineMicros',
           'startTracing',
           'action',
+          'getVMTimelineMicros',
           'stopTracing',
           'download',
         ]);
@@ -583,13 +591,77 @@
 
         expect(log, const <String>[
           'clear',
+          'getVMTimelineMicros',
           'startTracing',
           'action',
+          'getVMTimelineMicros',
           'stopTracing',
           'download',
         ]);
         expect(timeline.events.single.name, 'test event');
       });
+
+      test('with time interval', () async {
+        int count = 0;
+        when(mockPeer.sendRequest('getVMTimelineMicros'))
+          .thenAnswer((Invocation invocation) async {
+            log.add('getVMTimelineMicros');
+            return <String, Object>{
+              if (count++ == 0)
+                'timestamp': 0
+              else
+                'timestamp': 1000001,
+            };
+          });
+        when(mockPeer.sendRequest('getVMTimeline', argThat(equals(<String, dynamic>{
+          'timeOriginMicros': 0,
+          'timeExtentMicros': 999999
+        }))))
+          .thenAnswer((Invocation invocation) async {
+            log.add('download 1');
+            return <String, dynamic>{
+              'traceEvents': <dynamic>[
+                <String, String>{
+                  'name': 'test event 1',
+                },
+              ],
+            };
+          });
+        when(mockPeer.sendRequest('getVMTimeline', argThat(equals(<String, dynamic>{
+          'timeOriginMicros': 1000000,
+          'timeExtentMicros': 999999,
+        }))))
+          .thenAnswer((Invocation invocation) async {
+            log.add('download 2');
+            return <String, dynamic>{
+              'traceEvents': <dynamic>[
+                <String, String>{
+                  'name': 'test event 2',
+                },
+              ],
+            };
+          });
+
+
+        final Timeline timeline = await driver.traceAction(() async {
+          log.add('action');
+        });
+
+        expect(log, const <String>[
+          'clear',
+          'getVMTimelineMicros',
+          'startTracing',
+          'action',
+          'getVMTimelineMicros',
+          'stopTracing',
+          'download 1',
+          'download 2',
+        ]);
+        expect(timeline.events.map((TimelineEvent event) => event.name), <String>[
+          'test event 1',
+          'test event 2',
+        ]);
+      });
     });
 
     group('traceAction with timeline streams', () {
@@ -598,6 +670,12 @@
         bool startTracingCalled = false;
         bool stopTracingCalled = false;
 
+        when(mockPeer.sendRequest('getVMTimelineMicros'))
+          .thenAnswer((Invocation invocation) async {
+            log.add('getVMTimelineMicros');
+            return <String, Object>{};
+          });
+
         when(mockPeer.sendRequest('setVMTimelineFlags', argThat(equals(<String, dynamic>{'recordedStreams': '[Dart, GC, Compiler]'}))))
           .thenAnswer((Invocation invocation) async {
             startTracingCalled = true;