前端测试 chrome devtools protocol——Web 性能自动化实践介绍

· 2018年08月25日 · 最后由 回复于 2019年01月19日 · 14853 次阅读
本帖已被设为精华帖!

前言

在测试 Web 页面加载时间时,可能会是这样的:

  1. 打开 chrome 浏览器。
  2. 按 F12 打开开发者工具。
  3. 在浏览器上打开要测试的页面
  4. 查看开发者工具中Network面板的页面性能数据并记录
  5. 或者在开发者工具中Console面板运行performance.timingperformance.getEntries()收集数据

performance 相关信息看这里PerformanceTiming

几十上百个页面,每个版本都这样来,估计疯了,所以就想怎么把它做成自动化呢?

chrome devtools protocol

chrome devtools protocol允许第三方对基于 chrome 的 web 应用程序进行调试、分析等,它基于 WebSocket,利用 WebSocket 建立连接 DevTools 和浏览器内核的快速数据通道。一句话,有了这个协议就可以自己开发工具获取 chrome 的数据

协议详细内容看这里chrome devtools protocol

目前已经有很多大神针对这个协议封装出不同语言(nodejs,python,java...)的库,详细信息看这里awesome-chrome-devtools

这边我选择的是 python 的 pychromegithub 地址,使用方法很简单,直接看 github 上它的 Demo

这个库依赖websocket-client

获取 performance api 数据

这里使用 Runtime Domain 中运行 JavaScript 脚本的 APIRuntime.evaluate

# 开始前先启动chrome,启动chrome必须带上参数`--remote-debugging-port=9222`开启远程调试否则无法与chrome交互
browser = pychrome.Browser('http://127.0.0.1:%d' % 9222)
tab = browser.new_tab()
tab.start()
tab.Runtime.enable()
tab.Page.navigate(url={你的页面地址})
# 设置等待页面加载完成的时间
tab.wait(10)
# 运行js脚本
timing_remote_object = tab.Runtime.evaluate(
            expression='performance.timing'
        )
# 获取performance.timing结果数据
timing_properties = tab.Runtime.getProperties(
            objectId=timing_remote_object.get('result').get('objectId')
        )
timing = {}
for item in timing_properties.get('result'):
            if item.get('value', {}).get('type') == 'number':
                    timing[item.get('name')] = item.get('value').get('value')
# 获取performance.getEntries()数据
entries_remote_object = tab.Runtime.evaluate(
            expression='performance.getEntries()'
        )
entries_properties = tab.Runtime.getProperties(
            objectId=entries_remote_object.get('result').get('objectId')
        )
entries_values = []
for item in entries_properties.get('result'):
  if item.get('name').isdigit():
    url_timing_properties = tab.Runtime.getProperties(
                    objectId=item.get('value').get('objectId')
                )
     entries_value = {}
     for son_item in url_timing_properties.get('result'):
                    if (son_item.get('value', {}).get('type') == 'number'or
                            son_item.get('value', {}).get('type') == 'string'):
                        entries_value[son_item.get('name')] = son_item.get('value').get('value')
                entries_values.append(entries_value)

获取 Network 数据

实际上 performance.getEntries() 不会记录 404 的请求信息,另外当前页面通过 js 触发新 html 页面请求时它只会记录第一个页面的请求,在这些情况下就需要通过 Network Domain 的 API 来收集所有请求信息,先介绍用到的 API:

  1. Network.requestWillBeSent每个 http 请求发送前回调
  2. Network.responseReceived首次接送到 http 响应时回调
  3. Network.loadingFinished请求加载完成时回调
  4. Network.loadingFailed请求加载失败时回调

    # 封装上面4个事件对应的回调方法
    class NetworkAPIImplemention(object):
    
    def __init__(self):
        self.request_dict = {}
        # 首个请求开始时间
        self.start = None
    
    def request_will_be_sent(self, **kwargs):
        if self.start is None:
            self.start = time.time()
        dict_http = {
            'url':kwargs.get('request').get('url'),
            'start':kwargs.get('timestamp')
        }
        self.request_dict[kwargs.get('requestId')]=dict_http
        #print "loading:%s" % kwargs.get('request').get('url')
    
    def loading_finished(self, **kwargs):
        # 服务器返回code 例如404也是finished
        self.request_dict[kwargs.get('requestId')]['end'] = kwargs.get('timestamp')
        self.request_dict[kwargs.get('requestId')]['size'] = kwargs.get('encodedDataLength')
    
    def response_received(self, **kwargs):
        self.request_dict[kwargs.get('requestId')]['type'] = kwargs.get('type')
        self.request_dict[kwargs.get('requestId')]['response'] = kwargs.get('response')
    
    def loading_failed(self, **kwargs):
        self.request_dict[kwargs.get('requestId')]['end'] = kwargs.get('timestamp')
        self.request_dict[kwargs.get('requestId')]['error_text'] = kwargs.get('errorText')
    network_api = NetworkAPIImplemention()
    browser = pychrome.Browser('http://127.0.0.1:%d' % 9222)
    tab = browser.new_tab()
    # 绑定回调函数
    tab.Network.requestWillBeSent = network_api.request_will_be_sent
    tab.Network.responseReceived = network_api.response_received
    tab.Network.loadingFinished = network_api.loading_finished
    tab.Network.loadingFailed = network_api.loading_failed
    tab.start()
    tab.Network.enable()
    tab.Runtime.enable()
    # 是否禁用缓存
    if disable_cache:
    tab.Network.setCacheDisabled(cacheDisabled=True)
    tab.Page.navigate(url={你的页面地址})
    tab.wait(10)
    tab.stop()
    self.browser.close_tab(tab)
    # 获取的所有url详细信息
    print network_api.request_dict
    

监听页面事件

有时候特别是一些复杂的页面,页面依赖 js 和后端资源数据,并不是通常意义上页面 loadEventEnd 事件触发完就表示页面加载完成,这种情况可能需要依赖开发打点。
这里以开发设计了一个Loaded事件为例

# 具体事件注册方式和注册时机询问开发,所谓注册时机即要求在js对象生成后注册,我们项目中page是在一个js文件中声明的,需要等这个js文件请求完成后再注册
# 这边使用Promise方式,这种方式awaitPromise参数必须是True
js = """
    new Promise((resolve, reject) => {
        page.getController().getPageEvent().addEventListener("Loaded",
                function(){
                    resolve(new Date().getTime());
                });
        });
   """
custom_result = tab.Runtime.evaluate(
    expression=js,
    awaitPromise=True,
    timeout=timeout * 1000
)
print custom_result.get('result').get('value')

有个坑peformance.now()获取与 chrome 开发者工具协议一样类型的时间时,这个时间不准确,只好用new Date().getTime()

写在最后

一开始是使用 nodejs 的 chrome-remote-interface,但是发现Page.loadEventFired回调后不会再记录请求,事实上有些页面仍然有请求没有完成,不懂是不是我使用姿势不对😓
附赠 W3C 的一幅图

共收到 17 条回复 时间 点赞

写的真不错··

#2 · 2018年08月25日 Author

@jiazurongyu 谢谢

思寒_seveniruby 将本帖设为了精华贴 08月25日 14:31

mark 一下, 关于前端的性能测试资料较少,学习一下

可以跟云真机配合做一下移动性能测试

您好,我按照您的帖子,得到数据开始值和结束值都是一样是怎么回事呢?可否加个好友指点一下呢?多谢了
navigationStart--->1.540549461323E12
unloadEventStart--->0.0
unloadEventEnd--->0.0
redirectStart--->0.0
redirectEnd--->0.0
fetchStart--->1.540549461325E12
domainLookupStart--->1.540549461336E12
domainLookupEnd--->1.540549461336E12
connectStart--->1.540549461336E12
connectEnd--->1.540549461343E12
secureConnectionStart--->0.0
requestStart--->1.540549461343E12
responseStart--->1.540549461417E12
responseEnd--->1.540549461458E12
domLoading--->1.540549461466E12
domInteractive--->1.540549462562E12
domContentLoadedEventStart--->1.540549462563E12
domContentLoadedEventEnd--->1.540549462715E12
domComplete--->1.540549467418E12
loadEventStart--->1.540549467418E12
loadEventEnd--->1.540549467435E12

#7 · 2018年10月29日 Author

@success 同一个事件开始和结束一样的话表示时间小于毫秒级,

8楼 已删除

Nodejs Puppeteer 简直神器

#10 · 2018年11月07日 Author
lyu 回复

谢谢,我试试看

回复

我只是基于简单的需求用了下,觉得很棒,毕竟 github 4w+ star,可以了解下的。 你这个文章也给了我一些做性能的思路,接下来去试验下😀

可以实现个 chrome extension 开源:)

新版的 chrome 是不是在启动时必须要加上 --headless 参数,不然无法进行远程调试。

#15 · 2018年11月22日 Author
wing 回复

开始前先启动 chrome,启动 chrome 必须带上参数--remote-debugging-port=9222开启远程调试否则无法与 chrome 交互,而 headless 这个是无头模式

回复

在我机器上使用 chrome --remote-debugging-port=9222 启动的话,连接时会报错

requests.exceptions.ConnectionError: HTTPConnectionPool(host='localhost', port=9222): Max retries exceeded with url: /json (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x00000238FC78ED68>: Failed to establish a new connection: [WinError 10061] 由于目标计算机积极拒绝,无法连接。',))

但使用无头模式 chrome --remote-debugging-port=9222 --headless 启动的话则可以连接成功。[捂脸哭]

额,我找到原因了,我使用 chrome --remote-debugging-port=9222 启动 chrome 前打开有其它的页面,必须把其它开启的 chrome 进程都关了才行。
打扰了,打扰了....

#18 · 2018年11月22日 Author
wing 回复

那你这个就有点奇怪了,正常这个命令 chrome --remote-debugging-port=9222 就可以,运行这个命令,然后可以直接在浏览器访问 127.0.0.1:9222 ,正常的情况下可以看到浏览器当前打开的所有页面信息,如果不行就算远程调试没有开启了

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 12月13日 14:44
simple [精彩盘点] TesterHome 社区 2018 年 度精华帖 中提及了此贴 01月07日 12:08

Network.loadingFinished, timestamp 时间只有 5 位?怎么转为毫秒?

#22 · 2019年01月19日 Author

跟前面的 request 的 timestamp 相减来计算的,这个时间是 chrome 自己的时钟,跟 unix time 不是一个

23楼 已删除
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册