Skip to content

Commit

Permalink
Have the root be emitted as part of each allocation stack
Browse files Browse the repository at this point in the history
Summary:
For the heap timeline format, Chrome expects there to only be a single
root node, and it is the only node with children.

To do this, just emit the root that already exists as part of the output.
Rename it from `(invalid function name)` to `(root)` to match V8's output.

Reviewed By: neildhar

Differential Revision: D23882605

fbshipit-source-id: a9458d93d6244528c7331b45f459b8bd500e4edc
  • Loading branch information
dulinriley authored and facebook-github-bot committed Oct 2, 2020
1 parent 112abcb commit 801aa8a
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 35 deletions.
10 changes: 5 additions & 5 deletions include/hermes/VM/StackTracesTree.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,24 +93,24 @@ struct StackTracesTree {
std::shared_ptr<StringSetVector> strings_;

/// Pre-computed string IDs
const StringSetVector::size_type invalidFunctionID_;
const StringSetVector::size_type invalidScriptNameID_;
const StringSetVector::size_type rootFunctionID_;
const StringSetVector::size_type rootScriptNameID_;
const StringSetVector::size_type nativeFunctionID_;
const StringSetVector::size_type anonymousFunctionID_;

/// Every node in the try gets an ID which is used when writing out snapshot
/// data for Chrome.
size_t nextNodeID_{0};
size_t nextNodeID_{1};

/// The root of the tree is a sentinel which is always present and does not
/// represent a valid code location.
std::unique_ptr<StackTracesTreeNode> root_{new StackTracesTreeNode(
nextNodeID_++,
nullptr, /* parent */
{invalidScriptNameID_, 0, -1, -1},
{rootScriptNameID_, 0, 0, 0},
nullptr, /* codeBlock */
nullptr, /* ip */
invalidFunctionID_)};
rootFunctionID_)};

/// Current head of the tree, typically representing the last known call-site
/// in bytecode execution.
Expand Down
5 changes: 1 addition & 4 deletions lib/VM/HeapSnapshot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -394,10 +394,7 @@ void HeapSnapshot::emitAllocationTraceInfo() {
endSection(Section::TraceFunctionInfos);

beginSection(Section::TraceTree);
// Start from the nodes below the sentinel node as this is always invalid
for (auto child : stackTracesTree_->getRootNode()->getChildren()) {
nodeStack.push(child);
}
nodeStack.push(stackTracesTree_->getRootNode());
while (!nodeStack.empty()) {
auto curNode = nodeStack.top();
nodeStack.pop();
Expand Down
4 changes: 2 additions & 2 deletions lib/VM/StackTracesTree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ void StackTracesTreeNode::addMapping(

StackTracesTree::StackTracesTree()
: strings_(std::make_shared<StringSetVector>()),
invalidFunctionID_(strings_->insert("(invalid function name)")),
invalidScriptNameID_(strings_->insert("(invalid script name)")),
rootFunctionID_(strings_->insert("(root)")),
rootScriptNameID_(strings_->insert("")),
nativeFunctionID_(strings_->insert("(native)")),
anonymousFunctionID_(strings_->insert("(anonymous)")),
head_(root_.get()) {}
Expand Down
187 changes: 187 additions & 0 deletions unittests/API/HeapSnapshotAPITest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#ifdef HERMES_ENABLE_DEBUGGER

#include <folly/dynamic.h>
#include <folly/json.h>
#include <gtest/gtest.h>
#include <hermes/CompileJS.h>
#include <hermes/hermes.h>
#include <jsi/instrumentation.h>

using namespace facebook::jsi;
using namespace facebook::hermes;

class HeapSnapshotAPITest : public ::testing::TestWithParam<bool> {
public:
HeapSnapshotAPITest()
: rt(makeHermesRuntime(
::hermes::vm::RuntimeConfig::Builder()
.withGCConfig(::hermes::vm::GCConfig::Builder()
.withAllocationLocationTrackerFromStart(
trackingFromBeginning())
.build())
.withES6Proxy(true)
.build())) {}

protected:
Value eval(const char *code) {
return rt->evaluateJavaScript(
std::make_unique<StringBuffer>(code), "test.js");
}

bool trackingFromBeginning() const {
return GetParam();
}

void startTrackingHeapObjects() {
if (!trackingFromBeginning()) {
rt->instrumentation().startTrackingHeapObjectStackTraces();
}
}

void stopTrackingHeapObjects() {
rt->instrumentation().stopTrackingHeapObjectStackTraces();
}

std::shared_ptr<HermesRuntime> rt;
};

static std::string functionInfoToString(
int idx,
const folly::dynamic &traceFunctionInfos,
const folly::dynamic &strings) {
auto it = traceFunctionInfos.begin() + idx * 6;
auto functionID = it->asInt();
auto name = strings.at((it + 1)->asInt()).asString();
auto scriptName = strings.at((it + 2)->asInt()).asString();
auto scriptID = (it + 3)->asInt();
auto line = (it + 4)->asInt();
auto col = (it + 5)->asInt();

std::ostringstream os;
os << name << "(" << functionID << ") @ " << scriptName << "(" << scriptID
<< "):" << line << ":" << col << "";
return os.str();
}

struct ChromeStackTreeNode {
ChromeStackTreeNode(ChromeStackTreeNode *parent, int traceFunctionInfosId)
: parent_(parent), traceFunctionInfosId_(traceFunctionInfosId) {}

/// Recursively builds up a tree of trace nodes, and inserts a pair of (trace
/// node id, pointer to node in tree) into \p idNodeMap.
/// WARN: The return value of this function keeps the node pointers alive. It
/// must outlive \p idNodeMap or else \p idNodeMap will have dangling pointers
/// to the nodes.
static std::vector<std::unique_ptr<ChromeStackTreeNode>> parse(
const folly::dynamic &traceNodes,
ChromeStackTreeNode *parent,
std::map<int, ChromeStackTreeNode *> &idNodeMap) {
std::vector<std::unique_ptr<ChromeStackTreeNode>> res;
for (auto node = traceNodes.begin(); node != traceNodes.end(); node += 5) {
auto id = node->asInt();
auto functionInfoIndex = (node + 1)->asInt();
folly::dynamic children = *(node + 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 folly::dynamic &traceFunctionInfos,
const folly::dynamic &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_P(HeapSnapshotAPITest, HeapTimeline) {
startTrackingHeapObjects();
facebook::jsi::Function alloc = eval("function alloc() { return {}; }; alloc")
.asObject(*rt)
.asFunction(*rt);
facebook::jsi::Object obj = alloc.call(*rt).asObject(*rt);
const uint64_t objID = rt->getUniqueID(obj);

std::ostringstream os;
rt->instrumentation().collectGarbage("test");
rt->instrumentation().createSnapshotToStream(os);
stopTrackingHeapObjects();

const std::string heapTimeline = os.str();
folly::dynamic json = folly::parseJson(heapTimeline);
ASSERT_TRUE(json.isObject());
auto it = json.find("strings");
ASSERT_NE(it, json.items().end());
auto strings = it->second;
it = json.find("nodes");
ASSERT_NE(it, json.items().end());
auto nodes = it->second;
it = json.find("trace_tree");
ASSERT_NE(it, json.items().end());
auto traceTree = it->second;
it = json.find("trace_function_infos");
ASSERT_NE(it, json.items().end());
auto traceFunctionInfos = it->second;

// The root node should be the only thing at the top of the tree. There are 5
// fields per single node, and the last field is a children array.
EXPECT_EQ(traceTree.size(), 5)
<< "There should never be more than a single 5-tuple at the beginning of "
"the trace tree";

// Search nodes for the objID.
const auto nodeTupleSize = 6;
const auto nodeIDFieldIndex = 2;
const auto nodeTraceIDFieldIndex = 5;
uint64_t traceNodeID = 0;
ASSERT_EQ(nodes.size() % nodeTupleSize, 0)
<< "Nodes array must consist of tuples";
for (auto node = nodes.begin(); node != nodes.end(); node += nodeTupleSize) {
if (static_cast<uint64_t>((node + nodeIDFieldIndex)->asInt()) == objID) {
traceNodeID = (node + nodeTraceIDFieldIndex)->asInt();
EXPECT_NE(traceNodeID, 0ul) << "Object in node graph has a zero trace ID";
break;
}
}
ASSERT_NE(traceNodeID, 0ul) << "Object not found in nodes graph";

// Now use the trace node ID to locate the corresponding stack.
std::map<int, ChromeStackTreeNode *> idNodeMap;
auto roots = ChromeStackTreeNode::parse(traceTree, nullptr, idNodeMap);
(void)roots;
auto stackTreeNode = idNodeMap.find(traceNodeID);
ASSERT_NE(stackTreeNode, idNodeMap.end());
EXPECT_EQ(
stackTreeNode->second->buildStackTrace(traceFunctionInfos, strings),
R"#(
(root)(0) @ (0):0:0
global(1) @ test.js(1):1:1
alloc(2) @ test.js(1):1:27)#");
}

INSTANTIATE_TEST_CASE_P(
WithOrWithoutAllocationTracker,
HeapSnapshotAPITest,
::testing::Bool());

#endif
8 changes: 8 additions & 0 deletions unittests/VMRuntime/HeapSnapshotTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,12 @@ struct ChromeStackTreeNode {
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 =
Expand Down Expand Up @@ -944,6 +950,7 @@ baz();
EXPECT_STREQ(
fooStackStr.c_str(),
R"#(
(root)(0) @ (0):0:0
global(1) @ test.js(1):2:1
global(2) @ test.js(1):11:4
baz(7) @ test.js(1):9:19
Expand All @@ -957,6 +964,7 @@ foo(8) @ test.js(1):3:20)#");
ASSERT_STREQ(
barStackStr.c_str(),
R"#(
(root)(0) @ (0):0:0
global(1) @ test.js(1):2:1
global(2) @ test.js(1):11:4
baz(3) @ test.js(1):9:31
Expand Down
Loading

0 comments on commit 801aa8a

Please sign in to comment.