From a32fc986f3c636e64f9e34acac1ead7612340d97 Mon Sep 17 00:00:00 2001 From: liyuqian Date: Tue, 19 Nov 2019 15:48:41 -0800 Subject: [PATCH] Add a perf test for picture raster cache (#45050) This will catch issues like https://github.com/flutter/flutter/issues/44252, and this test is inspired by the `list_demo` sample app in https://github.com/flutter/flutter/issues/43083#issue-509586473 This is tested locally on a Moto G4 before and after the fix https://github.com/flutter/engine/pull/13710 The `average_frame_rasterizer_time_millis` of this test drops from ~5.4ms to ~4.9ms after that fix. --- .../macrobenchmarks/lib/common.dart | 1 + dev/benchmarks/macrobenchmarks/lib/main.dart | 9 + .../lib/src/picture_cache.dart | 294 ++++++++++++++++++ dev/benchmarks/macrobenchmarks/pubspec.yaml | 3 + .../test_driver/picture_cache_perf.dart | 11 + .../test_driver/picture_cache_perf_test.dart | 29 ++ .../macrobenchmarks/test_driver/util.dart | 11 +- .../picture_cache_perf__timeline_summary.dart | 14 + dev/devicelab/lib/tasks/perf_tests.dart | 8 + dev/devicelab/manifest.yaml | 6 + examples/flutter_gallery/pubspec.yaml | 1 + 11 files changed, 385 insertions(+), 2 deletions(-) create mode 100644 dev/benchmarks/macrobenchmarks/lib/src/picture_cache.dart create mode 100644 dev/benchmarks/macrobenchmarks/test_driver/picture_cache_perf.dart create mode 100644 dev/benchmarks/macrobenchmarks/test_driver/picture_cache_perf_test.dart create mode 100644 dev/devicelab/bin/tasks/picture_cache_perf__timeline_summary.dart diff --git a/dev/benchmarks/macrobenchmarks/lib/common.dart b/dev/benchmarks/macrobenchmarks/lib/common.dart index 7695b53397..0546ca3fee 100644 --- a/dev/benchmarks/macrobenchmarks/lib/common.dart +++ b/dev/benchmarks/macrobenchmarks/lib/common.dart @@ -6,3 +6,4 @@ const String kCullOpacityRouteName = '/cull_opacity'; const String kCubicBezierRouteName = '/cubic_bezier'; const String kBackdropFilterRouteName = '/backdrop_filter'; const String kSimpleAnimationRouteName = '/simple_animation'; +const String kPictureCacheRouteName = '/picture_cache'; diff --git a/dev/benchmarks/macrobenchmarks/lib/main.dart b/dev/benchmarks/macrobenchmarks/lib/main.dart index 267b0f2b36..3944a6c3a2 100644 --- a/dev/benchmarks/macrobenchmarks/lib/main.dart +++ b/dev/benchmarks/macrobenchmarks/lib/main.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:macrobenchmarks/src/picture_cache.dart'; import 'common.dart'; import 'src/backdrop_filter.dart'; @@ -26,6 +27,7 @@ class MacrobenchmarksApp extends StatelessWidget { kCubicBezierRouteName: (BuildContext context) => CubicBezierPage(), kBackdropFilterRouteName: (BuildContext context) => BackdropFilterPage(), kSimpleAnimationRouteName: (BuildContext conttext) => SimpleAnimationPage(), + kPictureCacheRouteName: (BuildContext conttext) => PictureCachePage(), }, ); } @@ -66,6 +68,13 @@ class HomePage extends StatelessWidget { Navigator.pushNamed(context, kSimpleAnimationRouteName); }, ), + RaisedButton( + key: const Key(kPictureCacheRouteName), + child: const Text('Picture Cache'), + onPressed: () { + Navigator.pushNamed(context, kPictureCacheRouteName); + }, + ), ], ), ); diff --git a/dev/benchmarks/macrobenchmarks/lib/src/picture_cache.dart b/dev/benchmarks/macrobenchmarks/lib/src/picture_cache.dart new file mode 100644 index 0000000000..ae38d7df05 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/lib/src/picture_cache.dart @@ -0,0 +1,294 @@ +// 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:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class PictureCachePage extends StatelessWidget { + static const List kTabNames = ['1', '2', '3', '4', '5']; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: DefaultTabController( + length: kTabNames.length, // This is the number of tabs. + child: NestedScrollView( + key: const Key('nested-scroll'), // this key is used by the driver test + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + // These are the slivers that show up in the "outer" scroll view. + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + child: SliverAppBar( + title: const Text('Picture Cache'), + pinned: true, + expandedHeight: 50.0, + forceElevated: innerBoxIsScrolled, + bottom: TabBar( + tabs: kTabNames.map((String name) => Tab(text: name)).toList(), + ), + ), + ), + ]; + }, + body: TabBarView( + children: kTabNames.map((String name) { + return SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (BuildContext context) { + return VerticalList(); + }, + ), + ); + }).toList(), + ), + ), + ), + ); + } +} + +class VerticalList extends StatelessWidget { + static const int kItemCount = 100; + + @override + Widget build(BuildContext context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + // This is the flip side of the SliverOverlapAbsorber above. + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) => ListItem(index: index), + childCount: kItemCount, + ), + ), + ], + ); + } +} + +class ListItem extends StatelessWidget { + const ListItem({Key key, this.index}) + : super(key: key); + + final int index; + + static const String kMockChineseTitle = '复杂的中文标题?复杂的中文标题!'; + static const String kMockName = '李耳123456'; + static const int kMockCount = 999; + + @override + Widget build(BuildContext context) { + final List contents = [ + const SizedBox( + height: 15, + ), + _buildUserInfo(), + const SizedBox( + height: 10, + ) + ]; + if (index % 3 != 0) { + contents.add(_buildImageContent()); + } else { + contents.addAll([ + Padding( + padding: const EdgeInsets.only(left: 40, right: 15), + child: _buildContentText(), + ), + const SizedBox( + height: 10, + ), + Padding( + padding: const EdgeInsets.only(left: 40, right: 15), + child: _buildBottomRow(), + ), + ]); + } + contents.addAll([ + const SizedBox( + height: 13, + ), + buildDivider(0.5, const EdgeInsets.only(left: 40, right: 15)), + ]); + return MaterialButton( + onPressed: () {}, + padding: EdgeInsets.zero, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: contents, + ), + ); + } + + Text _buildRankText() { + return Text( + (index + 1).toString(), + style: TextStyle( + fontSize: 15, + color: index + 1 <= 3 ? const Color(0xFFE5645F) : Colors.black, + fontWeight: FontWeight.bold, + ), + ); + } + + Widget _buildImageContent() { + return Row( + children: [ + const SizedBox( + width: 40, + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(right: 30), + child: _buildContentText(), + ), + const SizedBox( + height: 10, + ), + _buildBottomRow(), + ], + ), + ), + Image.asset( + index % 2 == 0 ? 'food/butternut_squash_soup.png' : 'food/cherry_pie.png', + package: 'flutter_gallery_assets', + fit: BoxFit.cover, + width: 110, + height: 70, + ), + const SizedBox( + width: 15, + ) + ], + ); + } + + Widget _buildContentText() { + return const Text( + kMockChineseTitle, + style: TextStyle( + fontSize: 16, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ); + } + + Widget _buildBottomRow() { + return Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 7, + ), + height: 16, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: const Color(0xFFFBEEEE), + ), + child: Row( + children: [ + const SizedBox( + width: 3, + ), + Text( + 'hot:${_convertCountToStr(kMockCount)}', + style: const TextStyle( + color: Color(0xFFE5645F), + fontSize: 11, + ), + ), + ], + ), + ), + const SizedBox( + width: 9, + ), + const Text( + 'ans:$kMockCount', + style: TextStyle( + color: Color(0xFF999999), + fontSize: 11, + ), + ), + const SizedBox( + width: 9, + ), + const Text( + 'like:$kMockCount', + style: TextStyle( + color: Color(0xFF999999), + fontSize: 11, + ), + ), + ], + ); + } + + String _convertCountToStr(int count) { + if (count < 10000) { + return count.toString(); + } else if (count < 100000) { + return (count / 10000).toStringAsPrecision(2) + 'w'; + } else { + return (count / 10000).floor().toString() + 'w'; + } + } + + Widget _buildUserInfo() { + return GestureDetector( + onTap: () {}, + child: Row( + children: [ + Container( + width: 40, alignment: Alignment.center, child: _buildRankText()), + const CircleAvatar( + radius: 11.5, + ), + const SizedBox( + width: 6, + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 250), + child: Text( + kMockName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox( + width: 4, + ), + const SizedBox( + width: 15, + ), + ], + ), + ); + } + + Widget buildDivider(double height, EdgeInsets padding) { + return Container( + padding: padding, + height: height, + color: const Color(0xFFF5F5F5), + ); + } +} diff --git a/dev/benchmarks/macrobenchmarks/pubspec.yaml b/dev/benchmarks/macrobenchmarks/pubspec.yaml index b5fd44aa0d..b9cafb9cf0 100644 --- a/dev/benchmarks/macrobenchmarks/pubspec.yaml +++ b/dev/benchmarks/macrobenchmarks/pubspec.yaml @@ -87,5 +87,8 @@ dev_dependencies: flutter: uses-material-design: true + assets: + - packages/flutter_gallery_assets/food/butternut_squash_soup.png + - packages/flutter_gallery_assets/food/cherry_pie.png # PUBSPEC CHECKSUM: eeee diff --git a/dev/benchmarks/macrobenchmarks/test_driver/picture_cache_perf.dart b/dev/benchmarks/macrobenchmarks/test_driver/picture_cache_perf.dart new file mode 100644 index 0000000000..80361f3704 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test_driver/picture_cache_perf.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:flutter_driver/driver_extension.dart'; +import 'package:macrobenchmarks/main.dart' as app; + +void main() { + enableFlutterDriverExtension(); + app.main(); +} diff --git a/dev/benchmarks/macrobenchmarks/test_driver/picture_cache_perf_test.dart b/dev/benchmarks/macrobenchmarks/test_driver/picture_cache_perf_test.dart new file mode 100644 index 0000000000..374ae59516 --- /dev/null +++ b/dev/benchmarks/macrobenchmarks/test_driver/picture_cache_perf_test.dart @@ -0,0 +1,29 @@ +// 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:flutter_driver/flutter_driver.dart'; +import 'package:macrobenchmarks/common.dart'; + +import 'util.dart'; + +void main() { + macroPerfTest( + 'picture_cache_perf', + kPictureCacheRouteName, + pageDelay: const Duration(seconds: 1), + driverOps: (FlutterDriver driver) async { + final SerializableFinder nestedScroll = find.byValueKey('nested-scroll'); + Future _scrollOnce(double offset) async { + await driver.scroll(nestedScroll, offset, 0.0, const Duration(milliseconds: 300)); + await Future.delayed(const Duration(milliseconds: 500)); + } + for (int i = 0; i < 3; i += 1) { + await _scrollOnce(-300.0); + await _scrollOnce(-300.0); + await _scrollOnce(300.0); + await _scrollOnce(300.0); + } + }, + ); +} diff --git a/dev/benchmarks/macrobenchmarks/test_driver/util.dart b/dev/benchmarks/macrobenchmarks/test_driver/util.dart index 79fe4f1a66..0b08d078e5 100644 --- a/dev/benchmarks/macrobenchmarks/test_driver/util.dart +++ b/dev/benchmarks/macrobenchmarks/test_driver/util.dart @@ -10,7 +10,10 @@ import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; void macroPerfTest( String testName, String routeName, - {Duration pageDelay, Duration duration = const Duration(seconds: 3)}) { + { Duration pageDelay, + Duration duration = const Duration(seconds: 3), + Future driverOps(FlutterDriver driver), + }) { test(testName, () async { final FlutterDriver driver = await FlutterDriver.connect(); @@ -32,7 +35,11 @@ void macroPerfTest( } final Timeline timeline = await driver.traceAction(() async { - await Future.delayed(duration); + final Future durationFuture = Future.delayed(duration); + if (driverOps != null) { + await driverOps(driver); + } + await durationFuture; }); final TimelineSummary summary = TimelineSummary.summarize(timeline); diff --git a/dev/devicelab/bin/tasks/picture_cache_perf__timeline_summary.dart b/dev/devicelab/bin/tasks/picture_cache_perf__timeline_summary.dart new file mode 100644 index 0000000000..998a449d5e --- /dev/null +++ b/dev/devicelab/bin/tasks/picture_cache_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 main() async { + deviceOperatingSystem = DeviceOperatingSystem.android; + await task(createPictureCachePerfTest()); +} diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart index 664aa0ab28..1cc4d20d90 100644 --- a/dev/devicelab/lib/tasks/perf_tests.dart +++ b/dev/devicelab/lib/tasks/perf_tests.dart @@ -72,6 +72,14 @@ TaskFunction createSimpleAnimationPerfTest({bool needsMeasureCpuGpu = false}) { ).run; } +TaskFunction createPictureCachePerfTest() { + return PerfTest( + '${flutterDirectory.path}/dev/benchmarks/macrobenchmarks', + 'test_driver/picture_cache_perf.dart', + 'picture_cache_perf', + ).run; +} + TaskFunction createFlutterGalleryStartupTest() { return StartupTest( '${flutterDirectory.path}/examples/flutter_gallery', diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml index 3aefda71bd..ab4a8951fc 100644 --- a/dev/devicelab/manifest.yaml +++ b/dev/devicelab/manifest.yaml @@ -167,6 +167,12 @@ tasks: stage: devicelab required_agent_capabilities: ["mac/android"] + picture_cache_perf__timeline_summary: + description: > + Measures the runtime performance of raster caching many pictures on Android. + stage: devicelab + required_agent_capabilities: ["mac/android"] + cubic_bezier_perf__timeline_summary: description: > Measures the runtime performance of cubic bezier animations on Android. diff --git a/examples/flutter_gallery/pubspec.yaml b/examples/flutter_gallery/pubspec.yaml index 98f9ba0a84..a16a27273b 100644 --- a/examples/flutter_gallery/pubspec.yaml +++ b/examples/flutter_gallery/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: rally_assets: 1.0.0 # Also update dev/benchmarks/complex_layout/pubspec.yaml + # and dev/benchmarks/macrobenchmarks/pubspec.yaml flutter_gallery_assets: 0.1.9+2 charcode: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"