Node.js Inspector 的实现原理

image

封面图拍摄于 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
2
3
4
5
// node/src/inspector_agent.cc

const std::unique_ptr<V8Inspector>& inspector
session_ = inspector->connect(CONTEXT_GROUP_ID, this, StringView());
session_->dispatchProtocolMessage(message);

v8 给调试客户端发送消息

connect 方法的第二个参数 ChannelImpl 的类型定义可知, v8 的任何响应结果会通过调用传入的 ChannelImpl 实例的 sendResponse 方法来告知到调试客户端

1
2
3
4
5
6
7
8
9
10
// v8/include/v8-inspector.h

class V8_EXPORT Channel {
public:
virtual ~Channel() = default;
virtual void sendResponse(int callId,
std::unique_ptr<StringBuffer> message) = 0;
virtual void sendNotification(std::unique_ptr<StringBuffer> message) = 0;
virtual void flushProtocolNotifications() = 0;
};

例子

以下是 Node.js 官方的示例, 如何借助 inspector api 直接获取到当前进程的 CPU Profiler。Profiler.enableProfiler.startProfiler.stop等调试指令 Node.js 都会通过 dispatchProtocolMessage 发送给 v8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const inspector = require('node:inspector');
const fs = require('node:fs');
const session = new inspector.Session();
session.connect();

session.post('Profiler.enable', () => {
session.post('Profiler.start', () => {
// Invoke business logic under measurement here...

// some time later...
session.post('Profiler.stop', (err, { profile }) => {
// Write profile to disk, upload, etc.
if (!err) {
fs.writeFileSync('./profile.cpuprofile', JSON.stringify(profile));
}
});
});
});

通信过程

上面的例子在当前线程内直接通过 api 即可通知到 v8。如果是通过客户端 Chrome Devtool 去调试 Node.js 程序就是另外的实现

此时 Node.js 是在子线程中起了一个 WebSocket Server, 来处理调试客户端 Chrome Devtool 发送来的调试指令, 然后通知主线程, 最后再发送给 v8

  1. WebSocket Server 接收到请求
1
2
3
4
5
6
// src/inspector_socket_server.cc

void SocketSession::Delegate::OnWsFrame(const std::vector<char>& data) {
server_->MessageReceived(session_id_,
std::string(data.data(), data.size()));
}
  1. 通知主线程
    通过 CrossThreadInspectorSession 类进行实现
    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_;
    };
    AnotherThreadObjectReference 类调用了 Post 方法, 该方法中通过 agent_->env()->RequestInterrupt 方法向 env->native_immediates_interrupts_ 队列 push 了一个数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void 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);
    }
    然后就是经典的 libuv 异步 i/o 通信模型, 在子线程中通过 uv_async_send 标识 task_queues_async_ 有数据可读
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template <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();
    }
    在主线程中的事件循环 epoll 阶段发现 task_queues_async_ 处于兴奋状态, 于是运行事先通过 uv_async_init 注册的回调函数
    1
    2
    3
    4
    5
    6
    uv_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();
    })
  2. 发送给 v8
    此时代码运行到主线程的回调函数 Dispatch, 如下 dispatchMessageFromFrontend 方法最终调用了 dispatchProtocolMessage 发送给 v8
    1
    2
    3
    4
    5
    6
    void SameThreadInspectorSession::Dispatch(
    const v8_inspector::StringView& message) {
    auto client = client_.lock();
    if (client)
    client->dispatchMessageFromFrontend(session_id_, message);
    }
    v8 响应数据从主线程发送给子线程 WebSocket Server 的跨线程通信方式与之类似, 最后 WebSocket Server 把数据发送给调试客户端 Chrome Devtool

image

WebSocket Server 与 Chrome Devtool 的数据请求可以通过 More tools > Protocol monitor 面板进行查看, 需要先在 Settings > Experiments 中 ☑️ 开启 Protocol monitor