前言

实验室摸鱼总结 part2:如何进行 Chrome 网页自动化性能分析

主要还是对 Chrome 远程调试协议 的学习和对 nodejs 封装的 chrome-remote-interface 的使用 (..•˘_˘•..)

认识 Chrome 性能分析工具

选择一个网页,比如说 https://www.bilibili.com,按 F12 调出开发者工具面板,选择 Performance 选项

![测试页面](/images/posts/Chrome 自动性能分析指北/bilibili.png)

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

![开始性能分析](/images/posts/Chrome 自动性能分析指北/profiling.png)

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

![分析结果](/images/posts/Chrome 自动性能分析指北/profiling-results.png)

点击左上角 Save profile 即可保存详细数据描述 JSON 文件

相应 Profiler 结果数据

Performance 由两部分数据构成,一部分是 Timeline 数据,一部分是 JavaScript Profiler 数据,Performance 前身是 Timeline 面板,在合并了 JavaScript Profiler 数据之后改成了 Performance 面板。JavaScript Profiler 面板虽然依旧可以被独立调出,但已经被标识为可能被废弃的功能,之后在某一版本的 Chrome 中就可能被正式废弃。

![JavaScript Profiler](/images/posts/Chrome 自动性能分析指北/javascript-profiler.png)

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

![JavaScript Profiler 抓取的数据](/images/posts/Chrome 自动性能分析指北/js-profiler-data.png)

如何自动化获得数据

分析 Chrome 开发者工具

分析 Chrome 开发者工具是不可能的,但我们可以分析远程协议版的 Chrome 开发者工具呀(笑

step1:打开开启端口 9222 的 Chrome 和待分析页面 https://www.bilibili.com

step2: 打开 http://localhost:9222 并选择进入 bilibili 相关远程协议版本的开发者工具页面

![开发者工具 远程协议页面](/images/posts/Chrome 自动性能分析指北/devtools-cdp.png)

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

![开发者工具](/images/posts/Chrome 自动性能分析指北/devtools.png)

现在的三个页面的关系如下:

bilibili(待测页面) —> 远程协议版开发者工具(对 bilibili 进行性能分析) —> 开发者工具 (远程协议页面的开发者工具,用来查看远程协议页面的网络请求)

step4: 选择开发者工具的 Network 面板,选择查看当前页面的 websocket 连接,然后在远程协议版本的 Performance 进行性能分析,查看相应 websocket 数据

![网络](/images/posts/Chrome 自动性能分析指北/network.png)

可以看到主要有这两帧比较重要:

{id: 79, method: "Profiler.start"}{"id":80,"method":"Tracing.start","params":{...}} ,分别表示对 JavaScritp ProfilerTimeline 两方面的性能分析数据请求

![网络](/images/posts/Chrome 自动性能分析指北/network2.png) 然后 {"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.jsget-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();
   });
})();

代码逻辑并不复杂,大致有以下步骤

  1. Profiler.enable(); 开启 Profiler
  2. Profiler.setSamplingInterval({interval: 100}); (可选)用以确定采样时间间隔,可以看到 Chrome 默认的是 100μs
  3. Profiler.start(); 开始采样
  4. 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]}}

数据格式参考如下,或可参考官方说明

![数据格式](/images/posts/Chrome 自动性能分析指北/profiler-format.png)

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() 所有数据全部返回后,该事件被触发

利用代码实现自动化性能分析

代码主要还是利用 ProfilerTracing 两个相关 domain 的 api 来获得 JavaScript ProfilerTimeline 的数据。

// 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);
    });
})();

参考链接