From a44f174efc218ac1b4b94b9d77dafdb5dfea1432 Mon Sep 17 00:00:00 2001 From: liyuqian Date: Fri, 22 Feb 2019 15:37:02 -0800 Subject: [PATCH] Shader warm up (#27660) This patch adds a default shader warm up process which moves shader compilation from the animation time to the startup time. This also provides an extension for `runApp` so developers can customize the warm up process. This should reduce our worst_frame_rasterizer_time_millis from ~100ms to ~20-30ms for both flutter_gallery and complex_layout benchmarks. Besides, this should also have a significant improvement on 90th and 99th percentile time (50%-100% speedup in some cases, but I haven't tested them thoroughly; I'll let our device lab collect the data afterwards). The tradeoff the is the startup time (time to first frame). Our `flutter run --profile --trace-startup` seems to be a little noisy and I see about 100ms-200ms increase in that measurement for complex_layout and flutter_gallery. Note that this only happens on the first run after install or data wipe. Later the Skia persistent cache will remove the overhead. This also adds a cubic_bezier benchmark to test the custom shader warm up process. This should fix https://github.com/flutter/flutter/issues/813 (either by `defaultShaderWarmUp`, or a `customShaderWarmUp`). --- .../macrobenchmarks/lib/common.dart | 5 + dev/benchmarks/macrobenchmarks/lib/main.dart | 13 + .../macrobenchmarks/lib/src/cubic_bezier.dart | 380 ++++++++++++++++++ .../test_driver/cubic_bezier_perf.dart | 40 ++ .../test_driver/cubic_bezier_perf_test.dart | 11 + .../test_driver/cull_opacity_perf_test.dart | 43 +- .../macrobenchmarks/test_driver/util.dart | 44 ++ .../cubic_bezier_perf__timeline_summary.dart | 14 + dev/devicelab/lib/tasks/perf_tests.dart | 8 + dev/devicelab/manifest.yaml | 7 + examples/layers/raw/shader_warm_up.dart | 33 ++ .../smoketests/raw/shader_warm_up_test.dart | 13 + packages/flutter/lib/painting.dart | 1 + .../flutter/lib/src/painting/binding.dart | 23 ++ .../lib/src/painting/shader_warm_up.dart | 148 +++++++ 15 files changed, 748 insertions(+), 35 deletions(-) create mode 100644 dev/benchmarks/macrobenchmarks/lib/src/cubic_bezier.dart create mode 100644 dev/benchmarks/macrobenchmarks/test_driver/cubic_bezier_perf.dart create mode 100644 dev/benchmarks/macrobenchmarks/test_driver/cubic_bezier_perf_test.dart create mode 100644 dev/benchmarks/macrobenchmarks/test_driver/util.dart create mode 100644 dev/devicelab/bin/tasks/cubic_bezier_perf__timeline_summary.dart create mode 100644 examples/layers/raw/shader_warm_up.dart create mode 100644 examples/layers/test/smoketests/raw/shader_warm_up_test.dart create mode 100644 packages/flutter/lib/src/painting/shader_warm_up.dart diff --git a/dev/benchmarks/macrobenchmarks/lib/common.dart b/dev/benchmarks/macrobenchmarks/lib/common.dart index e88a70800a..741920d712 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 c7a2778aa2..c9fb5f9b3f 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 @@ class MacrobenchmarksApp extends StatelessWidget { routes: { '/': (BuildContext context) => HomePage(), kCullOpacityRouteName: (BuildContext context) => CullOpacityPage(), + kCubicBezierRouteName: (BuildContext context) => CubicBezierPage(), }, ); } @@ -34,6 +40,13 @@ class HomePage extends StatelessWidget { 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 0000000000..85023be2c5 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/cubic_bezier.dart @@ -0,0 +1,380 @@ +// 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 '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 [ + 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 _getLogoPath() { + final List paths = []; + + 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: [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: [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: [122.48, 77.0], rotation: -4.71239)); + + return paths; + } + + @override + Widget build(BuildContext context) { + return Stack(children: [ + 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 translate = []; + 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 + with SingleTickerProviderStateMixin { + double scale; + AnimationController controller; + CurvedAnimation curve; + bool isPlaying = false; + List> pointList = >[] + ..add([]) + ..add([]) + ..add([]) + ..add([]); + bool isReversed = false; + + List _playForward() { + final List paths = []; + 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: [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: [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: [122.48, 77.0], rotation: -4.71239)); + + return paths; + } + + List _playReversed() { + for (List list in pointList) { + if (list.isNotEmpty) { + list.removeLast(); + } + } + + final List 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(path), + PathDetail(bezier2Path, translate: [29.45, 151.0], rotation: -1.5708), + PathDetail(bezier3Path, + translate: [53.0, 200.48], rotation: -3.14159), + PathDetail(bezier4Path, translate: [122.48, 77.0], rotation: -4.71239) + ]; + } + + List _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 list in pointList) { + list.clear(); + } + controller.reset(); + controller.forward(); + } + + void stopAnimation() { + isPlaying = false; + controller.stop(); + for (List 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 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 0000000000..fc0c0a7086 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test_driver/cubic_bezier_perf.dart @@ -0,0 +1,40 @@ +// Copyright 2018 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 0000000000..12fc4126bc --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test_driver/cubic_bezier_perf_test.dart @@ -0,0 +1,11 @@ +// Copyright 2018 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 6493a1abad..c5b15bce58 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.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.delayed(const Duration(seconds: 1)); - - final Timeline timeline = await driver.traceAction(() async { - await Future.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 0000000000..26745735d7 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test_driver/util.dart @@ -0,0 +1,44 @@ +// 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 '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.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.delayed(pageDelay); + } + + final Timeline timeline = await driver.traceAction(() async { + await Future.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 0000000000..3f7baf27fd --- /dev/null +++ b/dev/devicelab/bin/tasks/cubic_bezier_perf__timeline_summary.dart @@ -0,0 +1,14 @@ +// Copyright 2018 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 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 27e877fbb6..515d122636 100644 --- a/dev/devicelab/lib/tasks/perf_tests.dart +++ b/dev/devicelab/lib/tasks/perf_tests.dart @@ -46,6 +46,14 @@ TaskFunction createCullOpacityPerfTest() { ).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 0d89a03173..ec31344248 100644 --- a/dev/devicelab/manifest.yaml +++ b/dev/devicelab/manifest.yaml @@ -134,6 +134,13 @@ tasks: 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 0000000000..c2cc6809d2 --- /dev/null +++ b/examples/layers/raw/shader_warm_up.dart @@ -0,0 +1,33 @@ +// 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. + +// 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 0000000000..0124ac1375 --- /dev/null +++ b/examples/layers/test/smoketests/raw/shader_warm_up_test.dart @@ -0,0 +1,13 @@ +// Copyright 2017 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 6cce1f837a..fe432cb909 100644 --- a/packages/flutter/lib/painting.dart +++ b/packages/flutter/lib/painting.dart @@ -50,6 +50,7 @@ export 'src/painting/matrix_utils.dart'; 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 01596ad85f..1d4e72e371 100644 --- a/packages/flutter/lib/src/painting/binding.dart +++ b/packages/flutter/lib/src/painting/binding.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; 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 @@ mixin PaintingBinding on BindingBase, ServicesBinding { 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 0000000000..b71ac4743a --- /dev/null +++ b/packages/flutter/lib/src/painting/shader_warm_up.dart @@ -0,0 +1,148 @@ +// 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 '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= --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 paths = [rrectPath, circlePath, path]; + + final List paints = [ + 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)); + } +}