封面图拍摄于 2023-04-08 闵行文化公园
Node.js 内置的 Inspector 模块可以轻易的让开发者去调试一个 Node.js 程序, 常见的场景比如断点调试、查看内存占用与 CPU Profiler 等。下面简单记录一下它的实现原理
核心实现
Node.js 源码对这块封装的比较复杂, 弯弯绕绕的一下子很难看明白。拨开层层云雾其实 Node.js 只是在调试客户端比如 Chrome Devtool 与 v8 之间作了一层代理
调试客户端向 v8 发送消息
Node.js 通过调用 V8Inspector 的 connect 方法即可获得一个与 v8 通信的会话 V8InspectorSession, 把需要调试的指令通过 dispatchProtocolMessage 方法即可告知到 v8
1 | // node/src/inspector_agent.cc |
v8 给调试客户端发送消息
connect 方法的第二个参数 ChannelImpl 的类型定义可知, v8 的任何响应结果会通过调用传入的 ChannelImpl 实例的 sendResponse 方法来告知到调试客户端
1 | // v8/include/v8-inspector.h |
例子
以下是 Node.js 官方的示例, 如何借助 inspector api 直接获取到当前进程的 CPU Profiler。Profiler.enable
、Profiler.start
、Profiler.stop
等调试指令 Node.js 都会通过 dispatchProtocolMessage 发送给 v8
1 | const inspector = require('node:inspector'); |
通信过程
上面的例子在当前线程内直接通过 api 即可通知到 v8。如果是通过客户端 Chrome Devtool 去调试 Node.js 程序就是另外的实现
此时 Node.js 是在子线程中起了一个 WebSocket Server, 来处理调试客户端 Chrome Devtool 发送来的调试指令, 然后通知主线程, 最后再发送给 v8
- WebSocket Server 接收到请求
1 | // src/inspector_socket_server.cc |
- 通知主线程
通过 CrossThreadInspectorSession 类进行实现AnotherThreadObjectReference 类调用了 Post 方法, 该方法中通过 agent_->env()->RequestInterrupt 方法向 env->native_immediates_interrupts_ 队列 push 了一个数据1
2
3
4
5
6
7
8
9
10
11
12// src/inspector/main_thread_interface.cc
class CrossThreadInspectorSession : public InspectorSession {
void Dispatch(const StringView& message) override {
state_.Call(&MainThreadSessionState::Dispatch,
StringBuffer::create(message));
}
private:
AnotherThreadObjectReference<MainThreadSessionState> state_;
};然后就是经典的 libuv 异步 i/o 通信模型, 在子线程中通过 uv_async_send 标识 task_queues_async_ 有数据可读1
2
3
4
5
6
7
8
9
10
11
12
13void MainThreadInterface::Post(std::unique_ptr<Request> request) {
CHECK_NOT_NULL(agent_);
Mutex::ScopedLock scoped_lock(requests_lock_);
bool needs_notify = requests_.empty();
requests_.push_back(std::move(request));
if (needs_notify) {
std::weak_ptr<MainThreadInterface> weak_self {shared_from_this()};
agent_->env()->RequestInterrupt([weak_self](Environment*) {
if (auto iface = weak_self.lock()) iface->DispatchMessages();
});
}
incoming_message_cond_.Broadcast(scoped_lock);
}在主线程中的事件循环 epoll 阶段发现 task_queues_async_ 处于兴奋状态, 于是运行事先通过 uv_async_init 注册的回调函数1
2
3
4
5
6
7
8
9
10
11
12template <typename Fn>
void Environment::RequestInterrupt(Fn&& cb) {
auto callback = native_immediates_interrupts_.CreateCallback(
std::move(cb), CallbackFlags::kRefed);
{
Mutex::ScopedLock lock(native_immediates_threadsafe_mutex_);
native_immediates_interrupts_.Push(std::move(callback));
if (task_queues_async_initialized_)
uv_async_send(&task_queues_async_);
}
RequestInterruptFromV8();
}1
2
3
4
5
6uv_async_init(event_loop(), &task_queues_async_, [](uv_async_t* async) {
Environment* env = ContainerOf(&Environment::task_queues_async_, async);
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
env->RunAndClearNativeImmediates();
}) - 发送给 v8
此时代码运行到主线程的回调函数 Dispatch, 如下 dispatchMessageFromFrontend 方法最终调用了 dispatchProtocolMessage 发送给 v8v8 响应数据从主线程发送给子线程 WebSocket Server 的跨线程通信方式与之类似, 最后 WebSocket Server 把数据发送给调试客户端 Chrome Devtool1
2
3
4
5
6void SameThreadInspectorSession::Dispatch(
const v8_inspector::StringView& message) {
auto client = client_.lock();
if (client)
client->dispatchMessageFromFrontend(session_id_, message);
}
WebSocket Server 与 Chrome Devtool 的数据请求可以通过 More tools > Protocol monitor 面板进行查看, 需要先在 Settings > Experiments 中 ☑️ 开启 Protocol monitor