概览
性能测试是开发过程中非常重要的一环,通过合适的工具我们可以发现应用程序卡顿、变慢的潜在原因。本文,我们就来介绍一种在 Chrome 中测试 Flutter Web 应用程序性能的方法,此方法与官方测试 Flutter Gallery 应用性能方法类似。
示例应用
下图展示了我们本文测试的一个示例应用程序,其中包含一个顶部栏,一个悬浮按钮和一个无限列表,列表上方展示按下按钮的次数。
点击 Appbar 中的 action 图标进入第二个页面,如下:
应用程序的完成代码如下:
- https://github.com/material-components/material-components-flutter-experimental/tree/develop/web_benchmarks_example
测试点
我们主要测试该应用在 Chrome 中的以下几种情况:
- 用户在无限列表中滚动。
- 用户在两个页面之间切换。
- 用户点击悬浮操作按钮。
建立框架
在配置文件 pubspec.yaml
中添加如下配置项:
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
文件:
在该文件中添加如下代码:
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 在动画的开始或结束时除非到了特定的点否则将会不可见,但是,动画中间的一个不稳定帧将非常明显。
离群值可以一定程度上说明应用程序的简洁性,通过改进应用,我们可以降低离群值或减少离群数,这时就表明应用已经变得更加流畅了。
从 Chrome 的 DevTools 收集性能数据
该 benchmark 完全在 Chrome 内部运行,创建 test/run_benchmarks.dart
,添加如下代码:
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
。大约一分钟后,看到以下结果:
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 函数:
Future<void> main() async {
await runBenchmarks(
{
'scroll': () => ScrollRecorder(),
'page': () => PageRecorder(),
'tap': () => TapRecorder(),
},
);
}
然后,再添加两个继承 AppRecorder
的类:
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 中运行这些测试(并查看动画),执行下面这行命令:
flutter run -d chrome -t lib/benchmarks/runner.dart --profile
要运行这些测试并收集 DevTools 数据,执行下面这行命令:
dart test/run_benchmarks.dart
下一步
一旦用这种方式收集到了性能数据,就可以根据需要使用啦:
- 可以在 CI 中设置一个任务,每当有人提交 PR 时就运行这些 benchmark 测试,避免引入高性能消耗的 change。
- 也可以设置一个 dashboard 页面,来分析性能测试 benchmark 的趋势,如官方为 Flutter Gallery 做的 Flutter Dashboard
相关资源链接可点击「阅读全文」查看我的博客原文。也欢迎大家和我一起交流。