深入V8 Inspector源码,了解V8 Inspector指令处理机制。
文章较长,可以直接看镇楼图和结论。
问题背景
笔者最近在研究V8调试工具时,发现HeapProfiler
的Alllocation Sampling
功能异常,返回数据为空。打印log可以判断,前端启动Allocation Sampling
之后发送了HeapProfiler.startSampling
命令,内容如下图所示:
{"id":24,"method":"HeapProfiler.startSampling","params":{"samplingInterval":16384}}
停止Sampling
之后发送了HeapProfiler.stopSampling
命令,内容如下:
{"id":25,"method":"HeapProfiler.stopSampling","params":{}}
V8 Inspector返回内容如下:
{"id":25,"result":{"profile":{"head":{"callFrame":{"functionName":"(root)","scriptId":"0","url":"","lineNumber":-1,"columnNumber":-1},"selfSize":0,"id":1,"children":[]},"samples":[]}}}
其中"samples":[]
,为什么HeapProfiler.stopSampling
返回的结果是空的呢?
为了解决这个问题,需要先搞清楚V8 Inspector是如何介绍指令以及如何处理的。
V8 Inspector指令处理机制
由于源码分析比较枯燥且冗长,先放一张图镇楼吧。
考虑到SDK是通过V8InspectorSession
发送指令信息给V8 Inspector的,所以我们先从V8InspectorSession
开始分析。
V8InspectorSession
class V8_EXPORT V8InspectorSession {
// 判断V8是否支持当前指令method
static bool canDispatchMethod(StringView method);
// 分发一个消息到指定的指令域
virtual void dispatchProtocolMessage(StringView message) = 0;
};
V8InspectorSession
是一个基类,主要定义了和V8 Inspector调试功能相关的方法。其中和指令处理相关的有canDispatchMethod
、dispatchProtocolMessage
,canDispatchMethod
是一个静态方法,决定了V8当前支持的所有指令域,dispatchProtocolMessage
则由子类实现。
我们先看下canDispatchMethod
的实现。
// static
bool V8InspectorSession::canDispatchMethod(StringView method) {
return stringViewStartsWith(method,
// Runtime指令域
protocol::Runtime::Metainfo::commandPrefix) ||
stringViewStartsWith(method,
// Debugger指令域
protocol::Debugger::Metainfo::commandPrefix) ||
stringViewStartsWith(method,
// Profiler指令域
protocol::Profiler::Metainfo::commandPrefix) ||
stringViewStartsWith(method,
// HeapProfiler指令域
protocol::HeapProfiler::Metainfo::commandPrefix) ||
stringViewStartsWith(method,
// Console指令域
protocol::Console::Metainfo::commandPrefix) ||
stringViewStartsWith(method,
// Schema指令域
protocol::Schema::Metainfo::commandPrefix);
}
canDispatchMethod
主要是调用了stringViewStartsWith
来method是否具备指定的指令域前缀,例如:HeapProfiler
指令域的前缀为HeapProfiler.
。stringViewStartsWith
实现如下,比较简单,这里不再赘述。
bool stringViewStartsWith(const StringView& string, const char* prefix) {
if (!string.length()) return !(*prefix);
if (string.is8Bit()) {
for (size_t i = 0, j = 0; prefix[j] && i < string.length(); ++i, ++j) {
if (string.characters8()[i] != prefix[j]) return false;
}
} else {
for (size_t i = 0, j = 0; prefix[j] && i < string.length(); ++i, ++j) {
if (string.characters16()[i] != prefix[j]) return false;
}
}
return true;
}
V8 Inspector协议由指令域和指令名构成,格式为:Domain.CommandName。例如,
HeapProfiler.stopSampling
中HeapProfiler.
就是该指令的指令域,stopSampling
就是具体的指令名。
V8InspectorSessionImpl
接着看下V8InspectorSession
的实现类V8InspectorSessionImpl
,其主要定义如下:
class V8InspectorSessionImpl : public V8InspectorSession,
public protocol::FrontendChannel {
public:
// 静态方法,调用私有构造函数,用于创建V8InspectorSessionImpl实例
static std::unique_ptr<V8InspectorSessionImpl> create(V8InspectorImpl*,
int contextGroupId,
int sessionId,
V8Inspector::Channel*,
StringView state);
// 接收对端指令消息,并进行分发
void dispatchProtocolMessage(StringView message) override;
private:
// 回复消息
void SendProtocolResponse(
int callId, std::unique_ptr<protocol::Serializable> message) override;
// 数据推送
void SendProtocolNotification(
std::unique_ptr<protocol::Serializable> message) override;
// 会话ID
int m_sessionId;
// 关联的V8Inspector实例
V8InspectorImpl* m_inspector;
// 关联的Channel,Channel表示会话的对端
V8Inspector::Channel* m_channel;
// 指令域分发器,用于分发特定指定到对应的指令域
protocol::UberDispatcher m_dispatcher;
// 某个具体的指令域的代理对象,
// 包括Runtime、Debugger、HeapProfiler、Profiler、Console和Schema
std::unique_ptr<V8RuntimeAgentImpl> m_runtimeAgent;
std::unique_ptr<V8DebuggerAgentImpl> m_debuggerAgent;
std::unique_ptr<V8HeapProfilerAgentImpl> m_heapProfilerAgent;
std::unique_ptr<V8ProfilerAgentImpl> m_profilerAgent;
std::unique_ptr<V8ConsoleAgentImpl> m_consoleAgent;
std::unique_ptr<V8SchemaAgentImpl> m_schemaAgent;
};
看下核心方法的具体实现。
-
创建
V8InspectorSessionImpl
实例关键代码如下:
V8InspectorSessionImpl::V8InspectorSessionImpl(V8InspectorImpl* inspector, int contextGroupId, int sessionId, V8Inspector::Channel* channel, StringView savedState) : m_contextGroupId(contextGroupId), m_sessionId(sessionId), m_inspector(inspector), m_channel(channel), m_customObjectFormatterEnabled(false), // V8InspectorSessionImpl作为FrontendChannel实例,传入UberDispatcher m_dispatcher(this), m_state(ParseState(savedState)), m_runtimeAgent(nullptr), m_debuggerAgent(nullptr), m_heapProfilerAgent(nullptr), m_profilerAgent(nullptr), m_consoleAgent(nullptr), m_schemaAgent(nullptr) { // 重置Runtime指令域代理对象并注册 m_runtimeAgent.reset(new V8RuntimeAgentImpl( this, this, agentState(protocol::Runtime::Metainfo::domainName))); protocol::Runtime::Dispatcher::wire(&m_dispatcher, m_runtimeAgent.get()); // 重置Debugger指令域代理对象并注册 m_debuggerAgent.reset(new V8DebuggerAgentImpl( this, this, agentState(protocol::Debugger::Metainfo::domainName))); protocol::Debugger::Dispatcher::wire(&m_dispatcher, m_debuggerAgent.get()); // 重置Profiler指令域代理对象并注册 m_profilerAgent.reset(new V8ProfilerAgentImpl( this, this, agentState(protocol::Profiler::Metainfo::domainName))); protocol::Profiler::Dispatcher::wire(&m_dispatcher, m_profilerAgent.get()); // 重置HeapProfiler指令域代理对象并注册 m_heapProfilerAgent.reset(new V8HeapProfilerAgentImpl( this, this, agentState(protocol::HeapProfiler::Metainfo::domainName))); protocol::HeapProfiler::Dispatcher::wire(&m_dispatcher,m_heapProfilerAgent.get()); // 重置Console指令域代理对象并注册 m_consoleAgent.reset(new V8ConsoleAgentImpl( this, this, agentState(protocol::Console::Metainfo::domainName))); protocol::Console::Dispatcher::wire(&m_dispatcher, m_consoleAgent.get()); // 重置Schema指令域代理对象并注册 m_schemaAgent.reset(new V8SchemaAgentImpl( this, this, agentState(protocol::Schema::Metainfo::domainName))); protocol::Schema::Dispatcher::wire(&m_dispatcher, m_schemaAgent.get()); }
V8InspectorSessionImpl创建过程主要是对所有指令域代理对象重置,然后通过各指令域内部实现的静态方法
wire
连接并注册到指令域分发器中。以HeapProfiler::Dispatcher::wire()
为例,其主要逻辑如下:// static void Dispatcher::wire(UberDispatcher* uber, Backend* backend) { auto dispatcher = std::make_unique<DomainDispatcherImpl>(uber->channel(), backend); uber->WireBackend(v8_crdtp::SpanFrom("HeapProfiler"), SortedRedirects(), std::move(dispatcher)); }
wire
方法只干了一件事,就是把字符串"HeapProfiler"
和DomainDispatcherImpl
对象关联起来。UberDispatcher
和DomainDispatcherImpl
具体怎么关联的,我们稍后再做分析。 -
接收并分发指令消息
dispatchProtocolMessage
实现了接收对端指令消息,并进行分发的功能,其主要逻辑如下:void V8InspectorSessionImpl::dispatchProtocolMessage(StringView message) { using v8_crdtp::span; using v8_crdtp::SpanFrom; span<uint8_t> cbor; std::vector<uint8_t> converted_cbor; // 注释1 start if (IsCBORMessage(message)) { use_binary_protocol_ = true; m_state->setBoolean("use_binary_protocol", true); cbor = span<uint8_t>(message.characters8(), message.length()); } else { // We're ignoring the return value of the conversion function // intentionally. It means the |parsed_message| below will be nullptr. auto status = ConvertToCBOR(message, &converted_cbor); if (!status.ok()) { m_channel->sendNotification( serializeForFrontend(v8_crdtp::CreateErrorNotification( v8_crdtp::DispatchResponse::ParseError(status.ToASCIIString())))); return; } cbor = SpanFrom(converted_cbor); } // 注释1 end // 注释2 start v8_crdtp::Dispatchable dispatchable(cbor); // 注释2 end // 错误处理 // ... // 注释3 start m_dispatcher.Dispatch(dispatchable).Run(); // 注释3 end }
首先,
注释1
把输入的指令消息转换成CBOR
格式。CBOR
又叫简明二进制对象,是一种不需要版本协商的数据交换格式,详细信息可以参考cbor.io/。然后,
注释2
对CBOR
格式数据进行解析,并以Dispatchable
对象的形式在指令分发过程中传递。最后,
注释3
通过指令域分发器UberDispatcher
进行第一级分发。一个具体指令的分发可以分成两级,第一级是指令域的分发,第二级则是具体指令的分发。m_dispatcher.Dispatch(dispatchable)
完成两级分发后,拿到具体的指令处理接口,在Run()
中调用该接口,完成指令的执行。 -
指令回复/数据推送
指令消息的回复和数据推送,是向会话的对端发送数据。FrontendChannel是一个包含向对端发送数据能力的基类,其方法的实现在子类
V8InspectorSessionImpl
中。void V8InspectorSessionImpl::SendProtocolResponse( int callId, std::unique_ptr<protocol::Serializable> message) { m_channel->sendResponse(callId, serializeForFrontend(std::move(message))); } void V8InspectorSessionImpl::SendProtocolNotification( std::unique_ptr<protocol::Serializable> message) { m_channel->sendNotification(serializeForFrontend(std::move(message))); }
SendProtocolResponse
/SendProtocolNotification
调用了V8Inspector::Channel
中对应的接口,将数据发送给具体的V8Inspector::Channel
实例对象。
UberDispatcher
UberDispatcher
是V8指令域分发器。
class UberDispatcher {
public:
// 表示分发结果的对象
class DispatchResult {};
// 分发处理函数
DispatchResult Dispatch(const Dispatchable& dispatchable) const;
// 注册指令和处理器
void WireBackend(span<uint8_t> domain,
const std::vector<std::pair<span<uint8_t>, span<uint8_t>>>&,
std::unique_ptr<DomainDispatcher> dispatcher);
private:
// 查找指令对应的处理器,Dispatch中使用
DomainDispatcher* findDispatcher(span<uint8_t> method);
// 关联的channel
FrontendChannel* const frontend_channel_;
// 重定向列表,可以实现前者到后者的重定向
// 格式为("Domain1.method1","Domain2.method2"),按前者排序
std::vector<std::pair<span<uint8_t>, span<uint8_t>>> redirects_;
// 命令处理器队列,根据指令域域名映射到对应的处理器,按指令域域名排序
std::vector<std::pair<span<uint8_t>, std::unique_ptr<DomainDispatcher>>>
dispatchers_;
};
UberDispatcher
主要是实现了指令处理器的注册和指令的分发功能。
-
注册指令处理器
void UberDispatcher::WireBackend( span<uint8_t> domain, const std::vector<std::pair<span<uint8_t>, span<uint8_t>>>& sorted_redirects, std::unique_ptr<DomainDispatcher> dispatcher) { // 注释1 start auto it = redirects_.insert(redirects_.end(), sorted_redirects.begin(), sorted_redirects.end()); std::inplace_merge(redirects_.begin(), it, redirects_.end(), FirstLessThan<span<uint8_t>>()); // 注释1 end // 注释2 start auto jt = dispatchers_.insert(dispatchers_.end(), std::make_pair(domain, std::move(dispatcher))); std::inplace_merge(dispatchers_.begin(), jt, dispatchers_.end(), FirstLessThan<std::unique_ptr<DomainDispatcher>>()); // 注释2 end }
WireBackend
中做了两件事情。首先处理重定向列表,将参数sorted_redirects中内容合并到已有重定向列表中,并重新排序。
然后注册指令处理器,将指令域域名与对应处理器的键值对合并到已有处理器列表中,并重新排序。
-
分发指令
UberDispatcher::DispatchResult UberDispatcher::Dispatch( const Dispatchable& dispatchable) const { // 1. 尝试找到第一个重定向的指令,没找到则返回default_value,即dipatchable.Method() span<uint8_t> method = FindByFirst(redirects_, dispatchable.Method(), /*default_value=*/dispatchable.Method()); // 2. 找 “.” 的位置,指令格式为 Domain.Command size_t dot_idx = DotIdx(method); if (dot_idx != kNotFound) { span<uint8_t> domain = method.subspan(0, dot_idx); span<uint8_t> command = method.subspan(dot_idx + 1); // 3. 根据指令域 domain 查找第一个处理器,此为第一级分发 DomainDispatcher* dispatcher = FindByFirst(dispatchers_, domain); if (dispatcher) { // 4. 分发具体的指令到对应的处理器,并找到指令对应的接口 // 此为第二级分发,具体分发过程在各个指令域内部实现 std::function<void(const Dispatchable&)> dispatched = dispatcher->Dispatch(command); if (dispatched) { return DispatchResult( true, [dispatchable, dispatched = std::move(dispatched)]() { // 5. 执行具体的指令。此处lambda表达式在DispatchResult的Run方法调用时执行 dispatched(dispatchable); }); } } } // 6. 错误返回 return DispatchResult(false, [this, dispatchable]() { frontend_channel_->SendProtocolResponse( dispatchable.CallId(), CreateErrorResponse(dispatchable.CallId(), DispatchResponse::MethodNotFound( "'" + std::string(dispatchable.Method().begin(), dispatchable.Method().end()) + "' wasn't found"))); }); }
UberDispatcher
的分发过程类似一种路由,具体过程详见注释,这里不再赘述。二级分发的过程在具体的指令域内部实现,下文将已HeapProfiler
为例,继续分析。
DomainDispatcher
在分析HeapProfiler
指令域中二级分发之前,我们先看下DomainDispatcher
。DomainDispatcher
是二级指令分发器,定义了二级分发的接口,是各指令域内二级分发的共同的父类。
class DomainDispatcher {
// 二级分发逻辑,子类实现
virtual std::function<void(const Dispatchable&)> Dispatch(span<uint8_t> command_name) = 0;
// 处理完后响应,通过frontend_channel_实现
void sendResponse(int call_id, const DispatchResponse&, std::unique_ptr<Serializable> result = nullptr);
private:
// 关联的 channel
FrontendChannel* frontend_channel_;
};
DomainDispatcher中主要定义了二级分发和指令响应的接口,不同的指令域有不同的二级分发逻辑,但是响应逻辑是相同的,所以基类实现了sendResponse
逻辑。
void DomainDispatcher::sendResponse(int call_id,
const DispatchResponse& response,
std::unique_ptr<Serializable> result) {
if (!frontend_channel_)
return;
std::unique_ptr<Serializable> serializable;
if (response.IsError()) {
serializable = CreateErrorResponse(call_id, response);
} else {
serializable = CreateResponse(call_id, std::move(result));
}
frontend_channel_->SendProtocolResponse(call_id, std::move(serializable));
}
接下来看子类中Dispatch
的实现,以HeapProfiler为例。
DomainDispatcherImpl
// --------------------- Dispatcher.
class DomainDispatcherImpl : public protocol::DomainDispatcher {
public:
DomainDispatcherImpl(FrontendChannel* frontendChannel, Backend* backend)
: DomainDispatcher(frontendChannel)
, m_backend(backend) {}
~DomainDispatcherImpl() override { }
// 对 DomainDispatcherImpl 中指令处理函数的抽象
using CallHandler = void (DomainDispatcherImpl::*)(const v8_crdtp::Dispatchable& dispatchable);
// 实现具体的二级分发逻辑
std::function<void(const v8_crdtp::Dispatchable&)> Dispatch(v8_crdtp::span<uint8_t> command_name) override;
// 以下是所有的当前指令域支持的指令处理函数
void addInspectedHeapObject(const v8_crdtp::Dispatchable& dispatchable);
void collectGarbage(const v8_crdtp::Dispatchable& dispatchable);
void disable(const v8_crdtp::Dispatchable& dispatchable);
void enable(const v8_crdtp::Dispatchable& dispatchable);
void getHeapObjectId(const v8_crdtp::Dispatchable& dispatchable);
void getObjectByHeapObjectId(const v8_crdtp::Dispatchable& dispatchable);
void getSamplingProfile(const v8_crdtp::Dispatchable& dispatchable);
void startSampling(const v8_crdtp::Dispatchable& dispatchable);
void startTrackingHeapObjects(const v8_crdtp::Dispatchable& dispatchable);
void stopSampling(const v8_crdtp::Dispatchable& dispatchable);
void stopTrackingHeapObjects(const v8_crdtp::Dispatchable& dispatchable);
void takeHeapSnapshot(const v8_crdtp::Dispatchable& dispatchable);
protected:
Backend* m_backend;
};
DomainDispatcherImpl
中核心方法只有一个,就是父类中定义的 Dispatch
接口,其他方法都是当前指令域HeapProfiler
支持的指令所对应的处理函数。
下面我们看下具体的二级分发逻辑。
std::function<void(const v8_crdtp::Dispatchable&)> DomainDispatcherImpl::Dispatch(v8_crdtp::span<uint8_t> command_name) {
// 注释1,根据指令名查找对应的处理函数
CallHandler handler = CommandByName(command_name);
if (!handler) return nullptr;
// 注释2,返回包含调用指令处理函数的lambda函数
return [this, handler](const v8_crdtp::Dispatchable& dispatchable) {
// 注释3,调用指令处理函数
(this->*handler)(dispatchable);
};
}
二级分发的逻辑也比较简单,首先根据指令名找到对应的处理函数指针handler
,然后返回一个捕获了该函数指针的lambda函数,lambda函数中则调用了该处理函数。具体调用处理函数的时机,则在上面讲到的DispatchResult.Run()
方法中。
接着,我们看下指令函数的查找逻辑CommandByName
。
DomainDispatcherImpl::CallHandler CommandByName(v8_crdtp::span<uint8_t> command_name) {
static auto* commands = [](){
auto* commands = new std::vector<std::pair<v8_crdtp::span<uint8_t>, DomainDispatcherImpl::CallHandler>>{
// 太多,不一一列举
// ...
{
v8_crdtp::SpanFrom("startSampling"),
&DomainDispatcherImpl::startSampling
},
{
v8_crdtp::SpanFrom("stopSampling"),
&DomainDispatcherImpl::stopSampling
},
};
return commands;
}();
return v8_crdtp::FindByFirst<DomainDispatcherImpl::CallHandler>(*commands, command_name, nullptr);
}
CommandByName
内部定义了一个静态变量commands
,实现懒加载的效果,其中存储了指令名到指令处理函数的键值对。
我们以startSampling
为例,再看一下指令处理函数的逻辑。
void DomainDispatcherImpl::startSampling(const v8_crdtp::Dispatchable& dispatchable)
{
// Prepare input parameters.
auto deserializer = v8_crdtp::DeferredMessage::FromSpan(dispatchable.Params())->MakeDeserializer();
startSamplingParams params;
startSamplingParams::Deserialize(&deserializer, ¶ms);
if (MaybeReportInvalidParams(dispatchable, deserializer))
return;
std::unique_ptr<DomainDispatcher::WeakPtr> weak = weakPtr();
// 调用 m_backend 的startSampling方法
DispatchResponse response = m_backend->startSampling(std::move(params.samplingInterval));
if (response.IsFallThrough()) {
channel()->FallThrough(dispatchable.CallId(), v8_crdtp::SpanFrom("HeapProfiler.startSampling"), dispatchable.Serialized());
return;
}
if (weak->get())
weak->get()->sendResponse(dispatchable.CallId(), response);
return;
}
startSampling
处理函数中只做了参数的转换和处理,然后转发到m_backend
中执行具体的指令。
由此可见DomainDispatcherImpl
只是一层比较薄的封装,具体的指令处理则转发到m_backend指向的对象。有上文分析到的指令域的连接与注册可以知道,这里m_backend
指向的对象就是V8HeapProfilerAgentImpl
实例,HeapProfiler
指令域的代理对象。
那么代理对象是如何代理指令域的呢?接下来,我们继续往下分析V8HeapProfilerAgentImpl
的实现。
V8HeapProfilerAgentImpl
V8HeapProfilerAgentImpl
主要定义如下。
class V8HeapProfilerAgentImpl : public protocol::HeapProfiler::Backend {
public:
V8HeapProfilerAgentImpl(V8InspectorSessionImpl*, protocol::FrontendChannel*,
protocol::DictionaryValue* state);
Response startSampling(Maybe<double> samplingInterval) override;
Response stopSampling(std::unique_ptr<protocol::HeapProfiler::SamplingHeapProfile>*) override;
// ...
private:
V8InspectorSessionImpl* m_session;
v8::Isolate* m_isolate;
// protocol::HeapProfiler::Frontend 定义了支持哪些事件
protocol::HeapProfiler::Frontend m_frontend;
protocol::DictionaryValue* m_state;
};
V8HeapProfilerAgentImpl
继承了HeapProfiler::Backend
,HeapProfiler::Backend
中则定义了HeapProfiler
对外暴露的所有指令。
class Backend {
public:
virtual ~Backend() { }
virtual DispatchResponse addInspectedHeapObject(const String& in_heapObjectId) = 0;
virtual void collectGarbage(std::unique_ptr<CollectGarbageCallback> callback) = 0;
virtual DispatchResponse disable() = 0;
virtual DispatchResponse enable() = 0;
virtual DispatchResponse getHeapObjectId(const String& in_objectId, String* out_heapSnapshotObjectId) = 0;
virtual DispatchResponse getObjectByHeapObjectId(const String& in_objectId, Maybe<String> in_objectGroup, std::unique_ptr<protocol::Runtime::RemoteObject>* out_result) = 0;
virtual DispatchResponse getSamplingProfile(std::unique_ptr<protocol::HeapProfiler::SamplingHeapProfile>* out_profile) = 0;
virtual DispatchResponse startSampling(Maybe<double> in_samplingInterval) = 0;
virtual DispatchResponse startTrackingHeapObjects(Maybe<bool> in_trackAllocations) = 0;
virtual DispatchResponse stopSampling(std::unique_ptr<protocol::HeapProfiler::SamplingHeapProfile>* out_profile) = 0;
virtual DispatchResponse stopTrackingHeapObjects(Maybe<bool> in_reportProgress, Maybe<bool> in_treatGlobalObjectsAsRoots, Maybe<bool> in_captureNumericValue) = 0;
virtual DispatchResponse takeHeapSnapshot(Maybe<bool> in_reportProgress, Maybe<bool> in_treatGlobalObjectsAsRoots, Maybe<bool> in_captureNumericValue) = 0;
};
仍以startSampling
为例,分析下V8HeapProfilerAgentImpl
中具体如何代理startSampling
指令的。
Response V8HeapProfilerAgentImpl::startSampling(Maybe<double> samplingInterval) {
// 注释1 start
v8::HeapProfiler* profiler = m_isolate->GetHeapProfiler();
if (!profiler) return Response::ServerError("Cannot access v8 heap profiler");
// 注释1 end
// 注释2 start
const unsigned defaultSamplingInterval = 1 << 15;
double samplingIntervalValue = samplingInterval.fromMaybe(defaultSamplingInterval);
if (samplingIntervalValue <= 0.0) {
return Response::ServerError("Invalid sampling interval");
}
// 注释2 end
// 注释3 start
m_state->setDouble(HeapProfilerAgentState::samplingHeapProfilerInterval, samplingIntervalValue);
m_state->setBoolean(HeapProfilerAgentState::samplingHeapProfilerEnabled, true);
// 注释3 end
// 注释4 start
profiler->StartSamplingHeapProfiler(
static_cast<uint64_t>(samplingIntervalValue), 128,
v8::HeapProfiler::kSamplingForceGC);
return Response::Success();
// 注释4 end
}
注释1
:通过v8::Isolate
指针获取HeapProfiler
对象,如果HeapProfiler
对象不存在,则返回错误。
注释2
:参数处理与校验。
注释3
:设置当前状态参数。
注释4
:调用HeapProfiler
中StartSamplingHeapProfiler
方法,执行指令,并返回成功。
结论
以上是HeapProfiler相关指令的分发处理过程,其他指令类似,不再过多分析。至此,V8 Inspector的指令处理机制就分析完了,结合前文的镇楼图,总结以下结论:
- V8 Inspector指令由指令域和指令名组成,格式为
Domain.Command
。 - V8 Inspector支持的指令域有:
Runtime
、Debugger
、Profiler
、HeapProfiler
、Console
、Schema
,由V8InspectorSession
的canDispatchMethod
方法可以判断指令是否被V8 Inspector支持。 - V8 Inspector的指令调用与回复是通过
V8InspectorSession
完成的,指令调用入口为V8InspectorSession.dispatchProtocolMessage
,指令的注册与分发处理则由UberDispatcher
对象负责。 - V8 Inspector的指令分发分两级完成。其中,一级分发根据
Domain
路由到指令域内部,由UberDispatcher
的Dispatch
接口实现;二级分发则根据Command
路由到指令域代理对象对应的处理函数,由指令域内部DomainDispatcher
子类DomainDispatcherImpl
完成。 - 指令域代理对象继承了指令域内部
Backend
接口,其中包含指令域支持的所有指令。在代理对象的处理函数中完成指令的执行。 - 指令消息的回复和数据推送,是通过
FrontendChannel
对象来发送数据到会话的对端的,具体实现则是调用V8Inspector::Channel
对象相应的接口。
问题的解决
通过以上对V8 Inspector指令分发处理机制的分析,我们可以很清晰地了解到具体的指令的分发处理过程,解决HeapProfiler sampling返回数据为空问题也就自然比较简单了。
通过对以上消息分发过程中关键环节,以及HeapProfiler
中startSampling
和stopSampling
指令执行的关键环节增加log,我们很快经可以定位到之所以返回数据为空,是因为v8::AllocationProfile
中samples_
就是空的。而samples_
之所以为空,则是因为startSampling
指令参数samplingInterval
传入的值16384
太大,导致在简单代码场景下,没有命中采样条件,所以数据是空的。
既然设置太大会导致采样数据为空,那么是不是可以无限小呢?或者是否可以为0或者1呢?答案是不可以!
传入的参数samplingInterval
会传给V8内存分配的观察者,作为step_size参数。
// Allows observation of allocations.
class AllocationObserver {
public:
explicit AllocationObserver(intptr_t step_size) : step_size_(step_size) {
// kTaggedSize <= step_size
DCHECK_LE(kTaggedSize, step_size);
}
// ...
};
DCHECK_LE
是一个类似于断言的宏,实现<=
断言。其中kTaggedSize
相关定义如下:
constexpr int kSystemPointerSize = sizeof(void*);
constexpr int kTaggedSize = kSystemPointerSize;
kTaggedSize
为一个void指针的大小,即在64位系统上为8,32为系统上为4。因此,samplingInterval
不能小于8或者4。
我们将samplingInterval
值设置为8,就可以解决HeapProfiler sampling数据为空的问题了。
解决后的效果,如下所示。
{"id":21,"result":{"profile":{"head":{"callFrame":{"functionName":"(root)","scriptId":"0","url":"","lineNumber":-1,"columnNumber":-1},"selfSize":0,"id":1,"children":[{"callFrame":{"functionName":"(EXTERNAL)","scriptId":"0","url":"","lineNumber":-1,"columnNumber":-1},"selfSize":20,"id":2,"children":[]}]},"samples":[{"size":24,"nodeId":2,"ordinal":2},{"size":20,"nodeId":2,"ordinal":1}]}}}
总结
了解原理之后,再去解决奇怪的问题,往往就没有看起来那么困难了,关键是要沉下心来,了解其中关键的细节。掌握关键流程之后,定位具体的问题时,可采用由输入到输出,二分定位排查,找到异常的地方,往往问题就能迎刃而解了。