Reland "Shader warm up (#27660)" (#28537)

This reverts commit adc8e159a548121a45ff7a45b4e556aff998d4bd.

This should be safe to land once https://github.com/flutter/flutter/pull/28530 gets merged

Merge on yellow doc test because the doc test is actually green.
diff --git a/dev/benchmarks/macrobenchmarks/lib/common.dart b/dev/benchmarks/macrobenchmarks/lib/common.dart
index e88a708..741920d 100644
--- a/dev/benchmarks/macrobenchmarks/lib/common.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/common.dart
@@ -1 +1,6 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
 const String kCullOpacityRouteName = '/cull_opacity';
+const String kCubicBezierRouteName = '/cubic_bezier';
diff --git a/dev/benchmarks/macrobenchmarks/lib/main.dart b/dev/benchmarks/macrobenchmarks/lib/main.dart
index c7a2778..c9fb5f9 100644
--- a/dev/benchmarks/macrobenchmarks/lib/main.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/main.dart
@@ -1,6 +1,11 @@
+// Copyright 2015 The Chromium 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';
 
 import 'common.dart';
+import 'src/cubic_bezier.dart';
 import 'src/cull_opacity.dart';
 
 const String kMacrobenchmarks ='Macrobenchmarks';
@@ -16,6 +21,7 @@
       routes: <String, WidgetBuilder>{
         '/': (BuildContext context) => HomePage(),
         kCullOpacityRouteName: (BuildContext context) => CullOpacityPage(),
+        kCubicBezierRouteName: (BuildContext context) => CubicBezierPage(),
       },
     );
   }
@@ -34,6 +40,13 @@
             onPressed: (){
               Navigator.pushNamed(context, kCullOpacityRouteName);
             },
+          ),
+          RaisedButton(
+            key: const Key(kCubicBezierRouteName),
+            child: const Text('Cubic Bezier'),
+            onPressed: (){
+              Navigator.pushNamed(context, kCubicBezierRouteName);
+            },
           )
         ],
       ),
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/cubic_bezier.dart b/dev/benchmarks/macrobenchmarks/lib/src/cubic_bezier.dart
new file mode 100644
index 0000000..48fca89
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/lib/src/cubic_bezier.dart
@@ -0,0 +1,380 @@
+// Copyright 2019 The Chromium 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 'package:flutter/widgets.dart';
+import 'package:flutter/animation.dart';
+import 'package:flutter/material.dart';
+
+// Based on https://github.com/eseidelGoogle/bezier_perf/blob/master/lib/main.dart
+class CubicBezierPage extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: const <Widget>[
+          Bezier(Colors.amber, 1.0),
+        ],
+      ),
+    );
+  }
+}
+
+class Bezier extends StatelessWidget {
+  const Bezier(this.color, this.scale, {this.blur = 0.0, this.delay = 0.0});
+
+  final Color color;
+  final double scale;
+  final double blur;
+  final double delay;
+
+  List<PathDetail> _getLogoPath() {
+    final List<PathDetail> paths = <PathDetail>[];
+
+    final Path path = Path();
+    path.moveTo(100.0, 97.0);
+    path.cubicTo(100.0, 97.0, 142.0, 59.0, 169.91, 41.22);
+    path.cubicTo(197.82, 23.44, 249.24, 5.52, 204.67, 85.84);
+
+    paths.add(PathDetail(path));
+
+    // Path 2
+    final Path bezier2Path = Path();
+    bezier2Path.moveTo(0.0, 70.55);
+    bezier2Path.cubicTo(0.0, 70.55, 42.0, 31.55, 69.91, 14.77);
+    bezier2Path.cubicTo(97.82, -2.01, 149.24, -20.93, 104.37, 59.39);
+
+    paths.add(PathDetail(bezier2Path,
+        translate: <double>[29.45, 151.0], rotation: -1.5708));
+
+    // Path 3
+    final Path bezier3Path = Path();
+    bezier3Path.moveTo(0.0, 69.48);
+    bezier3Path.cubicTo(0.0, 69.48, 44.82, 27.92, 69.91, 13.7);
+    bezier3Path.cubicTo(95.0, -0.52, 149.24, -22.0, 104.37, 58.32);
+
+    paths.add(PathDetail(bezier3Path,
+        translate: <double>[53.0, 200.48], rotation: -3.14159));
+
+    // Path 4
+    final Path bezier4Path = Path();
+    bezier4Path.moveTo(0.0, 69.48);
+    bezier4Path.cubicTo(0.0, 69.48, 43.82, 27.92, 69.91, 13.7);
+    bezier4Path.cubicTo(96.0, -0.52, 149.24, -22.0, 104.37, 58.32);
+
+    paths.add(PathDetail(bezier4Path,
+        translate: <double>[122.48, 77.0], rotation: -4.71239));
+
+    return paths;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(children: <Widget>[
+      CustomPaint(
+        foregroundPainter:
+        BezierPainter(Colors.grey, 0.0, _getLogoPath(), false),
+        size: const Size(100.0, 100.0),
+      ),
+      AnimatedBezier(color, scale, blur: blur, delay: delay),
+    ]);
+  }
+}
+
+class PathDetail {
+  PathDetail(this.path, {this.translate, this.rotation});
+
+  Path path;
+  List<double> translate = <double>[];
+  double rotation;
+}
+
+class AnimatedBezier extends StatefulWidget {
+  const AnimatedBezier(this.color, this.scale, {this.blur = 0.0, this.delay});
+
+  final Color color;
+  final double scale;
+  final double blur;
+  final double delay;
+
+  @override
+  State createState() => AnimatedBezierState();
+}
+
+class Point {
+  Point(this.x, this.y);
+
+  double x;
+  double y;
+}
+
+class AnimatedBezierState extends State<AnimatedBezier>
+    with SingleTickerProviderStateMixin {
+  double scale;
+  AnimationController controller;
+  CurvedAnimation curve;
+  bool isPlaying = false;
+  List<List<Point>> pointList = <List<Point>>[]
+    ..add(<Point>[])
+    ..add(<Point>[])
+    ..add(<Point>[])
+    ..add(<Point>[]);
+  bool isReversed = false;
+
+  List<PathDetail> _playForward() {
+    final List<PathDetail> paths = <PathDetail>[];
+    final double t = curve.value;
+    final double b = controller.upperBound;
+    double pX;
+    double pY;
+
+    final Path path = Path();
+
+    if (t < b / 2) {
+      pX = _getCubicPoint(t * 2, 100.0, 100.0, 142.0, 169.91);
+      pY = _getCubicPoint(t * 2, 97.0, 97.0, 59.0, 41.22);
+      pointList[0].add(Point(pX, pY));
+    } else {
+      pX = _getCubicPoint(t * 2 - b, 169.91, 197.80, 249.24, 204.67);
+      pY = _getCubicPoint(t * 2 - b, 41.22, 23.44, 5.52, 85.84);
+      pointList[0].add(Point(pX, pY));
+    }
+
+    path.moveTo(100.0, 97.0);
+
+    for (Point p in pointList[0]) {
+      path.lineTo(p.x, p.y);
+    }
+
+    paths.add(PathDetail(path));
+
+    // Path 2
+    final Path bezier2Path = Path();
+
+    if (t <= b / 2) {
+      final double pX = _getCubicPoint(t * 2, 0.0, 0.0, 42.0, 69.91);
+      final double pY = _getCubicPoint(t * 2, 70.55, 70.55, 31.55, 14.77);
+      pointList[1].add(Point(pX, pY));
+    } else {
+      final double pX = _getCubicPoint(t * 2 - b, 69.91, 97.82, 149.24, 104.37);
+      final double pY = _getCubicPoint(t * 2 - b, 14.77, -2.01, -20.93, 59.39);
+      pointList[1].add(Point(pX, pY));
+    }
+
+    bezier2Path.moveTo(0.0, 70.55);
+
+    for (Point p in pointList[1]) {
+      bezier2Path.lineTo(p.x, p.y);
+    }
+
+    paths.add(PathDetail(bezier2Path,
+        translate: <double>[29.45, 151.0], rotation: -1.5708));
+
+    // Path 3
+    final Path bezier3Path = Path();
+    if (t <= b / 2) {
+      pX = _getCubicPoint(t * 2, 0.0, 0.0, 44.82, 69.91);
+      pY = _getCubicPoint(t * 2, 69.48, 69.48, 27.92, 13.7);
+      pointList[2].add(Point(pX, pY));
+    } else {
+      pX = _getCubicPoint(t * 2 - b, 69.91, 95.0, 149.24, 104.37);
+      pY = _getCubicPoint(t * 2 - b, 13.7, -0.52, -22.0, 58.32);
+      pointList[2].add(Point(pX, pY));
+    }
+
+    bezier3Path.moveTo(0.0, 69.48);
+
+    for (Point p in pointList[2]) {
+      bezier3Path.lineTo(p.x, p.y);
+    }
+
+    paths.add(PathDetail(bezier3Path,
+        translate: <double>[53.0, 200.48], rotation: -3.14159));
+
+    // Path 4
+    final Path bezier4Path = Path();
+
+    if (t < b / 2) {
+      final double pX = _getCubicPoint(t * 2, 0.0, 0.0, 43.82, 69.91);
+      final double pY = _getCubicPoint(t * 2, 69.48, 69.48, 27.92, 13.7);
+      pointList[3].add(Point(pX, pY));
+    } else {
+      final double pX = _getCubicPoint(t * 2 - b, 69.91, 96.0, 149.24, 104.37);
+      final double pY = _getCubicPoint(t * 2 - b, 13.7, -0.52, -22.0, 58.32);
+      pointList[3].add(Point(pX, pY));
+    }
+
+    bezier4Path.moveTo(0.0, 69.48);
+
+    for (Point p in pointList[3]) {
+      bezier4Path.lineTo(p.x, p.y);
+    }
+
+    paths.add(PathDetail(bezier4Path,
+        translate: <double>[122.48, 77.0], rotation: -4.71239));
+
+    return paths;
+  }
+
+  List<PathDetail> _playReversed() {
+    for (List<Point> list in pointList) {
+      if (list.isNotEmpty) {
+        list.removeLast();
+      }
+    }
+
+    final List<Point> points = pointList[0];
+    final Path path = Path();
+
+    path.moveTo(100.0, 97.0);
+
+    for (Point point in points) {
+      path.lineTo(point.x, point.y);
+    }
+
+    final Path bezier2Path = Path();
+
+    bezier2Path.moveTo(0.0, 70.55);
+
+    for (Point p in pointList[1]) {
+      bezier2Path.lineTo(p.x, p.y);
+    }
+
+    final Path bezier3Path = Path();
+    bezier3Path.moveTo(0.0, 69.48);
+
+    for (Point p in pointList[2]) {
+      bezier3Path.lineTo(p.x, p.y);
+    }
+
+    final Path bezier4Path = Path();
+
+    bezier4Path.moveTo(0.0, 69.48);
+
+    for (Point p in pointList[3]) {
+      bezier4Path.lineTo(p.x, p.y);
+    }
+
+    return <PathDetail>[
+      PathDetail(path),
+      PathDetail(bezier2Path, translate: <double>[29.45, 151.0], rotation: -1.5708),
+      PathDetail(bezier3Path,
+          translate: <double>[53.0, 200.48], rotation: -3.14159),
+      PathDetail(bezier4Path, translate: <double>[122.48, 77.0], rotation: -4.71239)
+    ];
+  }
+
+  List<PathDetail> _getLogoPath() {
+    if (!isReversed) {
+      return _playForward();
+    }
+
+    return _playReversed();
+  }
+
+  //From http://wiki.roblox.com/index.php?title=File:Beziereq4.png
+  double _getCubicPoint(double t, double p0, double p1, double p2, double p3) {
+    return pow(1 - t, 3) * p0 +
+        3 * pow(1 - t, 2) * t * p1 +
+        3 * (1 - t) * pow(t, 2) * p2 +
+        pow(t, 3) * p3;
+  }
+
+  void playAnimation() {
+    isPlaying = true;
+    isReversed = false;
+    for (List<Point> list in pointList) {
+      list.clear();
+    }
+    controller.reset();
+    controller.forward();
+  }
+
+  void stopAnimation() {
+    isPlaying = false;
+    controller.stop();
+    for (List<Point> list in pointList) {
+      list.clear();
+    }
+  }
+
+  void reverseAnimation() {
+    isReversed = true;
+    controller.reverse();
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    controller = AnimationController(
+        vsync: this, duration: const Duration(milliseconds: 1000));
+    curve = CurvedAnimation(parent: controller, curve: Curves.linear)
+      ..addListener(() {
+        setState(() {});
+      })
+      ..addStatusListener((AnimationStatus state) {
+        if (state == AnimationStatus.completed) {
+          reverseAnimation();
+        } else if (state == AnimationStatus.dismissed) {
+          playAnimation();
+        }
+      });
+
+    playAnimation();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return CustomPaint(
+        foregroundPainter: BezierPainter(widget.color,
+            curve.value * widget.blur, _getLogoPath(), isPlaying),
+        size: const Size(100.0, 100.0));
+  }
+}
+
+class BezierPainter extends CustomPainter {
+  BezierPainter(this.color, this.blur, this.path, this.isPlaying);
+
+  Color color;
+  final double blur;
+  List<PathDetail> path;
+  bool isPlaying;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final Paint paint = Paint();
+    paint.strokeWidth = 18.0;
+    paint.style = PaintingStyle.stroke;
+    paint.strokeCap = StrokeCap.round;
+    paint.color = color;
+    canvas.scale(0.5, 0.5);
+
+    for (int i = 0; i < path.length; i++) {
+      if (path[i].translate != null) {
+        canvas.translate(path[i].translate[0], path[i].translate[1]);
+      }
+
+      if (path[i].rotation != null) {
+        canvas.rotate(path[i].rotation);
+      }
+
+      if (blur > 0) {
+        final MaskFilter blur = MaskFilter.blur(BlurStyle.normal, this.blur);
+        paint.maskFilter = blur;
+        canvas.drawPath(path[i].path, paint);
+      }
+
+      paint.maskFilter = null;
+      canvas.drawPath(path[i].path, paint);
+    }
+  }
+
+  @override
+  bool shouldRepaint(BezierPainter oldDelegate) => true;
+
+  @override
+  bool shouldRebuildSemantics(BezierPainter oldDelegate) => false;
+}
diff --git a/dev/benchmarks/macrobenchmarks/test_driver/cubic_bezier_perf.dart b/dev/benchmarks/macrobenchmarks/test_driver/cubic_bezier_perf.dart
new file mode 100644
index 0000000..69afb9e
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/test_driver/cubic_bezier_perf.dart
@@ -0,0 +1,40 @@
+// Copyright 2019 The Chromium 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:ui';
+
+import 'package:flutter_driver/driver_extension.dart';
+import 'package:flutter/painting.dart' show DefaultShaderWarmUp, PaintingBinding;
+import 'package:macrobenchmarks/main.dart' as app;
+
+class CubicBezierShaderWarmUp extends DefaultShaderWarmUp {
+  @override
+  void warmUpOnCanvas(Canvas canvas) {
+    super.warmUpOnCanvas(canvas);
+
+    // Warm up the cubic shaders used by CubicBezierPage.
+    //
+    // This tests that our custom shader warm up is working properly.
+    // Without this custom shader warm up, the worst frame time is about 115ms.
+    // With this, the worst frame time is about 70ms. (Data collected on a Moto
+    // G4 based on Flutter version 704814c67a874077710524d30412337884bf0254.
+    final Path path = Path();
+    path.moveTo(20.0, 20.0);
+    // This cubic path is based on
+    // https://skia.org/user/api/SkPath_Reference#SkPath_cubicTo
+    path.cubicTo(300.0, 80.0, -140.0, 90.0, 220.0, 10.0);
+    final Paint paint = Paint();
+    paint.isAntiAlias = true;
+    paint.strokeWidth = 18.0;
+    paint.style = PaintingStyle.stroke;
+    paint.strokeCap = StrokeCap.round;
+    canvas.drawPath(path, paint);
+  }
+}
+
+void main() {
+  PaintingBinding.shaderWarmUp = CubicBezierShaderWarmUp();
+  enableFlutterDriverExtension();
+  app.main();
+}
diff --git a/dev/benchmarks/macrobenchmarks/test_driver/cubic_bezier_perf_test.dart b/dev/benchmarks/macrobenchmarks/test_driver/cubic_bezier_perf_test.dart
new file mode 100644
index 0000000..3ead887
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/test_driver/cubic_bezier_perf_test.dart
@@ -0,0 +1,11 @@
+// Copyright 2019 The Chromium 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('cubic_bezier_perf', kCubicBezierRouteName);
+}
diff --git a/dev/benchmarks/macrobenchmarks/test_driver/cull_opacity_perf_test.dart b/dev/benchmarks/macrobenchmarks/test_driver/cull_opacity_perf_test.dart
index 6493a1a..c5b15bc 100644
--- a/dev/benchmarks/macrobenchmarks/test_driver/cull_opacity_perf_test.dart
+++ b/dev/benchmarks/macrobenchmarks/test_driver/cull_opacity_perf_test.dart
@@ -2,42 +2,15 @@
 // 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_driver/flutter_driver.dart';
-import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
-
 import 'package:macrobenchmarks/common.dart';
 
+import 'util.dart';
+
 void main() {
-  const String kName = 'cull_opacity_perf';
-
-  test(kName, () async {
-    final FlutterDriver driver = await FlutterDriver.connect();
-
-    // The slight initial delay avoids starting the timing during a
-    // period of increased load on the device. Without this delay, the
-    // benchmark has greater noise.
-    // See: https://github.com/flutter/flutter/issues/19434
-    await Future<void>.delayed(const Duration(milliseconds: 250));
-
-    await driver.forceGC();
-
-    final SerializableFinder button = find.byValueKey(kCullOpacityRouteName);
-    expect(button, isNotNull);
-    await driver.tap(button);
-
-    // Wait for the page to load
-    await Future<void>.delayed(const Duration(seconds: 1));
-
-    final Timeline timeline = await driver.traceAction(() async {
-      await Future<void>.delayed(const Duration(seconds: 10));
-    });
-
-    final TimelineSummary summary = TimelineSummary.summarize(timeline);
-    summary.writeSummaryToFile(kName, pretty: true);
-    summary.writeTimelineToFile(kName, pretty: true);
-
-    driver.close();
-  });
+  macroPerfTest(
+    'cull_opacity_perf',
+    kCullOpacityRouteName,
+    pageDelay: const Duration(seconds: 1),
+    duration: const Duration(seconds: 10)
+  );
 }
diff --git a/dev/benchmarks/macrobenchmarks/test_driver/util.dart b/dev/benchmarks/macrobenchmarks/test_driver/util.dart
new file mode 100644
index 0000000..79fe4f1
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/test_driver/util.dart
@@ -0,0 +1,44 @@
+// Copyright 2019 The Chromium 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_driver/flutter_driver.dart';
+import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
+
+void macroPerfTest(
+    String testName,
+    String routeName,
+    {Duration pageDelay, Duration duration = const Duration(seconds: 3)}) {
+  test(testName, () async {
+    final FlutterDriver driver = await FlutterDriver.connect();
+
+    // The slight initial delay avoids starting the timing during a
+    // period of increased load on the device. Without this delay, the
+    // benchmark has greater noise.
+    // See: https://github.com/flutter/flutter/issues/19434
+    await Future<void>.delayed(const Duration(milliseconds: 250));
+
+    await driver.forceGC();
+
+    final SerializableFinder button = find.byValueKey(routeName);
+    expect(button, isNotNull);
+    await driver.tap(button);
+
+    if (pageDelay != null) {
+      // Wait for the page to load
+      await Future<void>.delayed(pageDelay);
+    }
+
+    final Timeline timeline = await driver.traceAction(() async {
+      await Future<void>.delayed(duration);
+    });
+
+    final TimelineSummary summary = TimelineSummary.summarize(timeline);
+    summary.writeSummaryToFile(testName, pretty: true);
+    summary.writeTimelineToFile(testName, pretty: true);
+
+    driver.close();
+  });
+}
diff --git a/dev/devicelab/bin/tasks/cubic_bezier_perf__timeline_summary.dart b/dev/devicelab/bin/tasks/cubic_bezier_perf__timeline_summary.dart
new file mode 100644
index 0000000..92e42cc
--- /dev/null
+++ b/dev/devicelab/bin/tasks/cubic_bezier_perf__timeline_summary.dart
@@ -0,0 +1,14 @@
+// Copyright 2019 The Chromium 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(createCubicBezierPerfTest());
+}
diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart
index 27e877f..515d122 100644
--- a/dev/devicelab/lib/tasks/perf_tests.dart
+++ b/dev/devicelab/lib/tasks/perf_tests.dart
@@ -46,6 +46,14 @@
   ).run;
 }
 
+TaskFunction createCubicBezierPerfTest() {
+  return PerfTest(
+    '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks',
+    'test_driver/cubic_bezier_perf.dart',
+    'cubic_bezier_perf',
+  ).run;
+}
+
 TaskFunction createFlutterGalleryStartupTest() {
   return StartupTest(
     '${flutterDirectory.path}/examples/flutter_gallery',
diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml
index 947f138..909d3bb 100644
--- a/dev/devicelab/manifest.yaml
+++ b/dev/devicelab/manifest.yaml
@@ -133,6 +133,13 @@
     stage: devicelab
     required_agent_capabilities: ["mac/android"]
 
+  cubic_bezier_perf__timeline_summary:
+    description: >
+      Measures the runtime performance of cubic bezier animations on Android.
+    stage: devicelab
+    required_agent_capabilities: ["mac/android"]
+    flaky: true
+
   flavors_test:
     description: >
       Checks that flavored builds work on Android.
diff --git a/examples/layers/raw/shader_warm_up.dart b/examples/layers/raw/shader_warm_up.dart
new file mode 100644
index 0000000..63f82b3
--- /dev/null
+++ b/examples/layers/raw/shader_warm_up.dart
@@ -0,0 +1,33 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This example shows the draw operations to warm up the GPU shaders by default.
+
+import 'dart:ui' as ui;
+
+import 'package:flutter/material.dart';
+import 'package:flutter/painting.dart' show DefaultShaderWarmUp;
+
+void beginFrame(Duration timeStamp) {
+  // PAINT
+  final ui.PictureRecorder recorder = ui.PictureRecorder();
+  final ui.Rect paintBounds = ui.Rect.fromLTRB(0, 0, 1000, 1000);
+  final ui.Canvas canvas = ui.Canvas(recorder, paintBounds);
+  final ui.Paint backgroundPaint = ui.Paint()..color = Colors.white;
+  canvas.drawRect(paintBounds, backgroundPaint);
+  const DefaultShaderWarmUp().warmUpOnCanvas(canvas);
+  final ui.Picture picture = recorder.endRecording();
+
+  // COMPOSITE
+  final ui.SceneBuilder sceneBuilder = ui.SceneBuilder()
+    ..pushClipRect(paintBounds)
+    ..addPicture(ui.Offset.zero, picture)
+    ..pop();
+  ui.window.render(sceneBuilder.build());
+}
+
+void main() {
+  ui.window.onBeginFrame = beginFrame;
+  ui.window.scheduleFrame();
+}
diff --git a/examples/layers/test/smoketests/raw/shader_warm_up_test.dart b/examples/layers/test/smoketests/raw/shader_warm_up_test.dart
new file mode 100644
index 0000000..895c7f8
--- /dev/null
+++ b/examples/layers/test/smoketests/raw/shader_warm_up_test.dart
@@ -0,0 +1,13 @@
+// Copyright 2019 The Chromium 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:test_api/test_api.dart' hide TypeMatcher, isInstanceOf;
+
+import '../../../raw/shader_warm_up.dart' as demo;
+
+void main() {
+  test('layers smoketest for raw/shader_warm_up.dart', () {
+    demo.main();
+  });
+}
diff --git a/packages/flutter/lib/painting.dart b/packages/flutter/lib/painting.dart
index 6cce1f8..fe432cb 100644
--- a/packages/flutter/lib/painting.dart
+++ b/packages/flutter/lib/painting.dart
@@ -50,6 +50,7 @@
 export 'src/painting/notched_shapes.dart';
 export 'src/painting/paint_utilities.dart';
 export 'src/painting/rounded_rectangle_border.dart';
+export 'src/painting/shader_warm_up.dart';
 export 'src/painting/shape_decoration.dart';
 export 'src/painting/stadium_border.dart';
 export 'src/painting/strut_style.dart';
diff --git a/packages/flutter/lib/src/painting/binding.dart b/packages/flutter/lib/src/painting/binding.dart
index 01596ad..1d4e72e 100644
--- a/packages/flutter/lib/src/painting/binding.dart
+++ b/packages/flutter/lib/src/painting/binding.dart
@@ -8,6 +8,7 @@
 import 'package:flutter/services.dart' show ServicesBinding;
 
 import 'image_cache.dart';
+import 'shader_warm_up.dart';
 
 const double _kDefaultDecodedCacheRatioCap = 0.0;
 
@@ -22,12 +23,34 @@
     super.initInstances();
     _instance = this;
     _imageCache = createImageCache();
+    if (shaderWarmUp != null) {
+      shaderWarmUp.execute();
+    }
   }
 
   /// The current [PaintingBinding], if one has been created.
   static PaintingBinding get instance => _instance;
   static PaintingBinding _instance;
 
+  /// [ShaderWarmUp] to be executed during [initInstances].
+  ///
+  /// If the application has scenes that require the compilation of complex
+  /// shaders that are not covered by [DefaultShaderWarmUp], it may cause jank
+  /// in the middle of an animation or interaction. In that case, set
+  /// [shaderWarmUp] to a custom [ShaderWarmUp] before calling [initInstances]
+  /// (usually before [runApp] for normal flutter apps, and before
+  /// [enableFlutterDriverExtension] for flutter drive tests). Paint the scene
+  /// in the custom [ShaderWarmUp] so Flutter can pre-compile and cache the
+  /// shaders during startup. The warm up is only costly (100ms-200ms,
+  /// depending on the shaders to compile) during the first run after the
+  /// installation or a data wipe. The warm up does not block the main thread
+  /// so there should be no "Application Not Responding" warning.
+  ///
+  /// Currently the warm-up happens synchronously on the GPU thread which means
+  /// the rendering of the first frame on the GPU thread will be postponed until
+  /// the warm-up is finished.
+  static ShaderWarmUp shaderWarmUp = const DefaultShaderWarmUp();
+
   /// The singleton that implements the Flutter framework's image cache.
   ///
   /// The cache is used internally by [ImageProvider] and should generally not
diff --git a/packages/flutter/lib/src/painting/shader_warm_up.dart b/packages/flutter/lib/src/painting/shader_warm_up.dart
new file mode 100644
index 0000000..c5e1d26
--- /dev/null
+++ b/packages/flutter/lib/src/painting/shader_warm_up.dart
@@ -0,0 +1,148 @@
+// Copyright 2019 The Chromium 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:developer';
+import 'dart:ui' as ui;
+
+import 'package:flutter/foundation.dart';
+
+/// Interface for drawing an image to warm up Skia shader compilations.
+///
+/// When Skia first sees a certain type of draw operations on GPU, it needs to
+/// compile the corresponding shader. The compilation can be slow (20ms-200ms).
+/// Having that time as a startup latency is often better than having a jank in
+/// the middle of an animation.
+///
+/// Therefore, we use this during the [PaintingBinding.initInstances] call to
+/// move common shader compilations from animation time to startup time. By
+/// default, a [DefaultShaderWarmUp] is used. Create a custom [ShaderWarmUp]
+/// subclass to replace [PaintingBinding.shaderWarmUp] before
+/// [PaintingBinding.initInstances] is called. Usually, that can be done before
+/// calling [runApp].
+///
+/// This warm up needs to be run on each individual device because the shader
+/// compilation depends on the specific GPU hardware and driver a device has. It
+/// can't be pre-computed during the Flutter engine compilation as the engine is
+/// device agnostic.
+///
+/// If no warm up is desired (e.g., when the startup latency is crucial), set
+/// [PaintingBinding.shaderWarmUp] either to a custom ShaderWarmUp with an empty
+/// [warmUpOnCanvas] or null.
+abstract class ShaderWarmUp {
+  /// Allow const constructors for subclasses.
+  const ShaderWarmUp();
+
+  /// The size of the warm up image.
+  ///
+  /// The exact size shouldn't matter much as long as it's not too far away from
+  /// the target device's screen. 1024x1024 is a good choice as it is within an
+  /// order of magnitude of most devices.
+  ///
+  /// A custom shader warm up can override this based on targeted devices.
+  ui.Size get size => const ui.Size(1024.0, 1024.0);
+
+  /// Trigger draw operations on a given canvas to warm up GPU shader
+  /// compilation cache.
+  ///
+  /// To decide which draw operations to be added to your custom warm up
+  /// process, try capture an skp using `flutter screenshot --observatory-
+  /// port=<port> --type=skia` and analyze it with https://debugger.skia.org.
+  /// Alternatively, one may run the app with `flutter run --trace-skia` and
+  /// then examine the GPU thread in the observatory timeline to see which
+  /// Skia draw operations are commonly used, and which shader compilations
+  /// are causing janks.
+  @protected
+  void warmUpOnCanvas(ui.Canvas canvas);
+
+  /// Construct an offscreen image of [size], and execute [warmUpOnCanvas] on a
+  /// canvas associated with that image.
+  void execute() {
+    final ui.PictureRecorder recorder = ui.PictureRecorder();
+    final ui.Canvas canvas = ui.Canvas(recorder);
+
+    warmUpOnCanvas(canvas);
+
+    final ui.Picture picture = recorder.endRecording();
+    final TimelineTask shaderWarmUpTask = TimelineTask();
+    shaderWarmUpTask.start('Warm-up shader');
+    picture.toImage(size.width.ceil(), size.height.ceil()).then((ui.Image image) {
+      shaderWarmUpTask.finish();
+    });
+  }
+}
+
+/// Default way of warming up Skia shader compilations.
+///
+/// The draw operations being warmed up here are decided according to Flutter
+/// engineers' observation and experience based on the apps and the performance
+/// issues seen so far.
+class DefaultShaderWarmUp extends ShaderWarmUp {
+  /// Allow [DefaultShaderWarmUp] to be used as the default value of parameters.
+  const DefaultShaderWarmUp();
+
+  /// Trigger common draw operations on a canvas to warm up GPU shader
+  /// compilation cache.
+  @override
+  void warmUpOnCanvas(ui.Canvas canvas) {
+    final ui.RRect rrect = ui.RRect.fromLTRBXY(20.0, 20.0, 60.0, 60.0, 10.0, 10.0);
+    final ui.Path rrectPath = ui.Path()..addRRect(rrect);
+
+    final ui.Path circlePath = ui.Path()..addOval(
+        ui.Rect.fromCircle(center: const ui.Offset(40.0, 40.0), radius: 20.0)
+    );
+
+    // The following path is based on
+    // https://skia.org/user/api/SkCanvas_Reference#SkCanvas_drawPath
+    final ui.Path path = ui.Path();
+    path.moveTo(20.0, 60.0);
+    path.quadraticBezierTo(60.0, 20.0, 60.0, 60.0);
+    path.close();
+    path.moveTo(60.0, 20.0);
+    path.quadraticBezierTo(60.0, 60.0, 20.0, 60.0);
+
+    final List<ui.Path> paths = <ui.Path>[rrectPath, circlePath, path];
+
+    final List<ui.Paint> paints = <ui.Paint>[
+      ui.Paint()
+        ..isAntiAlias = true
+        ..style = ui.PaintingStyle.fill,
+      ui.Paint()
+        ..isAntiAlias = true
+        ..style = ui.PaintingStyle.stroke
+        ..strokeWidth = 10,
+      ui.Paint()
+        ..isAntiAlias = true
+        ..style = ui.PaintingStyle.stroke
+        ..strokeWidth = 0.1  // hairline
+    ];
+
+    // Warm up path stroke and fill shaders.
+    for (int i = 0; i < paths.length; i += 1) {
+      canvas.save();
+      for (ui.Paint paint in paints) {
+        canvas.drawPath(paths[i], paint);
+        canvas.translate(80.0, 0.0);
+      }
+      canvas.restore();
+      canvas.translate(0.0, 80.0);
+    }
+
+    // Warm up shadow shaders.
+    const ui.Color black = ui.Color(0xFF000000);
+    canvas.save();
+    canvas.drawShadow(rrectPath, black, 10.0, true);
+    canvas.translate(80.0, 0.0);
+    canvas.drawShadow(rrectPath, black, 10.0, false);
+    canvas.restore();
+
+    // Warm up text shaders.
+    canvas.translate(0.0, 80.0);
+    final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(
+      ui.ParagraphStyle(textDirection: ui.TextDirection.ltr),
+    )..pushStyle(ui.TextStyle(color: black))..addText('_');
+    final ui.Paragraph paragraph = paragraphBuilder.build()
+      ..layout(const ui.ParagraphConstraints(width: 60.0));
+    canvas.drawParagraph(paragraph, const ui.Offset(20.0, 20.0));
+  }
+}