「译」Performance testing of Flutter apps

使用 Flutter Driver 我的桌面上运行 Developer Quest 的早期版本

原文链接:Performance testing of Flutter apps 作者:Filip Hracek (from Flutter team)

Flutter 在默认状态下就能运行得非常快,这点非常好! 但是否这就意味着你完全不需要考虑性能呢?

答案是否定的,编写一个速度非常慢的 Flutter 应用是完全可能的。然而,另一方面你也可以充分利用这个框架,让你的 app 不仅快速,高效,而且使用更少的 CPU 时间和电量。

这就是我们想要的!在一个有意义的指标上来比较两个版本的应用在统计上明显的差异

在 Flutter 中有一些性能优化的通用指导原则:

  • 更新状态时,影响范围尽可能地少。
  • 仅当必要时才更新状态。
  • 不要在 build 方法中进行密集型计算任务,理想的话,把这些操作放在 main isolate 之外。

你可能很难相信,对于大多数性能优化的问题来说,它们的答案统统指向了这句话——“它究竟取决于什么?”。对于特定 Widget 是否值得进行特定优化,并付出维护成本?在特定情况下的特殊处理是否合理?

对于这些问题唯一有用答案是测试和测量。量化每个选择对性能的影响,并根据该数据做出决定。

好消息是 Flutter 提供了出色的性能分析工具,例如包含 Flutter Inspector 的 Dart DevTools(目前还处于预览版),或是 Android Studio 中的 Flutter Inspector(安装了 Flutter 插件下)。你可以使用 Flutter Driver 操作应用,并在 Profile 模式下保存性能信息。

然而坏消息是,现在的手机实在是太过智能了。😂

管理器的问题(Governors)

系统级守护进程需要根据当前负载调整 CPU 和 GPU 单元的速度,但是 iOS 和 Android 的管理器却很难量化 Flutter 应用的性能。总的来说这还是一件好事,因为它确保了平稳的性能,同时也消耗尽可能少的电量。

然而缺点是你完全可以通过提高其效率以显著加快应用的运行速度。

下面的例子中,你可以看到如何在应用中循环打印一些无意义的 print 语句,使得管理器切换到更高的档位,从而让应用运行更快速,性能更加可预测。

管理器的问题:在默认情况下,你无法相信这些数字。在上面这个盒子图中,我们在 x 轴上进行单独运行(用它们开始的确切时间标记),并在 y 轴上表示构建时间。正如你所看到那样,当我们引入一些完全不必要的打印语句时,它竟然会缩短(而不是增加) build 时间。

在这个实验中(上图),更差的代码反而导致了更快的构建时间,更快的光栅化时间和更高的帧率。如果客观上更差的代码会得到更好的性能,那么你就不能遵循这些指标。

上面仅仅是为了解释为何移动应用性能基准不直观,以及测试困难的一个例子。

接下来,我将分享一些 Google I/O app 上 Flutter 的例子,Developer Quest

基本建议

  • 不要在 DEBUG 模式下测量性能。只有 profile 模式下才能测量其性能。
  • 在真机上测试,不要在 Android 或者 iOS 模拟器上测试。虽然模拟器软件非常适合开发使用,但是它们在性能表现上和真机差异非常大。Flutter 不允许在模拟器上以 profile 模式运行,因为这并没有任何意义。这种方式收集的性能数据并不是实际的性能。
  • 理想的情况下是使用相同物理设备。让它作为你专用的性能测试机,并不再用于其他用途。
  • 学习 Flutter 性能分析工具

CPU / GPU 管理器

正如我们刚才所说的,现代操作系统根据负载和一些其他的启发式调整每个 CPU 和 GPU 的频率。(例如,触摸屏幕通常会让 Android 手机将其优先级置于更高的档位。)

在 Android 上你能够直接关掉这些管理器,我们称之为“scale locking”。

  • 编写一个脚本来 scale-lock 你的测试机性能。你可以在 Skia’s recipe 中找到灵感,也可以查阅 Unix CPU API
  • 除非你正在运行像 Skia 这样的大型基准测试,通常情况下你可能想要一些更加轻量,不那么通用的东西。来看看 Developer Quest 中针对某些方面的 shell 脚本吧。 例如,下面这个将 CPU 管理器设为 userspace (唯一一个不会自动调整 CPU 频率的管理器)。
#!/usr/bin/env bash
GOV="userspace"
echo "Setting CPU governor to: ${GOV}"
adb shell "echo ${GOV} > /sys/devices/system/cpu/cpu${CPU_NO}/cpufreq/scaling_governor"
ACTUAL_GOV=`adb shell "cat /sys/devices/system/cpu/cpu${CPU_NO}/cpufreq/scaling_governor"`
echo "- result: ${ACTUAL_GOV}"
  • 现在的目标并不是要模拟真实性能(没有用户 scale-lock 的设备),而是获取运行时可比较的性能指标。
  • 最后你需要进行测试,并在设备上运行 shell 脚本。只有这样才是有效的,在这之前的性能数据都在欺骗你。
使用 Flutter Driver 我的桌面上运行 Developer Quest 的早期版本

Flutter Driver

Flutter Driver 能够自动执行应用。你可以阅读 flutter.dev 上性能分析的文章,了解如何使用它来分析应用。

  • 不要在性能测试时手动操作你的应用。始终使用 Flutter Driver 确保你的比较具有意义。
  • 编写你的 Flutter Driver 代码,以便能够让其执行你真正想要测试的内容。如果你正在进行常规程序性能测试,请尝试遍历全部应用,并执行用户会做的操作。
  • 如果你的应用具有偶然性(随机网络事件等),请使用 mock 数据,并且保证它们尽可能相似。
  • 如果需要的话,还可以使用 Timeline 的 startSync() 方法 和 finishSync() 方法,来添加自定义时间轴事件。例如,当你想要测试特定方法的性能时,这很有用。将 startSync() 放在它的开头,并在方法结束时使用 finishSync()
  • 对于各个不同版本的应用,需要进行多次测试。在 Developer Quest 时我汇总了 100 次。当你测量一些比如像“第 99 位百分数”这样繁杂的东西时,你得运行更多次才行。而对于基于 POSIX 的系统来说,只需要运行下面这样:
for i in {1..100}; do flutter drive --target=test_driver/perf.dart --profile; done

时间轴(Timeline)

时间轴是你运行 profile 模式下输出的原始资料。Flutter 将此信息转储到可被 chrome://tracing 加载的 JSON 文件中。

  • 了解如何在 Chrome 的 tracing timeline 中开启完整的 timeline。你只需要打开 Chrome 浏览器的 chrome://tracing,然后点击“Load”,选择那个 JSON 文件。你可以在这篇 简短指导 中获得关于它的更多信息。(同样有 Flutter Timeline tooling,但目前还处于 tech preview 阶段。因为在 Flutter 的 timeline tooling 准备就绪之前,Developer Quest 项目就已经开始了,所以我还没用过那个。)
  • 使用 WSAD 键在 chrome://tracing 的 timeline 中移动,以及使用 1234 来更改操作模式。
  • 首次设置性能测试时,考虑是否使用 Flutter Driver 运行完整的 Android systrace。这使您可以更深入地了解设备中实际发生的情况,包括 CPU 缩放信息。但是,不要使用完全开启的 systrace 测量您的应用程序,因为它会让一切变得非常慢而且更加不可预测。
  • 那么,应该如何使用 Flutter Driver 运行完整的 Android systrace 呢?首先,你得从 /path/to/your/android/sdk/platform-tools/systrace/systrace.py --atrace-categories=gfx,input,view,webview,wm,am,sm,audio,video,camera,hal,app,res,dalvik,rs,bionic,power,pm,ss,database,network,adb,pdx,sched,irq,freq,idle,disk,load,workq,memreclaim,regulators,binder_driver,binder_lock 开启 Android systrace。 然后通过 flutter run test_driver/perf.dart --profile --trace-systrace 运行 app。最后,通过 flutter drive --driver=test_driver/perf_test.dart --use-existing-app=http://127.0.0.1:NNNNN/ 启动 Flutter Driver(其中 NNNNN 是 Flutter run 上运行的端口给你的)。

度量

最好能看尽可能多的指标,但我发现一些相比其他更有用的指标。

  • build 的时间光栅化的时间 仅在真正严格的性能测试中才有用(默认情况下提供的度量标准是 TimelineSummary),这些测试除了 UI 之外不会包含太多其他内容。
  • 不要把 TimelineSummary.frameCount 看作计算每秒帧数(FPS)的方法。Flutter 的配置文件工具不能给你提供真实的帧速率信息。TimelineSummary 虽然提供了 countFrames() 方法,但它只会计算已完成的帧构建了多少次。一个优化良好的应用可以限制不必要的重建,每秒的帧数比经常重建的未经优化的应用程序少很多。
  • 我个人获得的最有用的数据是通过测量运行 Dart 代码所花费的总 CPU 时间得到的。这会算上 build 方法和外部执行的代码。假设你在 scale-locked 的设备上运行配置文件测试,总 CPU 时间可以很好地估算你的应用程序将消耗更多还是更少的电。
  • 要找出 Dart 代码运行所花费的总 CPU 时间,最简单的方法是看 timeline 中 MessageLoop:FlushTasks 事件的范围。在制作 Developer Quest 的时候,我编写了一个 Dart 工具来提取它们。
  • 要检测 jank (即跳过的帧),请寻找极端情况。例如,对于 Developer Quest 的特定情况以及我们用于测试的设备来说,观察 95% 的 build 时间很有帮助。(90% 的 build 时间都很相似,即使是在比较效率水平差距非常大的代码也是一样,而 99% 往往又过于乱了。但你的情况可能会有所不同。)
  • 正如我之前所提到的那样,对于你的每个版本的应用都演算它上百次, 然后使用有些许误差的平均值或百分位数据。如果使用箱形图就更好了!

结论

当这些都设置好了之后,你就能够自信地比较提交和实验。下面,你可以看到一个很常见困境的答案:“这种优化值得维护开销吗?”

我认为对于这个情况来说,答案是肯定的。只需几行代码,我们应用的每个自动测试平均可以减少 12% 的 CPU 时间。

然而,本文的主要内容是,不同的测量方式可能会反映处非常不同的东西。尝试过于宽泛地推断性能测量也许十分符合直觉,但却是错误的。

换句话说:“它究竟取决于什么”。我们应该信奉这句话。

以上便是翻译的全部内容,我认为这篇文章对我们构建高效的 Flutter 应用具有很大的指导意义,所以便翻译出来分享给大家。

如若有译误还请指出。