Node.js 抓取堆快照過(guò)程解析
前言:在 Node.js 中,我們有時(shí)候需要抓取進(jìn)程堆快照來(lái)判斷是否有內(nèi)存泄漏,本文介紹Node.js 中抓取堆快照的實(shí)現(xiàn)。
首先來(lái)看一下 Node.js 中如何抓取堆快照。
- const { Session } = require('inspector');
- const session = new Session();
- let chunk = '';
- const cb = (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 命令的實(shí)現(xiàn)。
- {
- v8_crdtp::SpanFrom("takeHeapSnapshot"),
- &DomainDispatcherImpl::takeHeapSnapshot
- }
對(duì)應(yīng) DomainDispatcherImpl::takeHeapSnapshot 函數(shù)。
- void DomainDispatcherImpl::takeHeapSnapshot(const v8_crdtp::Dispatchable& dispatchable){
- std::unique_ptr<DomainDispatcher::WeakPtr> weak = weakPtr();
- // 抓取快照
- DispatchResponse response = m_backend->takeHeapSnapshot(std::move(params.reportProgress), std::move(params.treatGlobalObjectsAsRoots), std::move(params.captureNumericValue));
- // 抓取完畢,響應(yīng)
- if (weak->get())
- weak->get()->sendResponse(dispatchable.CallId(), response);
- return;
- }
上面代碼中 m_backend 是 V8HeapProfilerAgentImpl 對(duì)象。
- Response V8HeapProfilerAgentImpl::takeHeapSnapshot(
- Maybe<bool> reportProgress, Maybe<bool> treatGlobalObjectsAsRoots,
- Maybe<bool> captureNumericValue) {
- v8::HeapProfiler* profiler = m_isolate->GetHeapProfiler();
- // 抓取快照
- const v8::HeapSnapshot* snapshot = profiler->TakeHeapSnapshot(
- progress.get(), &resolver, treatGlobalObjectsAsRoots.fromMaybe(true),
- captureNumericValue.fromMaybe(false));
- // 抓取完畢后通知調(diào)用方
- HeapSnapshotOutputStream stream(&m_frontend);
- snapshot->Serialize(&stream);
- const_cast<v8::HeapSnapshot*>(snapshot)->Delete();
- // HeapProfiler.takeHeapSnapshot 命令結(jié)束,回調(diào)調(diào)用方
- return Response::Success();
- }
我們重點(diǎn)看一下 profiler->TakeHeapSnapshot。
- const HeapSnapshot* HeapProfiler::TakeHeapSnapshot(
- ActivityControl* control, ObjectNameResolver* resolver,
- bool treat_global_objects_as_roots, bool capture_numeric_value) {
- return reinterpret_cast<const HeapSnapshot*>(
- reinterpret_cast<i::HeapProfiler*>(this)->TakeSnapshot(
- control, resolver, treat_global_objects_as_roots,
- capture_numeric_value));
- }
繼續(xù)看真正的 TakeSnapshot。
- HeapSnapshot* HeapProfiler::TakeSnapshot(
- v8::ActivityControl* control,
- v8::HeapProfiler::ObjectNameResolver* resolver,
- bool treat_global_objects_as_roots, bool capture_numeric_value) {
- is_taking_snapshot_ = true;
- HeapSnapshot* result = new HeapSnapshot(this, treat_global_objects_as_roots,
- capture_numeric_value);
- {
- HeapSnapshotGenerator generator(result, control, resolver, heap());
- if (!generator.GenerateSnapshot()) {
- delete result;
- result = nullptr;
- } else {
- snapshots_.emplace_back(result);
- }
- }
- return result;
- }
我們看到新建了一個(gè) HeapSnapshot 對(duì)象,然后通過(guò) HeapSnapshotGenerator 對(duì)象的 GenerateSnapshot 抓取快照。看一下 GenerateSnapshot。
- bool HeapSnapshotGenerator::GenerateSnapshot() {
- Isolate* isolate = Isolate::FromHeap(heap_);
- base::Optional<HandleScope> handle_scope(base::in_place, isolate);
- v8_heap_explorer_.CollectGlobalObjectsTags();
- // 抓取前先回收不用內(nèi)存,保證看到的是存活的對(duì)象,否則影響內(nèi)存泄漏的分析
- heap_->CollectAllAvailableGarbage(GarbageCollectionReason::kHeapProfiler);
- // 收集內(nèi)存信息
- snapshot_->AddSyntheticRootEntries();
- FillReferences();
- snapshot_->FillChildren();
- return true;
- }
GenerateSnapshot 的邏輯是首先進(jìn)行GC 回收不用的內(nèi)存,然后收集 GC 后的內(nèi)存信息到 HeapSnapshot 對(duì)象。接著看收集完后的邏輯。
- HeapSnapshotOutputStream stream(&m_frontend);
- snapshot->Serialize(&stream);
HeapSnapshotOutputStream 是用于通知調(diào)用方收集的數(shù)據(jù)(通過(guò) m_frontend)。
- explicit HeapSnapshotOutputStream(protocol::HeapProfiler::Frontend* frontend)
- : m_frontend(frontend) {}
- void EndOfStream() override {}
- int GetChunkSize() override { return 102400; }
- WriteResult WriteAsciiChunk(char* data, int size) override {
- m_frontend->addHeapSnapshotChunk(String16(data, size));
- m_frontend->flush();
- return kContinue;
- }
HeapSnapshotOutputStream 通過(guò) WriteAsciiChunk 告訴調(diào)用方收集的數(shù)據(jù),但是目前我們還沒(méi)有數(shù)據(jù)源,下面看看數(shù)據(jù)源怎么來(lái)的。
- snapshot->Serialize(&stream);
看一下 Serialize。
- void HeapSnapshot::Serialize(OutputStream* stream,
- HeapSnapshot::SerializationFormat format) const {
- i::HeapSnapshotJSONSerializer serializer(ToInternal(this));
- serializer.Serialize(stream);
- }
最終調(diào)了 HeapSnapshotJSONSerializer 的 Serialize。
- void HeapSnapshotJSONSerializer::Serialize(v8::OutputStream* stream) {
- // 寫者
- writer_ = new OutputStreamWriter(stream);
- // 開(kāi)始寫
- SerializeImpl();
- }
我們看一下 SerializeImpl。
- void HeapSnapshotJSONSerializer::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\":[");
- SerializeSamples();
- 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 函數(shù)的邏輯就是把快照數(shù)據(jù)通過(guò) OutputStreamWriter 對(duì)象 writer_ 寫到 writer_ 持有的 stream 中。寫的數(shù)據(jù)有很多種類型,這里以 AddCharacter 為例。
- void AddCharacter(char c) {
- chunk_[chunk_pos_++] = c;
- MaybeWriteChunk();
- }
每次寫的時(shí)候都會(huì)判斷是不達(dá)到閾值,是的話則先推給調(diào)用方??匆幌?MaybeWriteChunk。
- void MaybeWriteChunk() {
- if (chunk_pos_ == chunk_size_) {
- WriteChunk();
- }
- }
- void WriteChunk() {
- // stream 控制是否還需要寫入,通過(guò) kAbort 和 kContinue
- if (stream_->WriteAsciiChunk(chunk_.begin(), chunk_pos_) ==
- v8::OutputStream::kAbort)
- aborted_ = true;
- chunk_pos_ = 0;
- }
我們看到最終通過(guò) stream 的 WriteAsciiChunk 寫到 stream 中。
- WriteResult WriteAsciiChunk(char* data, int size) override {
- m_frontend->addHeapSnapshotChunk(String16(data, size));
- m_frontend->flush();
- return kContinue;
- }
WriteAsciiChunk 調(diào)用 addHeapSnapshotChunk 通知調(diào)用方。
- void Frontend::addHeapSnapshotChunk(const String& chunk){
- v8_crdtp::ObjectSerializer serializer;
- serializer.AddField(v8_crdtp::MakeSpan("chunk"), chunk);
- frontend_channel_->SendProtocolNotification(v8_crdtp::CreateNotification("HeapProfiler.addHeapSnapshotChunk", serializer.Finish()));
- }
觸發(fā) HeapProfiler.addHeapSnapshotChunk 事件,并傳入快照的數(shù)據(jù),最終觸發(fā) JS 層的事件。再看一下文章開(kāi)頭的代碼。
- let chunk = '';
- const cb = (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);
- });
這個(gè)過(guò)程是否清晰了很多。從過(guò)程中也看到,抓取快照雖然傳入了回調(diào),但是其實(shí)是以同步的方式執(zhí)行的,因?yàn)樘峤? HeapProfiler.takeHeapSnapshot 命令后,V8 就開(kāi)始收集內(nèi)存,然后不斷觸發(fā)
HeapProfiler.addHeapSnapshotChunk 事件,直到堆數(shù)據(jù)寫完,然后執(zhí)行 JS 回調(diào)。
總結(jié):整個(gè)過(guò)程不算復(fù)雜,因?yàn)槲覀儧](méi)有涉及到堆內(nèi)存管理那部分,V8 Inspector 提供了很多命令,有時(shí)間的話后續(xù)再分析其他的命令。