在 Chrome 中测试 Flutter Web 应用性能

本文翻译自:https://medium.com/flutter/performance-testing-on-the-web-25323252de69

概览

性能测试是开发过程中非常重要的一环,通过合适的工具我们可以发现应用程序卡顿、变慢的潜在原因。本文,我们就来介绍一种在 Chrome 中测试 Flutter Web 应用程序性能的方法,此方法与官方测试 Flutter Gallery 应用性能方法类似。

示例应用

下图展示了我们本文测试的一个示例应用程序,其中包含一个顶部栏,一个悬浮按钮和一个无限列表,列表中的每一项可以展示按下按钮的次数。

图片发布

点击 Appbar 中的 action 图标进入第二个页面,如下:

应用程序的完成代码如下:

测试点

我们主要测试该应用在 Chrome 中的以下几种情况:

  1. 用户在无限列表中滚动。
  2. 用户在两个页面之间切换。
  3. 用户点击悬浮操作按钮。

建立框架

在配置文件 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 中做性能测试最小依赖库。

该库基于 macrobenchmarksdevicelab ,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,其中就会通过驱动应用程序测试记录性能数据。

  • runBenchmarkspackage: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 ,添加如下代码:

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」,及时阅读最新前沿技术动态,不至于落后时代。

扫一扫,关注「Meandni」
文章作者: Joker
文章链接: https://meandni.com/2020/10/11/performance-testing-on-the-web/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Joker's Blog