如何实现 JS 运行时的 Inspector 能力

网站建设4年前发布
35 0 0

无论什么语言,调试能力都是非常重要的,像 C、C++ 等语言,我们可以使用现成的工具去调试。JS 也不例外,我们可以通过浏览器来实现对 JS 的调试,但是 JS 运行时就不太一样了,因为 JS 运行时通常独立于浏览器运行,所以无法直接使用浏览器提供的能力,这时候就需要自己实现了。当然 JS 运行时不需要完全实现调试的功能,核心的能力都是由 V8 提供,JS 运行时只需要按照 V8 的规范实现一个 Inspector 代理就行。本文介绍以 V8 为基础,实现一个简单的 JS 运行时(严格来说不算,本文只是用它来代替一个描述),并基于这个 JS 运行时实现调试 JS 的能力。,浏览器或者其他工具通常提供了 Inspector 客户端,所以这部分我们不需要重新实现,而 V8 内部本身已经实现了调试具体的实现,我们只需要实现这个调试代理就行,这个代理的主要功能就是透传客户端和服务器之间通信的数据,通信的数据是基于 V8 提供的调试协议,具体可以参考 https://chromedevtools.github.io/devtools-protocol/v8/。下面来看一下具体的实现。,上面的代码大部分是使用 V8 执行 JS 的通用例子。主要关注的地方是创建了一个 V8InspectorClientImpl 对象和新建了一个线程(为什么需要新建线程在之前的文章已经分析过,就不再介绍)。在介绍 V8InspectorClientImpl 之前,先看看子线程的实现。,子线程的逻辑也很简单,就是启动一个 UDP server 来传递调试客户端和服务器之间的数据。理论上来说,我们可以使用 TCP、UDP 甚至 Unix 域来实现数据的通信,因为重点是有一个数据通道完成数据透传到能力,具体的用什么协议去实现这个通道并不重要。但是因为调试客户端是基于 websocket 协议通信的,所以我们需要有一个 websocket 的服务器,为了实现的简单,这个 websocket 服务器我们使用 JS 实现,然后 JS 里再通过 UDP 把数据传递给子线程,子线程再传递给 V8,反过来, V8 的数据也是通过同样的方式传给客户端。架构如下。,20230306011322f5b83bb215a4eee4d35676461bb930b94113e2419,接下来看一下 V8 Inspector 部分的实现。,V8InspectorClientImpl 是一个负责和 V8 Inspector 通信的对象,它主要封装了 channel 和 session 对象,这两个对象是具体和 V8 通信的。介绍完整体和基础的数据结构后,接下来看看细节。刚才介绍中说到当收到客户端数据时,子线程会调用 onMessage 通知 Inspector。,onMessage 首先把数据插入主线程的任务队列,然后通过 RequestInterrupt 注册一个任务,等待 V8 处理这个 RequestInterrupt 任务时,就会通过 dispatchProtocolMessage 处理 requests_ 里面的任务。这里其实是一个非常关键的地方,在不同的 JS 运行时中,这个通知的方式不一样,比如在 Node.js 里,Node.js 除了调用 RequestInterrupt 还会通过线程间通信机制 async 通知主线程,因为这时候主线程可能阻塞在事件驱动模块中,也可能正在执行 JS,所以需要两种方式通知主线程,保证客户端的数据可以被处理。在本文这个简单的 JS 运行时中,目前只会在一个 while 循环中不断执行 JS,所以这里通过 RequestInterrupt 就可以了。V8 会在执行 JS 的时候,处理 RequestInterrupt 的任务,接下来看一下 dispatchProtocolMessage。,dispatchProtocolMessage 很简单,通过 session 把数据传给 V8。V8 处理完后会通过 channel 通知客户端。,V8 处理完数据后或者有新的事件触发时会通过 channel 通知 V8 使用者, V8 使用者接着通过 UDP 把数据透传给子线程,子线程再发送到客户端。这里也是实现的一个关键的地方,根据前面的分析,子线程是不断地阻塞在 recvfrom 等待数据的,所以唯一能唤醒子线程的就是给它发送数据,这里是为了实现上的简单。在 Node.js 里,子线程会跑一个事件循环,子线程除了可以在收到数据时被唤醒,还可以通过线程间通信机制 async 去唤醒。所以子线程收到数据时会根据发送发的端口进行不同的处理,如果是来自客户端,则转发给 V8 Inspector,如果数据是来自  V8 Inspector,则转发给客户端。,至此,大概的流程就介绍完了,但是还有一个非常关键的地方,那就是断点调试。刚才介绍的场景没有断点的场景,比如我们的代码正在正常地运行,然后通过客户端发送获取 CPU Profile 的请求。断点的实现在之前的文章里已经介绍过了,所以就不多介绍了,直接看代码。,当 V8 执行到一个断点时,就会执行 runMessageLoopOnPause 进入停住的状态。这里是通过条件变量来实现停住的状态,也就是阻塞线程。当客户端点击继续执行时,就会在刚才分析的 onMessage 中唤醒线程,接着通过 dispatchProtocolMessage 通知 V8 继续执行。从而实现断点的功能。实现了和 V8 Inspector 通信部分后,再看一下 JS 层。,JS 层主要是符合透传客户端和 V8 Inspector 的数据。最终实现的功能如下。,2023030601115563cb23a87562b099a28616fcda7b2bd68c7782595,通过 Chrome Dev Tools 就可以对我们的 JS 运行时进行调试。

© 版权声明

相关文章