2020-10-16 15:05:08 +00:00
|
|
|
// Copyright 2020 the V8 project authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
|
|
// found in the LICENSE file.
|
|
|
|
|
|
|
|
#include <cstring>
|
|
|
|
|
|
|
|
#include "include/cppgc/allocation.h"
|
|
|
|
#include "include/cppgc/cross-thread-persistent.h"
|
|
|
|
#include "include/cppgc/garbage-collected.h"
|
|
|
|
#include "include/cppgc/name-provider.h"
|
|
|
|
#include "include/cppgc/persistent.h"
|
|
|
|
#include "include/cppgc/platform.h"
|
2020-10-22 07:02:17 +00:00
|
|
|
#include "include/v8-cppgc.h"
|
2020-10-16 15:05:08 +00:00
|
|
|
#include "include/v8-profiler.h"
|
|
|
|
#include "src/api/api-inl.h"
|
|
|
|
#include "src/heap/cppgc-js/cpp-heap.h"
|
|
|
|
#include "src/objects/objects-inl.h"
|
|
|
|
#include "src/profiler/heap-snapshot-generator-inl.h"
|
|
|
|
#include "src/profiler/heap-snapshot-generator.h"
|
|
|
|
#include "test/unittests/heap/heap-utils.h"
|
2020-10-22 07:02:17 +00:00
|
|
|
#include "test/unittests/heap/unified-heap-utils.h"
|
2020-10-16 15:05:08 +00:00
|
|
|
|
|
|
|
namespace v8 {
|
|
|
|
namespace internal {
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
2020-10-28 08:13:33 +00:00
|
|
|
class UnifiedHeapSnapshotTest : public UnifiedHeapTest {
|
2020-10-16 15:05:08 +00:00
|
|
|
public:
|
|
|
|
const v8::HeapSnapshot* TakeHeapSnapshot() {
|
|
|
|
v8::HeapProfiler* heap_profiler = v8_isolate()->GetHeapProfiler();
|
|
|
|
return heap_profiler->TakeHeapSnapshot();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
bool IsValidSnapshot(const v8::HeapSnapshot* snapshot, int depth = 3) {
|
|
|
|
const HeapSnapshot* heap_snapshot =
|
|
|
|
reinterpret_cast<const HeapSnapshot*>(snapshot);
|
|
|
|
std::unordered_set<const HeapEntry*> visited;
|
|
|
|
for (const HeapGraphEdge& edge : heap_snapshot->edges()) {
|
|
|
|
visited.insert(edge.to());
|
|
|
|
}
|
|
|
|
size_t unretained_entries_count = 0;
|
|
|
|
for (const HeapEntry& entry : heap_snapshot->entries()) {
|
|
|
|
if (visited.find(&entry) == visited.end() && entry.id() != 1) {
|
|
|
|
entry.Print("entry with no retainer", "", depth, 0);
|
|
|
|
++unretained_entries_count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return unretained_entries_count == 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ContainsRetainingPath(const v8::HeapSnapshot& snapshot,
|
|
|
|
const std::vector<std::string> retaining_path,
|
|
|
|
bool debug_retaining_path = false) {
|
|
|
|
const HeapSnapshot& heap_snapshot =
|
|
|
|
reinterpret_cast<const HeapSnapshot&>(snapshot);
|
|
|
|
std::vector<HeapEntry*> haystack = {heap_snapshot.root()};
|
|
|
|
for (size_t i = 0; i < retaining_path.size(); ++i) {
|
|
|
|
const std::string& needle = retaining_path[i];
|
|
|
|
std::vector<HeapEntry*> new_haystack;
|
|
|
|
for (HeapEntry* parent : haystack) {
|
|
|
|
for (int j = 0; j < parent->children_count(); j++) {
|
|
|
|
HeapEntry* child = parent->child(j)->to();
|
|
|
|
if (0 == strcmp(child->name(), needle.c_str())) {
|
|
|
|
new_haystack.push_back(child);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (new_haystack.empty()) {
|
|
|
|
if (debug_retaining_path) {
|
|
|
|
fprintf(stderr,
|
|
|
|
"#\n# Could not find object with name '%s'\n#\n# Path:\n",
|
|
|
|
needle.c_str());
|
|
|
|
for (size_t j = 0; j < retaining_path.size(); ++j) {
|
|
|
|
fprintf(stderr, "# - '%s'%s\n", retaining_path[j].c_str(),
|
|
|
|
i == j ? "\t<--- not found" : "");
|
|
|
|
}
|
|
|
|
fprintf(stderr, "#\n");
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
std::swap(haystack, new_haystack);
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
class BaseWithoutName : public cppgc::GarbageCollected<BaseWithoutName> {
|
|
|
|
public:
|
|
|
|
static constexpr const char kExpectedName[] =
|
|
|
|
"v8::internal::(anonymous namespace)::BaseWithoutName";
|
|
|
|
|
|
|
|
virtual void Trace(cppgc::Visitor* v) const {
|
|
|
|
v->Trace(next);
|
|
|
|
v->Trace(next2);
|
|
|
|
}
|
|
|
|
cppgc::Member<BaseWithoutName> next;
|
|
|
|
cppgc::Member<BaseWithoutName> next2;
|
|
|
|
};
|
|
|
|
// static
|
|
|
|
constexpr const char BaseWithoutName::kExpectedName[];
|
|
|
|
|
|
|
|
class GCed final : public BaseWithoutName, public cppgc::NameProvider {
|
|
|
|
public:
|
|
|
|
static constexpr const char kExpectedName[] = "GCed";
|
|
|
|
|
|
|
|
void Trace(cppgc::Visitor* v) const final { BaseWithoutName::Trace(v); }
|
|
|
|
const char* GetName() const final { return "GCed"; }
|
|
|
|
};
|
|
|
|
// static
|
|
|
|
constexpr const char GCed::kExpectedName[];
|
|
|
|
|
|
|
|
constexpr const char kExpectedCppRootsName[] = "C++ roots";
|
|
|
|
constexpr const char kExpectedCppCrossThreadRootsName[] =
|
|
|
|
"C++ cross-thread roots";
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
constexpr const char* GetExpectedName() {
|
|
|
|
if (std::is_base_of<cppgc::NameProvider, T>::value ||
|
|
|
|
!cppgc::NameProvider::HideInternalNames()) {
|
|
|
|
return T::kExpectedName;
|
|
|
|
} else {
|
|
|
|
return cppgc::NameProvider::kHiddenName;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
TEST_F(UnifiedHeapSnapshotTest, EmptySnapshot) {
|
|
|
|
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
|
|
|
|
EXPECT_TRUE(IsValidSnapshot(snapshot));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(UnifiedHeapSnapshotTest, RetainedByCppRoot) {
|
|
|
|
cppgc::Persistent<GCed> gced =
|
|
|
|
cppgc::MakeGarbageCollected<GCed>(allocation_handle());
|
|
|
|
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
|
|
|
|
EXPECT_TRUE(IsValidSnapshot(snapshot));
|
|
|
|
EXPECT_TRUE(
|
|
|
|
ContainsRetainingPath(*snapshot, {
|
|
|
|
kExpectedCppRootsName, // NOLINT
|
|
|
|
GetExpectedName<GCed>() // NOLINT
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(UnifiedHeapSnapshotTest, RetainedByCppCrossThreadRoot) {
|
|
|
|
cppgc::subtle::CrossThreadPersistent<GCed> gced =
|
|
|
|
cppgc::MakeGarbageCollected<GCed>(allocation_handle());
|
|
|
|
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
|
|
|
|
EXPECT_TRUE(IsValidSnapshot(snapshot));
|
|
|
|
EXPECT_TRUE(ContainsRetainingPath(
|
|
|
|
*snapshot, {
|
|
|
|
kExpectedCppCrossThreadRootsName, // NOLINT
|
|
|
|
GetExpectedName<GCed>() // NOLINT
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(UnifiedHeapSnapshotTest, RetainingUnnamedType) {
|
|
|
|
cppgc::Persistent<BaseWithoutName> base_without_name =
|
|
|
|
cppgc::MakeGarbageCollected<BaseWithoutName>(allocation_handle());
|
|
|
|
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
|
|
|
|
EXPECT_TRUE(IsValidSnapshot(snapshot));
|
|
|
|
if (cppgc::NameProvider::HideInternalNames()) {
|
|
|
|
EXPECT_FALSE(ContainsRetainingPath(
|
|
|
|
*snapshot, {kExpectedCppRootsName, cppgc::NameProvider::kHiddenName}));
|
|
|
|
} else {
|
|
|
|
EXPECT_TRUE(ContainsRetainingPath(
|
|
|
|
*snapshot, {
|
|
|
|
kExpectedCppRootsName, // NOLINT
|
|
|
|
GetExpectedName<BaseWithoutName>() // NOLINT
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(UnifiedHeapSnapshotTest, RetainingNamedThroughUnnamed) {
|
|
|
|
cppgc::Persistent<BaseWithoutName> base_without_name =
|
|
|
|
cppgc::MakeGarbageCollected<BaseWithoutName>(allocation_handle());
|
|
|
|
base_without_name->next =
|
|
|
|
cppgc::MakeGarbageCollected<GCed>(allocation_handle());
|
|
|
|
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
|
|
|
|
EXPECT_TRUE(IsValidSnapshot(snapshot));
|
|
|
|
EXPECT_TRUE(ContainsRetainingPath(
|
|
|
|
*snapshot, {
|
|
|
|
kExpectedCppRootsName, // NOLINT
|
|
|
|
GetExpectedName<BaseWithoutName>(), // NOLINT
|
|
|
|
GetExpectedName<GCed>() // NOLINT
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(UnifiedHeapSnapshotTest, PendingCallStack) {
|
|
|
|
// Test ensures that the algorithm handles references into the current call
|
|
|
|
// stack.
|
|
|
|
//
|
|
|
|
// Graph:
|
|
|
|
// Persistent -> BaseWithoutName (2) <-> BaseWithoutName (1) -> GCed (3)
|
|
|
|
//
|
|
|
|
// Visitation order is (1)->(2)->(3) which is a corner case, as when following
|
|
|
|
// back from (2)->(1) the object in (1) is already visited and will only later
|
|
|
|
// be marked as visible.
|
|
|
|
auto* first =
|
|
|
|
cppgc::MakeGarbageCollected<BaseWithoutName>(allocation_handle());
|
|
|
|
auto* second =
|
|
|
|
cppgc::MakeGarbageCollected<BaseWithoutName>(allocation_handle());
|
|
|
|
first->next = second;
|
|
|
|
first->next->next = first;
|
|
|
|
auto* third = cppgc::MakeGarbageCollected<GCed>(allocation_handle());
|
|
|
|
first->next2 = third;
|
|
|
|
|
|
|
|
cppgc::Persistent<BaseWithoutName> holder(second);
|
|
|
|
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
|
|
|
|
EXPECT_TRUE(IsValidSnapshot(snapshot));
|
|
|
|
EXPECT_TRUE(
|
|
|
|
ContainsRetainingPath(*snapshot,
|
|
|
|
{
|
|
|
|
kExpectedCppRootsName, // NOLINT
|
|
|
|
GetExpectedName<BaseWithoutName>(), // NOLINT
|
|
|
|
GetExpectedName<BaseWithoutName>(), // NOLINT
|
|
|
|
GetExpectedName<GCed>() // NOLINT
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_F(UnifiedHeapSnapshotTest, ReferenceToFinishedSCC) {
|
|
|
|
// Test ensures that the algorithm handles reference into an already finished
|
|
|
|
// SCC that is marked as hidden whereas the current SCC would resolve to
|
|
|
|
// visible.
|
|
|
|
//
|
|
|
|
// Graph:
|
|
|
|
// Persistent -> BaseWithoutName (1)
|
|
|
|
// Persistent -> BaseWithoutName (2)
|
|
|
|
// + <-> BaseWithoutName (3) -> BaseWithoutName (1)
|
|
|
|
// + -> GCed (4)
|
|
|
|
//
|
|
|
|
// Visitation order (1)->(2)->(3)->(1) which is a corner case as (3) would set
|
|
|
|
// a dependency on (1) which is hidden. Instead (3) should set a dependency on
|
|
|
|
// (2) as (1) resolves to hidden whereas (2) resolves to visible. The test
|
|
|
|
// ensures that resolved hidden dependencies are ignored.
|
|
|
|
cppgc::Persistent<BaseWithoutName> hidden_holder(
|
|
|
|
cppgc::MakeGarbageCollected<BaseWithoutName>(allocation_handle()));
|
|
|
|
auto* first =
|
|
|
|
cppgc::MakeGarbageCollected<BaseWithoutName>(allocation_handle());
|
|
|
|
auto* second =
|
|
|
|
cppgc::MakeGarbageCollected<BaseWithoutName>(allocation_handle());
|
|
|
|
first->next = second;
|
|
|
|
second->next = *hidden_holder;
|
|
|
|
second->next2 = first;
|
|
|
|
first->next2 = cppgc::MakeGarbageCollected<GCed>(allocation_handle());
|
|
|
|
cppgc::Persistent<BaseWithoutName> holder(first);
|
|
|
|
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
|
|
|
|
EXPECT_TRUE(IsValidSnapshot(snapshot));
|
|
|
|
EXPECT_TRUE(
|
|
|
|
ContainsRetainingPath(*snapshot,
|
|
|
|
{
|
|
|
|
kExpectedCppRootsName, // NOLINT
|
|
|
|
GetExpectedName<BaseWithoutName>(), // NOLINT
|
|
|
|
GetExpectedName<BaseWithoutName>(), // NOLINT
|
|
|
|
GetExpectedName<BaseWithoutName>(), // NOLINT
|
|
|
|
GetExpectedName<GCed>() // NOLINT
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2020-10-22 07:02:17 +00:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
class GCedWithJSRef : public cppgc::GarbageCollected<GCedWithJSRef> {
|
|
|
|
public:
|
|
|
|
static constexpr const char kExpectedName[] =
|
|
|
|
"v8::internal::(anonymous namespace)::GCedWithJSRef";
|
|
|
|
|
|
|
|
virtual void Trace(cppgc::Visitor* v) const { v->Trace(v8_object_); }
|
|
|
|
|
|
|
|
void SetV8Object(v8::Isolate* isolate, v8::Local<v8::Object> object) {
|
2020-10-27 12:50:20 +00:00
|
|
|
v8_object_.Reset(isolate, object);
|
2020-10-22 07:02:17 +00:00
|
|
|
}
|
|
|
|
|
2020-10-27 14:58:26 +00:00
|
|
|
void SetWrapperClassId(uint16_t class_id) {
|
|
|
|
v8_object_.SetWrapperClassId(class_id);
|
|
|
|
}
|
|
|
|
|
2020-10-22 07:02:17 +00:00
|
|
|
private:
|
2020-10-27 12:50:20 +00:00
|
|
|
TracedReference<v8::Object> v8_object_;
|
2020-10-22 07:02:17 +00:00
|
|
|
};
|
|
|
|
constexpr const char GCedWithJSRef::kExpectedName[];
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
TEST_F(UnifiedHeapSnapshotTest, JSReferenceForcesVisibleObject) {
|
|
|
|
// Test ensures that a C++->JS reference forces an object to be visible in the
|
|
|
|
// snapshot.
|
2020-10-28 14:54:05 +00:00
|
|
|
cppgc::Persistent<GCedWithJSRef> gc_w_js_ref =
|
|
|
|
cppgc::MakeGarbageCollected<GCedWithJSRef>(allocation_handle());
|
|
|
|
v8::HandleScope scope(v8_isolate());
|
|
|
|
v8::Local<v8::Context> context = v8::Context::New(v8_isolate());
|
|
|
|
v8::Context::Scope context_scope(context);
|
|
|
|
v8::Local<v8::Object> api_object =
|
|
|
|
WrapperHelper::CreateWrapper(context, gc_w_js_ref.Get(), "LeafJSObject");
|
|
|
|
gc_w_js_ref->SetV8Object(v8_isolate(), api_object);
|
2020-10-22 07:02:17 +00:00
|
|
|
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
|
|
|
|
EXPECT_TRUE(IsValidSnapshot(snapshot));
|
|
|
|
EXPECT_TRUE(
|
|
|
|
ContainsRetainingPath(*snapshot,
|
|
|
|
{
|
|
|
|
kExpectedCppRootsName, // NOLINT
|
|
|
|
GetExpectedName<GCedWithJSRef>(), // NOLINT
|
|
|
|
"LeafJSObject" // NOLINT
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2020-10-27 14:58:26 +00:00
|
|
|
TEST_F(UnifiedHeapSnapshotTest, MergedWrapperNode) {
|
|
|
|
// Test ensures that the snapshot sets a wrapper node for C++->JS references
|
|
|
|
// that have a class id set and that object nodes are merged into the C++
|
|
|
|
// node, i.e., the directly reachable JS object is merged into the C++ object.
|
2020-10-28 14:54:05 +00:00
|
|
|
cppgc::Persistent<GCedWithJSRef> gc_w_js_ref =
|
|
|
|
cppgc::MakeGarbageCollected<GCedWithJSRef>(allocation_handle());
|
|
|
|
v8::HandleScope scope(v8_isolate());
|
|
|
|
v8::Local<v8::Context> context = v8::Context::New(v8_isolate());
|
|
|
|
v8::Context::Scope context_scope(context);
|
2020-10-28 11:49:56 +00:00
|
|
|
v8::Local<v8::Object> wrapper_object =
|
2020-10-28 14:54:05 +00:00
|
|
|
WrapperHelper::CreateWrapper(context, gc_w_js_ref.Get(), "MergedObject");
|
|
|
|
gc_w_js_ref->SetV8Object(v8_isolate(), wrapper_object);
|
|
|
|
gc_w_js_ref->SetWrapperClassId(1); // Any class id will do.
|
2020-10-27 14:58:26 +00:00
|
|
|
// Chain another object to `wrapper_object`. Since `wrapper_object` should be
|
|
|
|
// merged into `GCedWithJSRef`, the additional object must show up as direct
|
|
|
|
// child from `GCedWithJSRef`.
|
2020-10-28 14:54:05 +00:00
|
|
|
v8::Local<v8::Object> next_object =
|
|
|
|
WrapperHelper::CreateWrapper(context, nullptr, "NextObject");
|
2020-10-27 14:58:26 +00:00
|
|
|
wrapper_object
|
2020-10-28 14:54:05 +00:00
|
|
|
->Set(context,
|
2020-10-27 14:58:26 +00:00
|
|
|
v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), "link")
|
|
|
|
.ToLocalChecked(),
|
|
|
|
next_object)
|
|
|
|
.ToChecked();
|
|
|
|
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
|
|
|
|
EXPECT_TRUE(IsValidSnapshot(snapshot));
|
|
|
|
EXPECT_TRUE(
|
|
|
|
ContainsRetainingPath(*snapshot,
|
|
|
|
{
|
|
|
|
kExpectedCppRootsName, // NOLINT
|
|
|
|
GetExpectedName<GCedWithJSRef>(), // NOLINT
|
|
|
|
// MergedObject is merged into GCedWithJSRef.
|
|
|
|
"NextObject" // NOLINT
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2020-10-16 15:05:08 +00:00
|
|
|
} // namespace internal
|
|
|
|
} // namespace v8
|