前言
实验室摸鱼总结 part2:如何进行 Chrome 网页自动化性能分析
主要还是对 Chrome 远程调试协议
的学习和对 nodejs 封装的 chrome-remote-interface
的使用 (..•˘_˘•..)
认识 Chrome 性能分析工具
选择一个网页,比如说 https://www.bilibili.com,按 F12
调出开发者工具面板,选择 Performance
选项

选择开发者工具左上角处小圆点 Record
,开始性能分析

在缓冲区耗尽之前,我们点击 Stop
即可结束此次性能分析,然然后我们可以看到开发者工具呈现给我们的效果图

点击左上角 Save profile
即可保存详细数据描述 JSON 文件
相应 Profiler 结果数据
Performance
由两部分数据构成,一部分是 Timeline
数据,一部分是 JavaScript Profiler
数据,Performance
前身是 Timeline
面板,在合并了 JavaScript Profiler
数据之后改成了 Performance
面板。JavaScript Profiler
面板虽然依旧可以被独立调出,但已经被标识为可能被废弃的功能,之后在某一版本的 Chrome 中就可能被正式废弃。

JavaScript Profiler 抓取的数据是 Performance
面板数据的一部分

如何自动化获得数据
分析 Chrome 开发者工具
分析 Chrome 开发者工具是不可能的,但我们可以分析远程协议版的 Chrome 开发者工具呀(笑
step1:打开开启端口 9222
的 Chrome 和待分析页面 https://www.bilibili.com
step2: 打开 http://localhost:9222
并选择进入 bilibili 相关远程协议版本的开发者工具页面

step3: 在该页面按 F12
,打开开发者工具

现在的三个页面的关系如下:
bilibili(待测页面) —> 远程协议版开发者工具(对 bilibili 进行性能分析) —> 开发者工具 (远程协议页面的开发者工具,用来查看远程协议页面的网络请求)
step4: 选择开发者工具的 Network
面板,选择查看当前页面的 websocket 连接,然后在远程协议版本的 Performance
进行性能分析,查看相应 websocket 数据

可以看到主要有这两帧比较重要:
{id: 79, method: "Profiler.start"}
和
{"id":80,"method":"Tracing.start","params":{...}}
,分别表示对 JavaScritp Profiler
和 Timeline
两方面的性能分析数据请求

然后 {"id":84,"method":"Profiler.stop"}
和 {"id":85,"method":"Tracing.end"}
表示分析数据结束,Chrome 会返回相应数据
其中,Profiler
对应的数据会通过和 {"id":84,"method":"Profiler.stop"}
同一个 id 的返回消息返回,而 Tracing
则会通过 Tracing.dataCollected()
事件返回
研究优秀开源代码
写代码怎么能不借鉴 Github 上优秀的开源项目呢:automated-chrome-profiling
我们主要关注项目里的两个脚本:get-cpu-profile.js
和 get-timeline-trace.js
分别对应 JavaScript Profiler
面板和 Timeline
面板的数据
get-cpu-profile.js
// get-cpu-profile.js
(async function() {
// 实例化 Chrome
const chrome = await chromelauncher.launch({port: 9222});
// 实例化 CDP 接口
const client = await cdp();
// 从接口对象中解构处 Profiler Page Runtime 等域对象
const {Profiler, Page, Runtime} = client;
// enable domains to get events.
await Page.enable();
await Profiler.enable();
// Set JS profiler sampling resolution to 100 microsecond (default is 1000)
await Profiler.setSamplingInterval({interval: 100});
await Page.navigate({url});
await client.on('Page.loadEventFired', async _ => {
// on load we'll start profiling, kick off the test, and finish
await Profiler.start();
await Runtime.evaluate({expression: 'startTest();'});
await sleep(600); // sleep 600ms
const data = await Profiler.stop();
});
})();
代码逻辑并不复杂,大致有以下步骤
Profiler.enable();
开启Profiler
Profiler.setSamplingInterval({interval: 100});
(可选)用以确定采样时间间隔,可以看到 Chrome 默认的是 100μsProfiler.start();
开始采样Profiler.stop();
结束采样,并在回调函数中返回相应采样后的数据
可以看一下获得的数据:
{"profile":{"nodes":[{"id":1,"callFrame":{"functionName":"(root)","scriptId":"0","url":"","lineNumber":-1,"columnNumber":-1},"hitCount":0,"children":[2,3,4,5]},{"id":2,"callFrame":{"functionName":"(program)","scriptId":"0","url":"","lineNumber":-1,"columnNumber":-1},"hitCount":6},{"id":3,"callFrame":{"functionName":"(idle)","scriptId":"0","url":"","lineNumber":-1,"columnNumber":-1},"hitCount":116},{"id":4,"callFrame":{"functionName":"","scriptId":"136","url":"","lineNumber":0,"columnNumber":22},"hitCount":1,"positionTicks":[{"line":123,"ticks":1}]},{"id":5,"callFrame":{"functionName":"wrapObject","scriptId":"136","url":"","lineNumber":27,"columnNumber":112},"hitCount":0,"children":[6]},{"id":6,"callFrame":{"functionName":"_wrapObject","scriptId":"136","url":"","lineNumber":32,"columnNumber":91},"hitCount":0,"children":[7]},{"id":7,"callFrame":{"functionName":"InjectedScript.RemoteObject","scriptId":"136","url":"","lineNumber":122,"columnNumber":76},"hitCount":0,"children":[8,10]},{"id":8,"callFrame":{"functionName":"_describe","scriptId":"136","url":"","lineNumber":94,"columnNumber":46},"hitCount":0,"children":[9]},{"id":9,"callFrame":{"functionName":"_subtype","scriptId":"136","url":"","lineNumber":89,"columnNumber":94},"hitCount":1,"positionTicks":[{"line":90,"ticks":1}]},{"id":10,"callFrame":{"functionName":"_shouldPassByValue","scriptId":"136","url":"","lineNumber":26,"columnNumber":118},"hitCount":0,"children":[11]},{"id":11,"callFrame":{"functionName":"subtype","scriptId":"0","url":"","lineNumber":-1,"columnNumber":-1},"hitCount":1,"positionTicks":[{"line":27,"ticks":1}]}],"startTime":83365392500,"endTime":83365424937,"samples":[2,3,3,3,3,3,2,2,4,9,11,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2],"timeDeltas":[17143,118,118,113,110,119,109,107,111,109,109,107,122,112,109,110,110,109,110,109,109,110,187,187,187,188,187,185,167,118,118,117,117,117,118,117,117,117,118,118,117,118,118,118,118,117,118,118,118,118,118,117,118,118,117,117,117,118,117,117,117,118,118,118,118,118,117,118,118,118,118,118,117,118,118,118,118,118,117,118,118,118,118,117,118,118,118,118,118,117,118,118,118,118,118,117,118,117,117,118,117,117,117,118,117,118,117,117,118,117,118,117,117,118,117,118,118,111,114,108,108,108,110,109,129]}}
数据格式参考如下,或可参考官方说明

ps: Chrome 开发者工具默认采样间隔为 100μs,但不等价于获得的数据准确为 100μs,由于 CPU 性能和网络带宽等原因,通常会比设定的值大
get-timeline-trace.js
var Chrome = require('chrome-remote-interface');
var TRACE_CATEGORIES = ["-*", "devtools.timeline", "disabled-by-default-devtools.timeline", "disabled-by-default-devtools.timeline.frame", "toplevel", "blink.console", "disabled-by-default-devtools.timeline.stack", "disabled-by-default-devtools.screenshot", "disabled-by-default-v8.cpu_profile", "disabled-by-default-v8.cpu_profiler", "disabled-by-default-v8.cpu_profiler.hires"];
var rawEvents = [];
Chrome(function (chrome) {
with (chrome) {
Tracing.start({
"categories": TRACE_CATEGORIES.join(','),
"options": "sampling-frequency=10000" // 1000 is default and too slow.
});
Page.navigate({'url': 'http://paulirish.com'})
Page.loadEventFired(function () {
Tracing.end()
});
Tracing.tracingComplete(function () {
chrome.close();
});
Tracing.dataCollected(function(data){
var events = data.value;
});
}
})
大致代码如上,删去了部分对功能介绍无关的代码
可以看到 Timeline 对应的 domain 是 Tracing
,主要有以下几个函数需要注意
Tracing.start()
开始函数,需要指定相应参数Tracing.end()
结束函数Tracing.dataCollected()
事件,调用Tracing.end()
之后,该事件会被多次触发,相应数据会通过该事件返回Tracing.tracingComplete()
所有数据全部返回后,该事件被触发
利用代码实现自动化性能分析
代码主要还是利用 Profiler
和 Tracing
两个相关 domain 的 api 来获得 JavaScript Profiler
和 Timeline
的数据。
// demo.js
(async ()=>{
const chrome = await chromelauncher.launch({port: 9222});
const client = await cdp();
const {Profiler, Tracing} = client;
await Profiler.enable();
await Profiler.setSamplingInterval({interval: 100});
await Profiler.start();
await Tracing.start();
await sleep(600);
await Traing.end();
const data = await Profiler.stop();
console.log(data);
Tracing.dataCollected(function (data) {
console.log(data);
});
})();