使用 Raptor 对 Firefox OS 进行性能测试

当我们谈论 Web 的性能时,一些常见的问题会浮现在脑海中

  • 为什么这个页面加载时间如此之长?
  • 如何优化我的 JavaScript 代码使其运行得更快?
  • 如果我对这段代码进行一些更改,这会使这个应用程序变慢吗?

我一直致力于让这些问题更容易解答,特别是针对 Gaia,Firefox OS 的 UI 层,这是一个完全以 Web 为中心的移动设备操作系统。编写针对桌面的高性能网页有其自身的特殊性,而使用 Web 技术编写原生应用程序则将挑战提高了一个数量级。我想介绍我在 Firefox OS 中面临的挑战,这些挑战让性能成为一个更难处理的话题,同时记录我的解决方案并揭示 Web API 中需要填补的空白。

从现在开始,我将把网页、文档等称为应用程序,虽然 Web“文档”通常不需要我在这里所关注的性能,但相同的技术仍然适用。

修复应用程序的生命周期

关于 Firefox OS 应用程序,我经常被问到的一个问题是

应用程序加载需要多长时间?

这个问题很难回答,因为我们无法确定我们是否在说同一件事。根据 UX 和我在 Mozilla 的研究,我尝试采用以下定义来确定应用程序加载所需的时间

加载应用程序所需的时间是从用户发起对应用程序的请求的那一刻开始,到应用程序看起来已准备好供用户交互的那一刻结束。

在移动设备上,这通常是从用户点击图标启动应用程序的那一刻开始,到应用程序看起来已加载完毕的那一刻结束;当应用程序看起来用户可以开始与其交互时。其中一部分时间分配给操作系统,以便启动应用程序,这不在应用程序本身的控制范围内,但大部分加载时间应该在应用程序内部。

那么window load呢?

对于SPA(单页应用程序)、Ajax、脚本加载器、延迟执行等,window load已经没有多少意义了。如果我们仅仅能够测量达到load所需的时间,我们的工作将很容易。不幸的是,没有办法以一种对每个人都可预测的方式推断出应用程序何时已加载完毕。相反,我们依靠应用程序来为我们暗示这些时刻。

对于 Firefox OS,我帮助开发了一系列与几乎所有应用程序相关的约定时刻,这些时刻暗示着应用程序的加载生命周期(也记录为 MDN 上的性能指南)。

导航已加载navigationLoaded

应用程序指定其核心 chrome 或导航界面存在于 DOM 中,并且已标记为准备显示,例如,当元素不是display: none或任何其他影响界面元素可见性的功能时。

导航可交互navigationInteractive

应用程序指定其核心 chrome 或导航界面已绑定其事件,并且已准备好供用户交互。

视觉已加载visuallyLoaded

应用程序指定它已加载完毕,即“以上折叠”内容存在于 DOM 中,并且已标记为准备显示,再次强调不是display: none或其他隐藏功能。

内容可交互contentInteractive

应用程序指定它已绑定用于最少功能集的事件,以允许用户与在visuallyLoaded时提供的“以上折叠”内容进行交互。

完全加载fullyLoaded

应用程序已完全加载,即任何相关的“以下折叠”内容和功能都已注入 DOM,并标记为可见。应用程序已准备好供用户交互。任何必要的启动后台处理已完成,应该处于稳定状态,除非用户进行进一步的交互。

重要的时刻是视觉已加载。这与用户感知的“已准备好”直接相关。作为额外的好处,使用visuallyLoaded指标与基于相机的性能验证完美匹配。

指示时刻

有了明确定义的应用程序启动生命周期,我们可以使用用户计时 API来指示这些时刻,该 API 从 v2.2 版本开始在 Firefox OS 中可用。

window.performance.mark( string markName )

特别是在启动期间

performance.mark('navigationLoaded');
performance.mark('navigationInteractive');
...
performance.mark('visuallyLoaded');
...
performance.mark('contentInteractive');
performance.mark('fullyLoaded');

你甚至可以使用measure()方法在开始和另一个标记之间创建测量,甚至在 2 个其他标记之间创建测量

// Denote point of user interaction
performance.mark('tapOnButton');

loadUI();

// Capture the time from now (sectionLoaded) to tapOnButton
performance.measure('sectionLoaded', 'tapOnButton');

使用getEntriesgetEntriesByNamegetEntriesByType来获取这些性能指标非常简单,这些方法可以获取指标集合。本文的目的不是介绍用户计时的用法,所以我将继续前进。

有了应用程序何时已加载完毕的指标,我们知道了应用程序加载需要多长时间,因为我们可以将其与——等等,不对。我们不知道用户启动应用程序的时刻。

虽然桌面网站可能能够轻松获取启动请求的时刻,但在 Firefox OS 上却并不简单。为了启动应用程序,用户通常会点击主屏幕上的图标。主屏幕驻留在与正在启动的应用程序不同的进程中,我们无法在它们之间传递性能指标。

使用 Raptor 解决问题

由于平台中没有可用的 API 或交互机制来克服这一难题和其他困难,我们构建了工具来提供帮助。这就是Raptor 性能测试工具的起源。通过它,我们可以从 Gaia 应用程序中收集指标,并回答我们所拥有的性能问题。

Raptor 的构建考虑了以下几个目标

  • 在不影响性能的情况下对 Firefox OS 进行性能测试。我们不需要 polyfill、测试代码或 hackery 来获得真实的性能指标。
  • 尽可能使用 Web API,并通过其他方法填补空白。
  • 保持足够的灵活性,以适应各种不同的应用程序架构风格。
  • 可扩展性,以适应非正常情况的性能测试场景。
问题:确定用户启动应用程序的时刻

给定两个独立的应用程序——主屏幕和任何其他已安装的应用程序——我们如何在其中一个应用程序中创建一个性能指标,并在另一个应用程序中进行比较?即使我们能够将性能指标从一个应用程序发送到另一个应用程序,它们也是无法比较的。根据高分辨率时间,生成的数值将从页面的源代码开始时刻开始单调递增,在每个页面上下文中都不同。这些数值代表从一个时刻到另一个时刻所经过的时间,而不是到绝对时刻。

现有的性能 API 的第一个缺陷是,没有办法将一个应用程序中的性能指标与任何其他应用程序中的性能指标关联起来。Raptor 采用了一种简单的方法:日志解析。

没错,你没有看错。每次 Gecko 收到性能指标时,它都会记录一条消息(即,到adb logcat),Raptor 会流式传输和解析日志,以查找这些日志指标。一个典型的日志条目看起来像这样(我们将在稍后对其进行解释)

I/PerformanceTiming( 6118): Performance Entry: clock.gaiamobile.org|mark|visuallyLoaded|1074.739956|0.000000|1434771805380

在此日志条目中需要注意的是它的源代码:clock.gaiamobile.org,或时钟应用程序;这里,时钟应用程序创建了其视觉已加载的指标。在主屏幕的情况下,我们要创建一个针对完全不同的上下文的指标。这需要一些额外的元数据与指标一起使用,但不幸的是,用户计时 API目前还没有这种功能。在 Gaia 中,我们采用了@约定来覆盖指标的上下文。让我们使用它来标记由用户首次点击图标所确定的应用程序启动时刻

performance.mark('appLaunch@' + appOrigin)

从主屏幕启动时钟并分派此指标,我们将获得以下日志条目

I/PerformanceTiming( 5582): Performance Entry: verticalhome.gaiamobile.org|mark|appLaunch@clock.gaiamobile.org|80081.169720|0.000000|1434771804212

使用 Raptor,如果我们看到此@约定,我们将更改指标的上下文。

问题:不可比较的数值

现有的性能 API 的第二个缺陷是,跨进程的性能指标无法比较。在两个独立的应用程序中使用performance.mark()不会生成有意义的数值,这些数值可以用来确定时间长度,因为它们的数值没有共享一个共同的绝对时间参考点。幸运的是,所有 JS 都可以访问一个绝对时间参考:Unix 纪元。

在任何给定时刻查看Date.now()的输出将返回自1970 年 1 月 1 日以来经过的毫秒数。Raptor 必须做出一个重要的权衡:放弃高分辨率时间的精度,以获得 Unix 纪元的可比性。查看前面的日志条目,让我们分解其输出。请注意某些部分与其用户计时对应部分之间的关联

  • 日志级别和标签:I/PerformanceTiming
  • 进程 ID:5582
  • 基本上下文:verticalhome.gaiamobile.org
  • 条目类型:mark,但可以是measure
  • 条目名称:appLaunch@clock.gaiamobile.org@约定覆盖了指标的上下文
  • 开始时间:80081.169720,
  • 持续时间:0.000000,这是一个标记,不是测量
  • 纪元:1434771804212

对于每个性能指标和测量,Gecko 还会捕获指标的纪元,我们可以使用它来比较跨进程的时间。

优缺点

任何事情都有两面性,使用 Raptor 进行性能测试也不例外

  • 我们放弃高分辨率时间,而采用毫秒级分辨率,以便比较跨进程的数值。
  • 我们放弃 JavaScript API,而采用日志解析,以便能够在不将自定义逻辑注入每个应用程序(这会影响应用程序性能)的情况下访问数据。
  • 我们目前放弃了高级交互 API,Marionette,而采用幕后的低级交互,使用Orangutan。虽然这为我们提供了平台的透明事件,但也使编写丰富的测试变得困难。计划在未来通过添加 Marionette 集成来改进这一点。

为什么选择日志解析

你可能认为日志解析很糟糕,从某种程度上来说我同意你的观点。虽然我希望每个解决方案都能用性能 API 解决,但不幸的是,它还不存在。这也是 Firefox OS 之类的项目推动 Web 发展的重要原因之一:我们找到了 Web 尚未完全实现的用例,发现了漏洞,以发现缺少什么,并最终通过推动用标准填补这些空白来改进所有人的 API。在 Web 赶上之前,日志解析是 Raptor 的权宜之计。

Raptor 工作流程

Raptor 是一个 Node.js 模块,它内置在 Gaia 项目中,使项目能够对设备或模拟器进行性能测试。安装完项目依赖项后,从 Gaia 目录运行性能测试非常简单。

  1. 在设备上安装 Raptor 配置文件;这将配置各种设置以帮助进行性能测试。注意:这是一个不同的配置文件,它将重置 Gaia,因此如果您保存了特定设置,请记住这一点。
    make raptor
  2. 选择要运行的测试。目前,测试存储在 Gaia 树中的 tests/raptor 中,因此需要手动发现。计划很快改进命令行 API。
  3. 运行测试。例如,您可以使用以下命令对 Clock 应用的冷启动进行性能测试,指定要启动的运行次数。
    APP=clock RUNS=5 node tests/raptor/launch_test
  4. 观察控制台输出。在测试结束时,您将得到一个测试结果表,其中包含有关已完成的性能运行的一些统计信息。示例
[Cold Launch: Clock Results] Results for clock.gaiamobile.org

Metric                            Mean     Median   Min      Max      StdDev  p95
--------------------------------  -------  -------  -------  -------  ------  -------
coldlaunch.navigationLoaded       214.100  212.000  176.000  269.000  19.693  247.000
coldlaunch.navigationInteractive  245.433  242.000  216.000  310.000  19.944  274.000
coldlaunch.visuallyLoaded         798.433  810.500  674.000  967.000  71.869  922.000
coldlaunch.contentInteractive     798.733  810.500  675.000  967.000  71.730  922.000
coldlaunch.fullyLoaded            802.133  813.500  682.000  969.000  72.036  928.000
coldlaunch.rss                    10.850   10.800   10.600   11.300   0.180   11.200
coldlaunch.uss                    0.000    0.000    0.000    0.000    0.000   n/a
coldlaunch.pss                    6.190    6.200    5.900    6.400    0.114   6.300

可视化性能

访问原始性能数据有助于快速了解某件事需要多长时间,或者确定您所做的更改是否会导致数字增加,但它对监控随时间推移的更改帮助不大。Raptor 有两种方法可以可视化随时间推移的性能数据,以提高性能。

官方指标

raptor.mozilla.org 上,我们有仪表盘可以持久化性能指标的值随时间的推移。在我们的自动化基础设施中,我们针对每个由 mozilla-central 或 b2g-inbound 生成的新的构建,对设备执行性能测试(注意:构建来源将来可能会改变)。目前,这仅限于运行在 319MB 内存的 Flame 设备,但计划在不久的将来扩展到不同的内存配置和更多设备类型。当自动化收到新的构建时,我们对设备运行性能测试电池,捕获数字,例如应用程序启动时间和 fullyLoaded 时的内存,重启持续时间和电源电流。这些数字每天存储和可视化多次,根据当天的提交次数而有所不同。

查看这些图表,您可以深入了解特定应用,聚焦或扩展时间查询,并执行高级查询操作以深入了解性能。观察随时间的趋势,您甚至可以找出偷偷溜进 Firefox OS 的回归。

本地可视化

raptor.mozilla.org 使用的相同可视化工具和后端也可以用作 Docker 镜像。运行本地 Raptor 测试后,数据将根据这些本地指标报告到您自己的可视化仪表盘。本地可视化有一些额外的先决条件,因此请务必阅读 MDN 上的 Raptor 文档 以开始使用。

性能回归

构建显示指标的漂亮图表很好,但是找到数据中的趋势或噪声中的信号可能很困难。图表帮助我们理解数据,并使其更容易被其他人围绕该主题进行沟通,但是使用图表查找性能回归是反应式的;我们应该主动地保持快速。

在 CI 上进行回归狩猎

Rob Wood 在我们的预提交持续集成工作方面做了不可思议的工作,这些工作围绕着检测预期提交中的性能回归。对于对 Gaia 仓库 的每个拉取请求,我们的自动化对目标分支运行 Raptor 性能测试,无论是否应用补丁。在统计精度经过一定次数的迭代后,我们能够拒绝补丁在 Gaia 中落地,如果回归过于严重。出于可扩展性的目的,我们使用模拟器来运行这些测试,因此存在固有的缺点,例如报告的指标差异更大。这种差异限制了我们检测回归的精度。

在自动化中进行回归狩猎

幸运的是,我们已经到位了后提交自动化,可以在真实设备上运行性能测试,仪表盘就是从这里获取数据的。基于来自 Will Lachance 的出色 Python 工具,我们每天查询历史数据,试图发现过去七天可能潜入 Firefox OS 的任何较小的回归。发现的任何性能异常都会立即报告给 Bugzilla,并通知相关的错误组件观察者。

回顾和下一步

Raptor 与 User Timing 相结合,使我们能够了解如何询问关于 Gaia 性能的问题并获得准确的答案。将来,我们计划改进该工具的 API 并添加更高层的交互。Raptor 还应该能够与第三方应用程序更无缝地协作,而现在这并不容易做到。

Raptor 是一个令人兴奋的构建工具,同时帮助我们在性能领域推动 Web 向前发展。我们计划使用它来保持 Firefox OS 的快速,并积极主动地保护 Gaia 的性能。

关于 Eli Perelman

更多 Eli Perelman 的文章…