我们是如何在CI流水线统计web前端FPS的?
1. 背景
1.1 FPS 统计意义
FPS(帧率)是图像领域中的定义,是指画面每秒渲染帧数,FPS 一般在 0-60 之间,低于 30 时人眼能明显感觉到卡顿。页面交互过程中页面展示是否流畅,页面中的动画是否存在卡顿等,都需要通过 FPS 的统计指标作为页面性能的参考依据。
1.2 现有 web 前端 FPS 统计方式
1.2.1 Chrome devtools
如下图,通过 Chrome devtools 右侧菜单 -> more tools -> Rendering -> 勾选 Frame Rendering Stats,则会在页面左上角显示实时 Frame Rate(FPS)和 GPU 内存使用情况的小窗。
缺点 :生产环境数据无法收集上报,需要人工实时观测;比较适合在开发阶段进行自测
1.2.2 requestAnimationFrame API
window.requestAnimationFrame() 告诉浏览器你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行回调。回调函数执行次数通常与浏览器屏幕刷新次数相匹配,一般是每秒 60 次。
那么正好可以利用 requestAnimationFrame API 的特性来计算统计 FPS ,原理如下:
假设动画在时间 A 开始执行,在时间 B 结束,耗时 (B-A) s,这期间 requestAnimationFrame 一共执行了 n 次,则此段动画的 FPS = n / (B-A)。
requestAnimationFrame 在不掉帧的情况下一秒内会执行 60 次,即 FPS = 60 / 1。
统计 FPS 核心代码如下:
let lastTime = performance.now();
let frames = 0;
const loop = () => {
const currentTime = performance.now();
frames += 1;
if (currentTime > 1000 + lastTime) {
fps = Math.round((frames * 1000) / (currentTime - lastTime));
frames = 0;
lastTime = currentTime;
console.log(`fps:${fps}`);
}
window.requestAnimationFrame(loop);
}
loop();
在生产环境,只需要通过 requestAnimationFrame 统计出监控阶段的回调调用次数,即可计算出对应 FPS,对 FPS 也比较方便进行收集和上报,是目前使用最多的 FPS 统计方式。
缺点:
对业务代码 侵入性较强 ,需要引入脚本且实现代码指定统计阶段
统计的 FPS** 结果不够准确**,因为它是将每两次主线程执行的时间间隔当成一帧,而非主线程加合成线程所消耗的时间为一帧。js 执行属于主线程,主线程很容易遭到阻塞(例如:js 执行耗时较长),而此时合成器线程基本上是空闲的,合成器能够自己运行某些动画(合成滚动和加速 CSS 动画),它可以在不等待 JS 的情况下运行这些动画。例如这个 demo 页面:https://xdevilj136.github.io//main-thread-block.html,主线程被 js 执行完全阻塞,requestAnimationFrame 无法正常统计 FPS,这种情况下实际页面还是可以正常滚动的。
1.3 痛点
现有的前端 FPS 统计方式存在一些痛点,解决痛点希望满足以下方面:
不侵入业务代码,对 web 页面进行 FPS 统计
具有一定的通用性,适用于前端大部分动画、交互场景
统计 FPS 结果数据相对准确
可以在 CI 阶段进行 FPS 统计,生成性能报告
目前 alloyperf 的 FPS 统计工具模块,已经实现并满足以上要求,在 CI 流水线定时统计腾讯文档页面 FPS 数据并定时生成性能报告。后面章节,将介绍 alloyperf FPS 统计的实现原理。
2. alloyperf FPS 统计工具介绍
2.1 alloyperf FPS 统计工具
alloyperf FPS 统计工具实现主要利用 Selenium WebDriver 和 chrominum:
Selenium WebDriver 驱动 chrome 浏览器打开测试页面,并通过 API 模拟页面交互操作,以测试页面不同的交互场景;
chromnium 内部的 Chrome tracing,记录了 chrome 浏览器打开、展示页面整个过程中各个进程不同阶段的 tracing 记录,通过获取并分析 Chrome tracing 的记录 logs, 即可计算统计出页面对应测试阶段的 FPS 指标。
2.2 Selenium WebDriver 介绍
Selenium 是 ThoughtWorks 提供的一个强大的基于浏览器的开源自动化测试工具集,Selenium WebDriver 是工具集其中一个子工具,主要用于在各种浏览器上自动化测试 web 应用。
它对浏览器提供的原生 API 进行了封装,使其成为一套更加面向对象的 Selenium WebDriver API,使用这套 API 可以操控浏览器的开启、关闭,打开网页,操作界面元素,还可以操作浏览器 devtools 等,由于使用的原生 API,其速度与稳定性都会好很多。
Selenium WebDriver 通过 JsonWireProtocol 协议与各浏览器的 driver 进行通信(例如:ChromeDriver 即为 Chromium 实现了 JsonWireProtocol 协议),Selenium 对不同厂商的各个 driver 进行了封装,如:selenium-chrome-driver、selenium-edge-driver、selenium-firefox-driver 等,可支持各种主流浏览器的自动化测试。
Selenium WebDriver 架构如下图所示:
2.3 Chrome tracing 介绍
对于 FPS 的统计,Chrome tracing 是核心也是本文的重点,下面重点介绍。
2.3.1 Tracing ecosystem
Tracing ecosystem 即 tracing 的生态系统,tracing 即跟踪应用运行过程并生成记录的行为。Tracing ecosystem 的运行基于"trace 文件",trace 文件包含所有的跟踪记录数据,Tracing ecosystem 包含两种工具:
记录并生成 trace 文件的工具
解析展示 trace 文件的工具
记录并生成 trace 文件的工具有很多,比如:Android 的 systrace 命令行工具、开源的 adb_trace 等,web 前端常用的有 chrome devtools 中 performance record 功能、chrome tracing 的 record 功能。
解析展示 trace 文件的工具,web 前端常用的 chrome devtools performance、chrome tracing 同样具有这样的强大能力,chrome tracing 相对展示的信息更加详细。
2.3.2 Trace viewer
chrome tracing 是内置在 chrome 中的工具,可用来收集和解析展示非常详细的性能跟踪数据,在 devtools 无法满足需求时,可使用此工具来进行更加复杂或具体的性能分析。
通过 chrome tracing 的 record 按钮进行记录后即可生成对应的跟踪数据,chrome tracing 内部通过 trace viewer 可直接对产生的数据进行解析和展示:
Trace viewer 可以对 record 产生的 trace 数据直接进行展示,也可以 load 对应的 trace json 文件并进行解析展示。展示结果如上图,时序按从左到右排列,通过左侧的 Processes 和 Threads 进行细分,右侧每一个小色块对应一个 TRACEEVENT(即 Chromium 内部 tracing 库生成的单个记录事件点)。
在 trace viewer 中点选对应的 TRACEEVENT 色块,甚至可以直接点击下方的详情跳转到相关的 Chromnium 源码:
Chromnium 通过 TRACE_EVENT0 函数将对应的 EVENT 记录到对应的 category,例如上图将 ProxyImpl::NotifyReadyToCommitOnImpl 记录到 cc(即 Chrome Compositor 合成器)。
同时,Trace viewer 结果展示图中,还可以通过菜单选择对应的 flow 展示某个 event 流的轨迹走向,例如单帧在渲染进程中的 flow 大致是经历如下阶段:
输入事件来自于浏览器进程,并被传递给合成器线程,对应的 TRACE_EVENT 为 "InputEventFilter::ForwardToHandler"
输入事件从合成器线程到主线程,启动了 Blink 的输入事件处理
Blink 生成一个新的动画帧,并在 "WebViewImpl::animate "中调用 requestAnimationFrame 回调
如果在 RAF 回调或输入事件处理程序中 JavaScript 修改了页面,触发了一个重新布局,首先是样式的重新计算,对应于"Document::updateStyle"
Blink 重新绘制覆盖失效区域,对应 TRACE_EVENT "Picture::Record",layer 属性(如 transform、opacity)也在 Blink 的 layer tree 副本中被更新
通过"ThreadProxy::BeginMainFrame::Commit",新的记录和更新后的 layer tree 从 Blink 线程传递到合成器线程,在这期间主线程被合成器线程阻塞
之后合成器进行栅格化处理,然后传递给浏览器合成器并交换帧缓存"DelegatingRenderer:SwapBuffers",最终完成绘制
所以通过 TRACE_EVENT 的 flow 轨迹,即可以非常精细地看到页面每一帧的具体渲染流程。
2.3.3 trace 文件格式
Trace Viewer 可以识别四种不同格式的 trace 文件,JSON 类型格式包括 JSON 数组和 JSON 对象,另外两种是 Linux ftrace 数据类型。比较通用的是 JSON 格式,也是 chrome tracing 使用的格式,Linux ftrace 类型本文不做赘述。
JSON 数组(chrome devtools performance 生成格式):
[{"args":{"name":"swapper"},"cat":"__metadata","name":"thread_name","ph":"M","pid":337,"tid":0,"ts":0},
{"args":{"name":"CrBrowserMain"},"cat":"__metadata","name":"thread_name","ph":"M","pid":337,"tid":775,"ts":0}]
JSON 对象(chrome tracing 生成格式):
{
"traceEvents":[
{"args":{"name":"swapper"},"cat":"__metadata","name":"thread_name","ph":"M","pid":337,"tid":0,"ts":0},
{"args":{"name":"Compositor"},"cat":"__metadata","name":"thread_name","ph":"M","pid":7546,"tid":42243,"ts":0}
],
"displayTimeUnit": "ns",
"systemTraceEvents": "SystemTraceData",
"otherData": {"version": "My Application v1.0" },
"stackFrames": {...}
"samples": [...],
}
两种格式结构略有不同,但每条 TRACE_EVENT 对应的 args 字段基本一致,本文只需关注:
name: TRACE_EVENT 名称
cat: TRACE_EVENT 类别
ts: TRACE_EVENT 事件的追踪时时间戳,以微秒为单位
通过以上得出结论:通过 flow 确认每一帧渲染必定经过哪些关键 TRACE_EVENT ,然后分析对应的 trace 文件,即可计算得到 FPS 数据。
2.4 统计 FPS
2.4.1 FPS 统计关键 Trace Event
下图为帧绘制内容数据的 flow 流向示意图,与 Chrome tracing 的 flow 轨迹对应:
如图所示,绘制内容的数据流向要经过几个不同的进程和线程,不同的线程的任务由 Chromnium 中不同模块(对应 category)负责,blink 主要负责主线程、cc 主要负责合成器线程、viz 主要负责 gpu 相关。
在通过 Chrome tracing 跟踪 flow 和跟踪 chromnium 相关源码过程中,主要发现以下关键点:
主线程很容易遭到阻塞(例如:js 执行耗时较长),而此时合成器线程基本上是空闲的,合成器能够自己运行某些动画(合成滚动和加速 CSS 动画),它可以在不等待 JS 的情况下运行这些动画,所以不能选择主线程 TRACE_EVENT
虽然按照 flow 流向,最终走向的 TRACEEVENT 在 gpu 进程,但通过实际测试和 chromnium 源码的进一步分析,发现 chromnium 在跨平台处理时针对 linux 在 gpu 进程做了特殊处理,导致 linux 平台下 data flow 的 TRACEEVENT 不一定在每一帧都确定走到 gpu
Commit 是一种从主线程推送数据到合成器线程的方式,并且保证了该过程中的数据完整性。Commit 不是通过发送 ipc,而是通过阻塞主线程并复制数据的方式来完成提交。收到主线程请求后的某个时刻,调度器将调用 ScheduledActionBeginMainFrame 对请求进行响应,合成器线程会发送一个 BeginFrameArgs 到主线程启动 BeginMainFrame。完成此操作后,cc 再进行后续栅格化等一系列流程。Commit 流程如下图所示:
最终确定每一帧必定走到的 TRACEEVENT 有合成器线程 ScheduledActionBeginMainFrame 阶段,因此选取 cat="cc"、name="Scheduler::NotifyBeginMainFrameStarted"的 event 作为 FPS 统计的关键 TRACEEVENT。
2.4.2 统计流程
确定 FPS 统计关键 Trace Event 后,核心问题就得到了解决,计算 FPS 大体流程如下:
3. 总结
针对 1.3 中提到的目前现有 web 前端 FPS 统计方式的痛点,alloyperf fps 模块都已经实现了相应的解决。
对于测试页面,只需要提供页面 url 和简单配置,不会侵入业务代码
通过 webdriver 模拟页面交互操作,具有一定的通用性
通过 Chromnium 底层 TRACE_EVENT 分析统计 FPS,结果数据相对准确
可以在 CI 流水线引入进行 FPS 统计,生成性能报告
目前 alloyperf fps 模块已经在腾讯文档 CI 流水线运行,日常输出 FPS 性能报告。
alloyperf 其他模块(首屏统计、内存监测等)正在陆续开发中,后续 FPS 模块也将持续优化支持更多平台和场景的测试,流水线接入更多的应用品类。
内推社群
我组建了一个氛围特别好的腾讯内推社群,如果你对加入腾讯感兴趣的话(后续有计划也可以),我们可以一起进行面试相关的答疑、聊聊面试的故事、并且在你准备好的时候随时帮你内推。下方加 winty 好友回复「面试」即可。