Node.js 抓取堆快照过程解析

时间:2022-05-04 13:09:34

Node.js 抓取堆快照过程解析

前言:在 Node.js 中,我们有时候需要抓取进程堆快照来判断是否有内存泄漏,本文介绍Node.js 中抓取堆快照的实现。

首先来看一下 Node.js 中如何抓取堆快照。

  1. const { Session } = require('inspector'); 
  2.  
  3. const session = new Session(); 
  4.  
  5. let chunk = ''
  6.  
  7. const cb = (result) => { 
  8.  
  9.   chunk += result.params.chunk; 
  10.  
  11. }; 
  12.  
  13.  
  14. session.on('HeapProfiler.addHeapSnapshotChunk', cb); 
  15. session.post('HeapProfiler.takeHeapSnapshot', (err, r) => { 
  16.   session.off('HeapProfiler.addHeapSnapshotChunk', cb); 
  17.     console.log(err || chunk); 
  18.  
  19. }); 

下面看一下 HeapProfiler.addHeapSnapshotChunk 命令的实现。

  1.       v8_crdtp::SpanFrom("takeHeapSnapshot"), 
  2.       &DomainDispatcherImpl::takeHeapSnapshot 

对应 DomainDispatcherImpl::takeHeapSnapshot 函数。

<:weakptr>

  1. void DomainDispatcherImpl::takeHeapSnapshot(const v8_crdtp::Dispatchable& dispatchable){ 
  2.     std::unique_ptr<DomainDispatcher::WeakPtr> weak = weakPtr(); 
  3.     // 抓取快照  
  4.     DispatchResponse response = m_backend->takeHeapSnapshot(std::move(params.reportProgress), std::move(params.treatGlobalObjectsAsRoots), std::move(params.captureNumericValue)); 
  5.     // 抓取完毕,响应 
  6.     if (weak->get()) 
  7.         weak->get()->sendResponse(dispatchable.CallId(), response); 
  8.     return
  9.  

上面代码中 m_backend 是 V8HeapProfilerAgentImpl 对象。

  1. Response V8HeapProfilerAgentImpl::takeHeapSnapshot( 
  2.     Maybe<bool> reportProgress, Maybe<bool> treatGlobalObjectsAsRoots, 
  3.     Maybe<bool> captureNumericValue) { 
  4.   v8::HeapProfiler* profiler = m_isolate->GetHeapProfiler(); 
  5.   // 抓取快照 
  6.   const v8::HeapSnapshot* snapshot = profiler->TakeHeapSnapshot( 
  7.       progress.get(), &resolver, treatGlobalObjectsAsRoots.fromMaybe(true), 
  8.       captureNumericValue.fromMaybe(false)); 
  9.   // 抓取完毕后通知调用方     
  10.   HeapSnapshotOutputStream stream(&m_frontend); 
  11.   snapshot->Serialize(&stream); 
  12.   const_cast<v8::HeapSnapshot*>(snapshot)->Delete(); 
  13.   // HeapProfiler.takeHeapSnapshot 命令结束,回调调用方 
  14.   return Response::Success(); 
  15.  

我们重点看一下 profiler->TakeHeapSnapshot。

  1. const HeapSnapshot* HeapProfiler::TakeHeapSnapshot( 
  2.     ActivityControl* control, ObjectNameResolver* resolver, 
  3.     bool treat_global_objects_as_roots, bool capture_numeric_value) { 
  4.   return reinterpret_cast<const HeapSnapshot*>( 
  5.       reinterpret_cast<i::HeapProfiler*>(this)->TakeSnapshot( 
  6.           control, resolver, treat_global_objects_as_roots, 
  7.           capture_numeric_value)); 
  8.  

继续看真正的 TakeSnapshot。

  1. HeapSnapshot* HeapProfiler::TakeSnapshot( 
  2.     v8::ActivityControl* control, 
  3.     v8::HeapProfiler::ObjectNameResolver* resolver, 
  4.     bool treat_global_objects_as_roots, bool capture_numeric_value) { 
  5.   is_taking_snapshot_ = true
  6.   HeapSnapshot* result = new HeapSnapshot(this, treat_global_objects_as_roots, 
  7.                                           capture_numeric_value); 
  8.   { 
  9.     HeapSnapshotGenerator generator(result, control, resolver, heap()); 
  10.     if (!generator.GenerateSnapshot()) { 
  11.       delete result; 
  12.       result = nullptr; 
  13.     } else { 
  14.       snapshots_.emplace_back(result); 
  15.     } 
  16.   } 
  17.   return result; 
  18.  

我们看到新建了一个 HeapSnapshot 对象,然后通过 HeapSnapshotGenerator 对象的 GenerateSnapshot 抓取快照。看一下 GenerateSnapshot。

  1. bool HeapSnapshotGenerator::GenerateSnapshot() { 
  2.   Isolate* isolate = Isolate::FromHeap(heap_); 
  3.   base::Optional<HandleScope> handle_scope(base::in_place, isolate); 
  4.   v8_heap_explorer_.CollectGlobalObjectsTags(); 
  5.   // 抓取前先回收不用内存,保证看到的是存活的对象,否则影响内存泄漏的分析 
  6.   heap_->CollectAllAvailableGarbage(GarbageCollectionReason::kHeapProfiler); 
  7.   // 收集内存信息 
  8.   snapshot_->AddSyntheticRootEntries(); 
  9.   FillReferences(); 
  10.   snapshot_->FillChildren(); 
  11.   return true
  12.  

GenerateSnapshot 的逻辑是首先进行GC 回收不用的内存,然后收集 GC 后的内存信息到 HeapSnapshot 对象。接着看收集完后的逻辑。

  1. HeapSnapshotOutputStream stream(&m_frontend); 
  2. snapshot->Serialize(&stream); 

HeapSnapshotOutputStream 是用于通知调用方收集的数据(通过 m_frontend)。

  1. explicit HeapSnapshotOutputStream(protocol::HeapProfiler::Frontend* frontend) 
  2.       : m_frontend(frontend) {} 
  3.   void EndOfStream() override {} 
  4.   int GetChunkSize() override { return 102400; } 
  5.   WriteResult WriteAsciiChunk(char* data, int size) override { 
  6.     m_frontend->addHeapSnapshotChunk(String16(data, size)); 
  7.     m_frontend->flush(); 
  8.     return kContinue; 

HeapSnapshotOutputStream 通过 WriteAsciiChunk 告诉调用方收集的数据,但是目前我们还没有数据源,下面看看数据源怎么来的。

  1. snapshot->Serialize(&stream); 

看一下 Serialize。

  1. void HeapSnapshot::Serialize(OutputStream* stream, 
  2.                              HeapSnapshot::SerializationFormat format) const { 
  3.   i::HeapSnapshotJSONSerializer serializer(ToInternal(this)); 
  4.   serializer.Serialize(stream); 
  5.  

最终调了 HeapSnapshotJSONSerializer 的 Serialize。

  1. void HeapSnapshotJSONSerializer::Serialize(v8::OutputStream* stream) { 
  2.   // 写者 
  3.   writer_ = new OutputStreamWriter(stream); 
  4.   // 开始写 
  5.   SerializeImpl(); 
  6.  

我们看一下 SerializeImpl。

  1. void HeapSnapshotJSONSerializer::SerializeImpl() { 
  2.   DCHECK_EQ(0, snapshot_->root()->index()); 
  3.   writer_->AddCharacter('{'); 
  4.   writer_->AddString("\"snapshot\":{"); 
  5.   SerializeSnapshot(); 
  6.   if (writer_->aborted()) return
  7.   writer_->AddString("},\n"); 
  8.   writer_->AddString("\"nodes\":["); 
  9.   SerializeNodes(); 
  10.   if (writer_->aborted()) return
  11.   writer_->AddString("],\n"); 
  12.   writer_->AddString("\"edges\":["); 
  13.   SerializeEdges(); 
  14.   if (writer_->aborted()) return
  15.   writer_->AddString("],\n"); 
  16.  
  17.   writer_->AddString("\"trace_function_infos\":["); 
  18.   SerializeTraceNodeInfos(); 
  19.   if (writer_->aborted()) return
  20.   writer_->AddString("],\n"); 
  21.   writer_->AddString("\"trace_tree\":["); 
  22.   SerializeTraceTree(); 
  23.   if (writer_->aborted()) return
  24.   writer_->AddString("],\n"); 
  25.  
  26.   writer_->AddString("\"samples\":["); 
  27.   SerializeSamples(); 
  28.   if (writer_->aborted()) return
  29.   writer_->AddString("],\n"); 
  30.  
  31.   writer_->AddString("\"locations\":["); 
  32.   SerializeLocations(); 
  33.   if (writer_->aborted()) return
  34.   writer_->AddString("],\n"); 
  35.  
  36.   writer_->AddString("\"strings\":["); 
  37.   SerializeStrings(); 
  38.   if (writer_->aborted()) return
  39.   writer_->AddCharacter(']'); 
  40.   writer_->AddCharacter('}'); 
  41.   writer_->Finalize(); 
  42.  

SerializeImpl 函数的逻辑就是把快照数据通过 OutputStreamWriter 对象 writer_ 写到 writer_ 持有的 stream 中。写的数据有很多种类型,这里以 AddCharacter 为例。

  1. void AddCharacter(char c) { 
  2.   chunk_[chunk_pos_++] = c; 
  3.   MaybeWriteChunk(); 
  4.  

每次写的时候都会判断是不达到阈值,是的话则先推给调用方。看一下 MaybeWriteChunk。

  1. void MaybeWriteChunk() { 
  2.   if (chunk_pos_ == chunk_size_) { 
  3.     WriteChunk(); 
  4.   } 
  5.  
  6.  
  7.  
  8.  
  9. void WriteChunk() { 
  10.  
  11.   // stream 控制是否还需要写入,通过 kAbort 和 kContinue 
  12.   if (stream_->WriteAsciiChunk(chunk_.begin(), chunk_pos_) == 
  13.       v8::OutputStream::kAbort) 
  14.     aborted_ = true
  15.   chunk_pos_ = 0; 
  16.  

我们看到最终通过 stream 的 WriteAsciiChunk 写到 stream 中。

  1. WriteResult WriteAsciiChunk(char* data, int size) override { 
  2.   m_frontend->addHeapSnapshotChunk(String16(data, size)); 
  3.   m_frontend->flush(); 
  4.   return kContinue; 
  5.  

WriteAsciiChunk 调用 addHeapSnapshotChunk 通知调用方。

  1. void Frontend::addHeapSnapshotChunk(const String& chunk){ 
  2.     v8_crdtp::ObjectSerializer serializer; 
  3.     serializer.AddField(v8_crdtp::MakeSpan("chunk"), chunk); 
  4.     frontend_channel_->SendProtocolNotification(v8_crdtp::CreateNotification("HeapProfiler.addHeapSnapshotChunk", serializer.Finish())); 
  5.  

触发 HeapProfiler.addHeapSnapshotChunk 事件,并传入快照的数据,最终触发 JS 层的事件。再看一下文章开头的代码。

  1. let chunk = ''
  2.  
  3. const cb = (result) => { 
  4.  
  5.   chunk += result.params.chunk; 
  6.  
  7. }; 
  8.  
  9.  
  10.  
  11. session.on('HeapProfiler.addHeapSnapshotChunk', cb); 
  12. session.post('HeapProfiler.takeHeapSnapshot', (err, r) => { 
  13.   session.off('HeapProfiler.addHeapSnapshotChunk', cb); 
  14.     console.log(err || chunk); 
  15.  
  16. }); 

这个过程是否清晰了很多。从过程中也看到,抓取快照虽然传入了回调,但是其实是以同步的方式执行的,因为提交 HeapProfiler.takeHeapSnapshot 命令后,V8 就开始收集内存,然后不断触发

HeapProfiler.addHeapSnapshotChunk 事件,直到堆数据写完,然后执行 JS 回调。

总结:整个过程不算复杂,因为我们没有涉及到堆内存管理那部分,V8 Inspector 提供了很多命令,有时间的话后续再分析其他的命令。

原文链接:https://mp.weixin.qq.com/s/MNSpplVuD4gJG3we2_GgIQ