Taobao FED

无线性能优化:FPS 测试

无线性能优化:FPS 测试

时间回到几周前,这天,女神突然来找我,“我这里有几个页面想测量下页面滚动的顺畅性,你有啥办法不?”。Are you kidding me?这么简单,简直是道送分题啊,于是当着女神面,打开 Chrome 开发者工具,勾选上 Show FPS meter,醒目的 FPS 监控面板就出来了,滑动页面时 FPS 的曲线就基本反映出了页面滚动的顺畅性,坐等女神的夸奖~~

“这个我知道啊,但是我有好多页面,难道要一个一个人工看吗?而且这个也没有一个记录的导出,如果能有个方法帮我自动的测量,有问题再通知我,我再仔细排查就好了”。

这个需求开始有点技术含量了,不过应该也难不倒我,页面都接入了 UITest,在页面做 UI 测试的时候,跑一下测量 FPS 的测试用例就 ok 了,那如何测量呢?女神等我~~

mozPaintCount

mozPaintCount 变量是 Mozilla 提供的方法,其返回的是当前文档 paint 到屏幕上的数量,通过计算单位时间 paint 数量变化,即可计算出页面的 FPS,so easy。

等等,这个变量目前好像只有 Firefox 支持,Chrome 上并没有一个 webkitPaintCount 或者 paintCount 变量,而我们的 UITest 是跑在 ChromeDriver 或者 PhantomJS 上,并没有 Firefox 环境,好吧,这个可以做备选方案,依赖于 UITest 支持 Firefox 环境。为了完成女神的需求,我们还要考虑其他方案了。

requestAnimationFrame

在页面重绘前,浏览器会执行传入 requestAnimationFrame 的入参函数,一般多用来实现连贯的逐帧动画。那我们基于 requestAnimationFrame 不就可以获得页面的绘制频率,计算出 FPS,而且浏览器支持情况也不错,说干就干,示例代码如下(简单示例,没做兼容等处理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var lastTime = performance.now();
var frame = 0;
var lastFameTime = performance.now();

var loop = function(time) {
var now = performance.now();
var fs = (now - lastFameTime);
lastFameTime = now;
var fps = Math.round(1000/fs);
frame++;
if (now > 1000 + lastTime) {
var fps = Math.round( ( frame * 1000 ) / ( now - lastTime ) );
frame = 0;
lastTime = now;
};
window.requestAnimFrame(loop);
}

用例结果如下:

requestAnimationFrame 测试 FPS 结果

大功告成,可以去找女神答复了,等等,这样是不是太简单了点,无法让女神刮目相看啊(在女神面前装逼),我们是不是再深入点。

Chrome 浏览器渲染页面时,涉及了两个线程,Render 主线程和 Compositor 合成线程,且两个线程通过名为 Commit 的消息来保持同步,而每一帧消耗时间应该是包含两部分,Render 主线程消耗的时间和 Compositor 线程消耗的时间。典型的状态如下(以下三张图片来自 frame-timing-polyfill):

典型状态

主线程 Commit 消息提交给合成线程,任务都在 16.66ms 内完成。当然,对于一些输入事件,比如滚动,是先转移给合成线程进行处理,然后通知给主线程,这样可以保证对用户的输入操作做及时的响应,同时,对于一些页面更新,如 CSS 动画和 CSS 滤镜,只需合成线程处理,而无需请求主线程,如下所示:

只需要 Compositor 线程

当然,也有可能,主线程处理耗时较多,导致提交给合成线程的时间推迟到了下一帧,如下所示:

主线程超时

针对以上三种情况,那流畅性如何定义呢?就需要因地制宜,不同环境分别分析了。

  • 对于滚动和 CSS 动画,由于不涉及主线程的影响,我们会更关心合成线程的绘制频率,而合成线程的绘制频率也反映了滚动和 CSS 动画的流程性。

  • 对于 JS 帧动画而言,我们期望主线程和合成线程的消耗加起来到能在 16.66ms 内,且不丢帧。因此我们需要同时关注主线程的 Commit 频率和合成线程的绘制频率,并且我们期望每个主线程的 Commit 都对应唯一一个合成线程的绘制(保证不丢帧)。

那是否有方法可以让我们分别取到 Render 主线程和 Compositor 合成线程的数据呢?答案是 Frame Timing

Frame Timing

Frame Timing API 目前还只是草案,暂时还没发现有浏览器支持,不过我们可以先实现,万一浏览器支持了呢~~,目前的 API 如下:

1
2
var rendererEvents = window.performance.getEntriesByType("renderer");
var compositeEvents = window.performance.getEntriesByType("composite");

获取 Render 主线程和合成线程的记录,每条记录包含的信息基本如下:

1
2
3
4
5
{
sourceFrameNumber: 120,
startTime: 1342.549374253,
duration: 10.654313323
}

每个记录都包括唯一的 Frame Number、Frame 开始时间以及持续时间。根据 duration 就可以知道该帧是否达到 16.66ms 的标准,同时根据单位时间记录数(Frame)的个数就能算出主线程或者合成线程每秒的帧率。

同时,对于主线程 Commit 给合成线程绘制的情况,可以根据唯一的 sourceFrameNumber 将 renderEvents 的记录和 compositeEvents 的记录做关联,得出每个主线程 Commit 所对应的合成线程绘制的次数,如前所说,这也是判断 JS 动画流程性的一个可检测指标~~(具体实现代码较多且比较简单,就不贴了,小伙伴们动动脑筋,分分钟就写出来了~)

至此,终于可以向女神交差了,想想女神崇拜的目光,还有点小激动呢~~

再等等,既然 Chrome 能打开 FPS meter,而且我们的 UI 测试也是跑在 ChromDriver 中的,那是不是可以通过配置打开 Chrome 的 FPS meter 获取到 FPS 呢,女神,再等我下~

Show FPS Counter or Performance Log

果不其然,查询 ChromeDriver 的配置设置

–show-fps-counter: Draws a heads-up-display showing Frames Per Second as well as GPU memory usage. If you also use –vmodule=”head*=1” then FPS will also be output to the console log.

只要能输出到 console log 里,我们就能方便的取到了,于是果断在 UITest 的 ChromeDriver 配置里加上里这两项:

chromeOptions["args"] = [
    // 其他配置省略
    '--show-fps-counter',
    '--vmodule="head*=1"'
];

满心欢喜的一试,FPS meter 是出来的,但是说好的 console log 并没有,具体的讨论可以参见这个 issue

难道没有别的办法了吗?既然 Chrome 的 Timeline 那么强大,其中也包含了每一帧的耗时,那是不是可以取到 Timeline 的数据?当然没问题,我们使用 selenium-webdriver 就能方便的获取到页面的 Performance Log,示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var webdriver = require('selenium-webdriver');
var chrome = require('selenium-webdriver/chrome');

// ...

// 配置需要跟踪记录的数据
var options = new chrome.Options();
var traceCategories = [
'blink.console',
'devtools.timeline',
'toplevel',
'disabled-by-default-devtools.timeline',
'disabled-by-default-d.evtools.timeline.frame'
];
options.setLoggingPrefs({ performance: 'ALL' });
options.setPerfLoggingPrefs({
'traceCategories': traceCategories.join(',')
});

// ...

// 传入 chromedriver 实例
function getTrace(browser) {
return (new webdriver.WebDriver.Logs(browser))
.get('performance')
.then(function(logs) {
// performance log
});
}

其中,traceCategories 的配置和 chrome://tracing/ 的配置一样,打开 Chrome,地址栏输入 chrome://tracing/,点击 record 按钮,会出现如下所示的配置项:

Chrome Tracing

其中的配置就是我们可以获取的。获取到 Performance Log 后导出成 JSON 文件,导入到 Chrome 的 Timeline 里,你会惊奇的发现,这和直接用 Timeline 效果是一样一样的,如下所示:

Timeline

但是接下来,我发现真正头疼的问题来了,Performance Log 是一堆密密麻麻的数据,而且还没找到相关文档,目前只是可以取出平均的 FPS,计算方法如下,首先解析数据,取出类型为 DrawFrame 的记录个数,然后除以整个统计的持续时间,即可大体得出整体平均的 FPS,如何解析出更具体的数据,还在持续研究中,希望这下可以让女神满意,嘿嘿嘿~

参考文档