本文翻译自:https://medium.com/flutter/performance-testing-on-the-web-25323252de69
概览
性能测试是开发过程中非常重要的一环,通过合适的工具我们可以发现应用程序卡顿、变慢的潜在原因。本文,我们就来介绍一种在 Chrome 中测试 Flutter Web 应用程序性能的方法,此方法与官方测试 Flutter Gallery 应用性能方法类似。
示例应用
下图展示了我们本文测试的一个示例应用程序,其中包含一个顶部栏,一个悬浮按钮和一个无限列表,列表中的每一项可以展示按下按钮的次数。
点击 Appbar 中的 action 图标进入第二个页面,如下:
应用程序的完成代码如下:
测试点
我们主要测试该应用在 Chrome 中的以下几种情况:
- 用户在无限列表中滚动。
- 用户在两个页面之间切换。
- 用户点击悬浮操作按钮。
建立框架
在配置文件 pubspec.yaml
中添加如下配置项:
1 2 3 4 5 6 7 8
| dependencies: flutter: sdk: flutter web_benchmarks_framework: git: url: https://github.com/material-components/material-components-flutter-experimental.git ref: f6ebb4ed3b6489547d9ae58216df9999112be568 path: web_benchmarks_framework
|
引入 web_benchmarks_framework
,它是用在 Chrome 中做性能测试最小依赖库。
该库基于 macrobenchmarks
和 devicelab
,Flutter 官方主要就是使用两个库对 Flutter Gallery 进行了 Web 性能测试。目前,这两个库专用于 flutter/flutter
内部 sample 的 Web 性能测试 ,我们这里使用更加通用的 web_benchmarks_framework
。
运行 flutter pub get,
,同步依赖。
编写第一个测试
在 lib
中新建 benchmarks
文件夹,并创建 runner.dart
文件:
在该文件中添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:web_benchmarks_framework/recorder.dart'; import 'package:web_benchmarks_framework/driver.dart'; import 'package:web_benchmarks_example/main.dart'; import 'package:web_benchmarks_example/homepage.dart' show textKey;
abstract class AppRecorder extends WidgetRecorder { AppRecorder({@required this.benchmarkName}) : super(name: benchmarkName);
final String benchmarkName;
Future<void> automate();
@override Widget createWidget() { Future.delayed(Duration(milliseconds: 400), automate); return MyApp(); }
Future<void> animationStops() async { while (WidgetsBinding.instance.hasScheduledFrame) { await Future<void>.delayed(Duration(milliseconds: 200)); } } }
class ScrollRecorder extends AppRecorder { ScrollRecorder() : super(benchmarkName: 'scroll');
Future<void> automate() async { final scrollable = Scrollable.of(find.byKey(textKey).evaluate().single); await scrollable.position.animateTo( 30000, curve: Curves.linear, duration: Duration(seconds: 20), ); } }
Future<void> main() async { await runBenchmarks( { 'scroll': () => ScrollRecorder(), }, ); }
|
上面代码包括:
应用运行,创建一个 ScrollRecorder
对象,该对象可以执行自动化手势驱动应用程执行,在上面的代码中,应用启动后就会自动化向下滚动列表。
ScrollRecorder
继承 AppRecorder
,而 AppRecorder
继承自 WidgetRecorder
,其中就会通过驱动应用程序测试记录性能数据。
runBenchmarks
在 package:web_benchmarks_framework/driver.dart
中实现,该函数允许用户选择一个 benchmark 执行并在浏览器中显示测试结果。
automate
方法依赖 flutter_test
,可以使用它在应用程序中执行自动化手势和组件 find 等方法。
运行第一个测试
进入项目根目录,在终端运行 flutter run -d chrome -t lib/benchmarks/runner.dart
,该命令表示使用 runner.dart
代替 main.dart
作为程序入口点。
目前,我们只有一个 benchmark 测试(ScrollRecorder
),因此可以单击这里 “scroll” 直接启动。
测试开始后,列表会自动向下滚动,几秒钟后结束,页面内容如下:
该图表展示了记录时应用绘制每一帧所花费的时间,横轴表示时间线,纵轴表示每帧所花费的具体时间。
图表中前 2/3 背景为灰色,表示这些帧为预热帧(warm-up frames) ,需要从统计信息中省略,预热帧可以给 JIT 编译器一定时间来编译代码,并填充各种缓存,这样,之后所有的测试结果表达就是应用程序的真实性能数据了。但预热帧也不能总被忽略,它也可以在前几秒钟提供有关应用程序性能一些有价值的信息,这些信息也会影响我们对应用程序质量的分析。
红色框中的帧表示离群值(outliers),这些帧相比其他帧更耗时,也很容易被无视,例如,jank 在动画的开始或结束时除非到了特定的点否则将会不可见,但是,动画中间的一个不稳定帧将非常明显。
离群值可以一定程度上说明应用程序的简洁性,通过改进应用,我们可以降低离群值或减少离群数,这时就表明应用已经变得更加流畅了。
该 benchmark 完全在 Chrome 内部运行,创建 test/run_benchmarks.dart
,添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12
| import 'dart:convert' show JsonEncoder;
import 'package:web_benchmarks_framework/server.dart';
Future<void> main () async { final taskResult = await runWebBenchmark( macrobenchmarksDirectory: '.', entryPoint: 'lib/benchmarks/runner.dart', useCanvasKit: false, ); print (JsonEncoder.withIndent(' ').convert(taskResult.toJson())); }
|
运行 dart test/run_benchmarks.dart
。大约一分钟后,看到以下结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| Received profile data { "success": true, "data": { "scroll.html.preroll_frame.average": 93.88659793814433, "scroll.html.preroll_frame.outlierAverage": 1061.3333333333333, "scroll.html.preroll_frame.outlierRatio": 11.304417847077339, "scroll.html.preroll_frame.noise": 0.3103013467989926, "scroll.html.apply_frame.average": 391.1914893617021, "scroll.html.apply_frame.outlierAverage": 1462.3333333333333, "scroll.html.apply_frame.outlierRatio": 3.738152217266761, "scroll.html.apply_frame.noise": 0.24804233283684318, "scroll.html.drawFrameDuration.average": 1496.8690476190477, "scroll.html.drawFrameDuration.outlierAverage": 3622.8125, "scroll.html.drawFrameDuration.outlierRatio": 2.4202601461781335, "scroll.html.drawFrameDuration.noise": 0.38481902033678567, "scroll.html.totalUiFrame.average": 3441 }, "benchmarkScoreKeys": [ "scroll.html.drawFrameDuration.average", "scroll.html.drawFrameDuration.outlierRatio", "scroll.html.totalUiFrame.average" ] }
|
执行机器不同,这些性能值会有差异。
上面代码主要的内容包括:
- 运行
test/run_benchmarks.dart
构建 Flutter Web 应用,然后在 Chrome 中运行该应用。
test/run_benchmarks.dart
会连接到 Chrome 的 DevTools 端口,并从中监听并收集相关的性能数据。
结果含义如下:
- 每渲染一帧,layer tree 执行两个步骤。
- 第一步 “Preroll”,它不渲染任何东西,但是会计算稍后用于渲染的值,例如包括:变换矩阵,逆变换和片段。
- 第二步是 “Apply frame” ,UI 被实际渲染。
- “Draw frame” 表示框架渲染一帧所花费的总时间,包括 “Preroll” 和 “Apply frame”,也包括构建和布局组件所花费的时间。
- “ Total UI frame” 包括 “Draw frame” 中的所有内容,还包括浏览器执行的一些隐藏工作,例如层树更新,样式重新计算和浏览器侧布局(Flutter 自己的布局逻辑不同)。
- 收集数据集(持续时间列表)后,性能测试算法会省略离群值。
- 首先,计算数据的平均值和标准差,任何高于(均值+1个标准差)的数据点均被视为离群值。
- 非离群值(纯数据)的平均值和标准差用于计算数据集的平均值和噪声,然后将其报告。
- 还报告了所有异常值的平均值,以及“异常值平均值”和“非异常值平均值”的比率。
- 对于每个数据集,“ outlierRatio” 和 “noise” 都是表明应用程序性能有多少噪声的良好指标。如果结果太嘈杂,则可能表明性能不一致(例如,GC 暂停时出现不稳定的帧),通过降低噪音,可以使应用更流畅地运行。
添加更多测试
修改 lib/benchmarks/runner.dart
,添加两个测试。首先,修改 main 函数:
1 2 3 4 5 6 7 8 9
| Future<void> main() async { await runBenchmarks( { 'scroll': () => ScrollRecorder(), 'page': () => PageRecorder(), 'tap': () => TapRecorder(), }, ); }
|
然后,再添加两个继承 AppRecorder
的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class PageRecorder extends AppRecorder { PageRecorder() : super(benchmarkName: 'page');
bool _completed = false;
@override bool shouldContinue() => profile.shouldContinue() || !_completed;
Future<void> automate() async { final controller = LiveWidgetController(WidgetsBinding.instance); for (int i = 0; i < 10; ++i) { print('Testing round $i...'); await controller.tap(find.byKey(aboutPageKey)); await animationStops(); await controller.tap(find.byKey(backKey)); await animationStops(); } _completed = true; } }
class TapRecorder extends AppRecorder { TapRecorder() : super(benchmarkName: 'tap');
bool _completed = false;
@override bool shouldContinue() => profile.shouldContinue() || !_completed;
Future<void> automate() async { final controller = LiveWidgetController(WidgetsBinding.instance); for (int i = 0; i < 10; ++i) { print('Testing round $i...'); await controller.tap(find.byIcon(Icons.add)); await animationStops(); } _completed = true; } }
|
这里的内容包括:
- 这里添加了剩余的两个 benchmark 测试:一个用于在页面之间切换(PageRecorder),另一个用于点击悬浮操作按钮(TapRecorder)。
animationStops
会一直检查动画是否正在发生,所有动画停止后才停止,这就可以确保成功过渡到打开的第二个页面。
- 在 “page” 和 “tap” benchmarks 中,
_completed
表示自动手势是否完成。
- 在 “page” 和 “tap” benchmarks中,重写
shouldContinue
方法可以实现所有手势完成后 AppRecorder
停止记录帧。
运行测试
要在 Chrome 中运行这些测试(并查看动画),执行下面这行命令:
1
| flutter run -d chrome -t lib/benchmarks/runner.dart --profile
|
要运行这些测试并收集 DevTools 数据,执行下面这行命令:
1
| dart test/run_benchmarks.dart
|
下一步
一旦用这种方式收集到了性能数据,就可以根据需要使用啦:
- 可以在 CI 中设置一个任务,每当有人提交 PR 时就运行这些 benchmark 测试,避免引入高性能消耗的 change。
- 也可以设置一个 dashboard 页面,来分析性能测试 benchmark 的趋势,如官方为 Flutter Gallery 做的 Flutter Dashboard
关注公众号「Meandni」,及时阅读最新前沿技术动态,不至于落后时代。