前言:在Node.js中,我们有时需要抓取进程堆快照来判断是否存在内存泄漏。本文介绍在Node.js中获取堆快照的实现。我们先来看看如何在Node.js中进行堆快照。const{Session}=require('inspector');constsession=newSession();letchunk='';constcb=(result)=>{chunk+=result.params.chunk;};session.on('HeapProfiler.addHeapSnapshotChunk',cb);session.post('HeapProfiler.takeHeapSnapshot',(err,r)=>{session.off('HeapProfiler.addHeapSnapshotChunk',cb);console.log(err||chunk);});下面看一下HeapProfiler.addHeapSnapshotChunk命令的实现。{v8_crdtp::SpanFrom("takeHeapSnapshot"),&DomainDispatcherImpl::takeHeapSnapshot}对应DomainDispatcherImpl::takeHeapSnapshot函数。voidDomainDispatcherImpl::takeHeapSnapshot(constv8_crdtp::Dispatchable&dispatchable){std::unique_ptrweak=weakPtr();//快照DispatchResponseresponse=m_backend->takeHeapSnapshot(std::move(params.reportProgress),std::move(params.treatGlobalObjectsAsRoots),std::move(params.captureNumericValue));//捕获完成,响应if(weak->get())weak->get()->sendResponse(dispatchable.CallId(),response);return;}上面代码中的m_backend就是V8HeapProfilerAgentImpl对象。ResponseV8HeapProfilerAgentImpl::takeHeapSnapshot(MaybereportProgress,MaybetreatGlobalObjectsAsRoots,MaybecaptureNumericValue){v8::HeapProfiler*profiler=m_isolate->GetHeapProfiler();//捕捉快照constv8::HeapSnapshot=shot*snapshotprofiler->TakeHeapSnapshot(progress.get(),&resolver,treatGlobalObjectsAsRoots.fromMaybe(true),captureNumericValue.fromMaybe(false));//捕获HeapSnapshotOutputStream(&m_frontend);snapshot->Serialize(&stream);const_cast(snapshot)->Delete();//HeapProfiler.takeHeapSnapshot命令结束,回调调用者returnResponse::Success();}下面重点说profiler->TakeHeapSnapshot。constHeapSnapshot*HeapProfiler::TakeHeapSnapshot(ActivityControl*control,ObjectNameResolver*resolver,booltreat_global_objects_as_roots,boolcapture_numeric_value){returnreinterpret_cast(reinterpret_cast(this)->TakeSnapshot(control,resolver,treat_global_objects,capture_as)返回值HeapSnapshot*HeapProfiler::TakeSnapshot(v8::ActivityControl*control,v8::HeapProfiler::ObjectNameResolver*resolver,booltreat_global_objects_as_roots,boolcapture_numeric_value){is_taking_snapshot_=true;HeapSnapshot*result=new,heapSnapshotbal(this,heapSnapshotbal),capture_numeric_value);{HeapSnapshotGeneratorgenerator(result,control,resolver,heap());if(!generator.GenerateSnapshot()){deleteresult;result=nullptr;}else{snapshots_.emplace_back(result);}}returnresult;}我们看到新建了一个HeapSnapshot对象,然后通过HeapSnapshotGenerator对象的GenerateSnapshot抓取快照。看下GenerateSnapshot。boolHeapSnapshotGenerator::GenerateSnapshot(){Isolate*isolate=Isolate::FromHeap(heap_);base::Optionalhandle_scope(base::in_place,isolate);v8_heap_explorer_.CollectGlobalObjectsTags();//在抢内存之前回收,保证heap_->CollectAllAvailableGarbage(GarbageCollectionReason::kHeapProfiler);//收集内存信息snapshot_->AddSyntheticRootEntries();FillReferences();snapshot_->FillChildren();returntrue;}GenerateSnapshot的逻辑是先进行GC回收未使用的内存,然后将GC后的内存信息收集到HeapSnapshot对象中。然后看采集后的逻辑。HeapSnapshotOutputStream(&m_frontend);快照->序列化(&stream);HeapSnapshotOutputStream用于通知调用者收集的数据(通过m_frontend)。explicitHeapSnapshotOutputStream(协议::HeapProfiler::前端*前端):m_frontend(前端){}voidEndOfStream()覆盖{}intGetChunkSize()覆盖{return102400;}WriteResultWriteAsciiChunk(charhot*数据,intsize)覆盖(String-6ChunkSize)覆盖(字符串-6ChunkSize)override(data,size));m_frontend->flush();returnContinue;}HeapSnapshotOutputStream通过WriteAsciiChunk告诉调用者收集到的数据,但是我们还没有数据源,我们看看数据源是怎么来的.快照->序列化(&stream);看看序列化。voidHeapSnapshot::Serialize(OutputStream*stream,HeapSnapshot::SerializationFormatformat)const{i::HeapSnapshotJSONSerializerserializer(ToInternal(this));serializer.Serialize(stream);}最后调整了HeapSnapshotJSONSerializer的Serialize。voidHeapSnapshotJSONSerializer::Serialize(v8::OutputStream*stream){//Writerwriter_=newOutputStreamWriter(stream);//开始写SerializeImpl();}我们来看看SerializeImpl。voidHeapSnapshotJSONSerializer::SerializeImpl(){DCHECK_EQ(0,snapshot_->root()->index());writer_->AddCharacter('{');writer_->AddString("\"snapshot\":{");SerializeSnapshot();if(writer_->aborted())return;writer_->AddString("},\n");writer_->AddString("\"nodes\":[");SerializeNodes();if(writer_->aborted())return;writer_->AddString("],\n");writer_->AddString("\"edges\":[");SerializeEdges();if(writer_->aborted())return;writer_->AddString("],\n");writer_->AddString("\"trace_function_infos\":[");SerializeTraceNodeInfos();if(writer_->aborted())return;writer_->AddString("],\n");writer_->AddString("\"trace_tree\":[");SerializeTraceTree();if(writer_->aborted())return;writer_->AddString("),\n");writer_->AddString("\"samples\":[");序列化eSamples();if(writer_->aborted())return;writer_->AddString("],\n");writer_->AddString("\"locations\":[");SerializeLocations();if(writer_->aborted())return;writer_->AddString("],\n");writer_->AddString("\"strings\":[");SerializeStrings();if(writer_->aborted())return;writer_->AddCharacter(']');writer_->AddCharacter('}');writer_->Finalize();}SerializeImpl函数的逻辑是将快照数据写入到writer_持有的流中OutputStreamWriter对象writer_中写入的数据有很多种,这里以AddCharacter为例。voidAddCharacter(charc){chunk_[chunk_pos_++]=c;MaybeWriteChunk();}每次写入都会判断没有达到阈值,如果达到则先推送给调用者。看看MaybeWriteChunk。voidMaybeWriteChunk(){if(chunk_pos_==chunk_size_){WriteChunk();}}voidWriteChunk(){//stream控制是否还需要写入,通过kAbort和kContinueif(stream_->WriteAsciiChunk(chunk_.begin(),chunk_pos_)==v8::OutputStream::kAbort)aborted_=true;chunk_pos_=0;}我们看到流的WriteAsciiChunk终于写入了流。WriteResultWriteAsciiChunk(char*data,intsize)override{m_frontend->addHeapSnapshotChunk(String16(data,size));m_frontend->flush();returnkContinue;}WriteAsciiChunk调用addHeapSnapshotChunk通知调用者。voidFrontend::addHeapSnapshotChunk(constString&chunk){v8_crdtp::ObjectSerializerserializer;serializer.AddField(v8_crdtp::MakeSpan("chunk"),chunk);frontend_channel_->SendProtocolNotification(v8_crdtp::Create.FinishNotification("HeapProfilerhots.add)));}触发HeapProfiler.addHeapSnapshotChunk事件,并传入快照的数据,最后触发JS层的事件,再看文章开头的代码letchunk='';constcb=(result)=>{chunk+=result.params.chunk;};session.on('HeapProfiler.addHeapSnapshotChunk',cb);session.post('HeapProfiler.takeHeapSnapshot',(err,r)=>{session.off('HeapProfiler.addHeapSnapshotChunk',cb);console.log(err||chunk);});这个过程是不是清晰多了。从流程中也可以看出,虽然传入了callback来抓快照,但实际上是同步执行的,因为提交HeapProfiler.takeHeapSnapshot命令后,V8开始收集内存,然后不断触发HeapProfiler.addHeapSnapshotChunk事件直到数据写入完成后,执行JS回调。总结:整个过程并不复杂,因为我们还没有涉及到堆内存管理的部分。V8Inspector提供了很多命令,其他的命令我们以后有时间再分析。