benchmark animation performance of Opacity widget (#54903)

diff --git a/dev/benchmarks/macrobenchmarks/lib/common.dart b/dev/benchmarks/macrobenchmarks/lib/common.dart
index a025bf4..c4f015a 100644
--- a/dev/benchmarks/macrobenchmarks/lib/common.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/common.dart
@@ -12,3 +12,4 @@
 const String kTextRouteName = '/text';
 const String kAnimatedPlaceholderRouteName = '/animated_placeholder';
 const String kColorFilterAndFadeRouteName = '/color_filter_and_fade';
+const String kFadingChildAnimationRouteName = '/fading_child_animation';
diff --git a/dev/benchmarks/macrobenchmarks/lib/main.dart b/dev/benchmarks/macrobenchmarks/lib/main.dart
index 7937f8c..d7320eb 100644
--- a/dev/benchmarks/macrobenchmarks/lib/main.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/main.dart
@@ -12,6 +12,7 @@
 import 'src/backdrop_filter.dart';
 import 'src/cubic_bezier.dart';
 import 'src/cull_opacity.dart';
+import 'src/filtered_child_animation.dart';
 import 'src/post_backdrop_filter.dart';
 import 'src/simple_animation.dart';
 import 'src/text.dart';
@@ -40,6 +41,7 @@
         kTextRouteName: (BuildContext context) => TextPage(),
         kAnimatedPlaceholderRouteName: (BuildContext context) => AnimatedPlaceholderPage(),
         kColorFilterAndFadeRouteName: (BuildContext context) => ColorFilterAndFadePage(),
+        kFadingChildAnimationRouteName: (BuildContext context) => const FilteredChildAnimationPage(FilterType.opacity),
       },
     );
   }
@@ -124,6 +126,13 @@
               Navigator.pushNamed(context, kColorFilterAndFadeRouteName);
             },
           ),
+          RaisedButton(
+            key: const Key(kFadingChildAnimationRouteName),
+            child: const Text('Fading Child Animation'),
+            onPressed: () {
+              Navigator.pushNamed(context, kFadingChildAnimationRouteName);
+            },
+          ),
         ],
       ),
     );
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/filtered_child_animation.dart b/dev/benchmarks/macrobenchmarks/lib/src/filtered_child_animation.dart
new file mode 100644
index 0000000..15abc52
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/lib/src/filtered_child_animation.dart
@@ -0,0 +1,212 @@
+// 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:math';
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+
+enum FilterType {
+  opacity, rotateTransform, rotateFilter,
+}
+
+class FilteredChildAnimationPage extends StatefulWidget {
+  const FilteredChildAnimationPage(
+      this._filterType,
+      [
+        this._complexChild = true,
+        this._useRepaintBoundary = true,
+      ]);
+
+  final FilterType _filterType;
+  final bool _complexChild;
+  final bool _useRepaintBoundary;
+
+  @override
+  _FilteredChildAnimationPageState createState() => _FilteredChildAnimationPageState(_filterType, _complexChild, _useRepaintBoundary);
+}
+
+class _FilteredChildAnimationPageState extends State<FilteredChildAnimationPage> with SingleTickerProviderStateMixin {
+  _FilteredChildAnimationPageState(this._filterType, this._complexChild, this._useRepaintBoundary);
+
+  AnimationController _controller;
+  bool _useRepaintBoundary;
+  bool _complexChild;
+  FilterType _filterType;
+  final GlobalKey _childKey = GlobalKey(debugLabel: 'child to animate');
+  Offset _childCenter = Offset.zero;
+
+  @override
+  void initState() {
+    super.initState();
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      final RenderBox childBox = _childKey.currentContext.findRenderObject() as RenderBox;
+      final Offset localCenter = childBox.paintBounds.center;
+      _childCenter = childBox.localToGlobal(localCenter);
+    });
+    _controller = AnimationController(vsync: this, duration: const Duration(seconds: 2));
+    _controller.repeat();
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+
+  void _setFilterType(FilterType type, bool selected) {
+    setState(() => _filterType = selected ? type : null);
+  }
+
+  String get _title {
+    switch (_filterType) {
+      case FilterType.opacity: return 'Fading Child Animation';
+      case FilterType.rotateTransform: return 'Transformed Child Animation';
+      case FilterType.rotateFilter: return 'Matrix Filtered Child Animation';
+      default: return 'Static Child';
+    }
+  }
+
+  static Widget _makeChild(int rows, int cols, double fontSize, bool complex) {
+    final BoxDecoration decoration = BoxDecoration(
+      color: Colors.green,
+      boxShadow: complex ? <BoxShadow>[
+        const BoxShadow(
+          color: Colors.black,
+          blurRadius: 10.0,
+        ),
+      ] : null,
+      borderRadius: BorderRadius.circular(10.0),
+    );
+    return Stack(
+      alignment: Alignment.center,
+      children: <Widget>[
+        Column(
+          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+          children: List<Widget>.generate(rows, (int r) => Row(
+            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+            children: List<Widget>.generate(cols, (int c) => Container(
+              child: Text('text', style: TextStyle(fontSize: fontSize)),
+              decoration: decoration,
+            )),
+          )),
+        ),
+        const Text('child',
+          style: TextStyle(
+            color: Colors.blue,
+            fontSize: 36,
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget _animate({Widget child, bool protectChild}) {
+    if (_filterType == null) {
+      _controller.reset();
+      return child;
+    }
+    _controller.repeat();
+    Widget Function(BuildContext, Widget) builder;
+    switch (_filterType) {
+      case FilterType.opacity:
+        builder = (BuildContext context, Widget child) => Opacity(
+          opacity: (_controller.value * 2.0 - 1.0).abs(),
+          child: child,
+        );
+        break;
+      case FilterType.rotateTransform:
+        builder = (BuildContext context, Widget child) => Transform(
+          transform: Matrix4.rotationZ(_controller.value * 2.0 * pi),
+          alignment: Alignment.center,
+          child: child,
+        );
+        break;
+      case FilterType.rotateFilter:
+        builder = (BuildContext context, Widget child) => ImageFiltered(
+          imageFilter: ImageFilter.matrix((
+              Matrix4.identity()
+                ..translate(_childCenter.dx, _childCenter.dy)
+                ..rotateZ(_controller.value * 2.0 * pi)
+                ..translate(- _childCenter.dx, - _childCenter.dy)
+          ).storage),
+          child: child,
+        );
+        break;
+    }
+    return RepaintBoundary(
+      child: AnimatedBuilder(
+        animation: _controller,
+        child: protectChild ? RepaintBoundary(child: child) : child,
+        builder: builder,
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(_title),
+      ),
+      body: Center(
+        child: _animate(
+          child: Container(
+            key: _childKey,
+            color: Colors.yellow,
+            width: 300,
+            height: 300,
+            child: Center(
+              child: _makeChild(4, 3, 24.0, _complexChild),
+            ),
+          ),
+          protectChild: _useRepaintBoundary,
+        ),
+      ),
+      bottomNavigationBar: BottomAppBar(
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: <Widget>[
+                const Text('Opacity:'),
+                Checkbox(
+                  value: _filterType == FilterType.opacity,
+                  onChanged: (bool b) => _setFilterType(FilterType.opacity, b),
+                ),
+                const Text('Tx Rotate:'),
+                Checkbox(
+                  value: _filterType == FilterType.rotateTransform,
+                  onChanged: (bool b) => _setFilterType(FilterType.rotateTransform, b),
+                ),
+                const Text('IF Rotate:'),
+                Checkbox(
+                  value: _filterType == FilterType.rotateFilter,
+                  onChanged: (bool b) => _setFilterType(FilterType.rotateFilter, b),
+                ),
+              ],
+            ),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: <Widget>[
+                const Text('Complex child:'),
+                Checkbox(
+                  value: _complexChild,
+                  onChanged: (bool b) => setState(() => _complexChild = b),
+                ),
+                const Text('RPB on child:'),
+                Checkbox(
+                  value: _useRepaintBoundary,
+                  onChanged: (bool b) => setState(() => _useRepaintBoundary = b),
+                ),
+              ],
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/dev/benchmarks/macrobenchmarks/test_driver/fading_child_animation_perf.dart b/dev/benchmarks/macrobenchmarks/test_driver/fading_child_animation_perf.dart
new file mode 100644
index 0000000..8169d13
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/test_driver/fading_child_animation_perf.dart
@@ -0,0 +1,11 @@
+// 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/driver_extension.dart';
+import 'package:macrobenchmarks/main.dart' as app;
+
+void main() {
+  enableFlutterDriverExtension();
+  app.main();
+}
diff --git a/dev/benchmarks/macrobenchmarks/test_driver/fading_child_animation_perf_test.dart b/dev/benchmarks/macrobenchmarks/test_driver/fading_child_animation_perf_test.dart
new file mode 100644
index 0000000..86107b6
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/test_driver/fading_child_animation_perf_test.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:macrobenchmarks/common.dart';
+
+import 'util.dart';
+
+void main() {
+  macroPerfTest(
+    'fading_child_animation_perf',
+    kFadingChildAnimationRouteName,
+    pageDelay: const Duration(seconds: 1),
+    duration: const Duration(seconds: 10),
+  );
+}
diff --git a/dev/devicelab/bin/tasks/fading_child_animation_perf__timeline_summary.dart b/dev/devicelab/bin/tasks/fading_child_animation_perf__timeline_summary.dart
new file mode 100644
index 0000000..8c52f61
--- /dev/null
+++ b/dev/devicelab/bin/tasks/fading_child_animation_perf__timeline_summary.dart
@@ -0,0 +1,14 @@
+// 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/tasks/perf_tests.dart';
+import 'package:flutter_devicelab/framework/adb.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+
+Future<void> main() async {
+  deviceOperatingSystem = DeviceOperatingSystem.android;
+  await task(createFadingChildAnimationPerfTest());
+}
diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart
index 9916761..ac9c73f 100644
--- a/dev/devicelab/lib/tasks/perf_tests.dart
+++ b/dev/devicelab/lib/tasks/perf_tests.dart
@@ -189,6 +189,14 @@
   ).run;
 }
 
+TaskFunction createFadingChildAnimationPerfTest() {
+  return PerfTest(
+    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
+    'test_driver/fading_child_animation_perf.dart',
+    'fading_child_animation_perf',
+  ).run;
+}
+
 /// Measure application startup performance.
 class StartupTest {
   const StartupTest(this.testDirectory, { this.reportMetrics = true });
diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml
index 1a4ceec..51d8728 100644
--- a/dev/devicelab/manifest.yaml
+++ b/dev/devicelab/manifest.yaml
@@ -201,6 +201,12 @@
     stage: devicelab
     required_agent_capabilities: ["mac/android"]
 
+  fading_child_animation_perf__timeline_summary:
+    description: >
+      Measures the runtime performance of opacity filter with fade on Android.
+    stage: devicelab
+    required_agent_capabilities: ["mac/android"]
+
   flavors_test:
     description: >
       Checks that flavored builds work on Android.