我们是如何在CI流水线统计web前端FPS的?

前端Q

共 7111字,需浏览 15分钟

 ·

2021-07-01 01:56

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 统计方式。

缺点:

  1. 对业务代码 侵入性较强 ,需要引入脚本且实现代码指定统计阶段

  2. 统计的 FPS** 结果不够准确**,因为它是将每两次主线程执行的时间间隔当成一帧,而非主线程加合成线程所消耗的时间为一帧。js 执行属于主线程,主线程很容易遭到阻塞(例如:js 执行耗时较长),而此时合成器线程基本上是空闲的,合成器能够自己运行某些动画(合成滚动和加速 CSS 动画),它可以在不等待 JS 的情况下运行这些动画。例如这个 demo 页面:https://xdevilj136.github.io//main-thread-block.html,主线程被 js 执行完全阻塞,requestAnimationFrame 无法正常统计 FPS,这种情况下实际页面还是可以正常滚动的。

1.3 痛点

现有的前端 FPS 统计方式存在一些痛点,解决痛点希望满足以下方面:

  1. 不侵入业务代码,对 web 页面进行 FPS 统计

  2. 具有一定的通用性,适用于前端大部分动画、交互场景

  3. 统计 FPS 结果数据相对准确

  4. 可以在 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 包含两种工具:

  1. 记录并生成 trace 文件的工具

  2. 解析展示 trace 文件的工具

记录并生成 trace 文件的工具有很多,比如:Android 的 systrace 命令行工具、开源的 adb_trace 等,web 前端常用的有 chrome devtools 中 performance record 功能、chrome tracing 的 record 功能。

解析展示 trace 文件的工具,web 前端常用的 chrome devtools performance、chrome tracing 同样具有这样的强大能力,chrome tracing 相对展示的信息更加详细。

chrome devtools performance 图示


chrome tracing 图示

2.3.2 Trace viewer

chrome tracing 是内置在 chrome 中的工具,可用来收集和解析展示非常详细的性能跟踪数据,在 devtools 无法满足需求时,可使用此工具来进行更加复杂或具体的性能分析。

通过 chrome tracing 的 record 按钮进行记录后即可生成对应的跟踪数据,chrome tracing 内部通过 trace viewer 可直接对产生的数据进行解析和展示:

Trace viewer 结果展示


Trace viewer 可以对 record 产生的 trace 数据直接进行展示,也可以 load 对应的 trace json 文件并进行解析展示。展示结果如上图,时序按从左到右排列,通过左侧的 Processes 和 Threads 进行细分,右侧每一个小色块对应一个 TRACEEVENT(即 Chromium 内部 tracing 库生成的单个记录事件点)。

在 trace viewer 中点选对应的 TRACEEVENT 色块,甚至可以直接点击下方的详情跳转到相关的 Chromnium 源码:

跳转 Chromium 源码展示


Chromnium 通过 TRACE_EVENT0 函数将对应的 EVENT 记录到对应的 category,例如上图将 ProxyImpl::NotifyReadyToCommitOnImpl 记录到 cc(即 Chrome Compositor 合成器)。

同时,Trace viewer 结果展示图中,还可以通过菜单选择对应的 flow 展示某个 event 流的轨迹走向,例如单帧在渲染进程中的 flow 大致是经历如下阶段:

  1. 输入事件来自于浏览器进程,并被传递给合成器线程,对应的 TRACE_EVENT 为 "InputEventFilter::ForwardToHandler"

  2. 输入事件从合成器线程到主线程,启动了 Blink 的输入事件处理

  3. Blink 生成一个新的动画帧,并在 "WebViewImpl::animate "中调用 requestAnimationFrame 回调

  4. 如果在 RAF 回调或输入事件处理程序中 JavaScript 修改了页面,触发了一个重新布局,首先是样式的重新计算,对应于"Document::updateStyle"

  5. Blink 重新绘制覆盖失效区域,对应 TRACE_EVENT "Picture::Record",layer 属性(如 transform、opacity)也在 Blink 的 layer tree 副本中被更新

  6. 通过"ThreadProxy::BeginMainFrame::Commit",新的记录和更新后的 layer tree 从 Blink 线程传递到合成器线程,在这期间主线程被合成器线程阻塞

  7. 之后合成器进行栅格化处理,然后传递给浏览器合成器并交换帧缓存"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 轨迹对应:

帧绘制内容数据的 flow 流向示意图

如图所示,绘制内容的数据流向要经过几个不同的进程和线程,不同的线程的任务由 Chromnium 中不同模块(对应 category)负责,blink 主要负责主线程、cc 主要负责合成器线程、viz 主要负责 gpu 相关。

在通过 Chrome tracing 跟踪 flow 和跟踪 chromnium 相关源码过程中,主要发现以下关键点:

  1. 主线程很容易遭到阻塞(例如:js 执行耗时较长),而此时合成器线程基本上是空闲的,合成器能够自己运行某些动画(合成滚动和加速 CSS 动画),它可以在不等待 JS 的情况下运行这些动画,所以不能选择主线程 TRACE_EVENT

  2. 虽然按照 flow 流向,最终走向的 TRACEEVENT 在 gpu 进程,但通过实际测试和 chromnium 源码的进一步分析,发现 chromnium 在跨平台处理时针对 linux 在 gpu 进程做了特殊处理,导致 linux 平台下 data flow 的 TRACEEVENT 不一定在每一帧都确定走到 gpu

  3. Commit 是一种从主线程推送数据到合成器线程的方式,并且保证了该过程中的数据完整性。Commit 不是通过发送 ipc,而是通过阻塞主线程并复制数据的方式来完成提交。收到主线程请求后的某个时刻,调度器将调用 ScheduledActionBeginMainFrame 对请求进行响应,合成器线程会发送一个 BeginFrameArgs 到主线程启动 BeginMainFrame。完成此操作后,cc 再进行后续栅格化等一系列流程。Commit 流程如下图所示:

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 模块都已经实现了相应的解决。

  1. 对于测试页面,只需要提供页面 url 和简单配置,不会侵入业务代码

  2. 通过 webdriver 模拟页面交互操作,具有一定的通用性

  3. 通过 Chromnium 底层 TRACE_EVENT 分析统计 FPS,结果数据相对准确

  4. 可以在 CI 流水线引入进行 FPS 统计,生成性能报告

目前 alloyperf fps 模块已经在腾讯文档 CI 流水线运行,日常输出 FPS 性能报告。
alloyperf 其他模块(首屏统计、内存监测等)正在陆续开发中,后续 FPS 模块也将持续优化支持更多平台和场景的测试,流水线接入更多的应用品类。


内推社群


我组建了一个氛围特别好的腾讯内推社群,如果你对加入腾讯感兴趣的话(后续有计划也可以),我们可以一起进行面试相关的答疑、聊聊面试的故事、并且在你准备好的时候随时帮你内推。下方加 winty 好友回复「面试」即可。


浏览 32
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报