V8 Inspector指令分发原理剖析之HeapProfiler sampling返回空

596 阅读14分钟

深入V8 Inspector源码,了解V8 Inspector指令处理机制。

文章较长,可以直接看镇楼图和结论。

问题背景

笔者最近在研究V8调试工具时,发现HeapProfilerAlllocation 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指令处理机制

由于源码分析比较枯燥且冗长,先放一张图镇楼吧。

V8Inspector指令处理机制UML图

考虑到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调试功能相关的方法。其中和指令处理相关的有canDispatchMethoddispatchProtocolMessagecanDispatchMethod是一个静态方法,决定了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.stopSamplingHeapProfiler.就是该指令的指令域,stopSampling就是具体的指令名。

更多内容可以参考:chromedevtools.github.io/debugger-pr…

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

看下核心方法的具体实现。

  1. 创建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对象关联起来。UberDispatcherDomainDispatcherImpl具体怎么关联的,我们稍后再做分析。

  2. 接收并分发指令消息

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

    然后,注释2CBOR格式数据进行解析,并以Dispatchable对象的形式在指令分发过程中传递。

    最后,注释3通过指令域分发器UberDispatcher进行第一级分发。一个具体指令的分发可以分成两级,第一级是指令域的分发,第二级则是具体指令的分发。m_dispatcher.Dispatch(dispatchable)完成两级分发后,拿到具体的指令处理接口,在Run()中调用该接口,完成指令的执行。

  3. 指令回复/数据推送

    指令消息的回复和数据推送,是向会话的对端发送数据。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主要是实现了指令处理器的注册和指令的分发功能。

  1. 注册指令处理器

    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中内容合并到已有重定向列表中,并重新排序。

    然后注册指令处理器,将指令域域名与对应处理器的键值对合并到已有处理器列表中,并重新排序。

  2. 分发指令

    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指令域中二级分发之前,我们先看下DomainDispatcherDomainDispatcher是二级指令分发器,定义了二级分发的接口,是各指令域内二级分发的共同的父类。

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, &params);
    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::BackendHeapProfiler::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:调用HeapProfilerStartSamplingHeapProfiler方法,执行指令,并返回成功。

结论

以上是HeapProfiler相关指令的分发处理过程,其他指令类似,不再过多分析。至此,V8 Inspector的指令处理机制就分析完了,结合前文的镇楼图,总结以下结论:

  • V8 Inspector指令由指令域和指令名组成,格式为Domain.Command
  • V8 Inspector支持的指令域有:RuntimeDebuggerProfilerHeapProfilerConsoleSchema,由V8InspectorSessioncanDispatchMethod方法可以判断指令是否被V8 Inspector支持。
  • V8 Inspector的指令调用与回复是通过V8InspectorSession完成的,指令调用入口为V8InspectorSession.dispatchProtocolMessage,指令的注册与分发处理则由UberDispatcher对象负责。
  • V8 Inspector的指令分发分两级完成。其中,一级分发根据Domain路由到指令域内部,由UberDispatcherDispatch接口实现;二级分发则根据Command路由到指令域代理对象对应的处理函数,由指令域内部DomainDispatcher子类DomainDispatcherImpl完成。
  • 指令域代理对象继承了指令域内部Backend接口,其中包含指令域支持的所有指令。在代理对象的处理函数中完成指令的执行。
  • 指令消息的回复和数据推送,是通过FrontendChannel对象来发送数据到会话的对端的,具体实现则是调用V8Inspector::Channel对象相应的接口。

参考:mp.weixin.qq.com/s/b_93MGVVW…

问题的解决

通过以上对V8 Inspector指令分发处理机制的分析,我们可以很清晰地了解到具体的指令的分发处理过程,解决HeapProfiler sampling返回数据为空问题也就自然比较简单了。

通过对以上消息分发过程中关键环节,以及HeapProfilerstartSamplingstopSampling指令执行的关键环节增加log,我们很快经可以定位到之所以返回数据为空,是因为v8::AllocationProfilesamples_就是空的。而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}]}}}

总结

了解原理之后,再去解决奇怪的问题,往往就没有看起来那么困难了,关键是要沉下心来,了解其中关键的细节。掌握关键流程之后,定位具体的问题时,可采用由输入到输出,二分定位排查,找到异常的地方,往往问题就能迎刃而解了。