JS断点的实现的深入解读

JS 断点的功能相信大家都用过,当我们【men】设置一【yī】个断【duàn】点,然后代【dài】码执行到这个断点时,线程就会停【tíng】住,然后【hòu】我们【men】点击下一【yī】步的时候,又会再下【xià】一个【gè】断点停【tíng】住。那么这个停住到底意【yì】味着什么【me】呢?

断点的实现非常【cháng】复杂,这里并不是说要【yào】长篇大论讲解【jiě】 JS 断点【diǎn】在【zài】 V8 中是如何实现的【de】,而是想从宏观上【shàng】聊【liáo】一下断点的实现【xiàn】。这个【gè】问题【tí】来源于最近和同事讨【tǎo】论【lùn】的关于 V8 Inspector 实现的一些事情。

JS 断点【diǎn】的功【gōng】能相信大【dà】家都用过,当我们设【shè】置【zhì】一个断【duàn】点,然后代码执行到这个断点时,线【xiàn】程就会停住,然后我【wǒ】们点击下一步【bù】的【de】时候,又会再下一个断点停住。那【nà】么【me】这个【gè】停住到底意味着什么呢?下面这个【gè】图是执【zhí】行到一个断点时【shí】 Node.js 的调【diào】用栈【zhàn】。

我们知道 V8 有一个调试协议,客户端是和 V8 通过这个协【xié】议通信完成调试【shì】的,当 V8 收到【dào】客户端【duān】的信息并且处理【lǐ】完之后,就会调【diào】用 runMessageLoopOnPause。runMessageLoopOnPause 是【shì】 V8 提供【gòng】的一个【gè】约定的 API,当执行【háng】到 JS 断【duàn】点时就【jiù】会调用,具【jù】体在 runMessageLoopOnPause 里【lǐ】做什【shí】么事【shì】情由 V8 的使【shǐ】用方【fāng】实现。在看实【shí】现之前【qián】,先来思考一下,应该怎【zěn】么处理【lǐ】。首【shǒu】先执行到了 JS 断点,显然【rán】线程就要进入停住的状态,那么这个停住的状态具体【tǐ】是指什么【me】,应该怎么实【shí】现是【shì】一个最关键的问题。这个事件循环的实【shí】现有点类似【sì】,那【nà】就是当【dāng】线【xiàn】程【chéng】没【méi】有任务处理的时候,它应该在做什【shí】么,轮【lún】询显然太不【bú】可思【sī】议【yì】了,那另一种就是基于订阅 / 发布机制实现睡眠 / 唤醒,比如 Node.js 基于事件驱动模块实现了【le】睡眠 / 唤醒【xǐng】机制。类【lèi】似的 Inspector 也是这样实现,但是具【jù】体细节【jiē】不一样,因为如【rú】果情【qíng】况不【bú】一样,当【dāng】 Node.js 处于事【shì】件【jiàn】循环的【de】阻塞状【zhuàng】态时,任何注册到【dào】事件驱动【dòng】模块的事件都可以唤醒 Node.js,但是断【duàn】点不一样,当【dāng】线程处于【yú】断【duàn】点时,除了信号外,一【yī】般的任务,比如文件 IO、网络【luò】 IO 等【děng】,是不能也不应该能【néng】唤醒线程的,所以【yǐ】这【zhè】里使用的【de】是简单的【de】睡【shuì】眠 / 唤醒方式,那【nà】就是条件变量。当线程阻塞于条件变量时,只有通【tōng】过该条件变【biàn】量才能【néng】唤醒线程【chéng】。回到断点的场景,那就是【shì】客户端【duān】继【jì】续执行时才能唤醒【xǐng】线程。

分析完之后,来看看 Node.js 的实现。

void runMessageLoopOnPause(int context_group_id) override {
  waiting_for_resume_ = true;
  runMessageLoop();
}

void runMessageLoop() {
  if (running_nested_loop_)
    return;

  running_nested_loop_ = true;

  while (shouldRunMessageLoop()) {
    if (interface_) interface_->WaitForFrontendEvent();
    env_->RunAndClearInterrupts();
  }
  running_nested_loop_ = false;
}

重点在 WaitForFrontendEvent。

bool MainThreadInterface::WaitForFrontendEvent() {
  dispatching_messages_ = false;
  // 任务队列为空【kōng】则阻塞【sāi】
  if (dispatching_message_queue_.empty()) {
    Mutex::ScopedLock scoped_lock(requests_lock_);
    while (requests_.empty()) incoming_message_cond_.Wait(scoped_lock);
  }
  return true;
}

我们假【jiǎ】设这时候【hòu】队列【liè】为【wéi】空【kōng】,那么线程就会阻塞在条件变量 incoming_message_cond_ 中。接【jiē】下来看看如聊聊第二个问题。线程这【zhè】时【shí】候【hòu】阻塞了,那么客户端点击执行下一【yī】步的时候,Node.js 还还怎么处理?这里就【jiù】需要【yào】子线程帮忙了,所以 Node.js 中,和客户端的【de】数据通信是在子【zǐ】线程完成的,不讲太【tài】多【duō】代码【mǎ】和细节,直接看一个调用【yòng】栈【zhàn】。

这【zhè】是【shì】客户端和 Node.js 子线程【chéng】建立 websocket 连接成功后的调用栈,后续的数【shù】据【jù】通信也是类似。来看一【yī】下 Post。

void MainThreadInterface::Post(std::unique_ptr request) {
  Mutex::ScopedLock scoped_lock(requests_lock_);
  bool needs_notify = requests_.empty();
  requests_.push_back(std::move(request));
  if (needs_notify) {
    std::weak_ptr 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);
}

这里看到了刚才熟【shú】悉的数【shù】据结构,Post 就【jiù】是往【wǎng】主线【xiàn】程中【zhōng】插入一个任务,然后唤醒主【zhǔ】线程。接着回【huí】到 runMessageLoop。

while (shouldRunMessageLoop()) {
  if (interface_) interface_->WaitForFrontendEvent();
  env_->RunAndClearInterrupts();
}

WaitForFrontendEvent 执行完毕后,接【jiē】着【zhe】执行【háng】 RunAndClearInterrupts,RunAndClearInterrupts 正【zhèng】是处理 RequestInterrupt 插入的任务【wù】的。刚才插入任【rèn】务时我们看到插【chā】入了两个任【rèn】务【wù】 agent_->env()->RequestInterrupt 和 requests_.push_back(std::move(request)) ,RequestInterrupt 插入的任务中会【huì】调用 DispatchMessages,而 DispatchMessages 就是处理 requests_ 中的任务【wù】的。

void MainThreadInterface::DispatchMessages() {
  dispatching_messages_ = true;
  bool had_messages = false;
  do {
    if (dispatching_message_queue_.empty()) {
      Mutex::ScopedLock scoped_lock(requests_lock_);
      requests_.swap(dispatching_message_queue_);
    }
    had_messages = !dispatching_message_queue_.empty();
    while (!dispatching_message_queue_.empty()) {
      MessageQueue::value_type task;
      std::swap(dispatching_message_queue_.front(), task);
      dispatching_message_queue_.pop_front();

      v8::SealHandleScope seal_handle_scope(agent_->env()->isolate());
      task->Call(this);
    }
  } while (had_messages);
  dispatching_messages_ = false;
}

执行任【rèn】务【wù】的【de】时候,具体【tǐ】做的事情【qíng】就是把客户端传过【guò】来的数据投传给【gěi】 V8 Inspector,如【rú】果【guǒ】又执行到了一个断点,那么继【jì】续本文分析到这个逻辑,否则线【xiàn】程就【jiù】可以继【jì】续跑了。

阿里企业邮箱、网易企业邮箱、新网企业邮箱
【标准版】400元/年/5用户/无限容量
【外贸版】500元/年/5用户/无限容量
其【qí】它服务【wù】:网站建设、企【qǐ】业邮【yóu】箱、数字证书ssl、400电话、
联系方式:电话:13714666846 微信同号

声明:本站【zhàn】所有作品【pǐn】(图文【wén】、音视频)均由用户自行【háng】上【shàng】传分【fèn】享【xiǎng】,或互联网相关知识整合【hé】,仅供网友【yǒu】学习交流,若您【nín】的权【quán】利被侵害,请联【lián】系 管理员 删除。

本【běn】文链接:https://www.city96.com/article_32572.html