lib/VM/HeapSnapshot.cpp (441 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "hermes/VM/HeapSnapshot.h" #include "hermes/Support/Conversions.h" #include "hermes/Support/JSONEmitter.h" #include "hermes/Support/UTF8.h" #include "hermes/VM/GC.h" #include "hermes/VM/StackTracesTree.h" #include "hermes/VM/StringPrimitive.h" #include <type_traits> namespace hermes { namespace vm { namespace { /// Lower an instance \p e of enumeration \p Enum to its underlying type. template <typename Enum> constexpr typename std::underlying_type<Enum>::type index(Enum e) { return static_cast<typename std::underlying_type<Enum>::type>(e); } const char *kSectionLabels[] = { #define V8_SNAPSHOT_SECTION(enumerand, label) label, #include "hermes/VM/HeapSnapshot.def" }; } // namespace HeapSnapshot::HeapSnapshot(JSONEmitter &json, StackTracesTree *stackTracesTree) : json_(json), stackTracesTree_(stackTracesTree), stringTable_( stackTracesTree ? stackTracesTree->getStringTable() : std::make_shared<StringSetVector>()) { json_.openDict(); emitMeta(); } HeapSnapshot::~HeapSnapshot() { assert( edgeCount_ == expectedEdges_ && "Fewer edges added than were expected"); emitStrings(); json_.closeDict(); // top level } void HeapSnapshot::beginSection(Section section) { auto i = index(nextSection_); assert(!sectionOpened_ && "Sections must be explicitly close"); assert(section != Section::END && "Can't open the end section."); assert( i <= index(section) && "Trying to open a section after it has already been closed. Are your " "sections ordered correctly?"); for (; i < index(section); ++i) { json_.emitKey(kSectionLabels[i]); json_.openArray(); json_.closeArray(); } json_.emitKey(kSectionLabels[i]); json_.openArray(); nextSection_ = section; sectionOpened_ = true; } void HeapSnapshot::endSection(Section section) { assert(sectionOpened_ && "No section to close"); assert(section != Section::END && "Can't close the end section."); assert(nextSection_ == section && "Closing a different section."); json_.closeArray(); nextSection_ = static_cast<Section>(index(section) + 1); sectionOpened_ = false; } void HeapSnapshot::beginNode() { if (nextSection_ == Section::Edges) { // If the edges are being emitted, ignore node output. return; } assert(nextSection_ == Section::Nodes && sectionOpened_); // Reset the edge counter. currEdgeCount_ = 0; } void HeapSnapshot::endNode( NodeType type, llvh::StringRef name, NodeID id, HeapSizeType selfSize, HeapSizeType traceNodeID) { if (nextSection_ == Section::Edges) { // If the edges are being emitted, ignore node output. return; } auto &nodeStats = traceNodeStats_[traceNodeID]; nodeStats.count++; nodeStats.size += selfSize; assert(nextSection_ == Section::Nodes && sectionOpened_); auto res = nodeToIndex_.try_emplace(id, nodeCount_++); assert(res.second); (void)res; json_.emitValue(index(type)); json_.emitValue(stringTable_->insert(name)); json_.emitValue(id); json_.emitValue(selfSize); json_.emitValue(currEdgeCount_); json_.emitValue(traceNodeID); // detachedness is always zero for hermes, since there's no DOM to attach to. json_.emitValue(0); #ifndef NDEBUG expectedEdges_ += currEdgeCount_; #endif } void HeapSnapshot::addNamedEdge( EdgeType type, llvh::StringRef name, NodeID toNode) { if (nextSection_ == Section::Nodes) { // If we're emitting nodes, only count the number of edges being processed, // but don't actually emit them. currEdgeCount_++; return; } assert( edgeCount_++ < expectedEdges_ && "Added more edges than were expected"); assert(nextSection_ == Section::Edges && sectionOpened_); json_.emitValue(index(type)); json_.emitValue(stringTable_->insert(name)); auto nodeIt = nodeToIndex_.find(toNode); assert(nodeIt != nodeToIndex_.end()); // Point to the beginning of the target node in the `nodes` flat array. json_.emitValue(nodeIt->second * V8_SNAPSHOT_NODE_FIELD_COUNT); } void HeapSnapshot::addIndexedEdge( EdgeType type, EdgeIndex edgeIndex, NodeID toNode) { if (nextSection_ == Section::Nodes) { // If we're emitting nodes, only count the number of edges being processed, // but don't actually emit them. currEdgeCount_++; return; } assert( edgeCount_++ < expectedEdges_ && "Added more edges than were expected"); assert(nextSection_ == Section::Edges && sectionOpened_); json_.emitValue(index(type)); json_.emitValue(edgeIndex); auto nodeIt = nodeToIndex_.find(toNode); assert(nodeIt != nodeToIndex_.end()); // Point to the beginning of the target node in the `nodes` flat array. json_.emitValue(nodeIt->second * V8_SNAPSHOT_NODE_FIELD_COUNT); } void HeapSnapshot::addLocation( NodeID id, ::facebook::hermes::debugger::ScriptID script, uint32_t line, uint32_t column) { assert( nextSection_ == Section::Locations && sectionOpened_ && "Shouldn't be emitting locations until the location section starts"); auto nodeIt = nodeToIndex_.find(id); assert( nodeIt != nodeToIndex_.end() && "Couldn't add a location for an object that doesn't exist"); json_.emitValue(nodeIt->second * V8_SNAPSHOT_NODE_FIELD_COUNT); json_.emitValue(script); // The serialized format uses 0-based indexing for line and column, but the // parameters are 1-based. assert(line != 0 && "Line should be 1-based"); assert(column != 0 && "Column should be 1-based"); json_.emitValue(line - 1); json_.emitValue(column - 1); } void HeapSnapshot::addSample( std::chrono::microseconds timestamp, NodeID lastSeenObjectID) { assert( nextSection_ == Section::Samples && sectionOpened_ && "Shouldn't be emitting samples until the sample section starts"); assert( lastSeenObjectID != GCBase::IDTracker::kInvalidNode && "Last seen object ID must be valid"); json_.emitValues( {static_cast<uint64_t>(timestamp.count()), static_cast<uint64_t>(lastSeenObjectID)}); } const char *HeapSnapshot::nodeTypeToName(NodeType type) { switch (type) { #define V8_NODE_TYPE(enumerand, label) \ case NodeType::enumerand: \ return #label; #include "hermes/VM/HeapSnapshot.def" } assert(false && "Invalid NodeType"); return ""; } const char *HeapSnapshot::edgeTypeToName(EdgeType type) { switch (type) { #define V8_EDGE_TYPE(enumerand, label) \ case EdgeType::enumerand: \ return #label; #include "hermes/VM/HeapSnapshot.def" } assert(false && "Invalid EdgeType"); return ""; } void HeapSnapshot::emitMeta() { json_.emitKey("snapshot"); json_.openDict(); json_.emitKey("meta"); json_.openDict(); json_.emitKey("node_fields"); json_.openArray(); json_.emitValues({ "type", #define V8_NODE_FIELD(label, type) #label, #include "hermes/VM/HeapSnapshot.def" }); json_.closeArray(); // node_fields json_.emitKey("node_types"); json_.openArray(); json_.openArray(); json_.emitValues({ #define V8_NODE_TYPE(enumerand, label) label, #include "hermes/VM/HeapSnapshot.def" }); json_.closeArray(); json_.emitValues({ #define V8_NODE_FIELD(label, type) #type, #include "hermes/VM/HeapSnapshot.def" }); json_.closeArray(); // node_types json_.emitKey("edge_fields"); json_.openArray(); json_.emitValues({ "type", #define V8_EDGE_FIELD(label, type) #label, #include "hermes/VM/HeapSnapshot.def" }); json_.closeArray(); // edge_fields json_.emitKey("edge_types"); json_.openArray(); json_.openArray(); json_.emitValues({ #define V8_EDGE_TYPE(enumerand, label) label, #include "hermes/VM/HeapSnapshot.def" }); json_.closeArray(); json_.emitValues({ #define V8_EDGE_FIELD(label, type) #type, #include "hermes/VM/HeapSnapshot.def" }); json_.closeArray(); // edge_types json_.emitKey("trace_function_info_fields"); json_.openArray(); json_.emitValues({ #define V8_TRACE_FUNCTION_INFO_FIELD(name) #name, #include "hermes/VM/HeapSnapshot.def" }); json_.closeArray(); // trace_function_info_fields json_.emitKey("trace_node_fields"); json_.openArray(); json_.emitValues({ #define V8_TRACE_NODE_FIELD(name) #name, #include "hermes/VM/HeapSnapshot.def" }); json_.closeArray(); // trace_node_fields json_.emitKey("sample_fields"); json_.openArray(); json_.emitValues({ #define V8_SAMPLE_FIELD(name) #name, #include "hermes/VM/HeapSnapshot.def" }); json_.closeArray(); // sample_fields json_.emitKey("location_fields"); json_.openArray(); json_.emitValues({ #define V8_LOCATION_FIELD(label) #label, #include "hermes/VM/HeapSnapshot.def" }); json_.closeArray(); // location_fields json_.closeDict(); // "meta" json_.emitKey("node_count"); // This can be zero because it's only used as an optimization hint to // the viewer. json_.emitValue(0); json_.emitKey("edge_count"); // This can be zero because it's only used as an optimization hint to // the viewer. json_.emitValue(0); json_.emitKey("trace_function_count"); json_.emitValue(countFunctionTraceInfos()); json_.closeDict(); // "snapshot" } size_t HeapSnapshot::countFunctionTraceInfos() { if (!stackTracesTree_) { return 0; } size_t count = 0; llvh::DenseSet< StackTracesTreeNode::SourceLoc, StackTracesTreeNode::SourceLocMapInfo> sourceLocSet; llvh::SmallVector<StackTracesTreeNode *, 128> nodeStack; nodeStack.push_back(stackTracesTree_->getRootNode()); while (!nodeStack.empty()) { auto curNode = nodeStack.pop_back_val(); auto funcHashToFuncIdxMapEntry = sourceLocSet.find(curNode->sourceLoc); if (funcHashToFuncIdxMapEntry == sourceLocSet.end()) { count++; sourceLocSet.insert(curNode->sourceLoc); } for (auto child : curNode->getChildren()) { nodeStack.push_back(child); } } return count; } void HeapSnapshot::emitAllocationTraceInfo() { if (!stackTracesTree_) { return; } llvh::DenseMap< StackTracesTreeNode::SourceLoc, size_t, StackTracesTreeNode::SourceLocMapInfo> sourceLocToFuncIdxMap; size_t nextFunctionIdx = 0; std::stack< StackTracesTreeNode *, llvh::SmallVector<StackTracesTreeNode *, 128>> nodeStack; beginSection(Section::TraceFunctionInfos); nodeStack.push(stackTracesTree_->getRootNode()); while (!nodeStack.empty()) { auto curNode = nodeStack.top(); nodeStack.pop(); auto entry = sourceLocToFuncIdxMap.find(curNode->sourceLoc); if (entry == sourceLocToFuncIdxMap.end()) { const auto functionIdx = nextFunctionIdx++; sourceLocToFuncIdxMap.try_emplace(curNode->sourceLoc, functionIdx); // function_id needs to match the zero-based index of this function in the // list. json_.emitValue(functionIdx); // "function_id" json_.emitValue(curNode->name); // "name" json_.emitValue(curNode->sourceLoc.scriptName); // "script_name" json_.emitValue(curNode->sourceLoc.scriptID); // "script_id" // These should be emitted as 1-based, not 0-based like locations. json_.emitValue(curNode->sourceLoc.lineNo); // "line" json_.emitValue(curNode->sourceLoc.columnNo); // "column" } for (auto child : curNode->getChildren()) { nodeStack.push(child); } } endSection(Section::TraceFunctionInfos); beginSection(Section::TraceTree); nodeStack.push(stackTracesTree_->getRootNode()); while (!nodeStack.empty()) { auto curNode = nodeStack.top(); nodeStack.pop(); if (curNode == nullptr) { json_.closeArray(); continue; } json_.emitValue(curNode->id); auto sourceLocIdxIt = sourceLocToFuncIdxMap.find(curNode->sourceLoc); assert( sourceLocIdxIt != sourceLocToFuncIdxMap.end() && "Could not find trace function info ID for sourceLoc"); // This index must correspond to the "function_id" emitted in the // "trace_function_infos" section. json_.emitValue(sourceLocIdxIt->second); // "function_info_index" json_.emitValue(traceNodeStats_[curNode->id].count); // "count" json_.emitValue(traceNodeStats_[curNode->id].size); // "size" json_.openArray(); nodeStack.push(nullptr); for (auto child : curNode->getChildren()) { nodeStack.push(child); } } endSection(Section::TraceTree); } void HeapSnapshot::emitStrings() { beginSection(Section::Strings); for (const auto &str : *stringTable_) { json_.emitValue(str); } endSection(Section::Strings); } ChromeSamplingMemoryProfile::ChromeSamplingMemoryProfile(JSONEmitter &json) : json_(json) { json_.openDict(); } ChromeSamplingMemoryProfile::~ChromeSamplingMemoryProfile() { json_.closeDict(); } void ChromeSamplingMemoryProfile::emitTree( StackTracesTree *stackTracesTree, const llvh::DenseMap<StackTracesTreeNode *, llvh::DenseMap<size_t, size_t>> &sizesToCounts) { json_.emitKey("head"); emitNode( stackTracesTree->getRootNode(), *stackTracesTree->getStringTable(), sizesToCounts); } void ChromeSamplingMemoryProfile::emitNode( StackTracesTreeNode *node, StringSetVector &strings, const llvh::DenseMap<StackTracesTreeNode *, llvh::DenseMap<size_t, size_t>> &sizesToCounts) { json_.openDict(); json_.emitKey("callFrame"); json_.openDict(); json_.emitKeyValue("functionName", strings[node->name]); json_.emitKeyValue("scriptId", std::to_string(node->sourceLoc.scriptID)); json_.emitKeyValue("url", strings[node->sourceLoc.scriptName]); // For the sampling memory profiler, lines should be 0-based. The source // location is 1-based, so subtract 1 here. json_.emitKeyValue("lineNumber", node->sourceLoc.lineNo - 1); json_.emitKeyValue("columnNumber", node->sourceLoc.columnNo - 1); json_.closeDict(); size_t selfSize = 0; for (const auto &sizeAndCount : sizesToCounts.lookup(node)) { // Size is the key, count is the value. selfSize += sizeAndCount.first * sizeAndCount.second; } json_.emitKeyValue("selfSize", selfSize); json_.emitKeyValue("id", node->id); json_.emitKey("children"); json_.openArray(); for (StackTracesTreeNode *child : node->getChildren()) { emitNode(child, strings, sizesToCounts); } json_.closeArray(); json_.closeDict(); } void ChromeSamplingMemoryProfile::beginSamples() { json_.emitKey("samples"); json_.openArray(); } void ChromeSamplingMemoryProfile::emitSample( size_t size, StackTracesTreeNode *node, uint64_t id) { json_.openDict(); json_.emitKeyValue("size", size); json_.emitKeyValue("nodeId", node->id); json_.emitKeyValue("ordinal", id); json_.closeDict(); } void ChromeSamplingMemoryProfile::endSamples() { json_.closeArray(); } std::string converter(const char *name) { return std::string(name); } std::string converter(unsigned index) { return std::to_string(index); } std::string converter(int index) { return std::to_string(index); } std::string converter(const StringPrimitive *str) { llvh::SmallVector<char16_t, 16> buf; str->appendUTF16String(buf); std::string out; convertUTF16ToUTF8WithReplacements(out, UTF16Ref(buf)); return out; } std::string converter(const UTF16Ref ref) { std::string out; convertUTF16ToUTF8WithReplacements(out, ref); return out; } } // namespace vm } // namespace hermes