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 内存使用情况的小窗。

帧数 监控_面试_02

帧数 监控_帧数 监控_03

缺点 :生产环境数据无法收集上报,需要人工实时观测;比较适合在开发阶段进行自测

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 指标。

帧数 监控_帧数 监控_04

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 架构如下图所示:

帧数 监控_javascript_05

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 相对展示的信息更加详细。

帧数 监控_java_06

chrome devtools performance 图示


帧数 监控_编程语言_07

chrome tracing 图示


2.3.2 Trace viewer

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

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

帧数 监控_java_08

Trace viewer 结果展示


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

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

帧数 监控_编程语言_09

帧数 监控_面试_10

跳转 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 轨迹对应:

帧数 监控_面试_11

帧绘制内容数据的 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 流程如下图所示:

帧数 监控_java_12

Commit流程


最终确定每一帧必定走到的 TRACEEVENT 有合成器线程 ScheduledActionBeginMainFrame 阶段,因此选取 cat="cc"、name="Scheduler::NotifyBeginMainFrameStarted"的 event 作为 FPS 统计的关键 TRACEEVENT。

2.4.2 统计流程

确定 FPS 统计关键 Trace Event 后,核心问题就得到了解决,计算 FPS 大体流程如下:

帧数 监控_编程语言_13

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 模块也将持续优化支持更多平台和场景的测试,流水线接入更多的应用品类。