Memory test on scrolling large images quickly (#46184)

diff --git a/dev/benchmarks/macrobenchmarks/assets/999x1000.png b/dev/benchmarks/macrobenchmarks/assets/999x1000.png
new file mode 100644
index 0000000..e9b09f7
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/assets/999x1000.png
Binary files differ
diff --git a/dev/benchmarks/macrobenchmarks/lib/common.dart b/dev/benchmarks/macrobenchmarks/lib/common.dart
index d6902eb..c8d24dc 100644
--- a/dev/benchmarks/macrobenchmarks/lib/common.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/common.dart
@@ -7,3 +7,4 @@
 const String kBackdropFilterRouteName = '/backdrop_filter';
 const String kSimpleAnimationRouteName = '/simple_animation';
 const String kPictureCacheRouteName = '/picture_cache';
+const String kLargeImagesRouteName = '/large_images';
diff --git a/dev/benchmarks/macrobenchmarks/lib/main.dart b/dev/benchmarks/macrobenchmarks/lib/main.dart
index eaecea0..5a2160c 100644
--- a/dev/benchmarks/macrobenchmarks/lib/main.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/main.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'package:flutter/material.dart';
+import 'package:macrobenchmarks/src/large_images.dart';
 import 'package:macrobenchmarks/src/picture_cache.dart';
 
 import 'common.dart';
@@ -13,24 +14,29 @@
 
 const String kMacrobenchmarks ='Macrobenchmarks';
 
-void main() => runApp(MacrobenchmarksApp());
+void main() => runApp(const MacrobenchmarksApp());
 
 class MacrobenchmarksApp extends StatelessWidget {
+  const MacrobenchmarksApp({this.initialRoute = '/'});
+
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
       title: kMacrobenchmarks,
-      initialRoute: '/',
+      initialRoute: initialRoute,
       routes: <String, WidgetBuilder>{
         '/': (BuildContext context) => HomePage(),
         kCullOpacityRouteName: (BuildContext context) => CullOpacityPage(),
         kCubicBezierRouteName: (BuildContext context) => CubicBezierPage(),
         kBackdropFilterRouteName: (BuildContext context) => BackdropFilterPage(),
         kSimpleAnimationRouteName: (BuildContext conttext) => SimpleAnimationPage(),
-        kPictureCacheRouteName: (BuildContext conttext) => PictureCachePage(),
+        kPictureCacheRouteName: (BuildContext context) => PictureCachePage(),
+        kLargeImagesRouteName: (BuildContext context) => LargeImagesPage(),
       },
     );
   }
+
+  final String initialRoute;
 }
 
 class HomePage extends StatelessWidget {
@@ -75,6 +81,13 @@
               Navigator.pushNamed(context, kPictureCacheRouteName);
             },
           ),
+          RaisedButton(
+            key: const Key(kLargeImagesRouteName),
+            child: const Text('Large Images'),
+            onPressed: () {
+              Navigator.pushNamed(context, kLargeImagesRouteName);
+            },
+          ),
         ],
       ),
     );
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/large_images.dart b/dev/benchmarks/macrobenchmarks/lib/src/large_images.dart
new file mode 100644
index 0000000..5dd70ff
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/lib/src/large_images.dart
@@ -0,0 +1,48 @@
+// 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:typed_data';
+
+import 'package:flutter/material.dart';
+
+class LargeImagesPage extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    final ImageCache imageCache = PaintingBinding.instance.imageCache;
+    imageCache.maximumSize = 30;
+    imageCache.maximumSizeBytes = 50 << 20;
+    return GridView.builder(
+      itemCount: 1000,
+      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
+      itemBuilder: (BuildContext context, int index) => DummyImage(index),
+    ).build(context);
+  }
+}
+
+class DummyImage extends StatelessWidget {
+  DummyImage(this.index) : super(key: ValueKey<int>(index));
+
+  @override
+  Widget build(BuildContext context) {
+    final Future<ByteData> pngData = _getPngData(context);
+
+    return FutureBuilder<ByteData>(
+      future: pngData,
+      builder: (BuildContext context, AsyncSnapshot<ByteData> snapshot) {
+        // Use Image.memory instead of Image.asset to make sure that we're
+        // creating many copies of the image to trigger the memory issue.
+        return snapshot.data == null
+            ? Container()
+            : Image.memory(snapshot.data.buffer.asUint8List());
+      },
+    );
+  }
+
+  final int index;
+
+  Future<ByteData> _getPngData(BuildContext context) async {
+    return DefaultAssetBundle.of(context).load('assets/999x1000.png');
+  }
+}
diff --git a/dev/benchmarks/macrobenchmarks/pubspec.yaml b/dev/benchmarks/macrobenchmarks/pubspec.yaml
index 62a69cc..471d22c 100644
--- a/dev/benchmarks/macrobenchmarks/pubspec.yaml
+++ b/dev/benchmarks/macrobenchmarks/pubspec.yaml
@@ -90,5 +90,6 @@
   assets:
   - packages/flutter_gallery_assets/food/butternut_squash_soup.png
   - packages/flutter_gallery_assets/food/cherry_pie.png
+  - assets/999x1000.png
 
 # PUBSPEC CHECKSUM: 4cef
diff --git a/dev/benchmarks/macrobenchmarks/test_memory/large_images.dart b/dev/benchmarks/macrobenchmarks/test_memory/large_images.dart
new file mode 100644
index 0000000..62c14d6
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/test_memory/large_images.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/scheduler.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:macrobenchmarks/common.dart';
+import 'package:macrobenchmarks/main.dart';
+
+Future<void> endOfAnimation() async {
+  do {
+    await SchedulerBinding.instance.endOfFrame;
+  } while (SchedulerBinding.instance.hasScheduledFrame);
+}
+
+int iteration = 0;
+
+class LifecycleObserver extends WidgetsBindingObserver {
+  @override
+  void didChangeAppLifecycleState(AppLifecycleState state) {
+    debugPrint('==== MEMORY BENCHMARK ==== $state ====');
+    debugPrint('This was lifecycle event number $iteration in this instance');
+  }
+}
+
+Future<void> main() async {
+  runApp(const MacrobenchmarksApp(initialRoute: kLargeImagesRouteName));
+  await endOfAnimation();
+  await Future<void>.delayed(const Duration(milliseconds: 50));
+  debugPrint('==== MEMORY BENCHMARK ==== READY ====');
+  WidgetsBinding.instance.addObserver(LifecycleObserver());
+}
diff --git a/dev/devicelab/bin/tasks/fast_scroll_large_images__memory.dart b/dev/devicelab/bin/tasks/fast_scroll_large_images__memory.dart
new file mode 100644
index 0000000..b07f163
--- /dev/null
+++ b/dev/devicelab/bin/tasks/fast_scroll_large_images__memory.dart
@@ -0,0 +1,41 @@
+// 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_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';
+
+const String kPackageName = 'com.example.macrobenchmarks';
+const String kActivityName = 'com.example.macrobenchmarks.MainActivity';
+
+class FastScrollLargeImagesMemoryTest extends MemoryTest {
+  FastScrollLargeImagesMemoryTest()
+      : super(
+          '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
+          'test_memory/large_images.dart', kPackageName,
+        );
+
+  @override
+  AndroidDevice get device => super.device;
+
+  @override
+  int get iterationCount => 5;
+
+  @override
+  Future<void> useMemory() async {
+    await launchApp();
+    await recordStart();
+    await device.shellExec('input', <String>['swipe', '0 1500 0 0 50']);
+    await Future<void>.delayed(const Duration(milliseconds: 10000));
+    await recordEnd();
+  }
+}
+
+Future<void> main() async {
+  deviceOperatingSystem = DeviceOperatingSystem.android;
+  await task(FastScrollLargeImagesMemoryTest().run);
+}
diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml
index cf6f8cd..7df2641 100644
--- a/dev/devicelab/manifest.yaml
+++ b/dev/devicelab/manifest.yaml
@@ -670,6 +670,12 @@
     stage: devicelab
     required_agent_capabilities: ["linux/android"]
 
+  fast_scroll_large_images__memory:
+    description: >
+      Measures memory usage for scrolling through a list of large images.
+    stage: devicelab
+    required_agent_capabilities: ["mac/android"]
+
   analyzer_benchmark:
     description: >
       Measures the speed of Dart analyzer.