乐趣区

关于chrome:DevTools-实现原理与性能分析实战

一、引言

从 2008 年 Google 开释出第一版的 Chrome 后,整个 Web 开发畛域好像被注入了一股新鲜血液,慢慢突破了 IE 一家独大的时代。Chrome 和 Firefox 是 W3C Web 规范的动摇支持者,随着这两款开源浏览器市场份额逐步加大,迎来了开发者的春天。这就迎来了一个新的职业分工——前端工程师 frontend-engineer,前端工程师促成了 Web 利用的凋敝,功能强大的调试工具必不可少。Google 基于开源的根底上趁势推出了 DevTools,广受网页开发者的好评,随即也推动了 Chrome 的在商业的胜利。

本文通过剖析 Chrome 的 DevTools 的技术实现,特地是在浏览器内核中的实现局部,来展现这款被万千开发者所青睐的开发工具背地的机密。本文适宜浏览对象次要有前端开发者、有志于开发 Hybrid 利用调试工具或重写 webdriver 实现对 Chrome 或 WebView 管制的利用工程师。

注:本文所有代码剖析,基于 Android Chromium 87.0.4280.141 版本剖析而成。因为笔者所在团队次要从事 Android 平台的 Blink 内核开发,所以剖析过程次要集中在挪动端,其余平台只是数据通路的区别,实现原理差异不大。

二、网页调试工具发展史

2006 年之前,这属于 IE 时代,在 IE 时代编写 JavaScript 代码时的调试伎俩,次要靠 window.alert() 或将调试信息输入到网页上来剖析逻辑 bug。这种硬 debug 的伎俩,不亚于零碎底层开发,往往一个小问题要花费掉一整天工夫,开发效率极低。

2006 年 1 月份,Apple 的 WebKit 团队开释出第一版本的 Web Inspector,此版本性能还比拟俭朴,仅能够查看 DOM 节点的继承关系,节点所利用了哪些 CSS 的规定。但此版本曾经奠定了今后多年的网页调试工具的原型,具备划时代意义。

WebKit 团队的迭代速度十分快,2006 年 6 月公布了一个重量级性能,JavaScript 的断点调试性能,此时曾经具备开发者神器的雏形。

同时开源营垒呈现一款 Firefox 的插件 Firebug,专一于 Web 开发的调试,奠定了古代 DevTools 的 Web UI 的布局。晚期版本就反对了 JavaScript 的调试,CSS Box 模型可视化展现,反对 HTTP Archive 的性能剖析等优良个性,起初的 DevTools 参考了此插件的性能和产品定位。2016 年 Firebug 整合到 Firefox 内置调试工具,2017 年 Firebug 进行更新,一代神器就此谢幕。

此时迎来了一个开源界的狠角色 Google 团队,基于 WebKit 退出浏览器研发,推出的 Chrome 以「平安、极速、更稳固」吸引了不少 IT 极客的关注,同时开发者工具这方面,Google 排汇多款调试工具的优良性能,推出了明天的配角 DevTools。

晚期版本当初看起来这个布局有点简陋,但这可是十几年前的作品。反对 DOM + CSS 查看,查看资源加载剖析,脚本调试以及性能调试。当初开发中罕用 DevTools 的性能,根本也就这几个性能。

那个年代的 DevTools,根本是在追随 Firebug 的性能,只是交互方式上的差别。2007 年 Steve Jobs 公布了第一代 iPhone 手机,Google 相继推出了 Android 手机,互联网的倒退来到挪动互联网时代。DevTools 此时开始超过同类工具,反对了近程真机调试。Chrome 是多过程架构,DOM 和 JavaScript 是运行在子过程中的,所以 DevTools 的底层实现,已与同类产品齐全不同。Chrome 的架构师将 DevTools 实现架构调成在 client-server 模式,这个架构让近程真机调试成为可能。为了不便网络数据传输,Chrome 设计出了一套数据封装协定 Chrome DevTools Protocal(CDP),接下来的几年,这个架构的调整在开源世界大放异彩。

yan Dahl 基于 Chromium 的 JavaScript 虚拟机 V8 设计了 Node.js,Node.js 的面世让 JavaScript 这款 Web 脚本语言走出了浏览器,关上了服务端编程、桌面编程能够应用 JavaScript 语言的新场面。依靠于 DevTools 的 client-server 架构以及 Node.js 的开发者的数量一直减少,DevTools 也迅速出圈,Chrome 团队于 2016 年开始反对 Node.js 的调试。DevTools 已从一款 Web 调试工具,演变成 JavaScript 生态中重要一员,助力更多的开发者开发更多优良代码。Node.js 的生态都离不开 DevTools,比方桌面开发框架 Electron、开发者青睐的编辑器 Visual Studio Code、前端架构 Vue.js、Facebook 开源 Android 性能剖析工具 Stetho 等。

三、DevTools 架构

DevTools 是 client-server 架构,client 就是用户操作的 Web UI 界面,负责接管用户操作指令,而后将操作指令发往浏览器内核或 Node.js 中进行解决,并将处理结果数据展现在 Web UI 上。server 启动了两类服务,一种 HTTP 服务;另一种 WebSocket 服务。

HTTP 服务提供内核信息查问能力。比方获取内核版本、获取调试页的列表、启动或敞开调试。

WebSocket 服务提供与内核进行实在数据通信的能力,负责 Web UI 传递过去的所有操作指令的散发和解决,并将后果送回 Web UI 进行展现。

下图展现出了 Android DevTools 的整体架构图,从左侧开发者通过 Web UI 的发动的操作命令,是怎么一步一步地将操作命令,传递到手机中的 Browser Core(Browser Core 运行 Chrome 浏览器内核的利用,比方 Chrome 浏览器、Android WebView、NodeJs 利用等)中执行的过程。

Android 平台奇妙地应用 ADB forward 能力,解决了 PC 上的 WebUI 与 Android 手机中的 Chrome 内核的连贯问题。轻松了实现了近程调试的能力,不要小瞧这一实现,这对前端开发者效率晋升是极大的。因为前端开发者的工作环境,目前来看根本是在 PC(Windows、Mac、Linux 统称为 PC)下,通过近程调试能力的实现,让挪动端的开发实现了所见即所得。

正是 Chrome 团队基于网络通信形式,作为 DevTools 底层通信框架,才为起初的 Web 开发团队百花齐放奠定了根底。TCP/IP 是互联网的根底,没有哪种语言或平台不反对 TCP/IP 的。DevTools 选型 TCP/IP 形式间接抹平了不同平台或零碎框架之间的差别。

Chrome DevTools Protocol(简称 CDP) 这组凋谢协定的推出,再一次将 DevTools 的实现,真正做到了跨平台。CDP 实质就是一组 JSON 格局的数据封装协定,JSON 是轻量的文本替换协定,能够被任何平台任何语言进行解析。正因为此,官网举荐的反对 CDP 的语言库多达近十种。Google 官网举荐了 Node.js 版本 Puppeteer,通过 Puppeteer 残缺地实现了 CDP 协定,为 Chrome 内核通信的形式打了一个样,接着开源世界陆续推出了多个语言版本的 CDP 的应用库。对于 CDP 协定,在稍后的章节会具体介绍。

Chrome 的架构师通过高度形象能力,将 DevTools 的底层架构形象成 TCP/IP 和 CDP 两个局部,奠定了 DevTools 的跨平台跨终端的能力。当年 WebSocket 的实现计划还处在草案阶段,Chrome 架构师就大胆地采纳 WebSocket 实现了调试协定中的主协定局部。当初看来,开发者日常应用的页面的实时截图能力,能够实时察看到近程网页中所展现的界面,这个实时性就是基于 WebSocket 来提供的。笔者还很拜服 Chrome 架构师的眼光和设计气场,正是他们优良的能力,将网页开发者工具带到新高度。

四、DevTools 通信协议

Chrome DevTools Protocol(简称 CDP)此协定蕴含两局部 HTTP 和 WebSocket,DevTools 的 Web UI 将管制命令发往浏览器内核,其中的管制命令、参数以及返回值,都是通过 CDP 来进行封装。命令的发送时,由 Web UI 进行封装后,通过 WebSocket 发往浏览器内核。接管到浏览器内核反馈回后果后,再按协定进行解包,分发给 Web UI。

为了剖析 Web UI 与 Android 浏览器内核通信过程,须要做一下环境筹备。

4.1 环境筹备

为了能拜访到内核中数据,浏览器内核须要开启 DevTools Server,PC Chrome 和 Android Chrome / WebView 的开启形式略有不同。

PC Chrome 启动时,减少一个启动参数 -remote-debugging-port=9222 , 这样 DevTools Server 就会侦听本地的端口,能够向 http://localhost:9222 发动 HTTP / WebSocket 申请,即可获取 DevTools 中的数据。

对于 Android Chrome 与 WebView 略有差别,因为 WebView 默认是不开启调试性能的,须要在客户端手动开启,能力启动 Server。

// Android 4.4 以上 WebView 才真正应用 Blink 内核,所以须要在此版本及以上零碎。if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {WebView.setWebContentsDebuggingEnabled(true);
}

此时 Android Chrome / WebView 在手机内已启动了 Server,但为了在 PC 上可能拜访到,须要应用 ADB 工具的端口转发能力。

ADB 端口转发
您能够应用 forward 命令设置任意端口转发,将特定主机端口上的申请转发到设施上的其余端口。以下示例设置了主机端口 6100 到设施端口 7100 的转发:adb forward tcp:6100 tcp:7100

通过 forward 能够买通 PC 与 Android 设施之间的网络互相拜访 

Android Chrome / WebView 应用 unix domain socket 建设的 Server 端,此 socket 的连接符为:

chrome\_devtools\_remote 和 webview\_devtools\_remote_别离为 chrome 和 WebView 的连接符。WebView 的连贯因为可能不同利用都应用了 WebView,所以采纳了过程 ID(PID)作为后缀来辨别。

adb shell cat /proc/net/unix | grep "devtools_remote"
0000000000000000: 00000002 00000000 00010000 0001 01 528176 @chrome_devtools_remote
0000000000000000: 00000002 00000000 00010000 0001 01 276394 @webview_devtools_remote_23119

通过 ADB forward,将 PC 与 Android 设施拜访买通,执行如下命令:

# 在 PC 上侦听 9222 端口,对 localhost:9222 的申请将会转发到 android 设施上的 webview_devtools_remote_23119 上
adb forward tcp:9222 localabstract:webview_devtools_remote_23119

至此,就能够在 PC 上通过 9222 来拜访 Android 设施中的调试页面了。

4.2 HTTP 协定剖析

4.2.1 获取内核版本信息

# 应用 curl 工具,GET http://localhost:9222/json/version
curl http://localhost:9222/json/version                       
{
   "Android-Package": "com.vivo.browser",
   "Browser": "Chrome/87.0.4280.141",
   "Protocol-Version": "1.3",
   "User-Agent": "Mozilla/5.0 (Linux; Android 8.1.0; vivo X20Plus A Build/OPM1.171019.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.141 Mobile Safari/537.36",
   "V8-Version": "8.7.220.31",
   "WebKit-Version": "537.36 (@9f05d1d9ee7483a73e9fe91ddcb8274ebcec9d7f)",
   "webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser"
}

从下面返回值,能够失去如下几个信息:

  • Android-Package,应用 WebView 利用的包名。
  • Browser,内核的版本号。
  • Protocol-Version,为 CDP 的协定版本,以后版本为 1.3,从 1.0 开始,还有 1.1、1.2 等。
  • User-Agent,浏览器的 UA 信息。
  • V8-Version,所应用的 JavaScript 引擎版本号。
  • WebKit-Version,因为 Blink 内核是基于 WebKit 537.36 版本开发,所以会有此版本信息。
  • webSocketDebuggerUrl,这是 WebSocket 的调试 URL。

4.2.2 获取可调试页面列表

# 应用 curl 工具,GET http://localhost:9222/json/list
curl http://localhost:9222/json/list  
[ {"description": "{\"attached\":true,\"empty\":false,\"height\":1812,\"never_attached\":false,\"screenX\":0,\"screenY\":72,\"visible\":true,\"width\":1080}",
   "devtoolsFrontendUrl": "https://chrome-devtools-frontend.appspot.com/serve_rev/@9f05d1d9ee7483a73e9fe91ddcb8274ebcec9d7f/inspector.html?ws=localhost:9222/devtools/page/B86E67DEA526D5EEE83A170B1F62A72C",
   "faviconUrl": "https://mat1.gtimg.com/www/mobi/2017/image/logo/v0/192.png",
   "id": "B86E67DEA526D5EEE83A170B1F62A72C",
   "title": "腾讯网 -QQ.COM",
   "type": "page",
   "url": "https://xw.qq.com/#news",
   "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/B86E67DEA526D5EEE83A170B1F62A72C"
}, {"description": "{\"attached\":false,\"empty\":true,\"never_attached\":true,\"screenX\":0,\"screenY\":0,\"visible\":true}",
   "devtoolsFrontendUrl": "https://chrome-devtools-frontend.appspot.com/serve_rev/@9f05d1d9ee7483a73e9fe91ddcb8274ebcec9d7f/inspector.html?ws=localhost:9222/devtools/page/3F9E05905F1919D563DF01BAEC64D2E4",
   "id": "3F9E05905F1919D563DF01BAEC64D2E4",
   "title": "about:blank",
   "type": "page",
   "url": "about:blank",
   "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/3F9E05905F1919D563DF01BAEC64D2E4"
} ]

返回了一个 JSON 的数组,每一个调试页占用一个数据元素,下面的返回值能够看出,笔者环境下 vivo 浏览器关上了两个页面,一个 https://xw.qq.com/#news 和 about:blank。

  • description,是个 JSON 对象,展现以后页面的状态信息。比方页面宽、高、在屏幕上的偏移,WebView 是否曾经 attached 到 view 上了,只有 attach 上的页面,才会被展现进去,是否被调试。
  • devtoolsFrontendUrl,此值为一个 URL,就是日常应用 DevTools 的 WebUI 控制面板地址,这是个 Web APP 当拜访过一次后,会就缓存一份在浏览器下。此页面托管在某个在国内无奈失常拜访地址,所以常常会呈现打不开面板,而显示白屏的状况。Chrome 浏览器在打包时会内置一份与以后内核匹配的 WebUI 版本,所以 Chrome 能够间接调试本人的页面。
  • id,这是每个关上页面随机生成的 GUID 值,用于生成 WebSocket 链接,以辨别不同页面。
  • title,关上网页的题目,对应网页 head 中的 title 标签内容。
  • type,页面的类型,次要有以下几类 page、iframe、worker 以及 service_worker 等。
  • URL,以后关上的页面 URL。
  • webSocketDebuggerUrl,此参数为 WebSocket 连贯的 URL。

HTTP 协定还有其余几个子命令,比方 protocol、new、activate 等,次要是页面管制类的,就不一一介绍了。

4.2.3 WebSocket 协定剖析

WebSocket 协定由四局部组成: Domain、Method、Event 和 Type。

1)Domain,命名空间,相似 C++/Java 中的命名空间或包名,用于宰割不同的命令。用于将泛滥子命令按类划分,不便使用者调用,以及避免 Method 同名抵触。以 1.3 版本的 CDP 协定,一共划分出 15 个 Domain。

  • Browser: 用于治理浏览器对象。
  • Debugger: 用于调试 JavaScript 的分类,比方断点、调用栈等。
  • DOM: 所有 DOM 节点操作都在此 Domain 下,DOM 节点的批改,遍历等。
  • DOMDebugger: 治理 DOM 节点调试的 Domain,DevTools 中节点批改断点,就是通过这组 Domain 中提供的 Method 实现的。
  • Emulation: 此是一组环境模拟器汇合,DevTools 中的批改设施尺寸、UserAgent 等是由这个 Domain 实现。
  • Input: 事件散发办法的汇合。
  • IO: I/O 流操作汇合。
  • Log: Log 管制 Method 汇合。
  • Network: 浏览器网络通信数据,可能通过此 Domain 进行捕捉。
  • Page: 基于 Blink 中的 Page 操作 Method 汇合,比方刷新,关上 URL。
  • Performance: 集成了性能剖析 Method。
  • Profiler: 采样分析器的 Method 集成在此 Domain 下。
  • Runtime: 与 JavaScript 通信的 Method 被集成此 Domain 下,比方执行 JavaScript 代码。
  • Security: 安全类操作,比方证书谬误。
  • Target: DevTools 连贯的一些管制类 Method 在此 Domain 下。

2)Method,办法名称,每个 Domain 下都会有一组 Method,指明了具体操作浏览器内核的性能。有三局部组成:名称、参数 和 返回值。与 C++/Java 中办法形容统一。

  • 名称 :Debugger.setBreakpointByUrl;
  • 参数 :lineNumber integer [,url string,urlRegex string,scriptHash string,columnNumber integer,condition string];
  • 返回值 :breakpointId BreakpointId,actualLocation Location。
// Debugger.setBreakpointByUrl 到内核,带上如下参数
{
   "lineNumber":1,
   "url":"snippet:///Script%20snippet%20%231",
   "columnNumber":0,
   "condition":""
}
 
// 将会收到内核的返回值,返回断点胜利信息
{
   "breakpointId":"1:1:0:snippet:///Script%20snippet%20%231",
   "locations":[]}

3)Event,告诉事件 ,网页会有很多状态告诉,须要同步到 WebUI 或其余管制端上来。Event 就是用于告诉这些事件的。比方 DOM 属性产生了变动时,将会收到 Dom.attributeModified 事件;将 JavaScript 传递到内核去执行时,将会收到内核发回来的 Debugger.scriptParsed 事件和参数,参数如下:

{
   "scriptId":"238",
   "url":"","startLine":0,"startColumn":0,"endLine":0,"embedderName":"",
   "endColumn":7,
   "endLine":0,
   "executionContextAuxData":{
      "isDefault":true,
      "type":"default",
      "frameId":"2059AA1A2C1A535CF4C480DC01E7FDEC"
   },
   "frameId":"2059AA1A2C1A535CF4C480DC01E7FDEC",
   "isDefault":true,
   "type":"default",
   "executionContextId":5,
   "hasSourceURL":false,
   "hash":"035a9e1738252e22523ed8f1c52d9dbf81abe278",
   "isLiveEdit":false,
   "isModule":false,
   "length":7,
   "scriptId":"238",
   "scriptLanguage":"JavaScript",
   "sourceMapURL":"","startColumn":0,"startLine":0,"url":""
}

4)Type,是 Method 或 Event 传递参数的简单数据类型,这些类型与内核的对象绝对应。比方 DOM.Node 类型就对应着 Blink 中的 DOM 节点。次要属性如下:

  • nodeId: NodeId 也是 Type,节点 id,依据此值能够在内核找到对应的节点。
  • parentId: NodeId 也是 Type,父节点 id。
  • nodeType: integer,节点类型。
  • nodeName: string,节点名称。
  • nodeValue:string, 节点内容。
  • children: array,子节点数组。
  • attributes: array,节点属性数组 通过 Node 上这些属性,就能够将 DOM 树的节点在内存占用形容进去。DevTools 的 Web UI 中 Element 面板,就是通过 DOM.getDocument Method 将一棵 DOM 树展示进去。

通过 CDP 的这种数据组织形式,既能够传递管制命令来操作内核,也能够接管内核状态告诉(Event)。通过 CDP 能够让浏览器做任何事件,而且失去的信息远比应用 Chrome 图形界面还要多。因而,Google 推出 Chrome Headless 版本,被广泛应用于 web 自动化测试、网页爬虫以及网页沙箱等畛域。

当调试挪动端浏览器时,能够实时看到挪动设施上的所浏览的屏幕,这是怎么做到的呢?

其实,就是一张一张截图通过 Page.screencastFrame 事件将 base64 后的图片发回到 Web UI 中展现的。

从 Page.screencastFrame 告诉事件带回了图片和形容信息(Meta data):

{
   "data":"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBw...",
   "metadata":{
      "deviceHeight":604,
      "deviceWidth":360,
      "offsetTop":60,
      "pageScaleFactor":1,
      "scrollOffsetX":0,
      "scrollOffsetY":832.6666870117188,
      "timestamp":1631018056.565802
   },
   "sessionId":2
}

通过形容信息,即可将此图片的信息展现在 WebUI 上。一张截图近 1M 的大小,因为 DevTools 利用了 WebSocket 的双向长链接的个性,所以展现进去无比平滑和清晰。

4.3 DevTools 内核实现

以上章节,介绍了从 Web 开发者的角度登程,将操作命令传递到挪动端 Browser Core 的一个整体流程,以及 CDP 通信协议相干内容。本节重点介绍在 Browser Core 中的实现过程,先介绍 DevTools 在浏览器内核中实现,前面笔者会筛选 JavaScript 如何从字符串传递到 V8 中执行过程,开展来进行具体介绍,这一行为的实现计划。

4.3.1 内核架构介绍

DevTools 以启动一个 Web Server 为终点,而后将调用命令发到相应解决模块,整体架构图如下:

DevTools 在内核中大体上分为四层:

  • Server 层,用于接管内部网络发过来的操作申请。
  • Agent 层,对于 Server 层发过来的申请,进行拆解,依据操作的类型不同,再分发给不同的 Agent 来解决。
  • Session 层,Session 是对不同的业务模块进行了一层形象。过了 Session 层后,将会进入不同的业务模块,能够达到 V8,Blink 等。
  • 业务层,就是具体的功能模块,比方 V8 模块,次要负责 JavaScript 的调试相干能力的撑持。

Server 层由 DevToolsManager 这个单例对象来治理,因为是单例所以一个过程只会存在一个 Manger 对象,从而避免被反复创立出多个,导致状态错乱。

4.3.2 Web Server 数据接管入口

Server 收到的申请都会分发给 DevToolsHttpHandler 类,此类负责网络 Client 发过来的数据申请响应和将处理结果发送回网络 Client,此类有两个重要办法 OnJsonRequest 和 OnWebSocketMessage,别离用来解决 HTTP 协定和 WebSocket 协定。

void DevToolsHttpHandler::OnJsonRequest(
    int connection_id,
    const net::HttpServerRequestInfo& info) {
  // 查问内核版本信息
  if (command == "version") {
    base::DictionaryValue version;
    version.SetString("Protocol-Version",
                      DevToolsAgentHost::GetProtocolVersion());
    // ...
    SendJson(connection_id, net::HTTP_OK, &version, std::string());
    return;
  }
  // 获取内核所反对的协定
  if (command == "protocol") {DecompressAndSendJsonProtocol(connection_id);
    return;
  }
  // 获取可调试页
  if (command == "list") {DevToolsManager* manager = DevToolsManager::GetInstance();
    DevToolsAgentHost::List list =
        manager->delegate() ? manager->delegate()->RemoteDebuggingTargets()
                            : DevToolsAgentHost::GetOrCreateAll();
    RespondToJsonList(connection_id, info.GetHeaderValue("host"),
                      std::move(list));
    return;
  }
  // 启动一个新调试
  if (command == "new") {
    // ...
    std::string host = info.GetHeaderValue("host");
    std::unique_ptr<base::DictionaryValue> dictionary(SerializeDescriptor(agent_host, host));
    SendJson(connection_id, net::HTTP_OK, dictionary.get(), std::string());
    return;
  }
  // 激活或敞开一个调试
  if (command == "activate" || command == "close") {
   // ...
  SendJson(connection_id, net::HTTP_NOT_FOUND, nullptr,
           "Unknown command:" + command);
}
 
void DevToolsHttpHandler::OnWebSocketRequest(
    int connection_id,
    const net::HttpServerRequestInfo& request) {
  // 创立调试的 Agent
  if (base::StartsWith(request.path, browser_guid_,
                       base::CompareCase::SENSITIVE)) {
    scoped_refptr<DevToolsAgentHost> browser_agent =
        DevToolsAgentHost::CreateForBrowser(thread_->task_runner(),
            base::BindRepeating(&DevToolsSocketFactory::CreateForTethering,
                                base::Unretained(socket_factory_.get())));
    connection_to_client_[connection_id] =
        std::make_unique<DevToolsAgentHostClientImpl>(thread_->task_runner(), server_wrapper_.get(), connection_id,
            browser_agent);
    AcceptWebSocket(connection_id, request);
    return;
  }
 
  connection_to_client_[connection_id] =
      std::make_unique<DevToolsAgentHostClientImpl>(thread_->task_runner(), server_wrapper_.get(), connection_id, agent);
    // Accept websocket
  AcceptWebSocket(connection_id, request);
}
 
// WebSocket 数据接管接口,所有 WebUI 的申请都通过此接口散发
void DevToolsHttpHandler::OnWebSocketMessage(int connection_id,
                                             std::string data) {auto it = connection_to_client_.find(connection_id);
  if (it != connection_to_client_.end()) {it->second->OnMessage(base::as_bytes(base::make_span(data)));
  }
}
  • DevToolsHttpHandler::OnJsonRequest 用于响应 HTTP 申请,用于查问内核状态,比方内核版本、以后反对协定,将返回残缺协定内容,不便开发者适配对应的反对。
  • DevToolsHttpHandler::OnWebSocketRequest 用于接管 WebSocket 的连贯,依据此办法对不同的 Agent 对象进行了创立。
  • DevToolsHttpHandler::OnWebSocketMessage 所有调试申请数据,都通过此接口通过 Client 散发到不同的 Agent 下来。

Server 层数据响应时通过下面的三个接口来达到数据接管和散发的能力。

4.3.3 JavaScript 执行过程

V8 JavaScript 引擎用于解释执行网页中的 JavaScript 脚本,同时也能够通过 DevTools 接管内部传递过去的脚本,脚本在以后网页的 Context 下执行,所以能够通过 JavaScript 来操作网页行为,比方批改 DOM 节点属性。CDP 中设计了执行 JavaScript 接口 Runtime.evaluate,引办法的参数如下:

{
    allowUnsafeEvalBlockedByCSP: false,
    awaitPromise: false,
    contextId: 14,
    expression: "alert('hi');",
    generatePreview: true,
    includeCommandLineAPI: true,
    objectGroup: "console",
    replMode: true,
    returnByValue: false,
    silent: false
}

其中,最重要的一个参数就是 expression,此为一个 string 类型的参数,用于寄存须要执行的脚本内容。上例将会在网页中弹出一个内容为 hi 的 alert 确认框。

V8 中有个专门的模块,V8RuntimeAgentImpl 用于反对 CDP 中 Runtime 的这个 Domain,当然也有 V8DebuggerAgentImpl 是用来反对 Debug 这个 Domain 的具体实现。V8RuntimeAgentImpl 中 evaluate 办法,就是用于负责接管 DevTools 发过来的执行申请。

void V8RuntimeAgentImpl::evaluate(
    const String16& expression, Maybe<String16> objectGroup,
    Maybe<bool> includeCommandLineAPI, Maybe<bool> silent,
    Maybe<int> executionContextId, Maybe<bool> returnByValue,
    Maybe<bool> generatePreview, Maybe<bool> userGesture,
    Maybe<bool> maybeAwaitPromise, Maybe<bool> throwOnSideEffect,
    Maybe<double> timeout, Maybe<bool> disableBreaks, Maybe<bool> maybeReplMode,
    Maybe<bool> allowUnsafeEvalBlockedByCSP,
    std::unique_ptr<EvaluateCallback> callback);

V8RuntimeAgentImpl::evaluate 会启动一个 microtasks 来执行脚本,最终会走到 v8::internal::Execution::Call 中,Execution 模块会负责将脚本进行语法解析和编译成字节码,最终调度到虚构机器中运行。

执行流程如上图所示,Web UI 收回执行脚本的字符串,WebSocket 的 OnWebSocketMessage 将会收到此命令,而后通过 DevToolsSession 逐层向 V8 散发。因为 Chrome 是多过程架构,分为 Browser 过程和 Render 过程,之间通过 IPC 进行通信。上图左侧在 Browser 端执行流程,右侧为 Render 端执行流程。

Render 端的 DevToolsSession::DispatchProtocolCommand 是一个重要的散发接口,所以发到 V8 或 Blink 的管制命令,都会通过此接口。接着就会将管制命令发送到 V8RuntimeAgentImpl,依据命令性能的不同,调度到不同功能模块进行解决。

4.4 网页性能调优

4.4.1 性能剖析面板介绍

DevTools 提供一组功能强大的性能剖析工具,网络、JavaScript 调试、渲染、内存以及规范反对度检测等。上面介绍 Performance 面板中一些性能剖析时的一些性能。主界面被划分为这几块:

1)帧率(FPS):线性展现了做 Performance 期间,网页渲染的帧率。

2)CPU 使用率:CPU 占用走势图

3)加载过程中截屏:定时采集了网页截屏性能

4)网络加载时序:展现网络资源加载秩序及耗时状况

5)帧耗时(Frames):展现了渲染每帧耗时状况,红色示意存在耗时较长的帧。

6)Web Vitals 指标:Google 举荐一套性能体验指标,上面会具体介绍。

7)内核中次要线程:浏览器内核中存在多个线程各有分工,当呈现耗时较长帧时,须要在这些线程中排查,具体哪个线程在耗时。次要分为这几个:

  • Main,这是 Blink 主线程,负责网页的排版、解析、JavaScript 执行等。
  • Raster,光栅化线程,用于将渲染对象转化成 Bitmap。
  • GPU,硬件加速渲染线程,将 Texture 绘制到屏幕上。
  • Chrome_ChildIOThread,负责网络资源,文件操作。
  • Compositor,合成线程,负责将渲染时各个层,合成在一起而后进行光栅化。
  • ThreadPoolForegoundWorker,Worker 的工作线程池。

8)信息面板:用于展现抉择模块详细信息,几个指标含意:

  • Loading:网络申请和 HTML 解析耗时。
  • Scripting:JavaSript 解析、编译、在虚拟机中执行,以及 GC 耗时。
  • Rendering:Blink 排版渲染耗时。
  • Painting:绘制耗时,次要蕴含绘制、合成、图片解码以及上屏。
  • System 和 Idle:是系统调度和闲暇耗时。

4.4.2 性能剖析惯例思路

性能剖析基本思路从问题动手,网页常见性能问题,笔者遇到的次要有这几种情景。

  • 须要的资源没有及时被申请回来。排除服务器问题,资源申请发动太晚?资源太大?
  • 网页分层太多,导致 Rendering 和 Painting 工夫过长。
  • 内存占用过多,页面过于简单、资源多且大、JavaScript 大块资源持有生命周期太长。
  • 动画多且隐没后未移除。JavaScript 的轮播动画、CSS 的动画、带有动画的图片资源,比方 GIF, SVG、WebP 等。
  • 事件侦听不合理。事件侦听过多且可能被高频触达,比方节点变动、Move 事件等。

总的来说,不论是网页性能优化还是 Native 程序优化,只有协调好这两个资源占用即可:CPU + 内存。只有挖掘出问题点,性能问题都会迎刃而解,问题点的开掘除了源码级别的审查,DevTools 能够助一臂之力。

针对下面总结的惯例场景,利用 DevTools 性能剖析能力,先整体上扫视 Profile 图。

网络申请秩序和时长是否正当;

Main Thread 的长工作是否正当。

从 Network 板块察看资源申请发动的程序,是否存在长耗时工作,阻塞着首屏展现资源加载,如果不保障须要的及时加载,就会长工夫白屏。

资源问题就绪后,就须要排查哪些长耗时工作执行。先查看 Main Thread 中的 Long task,比方,上图的 Long task 就是 Scripting 的占了较长时间。通过 Bottom-Up / CallTree 查看具体的耗时点,相应地优化掉。

在排查具体优化点时,有个小技巧。通常开发环境都是在 PC 上进行模仿,当版本进来后,能力暴露出问题。因为挪动设施的碎片化,很多用户的设施,性能可能并不好。那如何在开发环境优化这类低配置机器上的体现呢?DevTools 提供了限流的模仿,能够限度网络制式为 2G/3G,CPU 降速。

在右上角有个“设置”,开展配置我的项目,能够看到 Network 和 CPU 的限流选项,抉择后从新录制一下 Profile。

下面提到,网页层数太多,极大地影响到网页渲染性能。“网页层数”是什么意思呢?目前,浏览器渲染引擎为了晋升网页绘制性能,绘制时会对网页进行分层。这样的益处就是,仅重绘批改过的层,其余层内容如果没有变动,就不须要从新绘制,间接取上次绘制后果,从而晋升绘制效率。不同的 WEB 引擎分层的策略不同,通常会将一般网页、CSS 动画、Canvas、WebGL、Fix 标签等各分为一层。分层会带来渲染效率的晋升,但也会带来内存的开销,从而会影响到性能。DevTools 是否剖析网页层数吗?能够,在下面的“设置”中有一个选项“Enable advanced paint instrumentation(slow)”启用它,从新做一次性能录制。

在“信息面板”多了一个“Layers”标签,抉择后将会看到网页分层状况。如果存在不合理的分层,能够尝试调整形式,将分层进行合并,从而达到晋升性能。

4.4.3 Web Vitals

Web Vitals 是 Google 推出的一套 Web 性能与体验兼顾的衡量标准。原先的掂量策略根本是基于“首字”和“首屏”来掂量,但从用户角度和技术优化角度,这两指标都存在这样那样的问题。所以,Google 推出了 Web Vitals 规范,并与 DevTools 进行配合,不便开发者在开发阶段,就辨认出 Web 的性能问题。因为规范始终随着时代的倒退,一直变动,开发者始终追着指标的变动有点吃不消,好在 Google 明确示意,目前推出的三个指标,短时间内不会变,笔者就不分明这个短时间是多长时间。

第一个指标:Largest Contentful Paint(LCP),大面积铺满工夫点,2.5 秒以内算优良。次要是指有大面积的文字、图片被展现进去,就算达到了 LCP。

第二个指标:First Input Delay(FID),首次可响应内部输出事件的工夫点,100 ms 内算优良。这个指标是从用户应用角度登程,达到 FID 的工夫点,意味着用户能够操作网页了。

第三个指标:Cumulative Layout Shift(CLS),排版跳跃指标,0.1 为优良。在网页加载过程中,如果呈现排好版的元素,发现大面积的挪动的话,这个指标就会很高。比方网页中 img 标签不设置宽和高,当图片加载结束后,按图片理论大小来排版本。这样的就会触发网页从新排版,从用户角度网页被整体向下推了一个图片高度,Google 认为这个体验不好。

LCP / FID / CLS 这三个指标,实质上是从用户视角看网页的性能掂量指标,开发者能够看看本人作品这三个指标属于什么程度。

五、工具在生态构建中的重要性

(数据来自 statcounter.com)

Chrome 凭借着本人优良的产品个性,平安、疾速以及稳定性,博得了少量用户青眼。从上图 StatCounter 统计数据,能够看出 Chrome 已成为相对的浏览器界的一哥,天经地义地获得商业上的胜利。然而 Chrome 在开源以及生态的建设,DevTools 堪称首功一件。Google 通过 DevTools 的超过竞品的个性,吸引了少量前端开发者,转到 Chrome 下开发本人的产品。晚期生态产品是 Chrome 插件,Chrome Store 中的插件数量就能够看出它的胜利。

当 Node.js 的问世,DevTools 首款反对 Node.js 的调试工具,推动了 Node.js 的遍及。而后 DevTools 依靠 Node.js 迅速出圈。另一方面,开源世界也开始反哺了 DevTools 我的项目,目前反对 CDP 协定的开源计划多达 10 几种语言,罕用的语言根本都反对上了。这个畛域目前还在飞速发展中,期待这个畛域能够有更好的倒退。

DevTools Web UI 曾经从 Chromium 仓库中独立进去,能够独自 Clone 下来进行二次开发,Web UI 本次限于篇幅,未做实现原理剖析。其实,Web UI 也是个十分优良的 Web APP,很适宜前端开发者深度钻研一下。

咱们从优良开源我的项目中学习到的不仅是代码实现与架构,也能够学习到更高维度的货色,比方产品思维以及工具思维,并落地到本人我的项目中。回顾一下网页调试畛域倒退过程,从一款 JavaScript 插件,是如何演变成明天的前端开发生态,其中有很多点值得学习。

六、结束语

笔者所在团队长期致力于 Chromium 内核的钻研与学习,基于其衍生进去的产品,服务咱们生态用户,为其提供优质的上网体验。同时,咱们孵化出的 Web 浏览服务,也为生态内利用提供弱小、疾速、稳固的 Web 服务能力。如果您有趣味于 Web 底层技术钻研,欢送退出咱们,与一群气味相投的小伙伴独特成长,同时也能服务好亿级用户。

七、参考文献

[1] Google Chrome

[2] 10 Years of Web Inspector

[3] 10 years of Speed in Chrome

[4] Chrome DevTools

[5] Chrome DevTools Protocol protocol

[6] Web Vitals

作者:vivo 互联网浏览器内核团队 -Li Qingmei

退出移动版