Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node.js Inspector 的实现原理 #56

Open
xiaoxiaojx opened this issue Apr 9, 2023 · 0 comments
Open

Node.js Inspector 的实现原理 #56

xiaoxiaojx opened this issue Apr 9, 2023 · 0 comments
Labels
Node.js Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

Comments

@xiaoxiaojx
Copy link
Owner

xiaoxiaojx commented Apr 9, 2023

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

// 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 方法来告知到调试客户端

// 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

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 接收到请求
// 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 类进行实现
// 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 了一个数据

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_ 有数据可读

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 注册的回调函数

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();
})
  1. 发送给 v8
    此时代码运行到主线程的回调函数 Dispatch, 如下 dispatchMessageFromFrontend 方法最终调用了 dispatchProtocolMessage 发送给 v8
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

@xiaoxiaojx xiaoxiaojx added the Node.js Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. label Apr 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Node.js Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.
Projects
None yet
Development

No branches or pull requests

1 participant