聊聊 JS 断点的实现

开发 前端
JS 断点的功能相信大家都用过,当我们设置一个断点,然后代码执行到这个断点时,线程就会停住,然后我们点击下一步的时候,又会再下一个断点停住。那么这个停住到底意味着什么呢?

断点的实现非常复杂,这里并不是说要长篇大论讲解 JS 断点在 V8 中是如何实现的,而是想从宏观上聊一下断点的实现。这个问题来源于最近和同事讨论的关于 V8 Inspector 实现的一些事情。

JS 断点的功能相信大家都用过,当我们设置一个断点,然后代码执行到这个断点时,线程就会停住,然后我们点击下一步的时候,又会再下一个断点停住。那么这个停住到底意味着什么呢?下面这个图是执行到一个断点时 Node.js 的调用栈。

图片

我们知道 V8 有一个调试协议,客户端是和 V8 通过这个协议通信完成调试的,当 V8 收到客户端的信息并且处理完之后,就会调用 runMessageLoopOnPause。runMessageLoopOnPause 是 V8 提供的一个约定的 API,当执行到 JS 断点时就会调用,具体在 runMessageLoopOnPause 里做什么事情由 V8 的使用方实现。在看实现之前,先来思考一下,应该怎么处理。首先执行到了 JS 断点,显然线程就要进入停住的状态,那么这个停住的状态具体是指什么,应该怎么实现是一个最关键的问题。这个事件循环的实现有点类似,那就是当线程没有任务处理的时候,它应该在做什么,轮询显然太不可思议了,那另一种就是基于订阅 / 发布机制实现睡眠 / 唤醒,比如 Node.js 基于事件驱动模块实现了睡眠 / 唤醒机制。类似的 Inspector 也是这样实现,但是具体细节不一样,因为如果情况不一样,当 Node.js 处于事件循环的阻塞状态时,任何注册到事件驱动模块的事件都可以唤醒 Node.js,但是断点不一样,当线程处于断点时,除了信号外,一般的任务,比如文件 IO、网络 IO 等,是不能也不应该能唤醒线程的,所以这里使用的是简单的睡眠 / 唤醒方式,那就是条件变量。当线程阻塞于条件变量时,只有通过该条件变量才能唤醒线程。回到断点的场景,那就是客户端继续执行时才能唤醒线程。

分析完之后,来看看 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;
// 任务队列为空则阻塞
if (dispatching_message_queue_.empty()) {
Mutex::ScopedLock scoped_lock(requests_lock_);
while (requests_.empty()) incoming_message_cond_.Wait(scoped_lock);
}
return true;
}

我们假设这时候队列为空,那么线程就会阻塞在条件变量 incoming_message_cond_ 中。接下来看看如聊聊第二个问题。线程这时候阻塞了,那么客户端点击执行下一步的时候,Node.js 还还怎么处理?这里就需要子线程帮忙了,所以 Node.js 中,和客户端的数据通信是在子线程完成的,不讲太多代码和细节,直接看一个调用栈。

图片

这是客户端和 Node.js 子线程建立 websocket 连接成功后的调用栈,后续的数据通信也是类似。来看一下 Post。

void MainThreadInterface::Post(std::unique_ptr<Request> request) {
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);
}

这里看到了刚才熟悉的数据结构,Post 就是往主线程中插入一个任务,然后唤醒主线程。接着回到 runMessageLoop。

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

WaitForFrontendEvent 执行完毕后,接着执行 RunAndClearInterrupts,RunAndClearInterrupts 正是处理 RequestInterrupt 插入的任务的。刚才插入任务时我们看到插入了两个任务 agent_->env()->RequestInterrupt 和 requests_.push_back(std::move(request)) ,RequestInterrupt 插入的任务中会调用 DispatchMessages,而 DispatchMessages 就是处理 requests_ 中的任务的。

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;
}

执行任务的时候,具体做的事情就是把客户端传过来的数据投传给 V8 Inspector,如果又执行到了一个断点,那么继续本文分析到这个逻辑,否则线程就可以继续跑了。

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2022-10-08 00:07:00

JSV8调用栈

2017-04-19 10:25:01

JS断点调试

2021-09-26 05:06:04

Node.js模块机制

2021-11-06 18:40:27

js底层模块

2017-08-08 08:45:44

前端文件断点续传

2021-07-14 14:05:24

Fragment项目结构

2022-02-18 08:26:12

TopK数组面试题

2022-03-26 16:51:27

Node.jstrace架构

2021-10-04 19:49:23

HTTP模块No.js

2022-05-27 07:01:48

JSGIF总帧数

2022-05-13 09:05:37

JSObject无序

2017-03-06 16:51:52

Java泛型实现

2023-01-26 00:59:39

B-Treegolang度量衡

2022-08-05 08:27:05

分布式系统线程并发

2021-12-15 23:10:34

JS Debugger 前端开发

2023-05-26 08:24:17

短信渠道模型

2024-04-07 08:23:01

JS隔离JavaScript

2022-09-19 08:32:46

AOP系统机器人

2022-03-30 08:36:32

Node.jsPRHTTP

2022-10-18 08:28:38

运营活动实现逻辑整体协作
点赞
收藏

51CTO技术栈公众号