unittests/VMRuntime/HeapSnapshotTest.cpp (1,110 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 "TestHelpers.h"
#include "gtest/gtest.h"
#include "hermes/Parser/JSONParser.h"
#include "hermes/Support/Algorithms.h"
#include "hermes/Support/Allocator.h"
#include "hermes/Support/Compiler.h"
#include "hermes/VM/CellKind.h"
#include "hermes/VM/DummyObject.h"
#include "hermes/VM/GC.h"
#include "hermes/VM/GCPointer-inline.h"
#include "hermes/VM/HermesValue.h"
#include "hermes/VM/JSWeakMapImpl.h"
#include "hermes/VM/SymbolID.h"
#include "llvh/ADT/StringRef.h"
#include "llvh/Support/raw_ostream.h"
#include <chrono>
#include <set>
#include <sstream>
using namespace hermes::vm;
using namespace hermes::parser;
namespace hermes {
namespace unittest {
namespace heapsnapshottest {
using vm::testhelpers::DummyObject;
struct Node {
HeapSnapshot::NodeType type;
std::string name;
HeapSnapshot::NodeID id;
size_t selfSize;
size_t edgeCount;
size_t traceNodeID;
int detachedness;
Node() = default;
explicit Node(
HeapSnapshot::NodeType type,
std::string name,
HeapSnapshot::NodeID id,
size_t selfSize,
size_t edgeCount,
size_t traceNodeID = 0,
int detachedness = 0)
: type{type},
name{std::move(name)},
id{id},
selfSize{selfSize},
edgeCount{edgeCount},
traceNodeID{traceNodeID},
detachedness{detachedness} {}
static Node parse(JSONArray::iterator nodes, const JSONArray &strings) {
// Need two levels of cast for enums because Windows complains about casting
// doubles to enums.
auto type = static_cast<HeapSnapshot::NodeType>(
static_cast<unsigned>(llvh::cast<JSONNumber>(*nodes)->getValue()));
nodes++;
std::string name = llvh::cast<JSONString>(
strings[llvh::cast<JSONNumber>(*nodes)->getValue()])
->str();
nodes++;
auto id = static_cast<HeapSnapshot::NodeID>(
llvh::cast<JSONNumber>(*nodes)->getValue());
nodes++;
auto selfSize =
static_cast<size_t>(llvh::cast<JSONNumber>(*nodes)->getValue());
nodes++;
auto edgeCount =
static_cast<size_t>(llvh::cast<JSONNumber>(*nodes)->getValue());
nodes++;
auto traceNodeID =
static_cast<size_t>(llvh::cast<JSONNumber>(*nodes)->getValue());
nodes++;
auto detachedness =
static_cast<int>(llvh::cast<JSONNumber>(*nodes)->getValue());
return Node{
type,
std::move(name),
id,
selfSize,
edgeCount,
traceNodeID,
detachedness};
}
bool operator==(const Node &that) const {
return type == that.type && name == that.name && id == that.id &&
selfSize == that.selfSize && edgeCount == that.edgeCount &&
traceNodeID == that.traceNodeID && detachedness == that.detachedness;
}
bool operator<(const Node &that) const {
// Just IDs for comparison.
return id < that.id;
}
};
std::ostream &operator<<(std::ostream &os, const Node &node);
std::ostream &operator<<(std::ostream &os, const Node &node) {
return os << "Node{type=" << HeapSnapshot::nodeTypeToName(node.type)
<< ", name=\"" << node.name << "\", id=" << node.id
<< ", selfSize=" << node.selfSize
<< ", edgeCount=" << node.edgeCount
<< ", traceNodeID=" << node.traceNodeID
<< ", detachedness=" << node.detachedness << "}";
}
struct Edge {
HeapSnapshot::EdgeType type;
bool isNamed;
std::string name;
int index;
HeapSnapshot::NodeID toNode;
Edge() = default;
explicit Edge(
HeapSnapshot::EdgeType type,
std::string name,
HeapSnapshot::NodeID toNode)
: type{type},
isNamed{true},
name{std::move(name)},
index{-1},
toNode{toNode} {}
explicit Edge(
HeapSnapshot::EdgeType type,
int index,
HeapSnapshot::NodeID toNode)
: type{type}, isNamed{false}, name{}, index{index}, toNode{toNode} {}
static Edge parse(
JSONArray::iterator edges,
const JSONArray &nodes,
const JSONArray &strings) {
Edge edge;
// Need two levels of cast for enums because Windows complains about casting
// doubles to enums.
edge.type = static_cast<HeapSnapshot::EdgeType>(
static_cast<unsigned>(llvh::cast<JSONNumber>(*edges)->getValue()));
++edges;
switch (edge.type) {
case HeapSnapshot::EdgeType::Context:
case HeapSnapshot::EdgeType::Internal:
case HeapSnapshot::EdgeType::Property:
case HeapSnapshot::EdgeType::Shortcut:
case HeapSnapshot::EdgeType::Weak:
edge.isNamed = true;
edge.name = llvh::cast<JSONString>(
strings[llvh::cast<JSONNumber>(*edges)->getValue()])
->str();
edge.index = -1;
break;
default:
edge.isNamed = false;
// Leave name as the empty string.
edge.index = llvh::cast<JSONNumber>(*edges)->getValue();
break;
}
++edges;
uint32_t toNode = llvh::cast<JSONNumber>(*edges)->getValue();
assert(
toNode % HeapSnapshot::V8_SNAPSHOT_NODE_FIELD_COUNT == 0 &&
"Invalid to node pointer");
assert(toNode < nodes.size() && "Out-of-bounds node from edge");
edge.toNode = Node::parse(nodes.begin() + toNode, strings).id;
return edge;
}
bool operator==(const Edge &that) const {
return type == that.type && isNamed == that.isNamed && name == that.name &&
index == that.index && toNode == that.toNode;
}
bool operator<(const Edge &that) const {
// Just toNode for comparison.
return toNode < that.toNode;
}
};
std::ostream &operator<<(std::ostream &os, const Edge &edge);
std::ostream &operator<<(std::ostream &os, const Edge &edge) {
std::ios_base::fmtflags f(os.flags());
os << std::boolalpha
<< "Edge{type=" << HeapSnapshot::edgeTypeToName(edge.type)
<< ", isNamed=" << edge.isNamed << ", name=\"" << edge.name
<< "\", index=" << edge.index << ", toNode=" << edge.toNode << "}";
// Reset original flags to remove the boolalpha.
os.flags(f);
return os;
}
struct Sample {
std::chrono::microseconds timestamp;
HeapSnapshot::NodeID lastSeenObjectID;
explicit Sample(
std::chrono::microseconds timestamp,
HeapSnapshot::NodeID lastSeenObjectID)
: timestamp(timestamp), lastSeenObjectID(lastSeenObjectID) {}
static Sample parse(JSONArray::iterator samples) {
std::chrono::microseconds timestamp{
static_cast<uint64_t>(llvh::cast<JSONNumber>(*samples)->getValue())};
samples++;
auto lastSeenObjectID = static_cast<HeapSnapshot::NodeID>(
llvh::cast<JSONNumber>(*samples)->getValue());
samples++;
return Sample{timestamp, lastSeenObjectID};
}
};
struct Location {
Node object;
facebook::hermes::debugger::ScriptID scriptID;
int line;
int column;
Location() = default;
explicit Location(
Node object,
facebook::hermes::debugger::ScriptID scriptID,
int line,
int column)
: object{object}, scriptID{scriptID}, line{line}, column{column} {}
static Location parse(
JSONArray::iterator locations,
const JSONArray &nodes,
const JSONArray &strings) {
Location loc;
size_t objectIndex =
static_cast<size_t>(llvh::cast<JSONNumber>(*locations)->getValue());
loc.object = Node::parse(nodes.begin() + objectIndex, strings);
locations++;
loc.scriptID = llvh::cast<JSONNumber>(*locations)->getValue();
locations++;
// Line numbers and column numbers are 0-based internally,
// but 1-based when viewed.
loc.line = llvh::cast<JSONNumber>(*locations)->getValue() + 1;
locations++;
loc.column = llvh::cast<JSONNumber>(*locations)->getValue() + 1;
locations++;
return loc;
}
bool operator==(const Location &that) const {
return object == that.object && scriptID == that.scriptID &&
line == that.line && column == that.column;
}
};
std::ostream &operator<<(std::ostream &os, const Location &loc);
std::ostream &operator<<(std::ostream &os, const Location &loc) {
return os << "Location{object=" << loc.object << ", scriptID=" << loc.scriptID
<< ", line=" << loc.line << ", column=" << loc.column << "}";
}
static ::testing::AssertionResult testListOfStrings(
JSONArray::iterator begin,
JSONArray::iterator end,
std::initializer_list<llvh::StringRef> strs) {
EXPECT_EQ(static_cast<unsigned long>(end - begin), strs.size());
auto strsIt = strs.begin();
for (auto it = begin; it != end; ++it) {
EXPECT_EQ(llvh::cast<JSONString>(*it)->str(), *strsIt);
++strsIt;
}
return ::testing::AssertionSuccess();
}
static ::testing::AssertionResult testListOfStrings(
const JSONArray &arr,
std::initializer_list<llvh::StringRef> strs) {
return testListOfStrings(arr.begin(), arr.end(), strs);
}
static Node findNodeForID(
HeapSnapshot::NodeID id,
const JSONArray &nodes,
const JSONArray &strings,
const char *file,
int line) {
for (auto it = nodes.begin(), e = nodes.end(); it != e;
it += HeapSnapshot::V8_SNAPSHOT_NODE_FIELD_COUNT) {
auto node = Node::parse(it, strings);
if (id == node.id) {
return node;
}
}
ADD_FAILURE_AT(file, line) << "No node found for id " << id;
return Node{};
}
static std::pair<Node, std::vector<Edge>> findNodeAndEdgesForID(
HeapSnapshot::NodeID id,
const JSONArray &nodes,
const JSONArray &edges,
const JSONArray &strings,
const char *file,
int line) {
auto nodesIt = nodes.begin();
const auto nodesEnd = nodes.end();
auto edgesIt = edges.begin();
while (nodesIt != nodesEnd) {
auto node = Node::parse(nodesIt, strings);
const auto nextNodesEdges =
edgesIt + (HeapSnapshot::V8_SNAPSHOT_EDGE_FIELD_COUNT * node.edgeCount);
if (id == node.id) {
std::vector<Edge> retEdges;
for (; edgesIt != nextNodesEdges;
edgesIt += HeapSnapshot::V8_SNAPSHOT_EDGE_FIELD_COUNT) {
assert(edgesIt != edges.end() && "Edges shouldn't roll off the end");
retEdges.emplace_back(Edge::parse(edgesIt, nodes, strings));
}
return {node, retEdges};
}
nodesIt += HeapSnapshot::V8_SNAPSHOT_NODE_FIELD_COUNT;
edgesIt = nextNodesEdges;
}
ADD_FAILURE_AT(file, line) << "No node found for id " << id;
return {Node{}, std::vector<Edge>{}};
}
#define FIND_NODE_FOR_ID(...) findNodeForID(__VA_ARGS__, __FILE__, __LINE__)
#define FIND_NODE_AND_EDGES_FOR_ID(...) \
findNodeAndEdgesForID(__VA_ARGS__, __FILE__, __LINE__)
static Location findLocationForID(
HeapSnapshot::NodeID id,
const JSONArray &locations,
const JSONArray &nodes,
const JSONArray &strings,
const char *file,
int line) {
for (auto it = locations.begin(), e = locations.end(); it != e;
it += HeapSnapshot::V8_SNAPSHOT_LOCATION_FIELD_COUNT) {
Location loc = Location::parse(it, nodes, strings);
if (loc.object.id == id) {
return loc;
}
}
ADD_FAILURE_AT(file, line) << "Location for id " << id << " not found";
return Location{};
}
#define FIND_LOCATION_FOR_ID(...) \
findLocationForID(__VA_ARGS__, __FILE__, __LINE__)
static JSONObject *parseSnapshot(
const std::string &json,
JSONFactory &factory,
const char *file,
int line) {
if (json.empty()) {
ADD_FAILURE_AT(file, line) << "Snapshot wasn't written out";
return nullptr;
}
SourceErrorManager sm;
JSONParser parser{factory, json, sm};
auto optSnapshot = parser.parse();
if (!optSnapshot) {
ADD_FAILURE_AT(file, line) << "Snapshot isn't valid JSON";
return nullptr;
}
JSONValue *root = optSnapshot.getValue();
if (!llvh::isa<JSONObject>(root)) {
ADD_FAILURE_AT(file, line) << "Snapshot isn't a JSON object";
return nullptr;
}
return llvh::cast<JSONObject>(root);
}
static JSONObject *
takeSnapshot(GC &gc, JSONFactory &factory, const char *file, int line) {
std::string result("");
llvh::raw_string_ostream str(result);
gc.collect("test");
gc.createSnapshot(str);
str.flush();
return parseSnapshot(result, factory, file, line);
}
#define PARSE_SNAPSHOT(...) parseSnapshot(__VA_ARGS__, __FILE__, __LINE__)
#define TAKE_SNAPSHOT(...) takeSnapshot(__VA_ARGS__, __FILE__, __LINE__)
TEST(HeapSnapshotTest, IDReversibleTest) {
// Make sure an ID <-> Object mapping is preserved across collections.
auto runtime = DummyRuntime::create(GCConfig::Builder()
.withInitHeapSize(1024)
.withMaxHeapSize(1024 * 100)
.build());
DummyRuntime &rt = *runtime;
auto &gc = rt.getHeap();
GCScope gcScope(rt);
// Make a dummy object.
auto obj = rt.makeHandle(DummyObject::create(&gc));
const auto objID = gc.getObjectID(obj.get());
// Make sure the ID can be translated back to the object pointer.
EXPECT_EQ(obj.get(), gc.getObjectForID(objID));
// Run a collection to move things around.
gc.collect("test");
// Test that the ID is the same and it can be reversed.
EXPECT_EQ(objID, gc.getObjectID(obj.get()));
EXPECT_EQ(obj.get(), gc.getObjectForID(objID));
}
TEST(HeapSnapshotTest, HeaderTest) {
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
auto runtime = DummyRuntime::create(GCConfig::Builder()
.withInitHeapSize(1024)
.withMaxHeapSize(1024 * 100)
.build());
DummyRuntime &rt = *runtime;
auto &gc = rt.getHeap();
JSONObject *root = TAKE_SNAPSHOT(gc, jsonFactory);
ASSERT_TRUE(root != nullptr);
JSONObject *snapshot = llvh::cast<JSONObject>(root->at("snapshot"));
EXPECT_EQ(llvh::cast<JSONNumber>(snapshot->at("node_count"))->getValue(), 0);
EXPECT_EQ(llvh::cast<JSONNumber>(snapshot->at("edge_count"))->getValue(), 0);
EXPECT_EQ(
llvh::cast<JSONNumber>(snapshot->at("trace_function_count"))->getValue(),
0);
JSONObject *meta = llvh::cast<JSONObject>(snapshot->at("meta"));
EXPECT_EQ(llvh::cast<JSONArray>(meta->at("sample_fields"))->size(), 2);
JSONArray &nodeFields = *llvh::cast<JSONArray>(meta->at("node_fields"));
JSONArray &nodeTypes = *llvh::cast<JSONArray>(meta->at("node_types"));
JSONArray &edgeFields = *llvh::cast<JSONArray>(meta->at("edge_fields"));
JSONArray &edgeTypes = *llvh::cast<JSONArray>(meta->at("edge_types"));
JSONArray &traceFunctionInfoFields =
*llvh::cast<JSONArray>(meta->at("trace_function_info_fields"));
JSONArray &traceNodeFields =
*llvh::cast<JSONArray>(meta->at("trace_node_fields"));
JSONArray &locationFields =
*llvh::cast<JSONArray>(meta->at("location_fields"));
JSONArray &nodes = *llvh::cast<JSONArray>(root->at("nodes"));
JSONArray &edges = *llvh::cast<JSONArray>(root->at("edges"));
const JSONArray &strings = *llvh::cast<JSONArray>(root->at("strings"));
// Check that node_fields/types are correct.
EXPECT_TRUE(testListOfStrings(
nodeFields,
{"type",
"name",
"id",
"self_size",
"edge_count",
"trace_node_id",
"detachedness"}));
const JSONArray &nodeTypeEnum = *llvh::cast<JSONArray>(nodeTypes[0]);
EXPECT_TRUE(testListOfStrings(
nodeTypeEnum,
{"hidden",
"array",
"string",
"object",
"code",
"closure",
"regexp",
"number",
"native",
"synthetic",
"concatenated string",
"sliced string",
"symbol",
"bigint"}));
EXPECT_TRUE(testListOfStrings(
nodeTypes.begin() + 1,
nodeTypes.end(),
{"string", "number", "number", "number", "number", "number"}));
// Check that edge_fields/types are correct.
EXPECT_TRUE(
testListOfStrings(edgeFields, {"type", "name_or_index", "to_node"}));
const JSONArray &edgeTypeEnum = *llvh::cast<JSONArray>(edgeTypes[0]);
EXPECT_TRUE(testListOfStrings(
edgeTypeEnum,
{"context",
"element",
"property",
"internal",
"hidden",
"shortcut",
"weak"}));
EXPECT_TRUE(testListOfStrings(
edgeTypes.begin() + 1, edgeTypes.end(), {"string_or_number", "node"}));
EXPECT_TRUE(testListOfStrings(
traceFunctionInfoFields,
{"function_id", "name", "script_name", "script_id", "line", "column"}));
EXPECT_TRUE(testListOfStrings(
traceNodeFields,
{"id", "function_info_index", "count", "size", "children"}));
EXPECT_TRUE(testListOfStrings(
locationFields, {"object_index", "script_id", "line", "column"}));
// Test the basic root nodes.
Node superRoot{
HeapSnapshot::NodeType::Synthetic,
"",
GC::IDTracker::reserved(GC::IDTracker::ReservedObjectID::SuperRoot),
0,
1};
Node gcRoots{
HeapSnapshot::NodeType::Synthetic,
"(GC roots)",
GC::IDTracker::reserved(GC::IDTracker::ReservedObjectID::GCRoots),
0,
1};
Node customRoots{
HeapSnapshot::NodeType::Synthetic,
"(Custom)",
GC::IDTracker::reserved(GC::IDTracker::ReservedObjectID::Custom),
0,
0};
EXPECT_EQ(
FIND_NODE_AND_EDGES_FOR_ID(superRoot.id, nodes, edges, strings),
std::make_pair(
superRoot,
std::vector<Edge>{
Edge{HeapSnapshot::EdgeType::Element, 1, gcRoots.id}}));
// Don't test edges here because they contain GC-specific nodes.
auto actualGCRootsNode = FIND_NODE_FOR_ID(gcRoots.id, nodes, strings);
// Since each individual GC can choose what edges
// exist, don't test the edge count.
gcRoots.edgeCount = actualGCRootsNode.edgeCount;
EXPECT_EQ(actualGCRootsNode, gcRoots);
EXPECT_EQ(
FIND_NODE_AND_EDGES_FOR_ID(customRoots.id, nodes, edges, strings),
std::make_pair(customRoots, std::vector<Edge>{}));
}
TEST(HeapSnapshotTest, TestNodesAndEdgesForDummyObjects) {
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
auto runtime = DummyRuntime::create(GCConfig::Builder()
.withInitHeapSize(1024)
.withMaxHeapSize(1024 * 100)
.build());
DummyRuntime &rt = *runtime;
auto &gc = rt.getHeap();
GCScope gcScope(rt);
auto dummy = rt.makeHandle(DummyObject::create(&gc));
auto *dummy2 = DummyObject::create(&gc);
dummy->setPointer(&gc, dummy2);
const auto blockSize = dummy->getAllocatedSize();
JSONObject *root = TAKE_SNAPSHOT(gc, jsonFactory);
ASSERT_TRUE(root != nullptr);
// Check the nodes and edges.
JSONArray &nodes = *llvh::cast<JSONArray>(root->at("nodes"));
JSONArray &edges = *llvh::cast<JSONArray>(root->at("edges"));
const JSONArray &strings = *llvh::cast<JSONArray>(root->at("strings"));
EXPECT_EQ(llvh::cast<JSONArray>(root->at("trace_function_infos"))->size(), 0);
EXPECT_EQ(llvh::cast<JSONArray>(root->at("trace_tree"))->size(), 0);
EXPECT_EQ(llvh::cast<JSONArray>(root->at("samples"))->size(), 0);
EXPECT_EQ(llvh::cast<JSONArray>(root->at("locations"))->size(), 0);
Node firstDummy{
HeapSnapshot::NodeType::Object,
cellKindStr(dummy->getKind()),
gc.getObjectID(dummy.get()),
blockSize,
// One edge to the second dummy, 4 for primitive singletons, and a WeakRef
// to self.
6};
Node undefinedNode{
HeapSnapshot::NodeType::Object,
"undefined",
GC::IDTracker::reserved(GC::IDTracker::ReservedObjectID::Undefined),
0,
0};
Node nullNode{
HeapSnapshot::NodeType::Object,
"null",
GC::IDTracker::reserved(GC::IDTracker::ReservedObjectID::Null),
0,
0};
Node trueNode(
HeapSnapshot::NodeType::Object,
"true",
GC::IDTracker::reserved(GC::IDTracker::ReservedObjectID::True),
0,
0);
Node numberNode{
HeapSnapshot::NodeType::Number,
"3.14",
gc.getIDTracker().getNumberID(dummy->hvDouble.getNumber()),
0,
0};
Node falseNode{
HeapSnapshot::NodeType::Object,
"false",
GC::IDTracker::reserved(GC::IDTracker::ReservedObjectID::False),
0,
0};
Node secondDummy{
HeapSnapshot::NodeType::Object,
cellKindStr(dummy->getKind()),
gc.getObjectID(dummy->other),
blockSize,
// No edges except for the primitive singletons and the WeakRef to self.
5};
// Common edges.
Edge trueEdge =
Edge(HeapSnapshot::EdgeType::Internal, "HermesBool", trueNode.id);
Edge numberEdge =
Edge(HeapSnapshot::EdgeType::Internal, "HermesDouble", numberNode.id);
Edge undefinedEdge = Edge(
HeapSnapshot::EdgeType::Internal, "HermesUndefined", undefinedNode.id);
Edge nullEdge =
Edge(HeapSnapshot::EdgeType::Internal, "HermesNull", nullNode.id);
// Two dummy objects.
EXPECT_EQ(
FIND_NODE_AND_EDGES_FOR_ID(firstDummy.id, nodes, edges, strings),
std::make_pair(
firstDummy,
std::vector<Edge>{
Edge{HeapSnapshot::EdgeType::Internal, "other", secondDummy.id},
trueEdge,
numberEdge,
undefinedEdge,
nullEdge,
Edge{HeapSnapshot::EdgeType::Weak, "0", firstDummy.id}}));
EXPECT_EQ(
FIND_NODE_AND_EDGES_FOR_ID(secondDummy.id, nodes, edges, strings),
std::make_pair(
secondDummy,
std::vector<Edge>{
trueEdge,
numberEdge,
undefinedEdge,
nullEdge,
Edge{HeapSnapshot::EdgeType::Weak, "0", secondDummy.id}}));
}
TEST(HeapSnapshotTest, SnapshotFromCallbackContext) {
bool triggeredTripwire = false;
std::ostringstream stream;
auto runtime = DummyRuntime::create(
kTestGCConfigSmall.rebuild()
.withTripwireConfig(GCTripwireConfig::Builder()
.withLimit(32)
.withCallback([&triggeredTripwire, &stream](
GCTripwireContext &ctx) {
triggeredTripwire = true;
ctx.createSnapshot(stream);
})
.build())
.build());
DummyRuntime &rt = *runtime;
GCScope scope{rt};
auto dummy = rt.makeHandle(DummyObject::create(&rt.getHeap()));
const auto dummyID = rt.getHeap().getObjectID(dummy.get());
rt.collect();
ASSERT_TRUE(triggeredTripwire);
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
JSONObject *root = PARSE_SNAPSHOT(stream.str(), jsonFactory);
ASSERT_TRUE(root != nullptr);
JSONArray &nodes = *llvh::cast<JSONArray>(root->at("nodes"));
const JSONArray &strings = *llvh::cast<JSONArray>(root->at("strings"));
// Check that the dummy object is in the snapshot.
auto dummyNode = FIND_NODE_FOR_ID(dummyID, nodes, strings);
Node expected{
HeapSnapshot::NodeType::Object,
"DummyObject",
dummyID,
dummy->getAllocatedSize(),
5};
EXPECT_EQ(dummyNode, expected);
}
using HeapSnapshotRuntimeTest = RuntimeTestFixture;
template <typename T>
size_t firstNamedPropertyEdge() {
// parent, __proto__, class, directProp$i
return JSObject::DIRECT_PROPERTY_SLOTS - JSObject::numOverlapSlots<T>() + 3;
}
TEST_F(HeapSnapshotRuntimeTest, FunctionLocationForLazyCode) {
// Similar test to the above, but for lazy-compiled source.
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
hbc::CompileFlags flags;
flags.debug = true;
flags.lazy = true;
// Build a function that will be lazily compiled.
std::string source = " function myGlobal() { ";
for (int i = 0; i < 100; i++)
source += " Math.random(); ";
source += "};\nmyGlobal;";
CallResult<HermesValue> res = runtime.run(source, "file:///fake.js", flags);
ASSERT_FALSE(isException(res));
Handle<JSFunction> func = runtime.makeHandle(vmcast<JSFunction>(*res));
const auto funcID = runtime.getHeap().getObjectID(func.get());
JSONObject *root = TAKE_SNAPSHOT(runtime.getHeap(), jsonFactory);
ASSERT_TRUE(root != nullptr);
const JSONArray &nodes = *llvh::cast<JSONArray>(root->at("nodes"));
const JSONArray &strings = *llvh::cast<JSONArray>(root->at("strings"));
// This test requires a location to be emitted.
auto node = FIND_NODE_FOR_ID(funcID, nodes, strings);
Node expected{
HeapSnapshot::NodeType::Closure,
"myGlobal",
funcID,
func->getAllocatedSize(),
firstNamedPropertyEdge<JSFunction>() + 3};
EXPECT_EQ(node, expected);
// Edges aren't tested in this test.
#ifdef HERMES_ENABLE_DEBUGGER
// The location isn't emitted in fully optimized builds.
const JSONArray &locations = *llvh::cast<JSONArray>(root->at("locations"));
Location loc = FIND_LOCATION_FOR_ID(funcID, locations, nodes, strings);
// The location should be the given file, at line 1 column 5 with indenting
auto scriptId = func->getRuntimeModule()->getScriptID();
EXPECT_EQ(loc, Location(expected, scriptId, 1, 5));
#else
(void)findLocationForID;
#endif
}
TEST_F(HeapSnapshotRuntimeTest, FunctionLocationAndNameTest) {
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
hbc::CompileFlags flags;
// Make sure that debug info is emitted for this source file when it's
// compiled.
flags.debug = true;
// Indent the function slightly to test that the source location is correct
CallResult<HermesValue> res =
runtime.run("\n function foo() {}; foo;", "file:///fake.js", flags);
ASSERT_FALSE(isException(res));
Handle<JSFunction> func = runtime.makeHandle(vmcast<JSFunction>(*res));
const auto funcID = runtime.getHeap().getObjectID(func.get());
JSONObject *root = TAKE_SNAPSHOT(runtime.getHeap(), jsonFactory);
ASSERT_TRUE(root != nullptr);
const JSONArray &nodes = *llvh::cast<JSONArray>(root->at("nodes"));
const JSONArray &strings = *llvh::cast<JSONArray>(root->at("strings"));
// This test requires a location to be emitted.
auto node = FIND_NODE_FOR_ID(funcID, nodes, strings);
Node expected{
HeapSnapshot::NodeType::Closure,
"foo",
funcID,
func->getAllocatedSize(),
firstNamedPropertyEdge<JSFunction>() + 3};
EXPECT_EQ(node, expected);
// Edges aren't tested in this test.
#ifdef HERMES_ENABLE_DEBUGGER
// The location isn't emitted in fully optimized builds.
const JSONArray &locations = *llvh::cast<JSONArray>(root->at("locations"));
Location loc = FIND_LOCATION_FOR_ID(funcID, locations, nodes, strings);
// The location should be the given file, second line, third column
auto scriptId = func->getRuntimeModule()->getScriptID();
EXPECT_EQ(loc, Location(expected, scriptId, 2, 3));
#else
(void)findLocationForID;
#endif
}
TEST_F(HeapSnapshotRuntimeTest, FunctionDisplayNameTest) {
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
hbc::CompileFlags flags;
// Make sure that debug info is emitted for this source file when it's
// compiled.
flags.debug = true;
CallResult<HermesValue> res = runtime.run(
R"(function foo() {}; foo.displayName = "bar"; foo;)",
"file:///fake.js",
flags);
ASSERT_FALSE(isException(res));
Handle<JSFunction> func = runtime.makeHandle(vmcast<JSFunction>(*res));
const auto funcID = runtime.getHeap().getObjectID(func.get());
JSONObject *root = TAKE_SNAPSHOT(runtime.getHeap(), jsonFactory);
ASSERT_TRUE(root != nullptr);
const JSONArray &nodes = *llvh::cast<JSONArray>(root->at("nodes"));
const JSONArray &strings = *llvh::cast<JSONArray>(root->at("strings"));
auto node = FIND_NODE_FOR_ID(funcID, nodes, strings);
Node expected{
HeapSnapshot::NodeType::Closure,
// Make sure the name that is reported is "bar", not "foo".
"bar",
funcID,
func->getAllocatedSize(),
firstNamedPropertyEdge<JSFunction>() + 8};
EXPECT_EQ(node, expected);
}
TEST_F(HeapSnapshotRuntimeTest, WeakMapTest) {
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
auto mapResult = JSWeakMap::create(
runtime, Handle<JSObject>::vmcast(&runtime.weakMapPrototype));
ASSERT_FALSE(isException(mapResult));
Handle<JSWeakMap> map = runtime.makeHandle(std::move(*mapResult));
Handle<JSObject> key = runtime.makeHandle(JSObject::create(runtime));
Handle<JSObject> value = runtime.makeHandle(JSObject::create(runtime));
// Add a key so the DenseMap will exist.
ASSERT_FALSE(isException(JSWeakMap::setValue(map, runtime, key, value)));
JSONObject *root = TAKE_SNAPSHOT(runtime.getHeap(), jsonFactory);
ASSERT_TRUE(root != nullptr);
const JSONArray &nodes = *llvh::cast<JSONArray>(root->at("nodes"));
const JSONArray &edges = *llvh::cast<JSONArray>(root->at("edges"));
const JSONArray &strings = *llvh::cast<JSONArray>(root->at("strings"));
const auto mapID = runtime.getHeap().getObjectID(map.get());
auto nodesAndEdges = FIND_NODE_AND_EDGES_FOR_ID(mapID, nodes, edges, strings);
auto firstNamed = firstNamedPropertyEdge<JSWeakMap>();
EXPECT_EQ(
nodesAndEdges.first,
Node(
HeapSnapshot::NodeType::Object,
"JSWeakMap",
mapID,
map->getAllocatedSize(),
firstNamed + 3));
EXPECT_EQ(nodesAndEdges.second.size(), firstNamed + 3);
// Test the weak edge.
EXPECT_EQ(
nodesAndEdges.second[firstNamed],
Edge(
HeapSnapshot::EdgeType::Weak,
"0",
runtime.getHeap().getObjectID(key.get())));
// Test the native edge.
const auto nativeMapID = map->getMapID(&runtime.getHeap());
EXPECT_EQ(
nodesAndEdges.second[firstNamed + 2],
Edge(HeapSnapshot::EdgeType::Internal, "map", nativeMapID));
EXPECT_EQ(
FIND_NODE_FOR_ID(nativeMapID, nodes, strings),
Node(
HeapSnapshot::NodeType::Native,
"DenseMap",
nativeMapID,
map->getMallocSize(),
0));
}
TEST_F(HeapSnapshotRuntimeTest, PropertyUpdatesTest) {
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
Handle<JSObject> obj = runtime.makeHandle(JSObject::create(runtime));
SymbolID fooSym, barSym;
{
vm::GCScope gcScope(runtime);
fooSym = vm::stringToSymbolID(
runtime, vm::StringPrimitive::createNoThrow(runtime, "foo"))
->getHermesValue()
.getSymbol();
barSym = vm::stringToSymbolID(
runtime, vm::StringPrimitive::createNoThrow(runtime, "bar"))
->getHermesValue()
.getSymbol();
}
DefinePropertyFlags dpf = DefinePropertyFlags::getDefaultNewPropertyFlags();
// Add two properties to the hidden class chain.
ASSERT_FALSE(isException(JSObject::defineOwnProperty(
obj,
runtime,
fooSym,
dpf,
runtime.makeHandle(HermesValue::encodeNumberValue(100)))));
ASSERT_FALSE(isException(JSObject::defineOwnProperty(
obj,
runtime,
barSym,
dpf,
runtime.makeHandle(HermesValue::encodeNumberValue(200)))));
// Trigger update transitions for both properties.
dpf.writable = false;
ASSERT_FALSE(isException(JSObject::defineOwnProperty(
obj,
runtime,
fooSym,
dpf,
runtime.makeHandle(HermesValue::encodeNumberValue(100)))));
ASSERT_FALSE(isException(JSObject::defineOwnProperty(
obj,
runtime,
barSym,
dpf,
runtime.makeHandle(HermesValue::encodeNumberValue(200)))));
// Forcibly clear the final hidden class's property map.
auto *clazz = obj->getClass(runtime);
clazz->clearPropertyMap(&runtime.getHeap());
JSONObject *root = TAKE_SNAPSHOT(runtime.getHeap(), jsonFactory);
ASSERT_TRUE(root != nullptr);
const JSONArray &nodes = *llvh::cast<JSONArray>(root->at("nodes"));
const JSONArray &edges = *llvh::cast<JSONArray>(root->at("edges"));
const JSONArray &strings = *llvh::cast<JSONArray>(root->at("strings"));
const auto objID = runtime.getHeap().getObjectID(obj.get());
auto nodesAndEdges = FIND_NODE_AND_EDGES_FOR_ID(objID, nodes, edges, strings);
const auto FIRST_NAMED_PROPERTY_EDGE = firstNamedPropertyEdge<JSObject>();
EXPECT_EQ(
nodesAndEdges.first,
Node(
HeapSnapshot::NodeType::Object,
"JSObject(foo, bar)",
objID,
obj->getAllocatedSize(),
FIRST_NAMED_PROPERTY_EDGE + 2));
EXPECT_EQ(nodesAndEdges.second.size(), FIRST_NAMED_PROPERTY_EDGE + 2);
EXPECT_EQ(
nodesAndEdges.second[FIRST_NAMED_PROPERTY_EDGE],
Edge(
HeapSnapshot::EdgeType::Property,
"foo",
runtime.getHeap().getIDTracker().getNumberID(100)));
EXPECT_EQ(
nodesAndEdges.second[FIRST_NAMED_PROPERTY_EDGE + 1],
Edge(
HeapSnapshot::EdgeType::Property,
"bar",
runtime.getHeap().getIDTracker().getNumberID(200)));
}
TEST_F(HeapSnapshotRuntimeTest, ArrayElements) {
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
hbc::CompileFlags flags;
// Build an array that doesn't start at index 0.
std::string source =
"var a = []; a[10] = {}; a[15] = {}; a[(1 << 20) + 1000] = {}; a";
CallResult<HermesValue> res = runtime.run(source, "file:///fake.js", flags);
ASSERT_FALSE(isException(res));
Handle<JSArray> array = runtime.makeHandle(vmcast<JSArray>(*res));
Handle<JSObject> firstElement =
runtime.makeHandle<JSObject>(array->at(runtime, 10));
Handle<JSObject> secondElement =
runtime.makeHandle<JSObject>(array->at(runtime, 15));
auto cr = JSObject::getComputed_RJS(
array,
runtime,
runtime.makeHandle(HermesValue::encodeNumberValue((1 << 20) + 1000)));
ASSERT_FALSE(isException(cr));
Handle<JSObject> thirdElement = runtime.makeHandle<JSObject>(std::move(*cr));
const auto arrayID = runtime.getHeap().getObjectID(array.get());
JSONObject *root = TAKE_SNAPSHOT(runtime.getHeap(), jsonFactory);
ASSERT_TRUE(root != nullptr);
const JSONArray &nodes = *llvh::cast<JSONArray>(root->at("nodes"));
const JSONArray &edges = *llvh::cast<JSONArray>(root->at("edges"));
const JSONArray &strings = *llvh::cast<JSONArray>(root->at("strings"));
const auto FIRST_NAMED_PROPERTY_EDGE = firstNamedPropertyEdge<JSArray>();
auto nodeAndEdges =
FIND_NODE_AND_EDGES_FOR_ID(arrayID, nodes, edges, strings);
EXPECT_EQ(
nodeAndEdges.first,
Node(
HeapSnapshot::NodeType::Object,
"JSArray",
arrayID,
array->getAllocatedSize(),
FIRST_NAMED_PROPERTY_EDGE + 6));
// The last edges are the element edges.
EXPECT_EQ(
nodeAndEdges.second[FIRST_NAMED_PROPERTY_EDGE + 2],
Edge(
HeapSnapshot::EdgeType::Element,
(1 << 20) + 1000,
runtime.getHeap().getObjectID(thirdElement.get())));
EXPECT_EQ(
nodeAndEdges.second[FIRST_NAMED_PROPERTY_EDGE + 4],
Edge(
HeapSnapshot::EdgeType::Element,
10,
runtime.getHeap().getObjectID(firstElement.get())));
EXPECT_EQ(
nodeAndEdges.second[FIRST_NAMED_PROPERTY_EDGE + 5],
Edge(
HeapSnapshot::EdgeType::Element,
15,
runtime.getHeap().getObjectID(secondElement.get())));
}
#ifdef HERMES_ENABLE_DEBUGGER
static HeapSnapshot::NodeID findHighestNodeID(
const JSONArray &nodes,
const JSONArray &strings) {
HeapSnapshot::NodeID maxID = GCBase::IDTracker::kInvalidNode;
for (auto it = nodes.begin(), e = nodes.end(); it != e;
it += HeapSnapshot::V8_SNAPSHOT_NODE_FIELD_COUNT) {
auto node = Node::parse(it, strings);
if (node.id > maxID) {
maxID = node.id;
}
}
return maxID;
}
static std::string functionInfoToString(
int idx,
const JSONArray &traceFunctionInfos,
const JSONArray &strings) {
auto base = idx * 6;
int functionID = llvh::cast<JSONNumber>(traceFunctionInfos[base])->getValue();
assert(
functionID == idx &&
"The function info must have a matching index and id");
auto name = llvh::cast<JSONString>(
strings[llvh::cast<JSONNumber>(traceFunctionInfos[base + 1])
->getValue()])
->str();
auto scriptName =
llvh::cast<JSONString>(
strings[llvh::cast<JSONNumber>(traceFunctionInfos[base + 2])
->getValue()])
->str();
auto scriptID =
llvh::cast<JSONNumber>(traceFunctionInfos[base + 3])->getValue();
auto line = llvh::cast<JSONNumber>(traceFunctionInfos[base + 4])->getValue();
auto col = llvh::cast<JSONNumber>(traceFunctionInfos[base + 5])->getValue();
return std::string(name) + "(" + std::to_string((int)functionID) + ") @ " +
std::string(scriptName) + "(" + std::to_string((int)scriptID) +
"):" + std::to_string((int)line) + ":" + std::to_string((int)col);
}
struct ChromeStackTreeNode {
ChromeStackTreeNode(ChromeStackTreeNode *parent, int traceFunctionInfosId)
: parent_(parent), traceFunctionInfosId_(traceFunctionInfosId) {}
static std::vector<std::unique_ptr<ChromeStackTreeNode>> parse(
const JSONArray &traceNodes,
ChromeStackTreeNode *parent,
std::map<int, ChromeStackTreeNode *> &idNodeMap) {
std::vector<std::unique_ptr<ChromeStackTreeNode>> res;
if (!parent) {
assert(
traceNodes.size() == 5 &&
"Allocation trace should only have a"
"single root node");
}
for (size_t i = 0; i < traceNodes.size(); i += 5) {
auto id = llvh::cast<JSONNumber>(traceNodes[i])->getValue();
auto functionInfoIndex =
llvh::cast<JSONNumber>(traceNodes[i + 1])->getValue();
auto children = llvh::cast<JSONArray>(traceNodes[i + 4]);
auto treeNode =
std::make_unique<ChromeStackTreeNode>(parent, functionInfoIndex);
idNodeMap.emplace(id, treeNode.get());
treeNode->children_ = parse(*children, treeNode.get(), idNodeMap);
res.emplace_back(std::move(treeNode));
}
return res;
};
std::string buildStackTrace(
const JSONArray &traceFunctionInfos,
const JSONArray &strings) {
std::string res =
parent_ ? parent_->buildStackTrace(traceFunctionInfos, strings) : "";
res += "\n" +
functionInfoToString(
traceFunctionInfosId_, traceFunctionInfos, strings);
return res;
};
private:
ChromeStackTreeNode *parent_;
int traceFunctionInfosId_;
std::vector<std::unique_ptr<ChromeStackTreeNode>> children_;
};
TEST_F(HeapSnapshotRuntimeTest, AllocationTraces) {
runtime.enableAllocationLocationTracker();
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
hbc::CompileFlags flags;
CallResult<HermesValue> res = runtime.run(
R"#(
function foo() {
return new Object();
}
function bar() {
return new Object();
}
function baz() {
return {foo: foo(), bar: bar()};
}
baz();
)#",
"test.js",
flags);
ASSERT_FALSE(isException(res));
ASSERT_TRUE(res->isObject());
Handle<JSObject> resObj = runtime.makeHandle(vmcast<JSObject>(*res));
SymbolID fooSym, barSym;
{
vm::GCScope gcScope(runtime);
fooSym = vm::stringToSymbolID(
runtime, vm::StringPrimitive::createNoThrow(runtime, "foo"))
->getHermesValue()
.getSymbol();
barSym = vm::stringToSymbolID(
runtime, vm::StringPrimitive::createNoThrow(runtime, "bar"))
->getHermesValue()
.getSymbol();
}
auto fooObj = JSObject::getNamed_RJS(resObj, runtime, fooSym);
auto barObj = JSObject::getNamed_RJS(resObj, runtime, barSym);
auto fooObjID =
runtime.getHeap().getObjectID(vmcast<JSObject>(fooObj->get()));
auto barObjID =
runtime.getHeap().getObjectID(vmcast<JSObject>(barObj->get()));
JSONObject *root = TAKE_SNAPSHOT(runtime.getHeap(), jsonFactory);
ASSERT_TRUE(root != nullptr);
const JSONArray &nodes = *llvh::cast<JSONArray>(root->at("nodes"));
const JSONArray &strings = *llvh::cast<JSONArray>(root->at("strings"));
const JSONArray &traceFunctionInfos =
*llvh::cast<JSONArray>(root->at("trace_function_infos"));
std::map<int, ChromeStackTreeNode *> idNodeMap;
auto roots = ChromeStackTreeNode::parse(
*llvh::cast<JSONArray>(root->at("trace_tree")), nullptr, idNodeMap);
auto fooAllocNode = FIND_NODE_FOR_ID(fooObjID, nodes, strings);
auto fooStackTreeNode = idNodeMap.find(fooAllocNode.traceNodeID);
ASSERT_NE(fooStackTreeNode, idNodeMap.end());
auto fooStackStr =
fooStackTreeNode->second->buildStackTrace(traceFunctionInfos, strings);
EXPECT_STREQ(
fooStackStr.c_str(),
R"#(
(root)(0) @ (0):0:0
global(1) @ test.js(2):2:1
global(2) @ test.js(2):11:4
baz(7) @ test.js(2):9:19
foo(8) @ test.js(2):3:20)#");
auto barAllocNode = FIND_NODE_FOR_ID(barObjID, nodes, strings);
auto barStackTreeNode = idNodeMap.find(barAllocNode.traceNodeID);
ASSERT_NE(barStackTreeNode, idNodeMap.end());
auto barStackStr =
barStackTreeNode->second->buildStackTrace(traceFunctionInfos, strings);
ASSERT_STREQ(
barStackStr.c_str(),
R"#(
(root)(0) @ (0):0:0
global(1) @ test.js(2):2:1
global(2) @ test.js(2):11:4
baz(4) @ test.js(2):9:31
bar(5) @ test.js(2):6:20)#");
const JSONArray &samples = *llvh::cast<JSONArray>(root->at("samples"));
// Must have at least one sample
EXPECT_GT(samples.size(), 0u);
for (auto it = samples.begin(), e = samples.end(); it != e;
it += HeapSnapshot::V8_SNAPSHOT_SAMPLE_FIELD_COUNT) {
auto sample = Sample::parse(it);
EXPECT_NE(sample.lastSeenObjectID, toRValue(GC::IDTracker::kInvalidNode));
if (it != samples.begin()) {
auto prevSample =
Sample::parse(it - HeapSnapshot::V8_SNAPSHOT_SAMPLE_FIELD_COUNT);
EXPECT_GT(sample.timestamp, prevSample.timestamp);
EXPECT_GT(sample.lastSeenObjectID, prevSample.lastSeenObjectID);
}
}
auto highestNodeID = findHighestNodeID(nodes, strings);
auto lastSample = Sample::parse(
samples.end() - HeapSnapshot::V8_SNAPSHOT_SAMPLE_FIELD_COUNT);
EXPECT_GE(lastSample.lastSeenObjectID, highestNodeID);
}
TEST_F(HeapSnapshotRuntimeTest, TwoPathsToFunction) {
runtime.enableAllocationLocationTracker();
JSONFactory::Allocator alloc;
JSONFactory jsonFactory{alloc};
hbc::CompileFlags flags;
CallResult<HermesValue> res = runtime.run(
R"#(
var objects = [];
function A() {
B(A);
}
function B(allocatingFunction) {
objects.push({ AllocatingFunction: allocatingFunction });
}
function C() {
B(C);
}
function D() {
B(D);
}
A();
C();
D();
objects[0];
)#",
"test.js",
flags);
ASSERT_FALSE(isException(res));
ASSERT_TRUE(res->isObject());
Handle<JSObject> obj = runtime.makeHandle(vmcast<JSObject>(*res));
auto objID = runtime.getHeap().getObjectID(*obj);
JSONObject *root = TAKE_SNAPSHOT(runtime.getHeap(), jsonFactory);
ASSERT_TRUE(root != nullptr);
const JSONArray &nodes = *llvh::cast<JSONArray>(root->at("nodes"));
const JSONArray &strings = *llvh::cast<JSONArray>(root->at("strings"));
const JSONArray &traceFunctionInfos =
*llvh::cast<JSONArray>(root->at("trace_function_infos"));
std::map<int, ChromeStackTreeNode *> idNodeMap;
auto roots = ChromeStackTreeNode::parse(
*llvh::cast<JSONArray>(root->at("trace_tree")), nullptr, idNodeMap);
auto allocNode = FIND_NODE_FOR_ID(objID, nodes, strings);
auto stackTreeNode = idNodeMap.find(allocNode.traceNodeID);
ASSERT_NE(stackTreeNode, idNodeMap.end());
auto stackStr =
stackTreeNode->second->buildStackTrace(traceFunctionInfos, strings);
EXPECT_STREQ(
stackStr.c_str(),
R"#(
(root)(0) @ (0):0:0
global(1) @ test.js(2):2:1
global(10) @ test.js(2):15:2
A(11) @ test.js(2):4:4
B(4) @ test.js(2):7:15)#");
}
#endif // HERMES_ENABLE_DEBUGGER
} // namespace heapsnapshottest
} // namespace unittest
} // namespace hermes