当前位置: 首页 > 科技观察

说说JS断点的实现

时间:2023-03-20 11:29:55 科技观察

断点的实现很复杂。这里不是要解释V8中JS断点是如何实现的,而是从宏观的角度来谈谈断点的实现。这个问题源于最近与一位同事关于V8Inspector实现的讨论。JS断点功能相信大家都用过。当我们设置了一个断点,然后代码执行到这个断点,线程就会停止,然后当我们点击Next时,就会停在下一个断点处。那么这个暂停是什么意思呢?下图是Node.js执行到断点时的调用栈。我们知道,V8有一个调试协议,客户端通过这个协议与V8通信,完成调试。当V8收到客户端的信息并进行处理时,会调用runMessageLoopOnPause。runMessageLoopOnPause是V8提供的契约式API。执行JS断点时将调用它。runMessageLoopOnPause中的操作由V8的用户实现。在看实现之前,我们先想想怎么处理。首先,当JS断点执行时,线程显然会进入停止状态,那么这个停止状态到底是什么意思,如何实现是最关键的问题。这个事件循环的实现有点类似,就是当线程没有任务要处理的时候,它应该怎么办,轮询显然太不可思议了,另外一个就是基于订阅/发布机制实现sleep/wake,比如Node.js的sleep/wake机制就是基于事件驱动模块实现的。类似的Inspector也是这样实现的,只是细节不同,因为如果情况不同,当Node.js处于事件循环的阻塞状态时,任何注册到事件驱动模块的事件都可以唤醒Node。js,但是断点不一样,线程在断点的时候,除了信号,一般的任务,比如文件IO,网络IO等,是不能也不应该能够唤醒线程的,所以这里是一个简单的睡眠/唤醒方法,即条件变量。当线程阻塞在条件变量上时,线程只能通过条件变量被唤醒。回到断点场景,也就是线程只有在client继续执行的时候才能被唤醒。分析完我们再来看看Node.js的实现。voidrunMessageLoopOnPause(intcontext_group_id)override{waiting_for_resume_=true;runMessageLoop();}voidrunMessageLoop(){if(running_nested_loop_)return;running_nested_loop_=true;env_->RunAndClearInterrupts();}running_nested_loop_=false;}重点是WaitForFrontendEvent。boolMainThreadInterface::WaitForFrontendEvent(){dispatching_messages_=false;//如果任务队列为空则阻塞if(dispatching_message_queue_.empty()){Mutex::ScopedLockscoped_lock(requests_lock_);while(requests_.empty())incoming_message_cond_.Wait(scoped_lock);}returntrue;}我们假设此时队列为空,那么线程会阻塞在条件变量incoming_message_cond_中。我们来看看第二个问题怎么说。这个时候线程就阻塞了,那么当客户端点击执行下一步的时候,Node.js是怎么处理的呢?这里就需要子线程的帮助了,所以在Node.js中,与客户端的数据通信都是在子线程中完成的,没有过多的代码和细节,看一个调用栈就可以了。这是客户端和Node.js子线程成功建立websocket连接后的调用栈,后面的数据通信类似。看看帖子。voidMainThreadInterface::Post(std::unique_ptrrequest){Mutex::ScopedLockscoped_lock(requests_lock_);boolneeds_notify=requests_.empty();requests_.push_back(std::move(request));如果(需要通知){std::weak_ptrweak_self{shared_from_this()};agent_->env()->RequestInterrupt([weak_self](Environment*){if(autoiface=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_的那个。voidMainThreadInterface::DispatchMessages(){dispatching_messages_=true;boolhad_messages=false;做{if(dispatching_message_queue_.empty()){Mutex::ScopedLockscoped_lock(requests_lock_);requests_.swap(dispatching_message_queue_);}had_messages=!dispatching_message_queue_。空的();while(!dispatching_message_queue_.empty()){MessageQueue::value_type任务;std::swap(dispatching_message_queue_.front(),task);dispatching_message_queue_.pop_front();v8::SealHandleScopeseal_handle_scope(agent_->env()->isolate());任务->调用(这个);}}while(had_messages);dispatching_messages_=false;}执行任务时,具体要做的是将客户端传递过来的数据发送给V8Inspector,如果执行到另一个断点,继续分析本文的这个逻辑,否则线程可以继续运行.