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));
+ }
+}