Add benchmark for number of GCs in animated GIF (#81240)

diff --git a/dev/benchmarks/macrobenchmarks/lib/common.dart b/dev/benchmarks/macrobenchmarks/lib/common.dart
index c867a62..5665e60 100644
--- a/dev/benchmarks/macrobenchmarks/lib/common.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/common.dart
@@ -21,6 +21,7 @@
 const String kSimpleScrollRouteName = '/simple_scroll';
 const String kStackSizeRouteName = '/stack_size';
 const String kAnimationWithMicrotasksRouteName = '/animation_with_microtasks';
+const String kAnimatedImageRouteName = '/animated_image';
 
 const String kScrollableName = '/macrobenchmark_listview';
 
diff --git a/dev/benchmarks/macrobenchmarks/lib/main.dart b/dev/benchmarks/macrobenchmarks/lib/main.dart
index 34d063a..645c433 100644
--- a/dev/benchmarks/macrobenchmarks/lib/main.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/main.dart
@@ -6,6 +6,7 @@
 
 import 'common.dart';
 
+import 'src/animated_image.dart';
 import 'src/animated_placeholder.dart';
 import 'src/animation_with_microtasks.dart';
 import 'src/backdrop_filter.dart';
@@ -58,6 +59,7 @@
         kSimpleScrollRouteName: (BuildContext context) => const SimpleScroll(),
         kStackSizeRouteName: (BuildContext context) => const StackSizePage(),
         kAnimationWithMicrotasksRouteName: (BuildContext context) => const AnimationWithMicrotasks(),
+        kAnimatedImageRouteName: (BuildContext context) => const AnimatedImagePage(),
       },
     );
   }
@@ -201,6 +203,13 @@
               Navigator.pushNamed(context, kAnimationWithMicrotasksRouteName);
             },
           ),
+          ElevatedButton(
+            key: const Key(kAnimatedImageRouteName),
+            child: const Text('Animated Image'),
+            onPressed: () {
+              Navigator.pushNamed(context, kAnimatedImageRouteName);
+            },
+          ),
         ],
       ),
     );
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/animated_image.dart b/dev/benchmarks/macrobenchmarks/lib/src/animated_image.dart
new file mode 100644
index 0000000..fc68f9e
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/lib/src/animated_image.dart
@@ -0,0 +1,30 @@
+// 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 'package:flutter/material.dart';
+
+class AnimatedImagePage extends StatelessWidget {
+  const AnimatedImagePage({Key key, this.onFrame}) : super(key: key);
+
+  final ValueChanged<int> onFrame;
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Animated Image'),
+      ),
+      body: Image.asset(
+        'animated_images/animated_flutter_lgtm.gif',
+        package: 'flutter_gallery_assets',
+        frameBuilder: (BuildContext context, Widget child, int/*?*/ frame, bool syncCall) {
+          if (onFrame != null && frame != null) {
+            onFrame(frame);
+          }
+          return child;
+        },
+      ),
+    );
+  }
+}
diff --git a/dev/benchmarks/macrobenchmarks/pubspec.yaml b/dev/benchmarks/macrobenchmarks/pubspec.yaml
index 6a6ae82..de31994 100644
--- a/dev/benchmarks/macrobenchmarks/pubspec.yaml
+++ b/dev/benchmarks/macrobenchmarks/pubspec.yaml
@@ -81,6 +81,7 @@
 flutter:
   uses-material-design: true
   assets:
+    - packages/flutter_gallery_assets/animated_images/animated_flutter_lgtm.gif
     - packages/flutter_gallery_assets/food/butternut_squash_soup.png
     - packages/flutter_gallery_assets/food/cherry_pie.png
     - assets/999x1000.png
diff --git a/dev/benchmarks/macrobenchmarks/test_driver/animated_image.dart b/dev/benchmarks/macrobenchmarks/test_driver/animated_image.dart
new file mode 100644
index 0000000..e8b1091
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/test_driver/animated_image.dart
@@ -0,0 +1,35 @@
+// 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 'package:flutter/material.dart';
+import 'package:flutter_driver/driver_extension.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:macrobenchmarks/src/animated_image.dart';
+
+/// This test is slightly different than most of the other tests in this
+/// application, in that it directly instantiates the page we care about and
+/// passes a callback. This way, we can make sure to consistently wait for a
+/// set number of image frames to render.
+Future<void> main() async {
+  final Completer<void> waiter = Completer<void>();
+  enableFlutterDriverExtension(handler: (String request) async {
+    if (request != 'waitForAnimation') {
+      throw UnsupportedError('Unrecognized request $request');
+    }
+    await waiter.future;
+    return 'done';
+  });
+  runApp(MaterialApp(
+    home: AnimatedImagePage(
+      onFrame: (int frameNumber) {
+        if (frameNumber == 250) {
+          waiter.complete();
+        }
+      },
+    ),
+  ));
+}
diff --git a/dev/benchmarks/macrobenchmarks/test_driver/animated_image_test.dart b/dev/benchmarks/macrobenchmarks/test_driver/animated_image_test.dart
new file mode 100644
index 0000000..f0e62af
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/test_driver/animated_image_test.dart
@@ -0,0 +1,24 @@
+// 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 'package:flutter_driver/flutter_driver.dart';
+import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
+
+Future<void> main() async {
+  const String fileName = 'large_image_changer';
+
+  test('Animate for 250 frames', () async {
+    final FlutterDriver driver = await FlutterDriver.connect();
+    await driver.forceGC();
+
+
+    final Timeline timeline = await driver.traceAction(() async {
+      await driver.requestData('waitForAnimation');
+    });
+    final TimelineSummary summary = TimelineSummary.summarize(timeline);
+    await summary.writeTimelineToFile(fileName, pretty: true);
+
+    await driver.close();
+  });
+}
diff --git a/dev/devicelab/bin/tasks/animated_image_gc_perf.dart b/dev/devicelab/bin/tasks/animated_image_gc_perf.dart
new file mode 100644
index 0000000..7371f69
--- /dev/null
+++ b/dev/devicelab/bin/tasks/animated_image_gc_perf.dart
@@ -0,0 +1,16 @@
+// 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 'package:flutter_devicelab/framework/adb.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/framework/utils.dart';
+import 'package:flutter_devicelab/tasks/perf_tests.dart';
+
+Future<void> main() async {
+  deviceOperatingSystem = DeviceOperatingSystem.android;
+  await task(DevToolsMemoryTest(
+    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
+    'test_driver/animated_image.dart',
+  ).run);
+}
diff --git a/dev/integration_tests/flutter_gallery/lib/demo/images_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/images_demo.dart
index a3f45c4..b190b1d 100644
--- a/dev/integration_tests/flutter_gallery/lib/demo/images_demo.dart
+++ b/dev/integration_tests/flutter_gallery/lib/demo/images_demo.dart
@@ -34,7 +34,7 @@
           exampleCodeTag: 'animated_image',
           demoWidget: Semantics(
             label: 'Example of animated GIF',
-            child:Image.asset(
+            child: Image.asset(
               'animated_images/animated_flutter_lgtm.gif',
               package: 'flutter_gallery_assets',
             ),
diff --git a/dev/prod_builders.json b/dev/prod_builders.json
index 9e6a57d..b05b9cd 100644
--- a/dev/prod_builders.json
+++ b/dev/prod_builders.json
@@ -325,6 +325,12 @@
       "flaky": false
     },
     {
+      "name": "Linux animated_image_gc_perf",
+      "repo": "flutter",
+      "task_name": "animated_image_gc_perf",
+      "flaky": true
+    },
+    {
       "name": "Linux linux_chrome_dev_mode",
       "repo": "flutter",
       "task_name": "linux_linux_chrome_dev_mode",
diff --git a/packages/flutter_driver/lib/src/driver/timeline_summary.dart b/packages/flutter_driver/lib/src/driver/timeline_summary.dart
index e781ce2..979d17a 100644
--- a/packages/flutter_driver/lib/src/driver/timeline_summary.dart
+++ b/packages/flutter_driver/lib/src/driver/timeline_summary.dart
@@ -95,6 +95,20 @@
   /// The total number of rasterizer cycles recorded in the timeline.
   int countRasterizations() => _extractGpuRasterizerDrawDurations().length;
 
+  /// The total number of old generation garbage collections recorded in the timeline.
+  int oldGenerationGarbageCollections() {
+    return _timeline.events!.where((TimelineEvent event) {
+      return event.category == 'GC' && event.name == 'CollectOldGeneration';
+    }).length;
+  }
+
+  /// The total number of new generation garbage collections recorded in the timeline.
+  int newGenerationGarbageCollections() {
+    return _timeline.events!.where((TimelineEvent event) {
+      return event.category == 'GC' && event.name == 'CollectNewGeneration';
+    }).length;
+  }
+
   /// Encodes this summary as JSON.
   ///
   /// Data ends with "_time_millis" means time in milliseconds and numbers in
@@ -176,6 +190,8 @@
       'missed_frame_rasterizer_budget_count': computeMissedFrameRasterizerBudgetCount(),
       'frame_count': countFrames(),
       'frame_rasterizer_count': countRasterizations(),
+      'new_gen_gc_count': newGenerationGarbageCollections(),
+      'old_gen_gc_count': oldGenerationGarbageCollections(),
       'frame_build_times': _extractFrameDurations()
           .map<int>((Duration duration) => duration.inMicroseconds)
           .toList(),
diff --git a/packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart b/packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart
index 8a1f53b..a62b204 100644
--- a/packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart
+++ b/packages/flutter_driver/test/src/real_tests/timeline_summary_test.dart
@@ -95,6 +95,38 @@
       'ts': timeStamp,
     };
 
+    List<Map<String, dynamic>> newGenGC(int count) => List<Map<String, dynamic>>.filled(
+      count,
+      <String, dynamic>{
+        'name': 'CollectNewGeneration',
+        'cat': 'GC',
+        'tid': 19695,
+        'pid': 19650,
+        'ts': 358849612473,
+        'tts': 476761,
+        'ph': 'B',
+        'args': <String, dynamic>{
+          'isolateGroupId': 'isolateGroups/10824099774666259225',
+        },
+      },
+    );
+
+    List<Map<String, dynamic>> oldGenGC(int count) => List<Map<String, dynamic>>.filled(
+      count,
+      <String, dynamic>{
+        'name': 'CollectOldGeneration',
+        'cat': 'GC',
+        'tid': 19695,
+        'pid': 19650,
+        'ts': 358849612473,
+        'tts': 476761,
+        'ph': 'B',
+        'args': <String, dynamic>{
+          'isolateGroupId': 'isolateGroups/10824099774666259225',
+        },
+      },
+    );
+
     List<Map<String, dynamic>> rasterizeTimeSequenceInMillis(List<int> sequence) {
       final List<Map<String, dynamic>> result = <Map<String, dynamic>>[];
       int t = 0;
@@ -388,6 +420,8 @@
             begin(1000), end(19000),
             begin(19000), end(29000),
             begin(29000), end(49000),
+            ...newGenGC(4),
+            ...oldGenGC(5),
             frameBegin(1000), frameEnd(18000),
             frameBegin(19000), frameEnd(28000),
             frameBegin(29000), frameEnd(48000),
@@ -405,6 +439,8 @@
             'missed_frame_rasterizer_budget_count': 2,
             'frame_count': 3,
             'frame_rasterizer_count': 3,
+            'new_gen_gc_count': 4,
+            'old_gen_gc_count': 5,
             'frame_build_times': <int>[17000, 9000, 19000],
             'frame_rasterizer_times': <int>[18000, 10000, 20000],
             'frame_begin_times': <int>[0, 18000, 28000],
@@ -475,6 +511,8 @@
           lagBegin(1000, 4), lagEnd(2000, 4),
           lagBegin(1200, 12), lagEnd(2400, 12),
           lagBegin(4200, 8), lagEnd(9400, 8),
+          ...newGenGC(4),
+          ...oldGenGC(5),
           cpuUsage(5000, 20), cpuUsage(5010, 60),
           memoryUsage(6000, 20, 40), memoryUsage(6100, 30, 45),
           platformVsync(7000), vsyncCallback(7500),
@@ -494,6 +532,8 @@
           'missed_frame_rasterizer_budget_count': 2,
           'frame_count': 3,
           'frame_rasterizer_count': 3,
+          'new_gen_gc_count': 4,
+          'old_gen_gc_count': 5,
           'frame_build_times': <int>[17000, 9000, 19000],
           'frame_rasterizer_times': <int>[18000, 10000, 20000],
           'frame_begin_times': <int>[0, 18000, 28000],