调试机制

来源:互联网 发布:墨客网络 编辑:程序博客网 时间:2024/06/05 22:42

支持调试HTML、CSS和JavaScript代码是游览器或者渲染引擎需要提供的一项十分重要的功能,这里调试包括两种类型:其一是功能,其二是性能。功能调试能够帮助HTML开发者使用单步调试等技术来查找代码中问题,性能调试能够采集JavaScript代码、网络等性能瓶颈,这只是对于HTML开发者来说,对于性能来说,问题可能存在域HTML代码,也可能是游览器本身的问题,为此Chromium开发出另一套机制-“tracing”技术,它能够收集Chromium内部代码的工作方式和性能瓶颈,以帮助定位Chromium本身的问题

Web Inspector

基本原理

Chromium开发者工具被用来帮助了解渲染引擎和游览器被狗的原理,这一工具实际上是基于WebKit的Web Inspector技术开发出来的,它的功能丰富,下图是Chromium游览器的开发者工具调试页面的示例图:
这里写图片描述
图中上半部分表示需要被调试的网页,下半部分表示调试其的界面,该界面由WebKit提供,也就是说,使用WebKit的内核就可以看到类似的界面,不同点在于Chromium使用了多进程架构,根据WebKit中的定义,上面的部分称为后端(Backend),下面的部分被称为前端(Frontend),下图还有一个特点就是掉时期的界面本身也是使用HTML、CSS和JavaScript技术来编写。Chromium开发者工具提供了众多的功能,主要包括一下几种:

  • 元素审查(Elements):该功能能够帮助开发者查看每一个DOM元素,如“body”元素,同样可以查看它的样式信息
  • 资源(Resources):该功能能够帮助开发者查看各种资源信息,如内部存储、Cookie、离线缓存等
  • 网络(Network):该功能能够帮助开发者了解和诊断网络功能和性能
  • JavaScript代码(Sources):就是调试JavaScript代码,同其他语言的调试器一样,它能够设置断点、单步调式JavaScript语句等
  • 事件序列(Timeline):该功能能够按照时间次序来手机网页消耗的内存、绘制的帧数和生成各种事件,帮助开发者分析网页性能
  • 性能收集器(Profiles):它能够手机JavaScript代码使用CPU的情况、JavaScript堆栈、CSS选择器等信息,以版主开发者分析网页的运行行为
  • 诊断器(Audits):这是帮助开发者分析网页可能存在的问题或者可以改善的地方
  • 控制台(Console):该控制台可以输入JavaScript语句,由JavaScript引擎计算出结果
协议

调试机制的前端和后端通过使用一定格式的数据来进行通信,这些数据使用JSON格式来表示,具体到如何理解数据的内容,那就是Web Inspector使用的特殊调试协议,该协议定义了如何理解双方发送的数据内容,在WebKit中,协议被定义在Inspector.json文件中,而遵照该协议传输的数据同样使用JSON格式,下图定义Web Inspector的前端和后端交互信息的协议,如上所说,该协议使用的是一个JSON格式的文档,从全局来看,协议中主要包括两个属性,一个是“version”,用来表示协议的版本号,Web Inspector有多个版本,需要注意的是版本的兼容性问题,下图中显示的是版本1.0,另一个是“domains”,它定义了多个协议细节,并包含了多个“domain”,一个“domain”通常是一类功能,如“memory”、“CSS”等:

{  "version":{"major":"1", "minor":"0"},  "domains":[  {    "domain":"CSS",    "hidden":"true",    "description":"...",    "types":[    {      "id":"StyleSheetId",      "type":"string"    },    ...    ],    "commands":[    {       "name":"toggleProperty",       "parameters":[       { "name": "styleId", "$ref":"cssStyleId"},       { "name": "propertyIndex", "type":"integer"},       { "name": "disable", "type": "boolean"}       ],       "returns":[         { "name": "style", "$ref": "cssstyle", "description": "..."}       ],       "description":"..."    },    ],    "events": [    {      "name": "styleSheetChanged",      "parameters": [        { "name": "styleSheetId", "$ref": "StyleSheetId" }      ],      "description": "..."    },    ]  }  ]}

一个”domain“包括6个属性,下面介绍后三个比较难以理解的属性:

  • 第一个是”types“,它有点像预先定义的类型,这些类型表示一些特定的数据,在后面的定义中可以声明使用这些类型来表示一定的数据结构,图中定义的”id“为”StyleSheetId“,它表示的是一个字符串
  • 第二个属性是”commands“定义“domain”中包含的所有命令,这些命令类似于远程过程调用,表示前端和后端之间发送请求并响应的方式
  • 最后一个属性是“events”,它是用来描述事件的,同样可以包含一个或者多个事件,主要是向对方发送当前的一些状态信息,域命令不同的是,它没有也不需要返回值

WebKit内部机制

对于前端调试器,调试器界面本身也是使用Web技术来实现的,前面介绍的所有功能都是使用最新HTML5技术来完成的,目前有两个接口需要具体WebKit移植的实现,第一是发送消息到后端的接口,第二个从对方接收消息后将消息派发给调试器,下图是WebInspector前端的主要结构和基础设施:
这里写图片描述
最上层的是Inspector.html,为读者看到的调试器主界面,采用HTML5,因为调试器包含众多的功能,所以它实际上使用了各种功能的JavaScript代码,在其典型的三个JavaScript文件中,首先是InspectorFrontendAPI,它是前端的公共接口,被上层的调试器包含的JavaScript代码使用,同时该类也包括公共的派发消息的接口;然后是inspector.js,它是一个总的入口,包括所有主要对象的创建,而InspectorBackend.js是一个背后的具体实现类,它能够提供接口来将消息发送到被调试的页面,也就是后端,调试器主要需要两个能力,一是发送消息给前端,而是接收后端的消息,这两个接口在WebInspector框架中被定义,图中左边是发送消息给前端的过程,Web INspector使用一个称为InspectorFrontendHost的类作为接口,当然它本身没有具体实现,在一般的情况下,InspectorFrontendHost是一个使用C++编写的接口类,它通过V8的绑定机制来实现,最后会调用到InspectorFrontendHost,之后就依赖于具体移植的实现,另外一个接口就是定义了派发消息的JavaScript接口,也就是InspectorFrontApi.js定义的派发消息的两个接口,在Web Inspector中,一个默认的实现是InspectorClient类中有一个静态方法,该方法使用ScriptController类,将通过C++代码获得的消息传入JavaScript代码,这样整个前端依赖的两个本地接口就得到了完美的实现,不仅如此,该结构还能很好的满足之后的远程调试的需求。下面来看后端,下图描述了后端所需要的主要类和它们的关系,同前端不一样的是,后端的的主要功能都是使用C++代码来完成的,其中最重要的类是InspectorController,它控制着后端的所有动作及其和被调试网页之间的联系,InspectorController类包含一个InspectorClient对象,该对象负责实现基本功能,如情况缓存、高亮等,同时它包含一个主要的对外接口,那就是dispatcheMessageFromFrontend类,它有WebKit移植将前端的消息传递给后端的时候被调用,这些消息都是由InspectorBackendDispatcherImpl这个自动生成类处理的,这个类能够处理所有的请求消息,并解析这些消息,然后转换成相应的C++对象和函数的调用,具体做法是每个“domain”都会有相应的称为CommandHandle的类,如图中的CSSCommandHandler类,每个类的对象都会注册到InspectorBackendDispatcherImple对象中,该对象很容易知道调用的“domain”、命令或者事件等,InspectorBackendDispatcherImpl类也能够同V8等JavaScript引擎交互,典型的应用就是审查(Inspect)一个元素,用户单机一个元素的时候(可以从后端的被调式我那工业中单击),JavaScript引擎接收到事件,然后处理并调用该类来处理,本身CommandHandler类包含一些接口,以图中CSSCommandHandler类为例,它的具体实现类是InspectorCSSAgent,借助于一些其他设施类,它能够知道被调试网页有关CSS方面的消息,如借用InspectorStyleSheet类:
这里写图片描述
图中的InspectorBaseAgent是支持所有功能的子类,由于Web Inspector需要众多的功能,如前面介绍的CSS、内存、性能等,所有这些功能都是基于该类实现的,这些类的对象使用一个注册类来管理,如图中的InspectorAgentRegistry类。以域CSS相关的Agent为例,该类被调用后能够做正确的处理并按需分那会相应的结构,但是这里不进行消息的编码,而是使用一个InspectorFrontend自动生成类来帮助这些C++对象和数据转换成JSON格式的数据。另外一方面就是后端发送消息到前端,WebKit定义了一个抽象接口就是InspectorFrontendChannel类,它是一个传输通道,所有后端到前端的消息都是从它传出的,消息本身不做任何转换,只是传输数据,InspectorFrontend是一个自动生成的类,这里是一个模拟前端的工具类,由于它是一个根据协议自动生成的类,后端调用协议中定义的方法和事件,而该类提供这些接口并将调用转变成JSON格式,包括命令的名称、参数等消息,转换后的JSON字符串通过通道传输,从而完成了消息的发送过程,因为WebKit的特殊性,WebCore只是提供框架,具体实现交由移植来完成,下图是基本的通信框架:
这里写图片描述
图中左边是前端,右边是后端,通信框架主要定义前后端的一些用来双向通信的基础类和提供的接口,这些接口需依赖实际的通信机制才能完成,设想一下如果前后端都工作在一个进程中,那么非常简单,只需将消息传递到另一线程中即可,不需要复杂的机制,不过这里它只是定义了抽象接口,而没有定义通信方面的具体规定,这位跨进程的调试机制和圆仓调试提供了可能。

Chromium开发者工具

Chromium开发者工具(通常称为DevTools)是基于Web Inspector机制的一套跨进程的调试工具,因为Chromium的多进程架构,后端中被调试的网页是一个Rednerer进程,前端的网页同样也是一个Renderer进程,根据前面Web Inspector的架构,Chromium所要做的是将前后端的通信机制连接起来,由于Chromium架构的特殊性,消息的传递实际上经过了一个中转站Browser进程,也就是说这两个Renderer进程不是直接通信的,而是将消息传递给Browser进程,由它派发给相应的Renderer进程,下图描述了Chromium支持多进程调试的整个架构,上半部分是前端和后端两个Renderer进程,而下半部分是Browser进程,整个架构可以说非常简洁明确,首先是看前端的接收和发送消息是如何被支持的,首先是Chromium对前端发送消息到后端的支持,WebKit中的基类InspectorFrontHost类其实是调用InspectorFrontendClient类发送消息的,而WebKit的Chromium移植做了一个具体的实现类InspectorFrontendClientImpl类,该类会调用WebDevToosFrontendImpl类,最后的跨进程通信类是content::DevToosClient,该类是Chromium项目中用于开发者工具的进程间通信类,然后是Chromium对前端接收来自后端消息的支持,当WebDevToosFrontendImpl类接收到content::DevToosClient传递过来消息的时候,它直接通过V8提供的机制调用InspectorFrontendAPI.js的dispatcheMessage方法,经过这一过程,Chromium已经将WebInspector的两个用于传递消息的接口实现了,接下来是Browser进程,每个前端进程都有一个相应的DevToolsFrontendHost对象,当前端的一个消息到达时,如果找到相应的后端是通过DevToosManagerImpl类,它管理了所有的“前后端对”,实际上包含了两个哈希表,第一个是从前端到后端的映射,第二个是相反的映射,有了这两个哈希表,Browser进程只是将前端的消息根据映射关系找到后端并传递给它,相反方向也是一样,最后是后端进程的工作过程,content::DevToosAgent也是负责同Browser进程交互消息的具体实现类,包括接收和发送,在这之上主要的WebKit的Chromium移植提供的接口,其具体的实现是通过WebDevTOosAgentImpl类,它会将接收的消息传递给InspectorController,至此,Chromium连接上WebKit中后端接收消息的处理机制,而对于发送消息,WebDevToosAgentImpl是InspectorFrontendChannel的子类,会实现sendMessageToFrontend接口,这样双向过程完整的得到了支持:
这里写图片描述

远程调试

远程调试是指前端和后端在不同的有了拿起实例中,但这两个实例可能在同一个环境中,也可能在不同的环境中,例如两台机器甚至网络上的两个设备。根据前面描述的Web Inspector机制,本身Web Inspector机制没有定义通信的方式,而且前后端只是通过JSON消息和一定的协议来交互的,所以理论上来讲,远程调试也只是需要建立一定的通信方式就能够支持远程调试,好消息是现在已经得到实现,Web Inspector没有提供或者规定圆仓调试的方式,在Chromium中,远程调试得到了比较好的支持,具体做法如下:

  • 首先在后端所在的游览器中需要建立一个HTTP服务器,在桌面系统上,建立和打开TCP监听一个端口,如9222,然后在另外一个Chromium游览器中输入“http://localhost:9222”就可以看到被调试的网页,目前,这种方式并不支持网络上的不同及其,可能是实现着考虑到安全性问题,如果调试Android平台上的Chrome游览器中的网页,首先需要将Android设备通过USB连接上开发机器,这时候用户可以在Android上Chrome游览器的设置中打开远程调试开关,这一操作实际上创建了一个Unix Domain Socket,此时,开发者在Linux系统中只要打开Chrome游览器,并在地址栏输入“chrome://respect”就能够看到需要调试的网页了
  • 建立连接之后,通过HTTP协议将被调试网页的HTML、CSS和JS等资源文件从被调试网页所在的游览器传输到前端调试器所在的网页中
  • 当开始调试时,前端调试器会尝试使用Web Sokcet建立前端和后端传输消息的通道,这是一种基于 Web的新技术,能够建立类似域套接字的数据传输通道

下图描述了使用WebSocket技术来传输调试消息的远程调试机制,根据前面介绍的WebKit中前端和后端传输消息的机制,主要是InspectorFrontendHost(前端使用)和InspectorFrontendChannel(后端使用)这两个类,它们具体的实现由子类来完成,在远程调试中,通信的基础设施是由HTML5的WebSocket技术来支撑的,Chromium将InspectorFrontendHost接口同时暴露到JavaScript中,当本地代码需要发送消息的时候,通过调用InspectorFrontendHost类的本地接口,而这个本地接口已经同JavaScript中的实际发送接口连接起来,这样本地代码中的消息就能够使用WebSocket技术来发送了:
这里写图片描述
下面代码是Chromium中连接本地代码和JavaScript代码发送消息的部分代码节选:

WebInspector.loader = function() {  // 首先构建ws,这个是WebSocket的URL,下面是创建WebSokcet对象  if (ws) {    WebInspector.socket=new WebSocket(ws);    // 接收对方过来的消息,直接交给相应的模块去处理    WebInspector.socket.onmessage=function(message){      InspectorBackend.diapatch(message.data);  }    WebInspector.socket.onerror=function(error) {console.error(error);}    WebInspector.socket.onopen=function(){      // 当连接打开后,就将InspectorFrontendHost的发送消息      InspectorFrontendHost.sendMeesageToBackend =                                      WebInspector.socket.send.bind(WebInspector.socket);      WebInspector.doLoadedDone();    }    ...  }}

这个代码是前端的代码,而对于接收消息,同样比较简单,Chromium将WebSocket的onMessage事件同后端的消息派发函数连接起来,对于后端而言,它使用C++代码来完成WebSocket的连接,原理类似。Weinre是一个支持圆仓调试功能的开源项目,它除了能够支持WebInspector协议,还能够支持Firebug(Firefox的调试工具)的协议,其原理也是类似的。

Chromium Tracing机制

Chroimum开发和工具能偶帮助Web开发者理解网页运行过程中的行为并帮助分析一些性能问题,但是如果出现问题,特别是绘制网页的时候,开发者非常希望了解为什么Chromium会使用如此多的时间,同时,对于Chromium的开发者来说,如果需要分析Chromium自身问题,就需要相应的工具来帮助分析,在Chromium中,chrome://tracing这个诊断工具能够满足上面的要求。这是一个基于事件收集的分析工具,它能够版主诊断一些WebKit和Chromium内部代码在绘制网页过程中存在的问题,其中最主要的还是同图形相关的操作。这一机制的实现采用的思想非常简单,Tracing机制在Chromium代码韩总插入相应的跟踪代码,然后计算开始和结束之间的时间差,虽然简单,但是非常有用,如下:

TRACE_EVENT_BEGIN0("SUBSYSTEM", "Something happen");toDoSomething()TRACE_EVENT_END0("SUBSYSTEM", "Something happen");

Tracing机制在某个动作执行前加入“开始事件”代码,然后在动作结束后加入“结束事件”代码,机制中的TRACE_EVENT宏自动计算获得该动作执行的事件,当然,一般典型的例子是在函数或者一段代码开始的时候加入TRACE_EVENT0,在函数退出时候该事件自动记录下结束的时间,这是使用对象的自动析构机制来完成的,这样Tracing机制就能够计算出该函数运行所需要的时间,而不再需要额外插入结束代码,如下面代码所示的三个记录点,下述下面首先在函数入口出创建Tracing对象并记录时间点,在该函数退出时,对象析构前就能够自动记录整个函数执行的总时间,然后在第一个if语句中,又加入了一个记录点记录了这种条件下的时间消耗:

bool ThreadProxy::CompositeAndReadback(void* pixels, gfx::Rect rect) {  TRACE_EVENT0("cc", "ThreadProxy::CompositeAndReadback");  DCHECK(IsMainThread());  DCHECK(layer_tree_host_);  if (defer_commits_) {    TRACE_EVENT0("cc", "CompositeAndReadback_DeferCommit");    return false;  }  if (!layer_tree_host_->InitializeOutputSurfaceIfNeeded()) {    TRACE_EVENT0("cc", "CompositeAndReadback_EarlyOut_LR_Uninitialized");    return false;  }  ...}

实践–基础和性能调试

Chromium开发者工具基本上沿用了Web Inspector的功能,其主要包括两个部分,基础功能部分的调试和性能部分的调试

基础调试

基础部分的调试大致可以分成DOM元素的修改等访问、CSS样式值修改、日志和控制台信息,以及JavaScript代码单步调试、断点设置等部分功能,开启或者关闭开发者工具的快捷键是F12或者在游览器地址最右侧的按钮中调用开发者工具即可,还有一个直接的方法为右键单击一个HTML元素,然后右键菜单中能够找到“Inspect Element”选项,那就是审查该元素,这种方式也可以打开开发者工具,当开发者工具被打开后,开发者就会发现在“Elements”标签下显示的其实是被调试页面的源代码,同时,Chromium游览器会高亮被审查元素的源代码,这一做法可以帮助开发者获悉当前的元素的源代码,右侧下方是当前元素的CSS属性值,包括经过WebKit计算之后应用在该元素上的属性值和支持获得这些属性值的规则等信息,这对开发者而言非常方便,开发者工具的方便之处在于开发者可以任意修改源代码或者CSS属性值,而且这些修改都是及时显示在网页的渲染结果中的。同时为了查看网页的DOM结构和网页中的各种对象,开发者工具提供了命令形式的控制台,开发者可以一次查看任何DOM中的节点和各种对象甚至可以执行JavaScript语句,下图为查看“window”对象下所有的其他对象:
这里写图片描述
控制台的另外一个功能就是能够显示所有的JavaScript代码执行的日志信息和错误信息,调试JavaScript代码的一种方式是使用日志打印出一些值来帮助确定代码的正确性,常用的是console.log函数,该函数的输出可以在控制台中看到。代码的调试是每个调试器必须支持的功能,在网页中就是对JavaScript代码的调试功能,包括单步、设置或取消断点、调用栈、变量信息等,这些都在“Sources”标签中得到了支持,下图为开发者工具中的JavaScript代码调试器:
这里写图片描述
左侧是当前网页的所有包含JavaScript代码的文件,中间是当前代码和调试的位置,开发者可以单击左侧的函数设置或者取消断点,网页就能够在执行到该行的时候停下来,右侧最上面的是控制执行的各种按钮,包括继续执行、单步执行、进入内部、跳出等,下面则是各种信息,包括查看的变量值、调用栈,当前作用域中的变量值、断点信息等。

性能调试

除了修改网页的DOM结构和CSS样式,以及调试JavaScript代码之外,开发者工具还能够帮助网页开发者改善性能和内存等方面的问题,性能方面包括网络资源的加载性能、网页绘制的性能,甚至包括根据网页加载和渲染过程给出一些优化建议,内存方面主要是网页使用的总内存、JavaScript引擎堆栈内存使用情况等方面的信息。首先看性能方面,这其中网络资源加载的分析和网页绘制支持开发者可能需要解决的一部分问题,开发者工具还提供了一种能够手机整个网页工作过程中的一段时间内各个JavaScript代码消息时间的分布情况,开发者显示选择“Profiles”标签,然后选择“Collect JavaScript CPU profile”按钮,此时开发者工具将收集被调试网页重新加载的整个过冲CPU消耗在各个JavaScript模块的分布,如下:
这里写图片描述
其中“(program)”是住网页的HTML文件所消耗的时间,因为HTML文件中内嵌了很多JavaScript代码,所以它占据了绝大部分时间,而其他的一些JavaScript文件则占用了很少的时间。还有一个非常有用的能力,就是使用开发者工具中的“Audits”功能,下图是“Audits”分析一个网页所生成的结果,它明确了哪些方面的问题可以进行优化,这对改善网站性能来说是一个极大的福音:
这里写图片描述
其次来看关于内存性能方面功能,如上所述,开发者工具不仅提供了网页整体使用内存的情况,也提供了分析JavaScript引擎内部堆上的内存使用情况,下图显示单击开始按钮之后,重新加载网页所收集的内存使用情况,它是按照时间轴来显示的:
这里写图片描述
可以看出,在某个时间点之后内存的使用了突然增大,这是因为在前面一小段的时间内,由于还没有开始重新加载网页,所以没有出现内存答复增长的情况,在单击“开始”按钮和重新加载网页,WebKit在等待网络响应之后才会逐渐增加对内存的需求,当网络下载数据完成时,WebKit使用内存量也在增加,而WebKit完成渲染之后,解释过程中的某些结构不在需要,这些不需要接口被销毁后内存就会降低到一个稳定的过程。

0 0
原创粉丝点击