v8/test/unittests/heap/unified-heap-snapshot-unittest.cc

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

502 lines
19 KiB
C++
Raw Normal View History

Reland "cppgc-js: Add snapshot for C++ objects" This reverts commit fba14bde5fa0ac26ee6732677acbc3f21e135c74. Reland fixes: - const vector<const string> -> const vector<string> Original message: The following implements a snapshotting algorithm for C++ objects that also filters strongly-connected components (SCCs) of only "hidden" objects that are not (transitively) referencing any non-hidden objects. C++ objects come in two versions. a. Named objects that have been assigned a name through NameProvider. b. Unnamed objects, that are potentially hidden if the build configuration requires Oilpan to hide such names. Hidden objects have their name set to NameProvider::kHiddenName. The main challenge for the algorithm is to avoid blowing up the final object graph with hidden nodes that do not carry information. For that reason, the algorithm filters SCCs of only hidden objects, e.g.: ... -> (object) -> (object) -> (hidden) -> (hidden) In this case the (hidden) objects are filtered from the graph. The trickiest part is maintaining visibility state for objects referencing other objects that are currently being processed. Main algorithm idea (two passes): 1. First pass marks all non-hidden objects and those that transitively reach non-hidden objects as visible. Details: - Iterate over all objects. - If object is non-hidden mark it as visible and also mark parent as visible if needed. - If object is hidden, traverse children as DFS to find non-hidden objects. Post-order process the objects and mark those objects as visible that have child nodes that are visible themselves. - Maintain an epoch counter (StateStorage::state_count_) to allow deferring the visibility decision to other objects in the same SCC. This is similar to the "lowlink" value in Tarjan's algorithm for SCC. - After the first pass it is guaranteed that all deferred visibility decisions can be resolved. 2. Second pass adds nodes and edges for all visible objects. - Upon first checking the visibility state of an object, all deferred visibility states are resolved. For practical reasons, the recursion is transformed into an iteration. We do not use plain Tarjan's algorithm to avoid another pass over all nodes to create SCCs. Follow ups: 1. Adding wrapper nodes for cpp objects that are wrappables for V8 wrappers. 2. Adding detachedness information. Bug: chromium:1056170 Change-Id: Ib47df5c912c57d644d052f209276e9d926cece0f Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2480362 Commit-Queue: Michael Lippautz <mlippautz@chromium.org> Commit-Queue: Ulan Degenbaev <ulan@chromium.org> Auto-Submit: Michael Lippautz <mlippautz@chromium.org> Reviewed-by: Ulan Degenbaev <ulan@chromium.org> Cr-Commit-Position: refs/heads/master@{#70577}
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"
#include "include/v8-cppgc.h"
Reland "cppgc-js: Add snapshot for C++ objects" This reverts commit fba14bde5fa0ac26ee6732677acbc3f21e135c74. Reland fixes: - const vector<const string> -> const vector<string> Original message: The following implements a snapshotting algorithm for C++ objects that also filters strongly-connected components (SCCs) of only "hidden" objects that are not (transitively) referencing any non-hidden objects. C++ objects come in two versions. a. Named objects that have been assigned a name through NameProvider. b. Unnamed objects, that are potentially hidden if the build configuration requires Oilpan to hide such names. Hidden objects have their name set to NameProvider::kHiddenName. The main challenge for the algorithm is to avoid blowing up the final object graph with hidden nodes that do not carry information. For that reason, the algorithm filters SCCs of only hidden objects, e.g.: ... -> (object) -> (object) -> (hidden) -> (hidden) In this case the (hidden) objects are filtered from the graph. The trickiest part is maintaining visibility state for objects referencing other objects that are currently being processed. Main algorithm idea (two passes): 1. First pass marks all non-hidden objects and those that transitively reach non-hidden objects as visible. Details: - Iterate over all objects. - If object is non-hidden mark it as visible and also mark parent as visible if needed. - If object is hidden, traverse children as DFS to find non-hidden objects. Post-order process the objects and mark those objects as visible that have child nodes that are visible themselves. - Maintain an epoch counter (StateStorage::state_count_) to allow deferring the visibility decision to other objects in the same SCC. This is similar to the "lowlink" value in Tarjan's algorithm for SCC. - After the first pass it is guaranteed that all deferred visibility decisions can be resolved. 2. Second pass adds nodes and edges for all visible objects. - Upon first checking the visibility state of an object, all deferred visibility states are resolved. For practical reasons, the recursion is transformed into an iteration. We do not use plain Tarjan's algorithm to avoid another pass over all nodes to create SCCs. Follow ups: 1. Adding wrapper nodes for cpp objects that are wrappables for V8 wrappers. 2. Adding detachedness information. Bug: chromium:1056170 Change-Id: Ib47df5c912c57d644d052f209276e9d926cece0f Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2480362 Commit-Queue: Michael Lippautz <mlippautz@chromium.org> Commit-Queue: Ulan Degenbaev <ulan@chromium.org> Auto-Submit: Michael Lippautz <mlippautz@chromium.org> Reviewed-by: Ulan Degenbaev <ulan@chromium.org> Cr-Commit-Position: refs/heads/master@{#70577}
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/heap/cppgc/heap-object-header.h"
#include "src/heap/cppgc/object-allocator.h"
#include "src/objects/heap-object.h"
Reland "cppgc-js: Add snapshot for C++ objects" This reverts commit fba14bde5fa0ac26ee6732677acbc3f21e135c74. Reland fixes: - const vector<const string> -> const vector<string> Original message: The following implements a snapshotting algorithm for C++ objects that also filters strongly-connected components (SCCs) of only "hidden" objects that are not (transitively) referencing any non-hidden objects. C++ objects come in two versions. a. Named objects that have been assigned a name through NameProvider. b. Unnamed objects, that are potentially hidden if the build configuration requires Oilpan to hide such names. Hidden objects have their name set to NameProvider::kHiddenName. The main challenge for the algorithm is to avoid blowing up the final object graph with hidden nodes that do not carry information. For that reason, the algorithm filters SCCs of only hidden objects, e.g.: ... -> (object) -> (object) -> (hidden) -> (hidden) In this case the (hidden) objects are filtered from the graph. The trickiest part is maintaining visibility state for objects referencing other objects that are currently being processed. Main algorithm idea (two passes): 1. First pass marks all non-hidden objects and those that transitively reach non-hidden objects as visible. Details: - Iterate over all objects. - If object is non-hidden mark it as visible and also mark parent as visible if needed. - If object is hidden, traverse children as DFS to find non-hidden objects. Post-order process the objects and mark those objects as visible that have child nodes that are visible themselves. - Maintain an epoch counter (StateStorage::state_count_) to allow deferring the visibility decision to other objects in the same SCC. This is similar to the "lowlink" value in Tarjan's algorithm for SCC. - After the first pass it is guaranteed that all deferred visibility decisions can be resolved. 2. Second pass adds nodes and edges for all visible objects. - Upon first checking the visibility state of an object, all deferred visibility states are resolved. For practical reasons, the recursion is transformed into an iteration. We do not use plain Tarjan's algorithm to avoid another pass over all nodes to create SCCs. Follow ups: 1. Adding wrapper nodes for cpp objects that are wrappables for V8 wrappers. 2. Adding detachedness information. Bug: chromium:1056170 Change-Id: Ib47df5c912c57d644d052f209276e9d926cece0f Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2480362 Commit-Queue: Michael Lippautz <mlippautz@chromium.org> Commit-Queue: Ulan Degenbaev <ulan@chromium.org> Auto-Submit: Michael Lippautz <mlippautz@chromium.org> Reviewed-by: Ulan Degenbaev <ulan@chromium.org> Cr-Commit-Position: refs/heads/master@{#70577}
2020-10-16 15:05:08 +00:00
#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"
#include "test/unittests/heap/unified-heap-utils.h"
Reland "cppgc-js: Add snapshot for C++ objects" This reverts commit fba14bde5fa0ac26ee6732677acbc3f21e135c74. Reland fixes: - const vector<const string> -> const vector<string> Original message: The following implements a snapshotting algorithm for C++ objects that also filters strongly-connected components (SCCs) of only "hidden" objects that are not (transitively) referencing any non-hidden objects. C++ objects come in two versions. a. Named objects that have been assigned a name through NameProvider. b. Unnamed objects, that are potentially hidden if the build configuration requires Oilpan to hide such names. Hidden objects have their name set to NameProvider::kHiddenName. The main challenge for the algorithm is to avoid blowing up the final object graph with hidden nodes that do not carry information. For that reason, the algorithm filters SCCs of only hidden objects, e.g.: ... -> (object) -> (object) -> (hidden) -> (hidden) In this case the (hidden) objects are filtered from the graph. The trickiest part is maintaining visibility state for objects referencing other objects that are currently being processed. Main algorithm idea (two passes): 1. First pass marks all non-hidden objects and those that transitively reach non-hidden objects as visible. Details: - Iterate over all objects. - If object is non-hidden mark it as visible and also mark parent as visible if needed. - If object is hidden, traverse children as DFS to find non-hidden objects. Post-order process the objects and mark those objects as visible that have child nodes that are visible themselves. - Maintain an epoch counter (StateStorage::state_count_) to allow deferring the visibility decision to other objects in the same SCC. This is similar to the "lowlink" value in Tarjan's algorithm for SCC. - After the first pass it is guaranteed that all deferred visibility decisions can be resolved. 2. Second pass adds nodes and edges for all visible objects. - Upon first checking the visibility state of an object, all deferred visibility states are resolved. For practical reasons, the recursion is transformed into an iteration. We do not use plain Tarjan's algorithm to avoid another pass over all nodes to create SCCs. Follow ups: 1. Adding wrapper nodes for cpp objects that are wrappables for V8 wrappers. 2. Adding detachedness information. Bug: chromium:1056170 Change-Id: Ib47df5c912c57d644d052f209276e9d926cece0f Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2480362 Commit-Queue: Michael Lippautz <mlippautz@chromium.org> Commit-Queue: Ulan Degenbaev <ulan@chromium.org> Auto-Submit: Michael Lippautz <mlippautz@chromium.org> Reviewed-by: Ulan Degenbaev <ulan@chromium.org> Cr-Commit-Position: refs/heads/master@{#70577}
2020-10-16 15:05:08 +00:00
namespace v8 {
namespace internal {
namespace {
class UnifiedHeapSnapshotTest : public UnifiedHeapTest {
Reland "cppgc-js: Add snapshot for C++ objects" This reverts commit fba14bde5fa0ac26ee6732677acbc3f21e135c74. Reland fixes: - const vector<const string> -> const vector<string> Original message: The following implements a snapshotting algorithm for C++ objects that also filters strongly-connected components (SCCs) of only "hidden" objects that are not (transitively) referencing any non-hidden objects. C++ objects come in two versions. a. Named objects that have been assigned a name through NameProvider. b. Unnamed objects, that are potentially hidden if the build configuration requires Oilpan to hide such names. Hidden objects have their name set to NameProvider::kHiddenName. The main challenge for the algorithm is to avoid blowing up the final object graph with hidden nodes that do not carry information. For that reason, the algorithm filters SCCs of only hidden objects, e.g.: ... -> (object) -> (object) -> (hidden) -> (hidden) In this case the (hidden) objects are filtered from the graph. The trickiest part is maintaining visibility state for objects referencing other objects that are currently being processed. Main algorithm idea (two passes): 1. First pass marks all non-hidden objects and those that transitively reach non-hidden objects as visible. Details: - Iterate over all objects. - If object is non-hidden mark it as visible and also mark parent as visible if needed. - If object is hidden, traverse children as DFS to find non-hidden objects. Post-order process the objects and mark those objects as visible that have child nodes that are visible themselves. - Maintain an epoch counter (StateStorage::state_count_) to allow deferring the visibility decision to other objects in the same SCC. This is similar to the "lowlink" value in Tarjan's algorithm for SCC. - After the first pass it is guaranteed that all deferred visibility decisions can be resolved. 2. Second pass adds nodes and edges for all visible objects. - Upon first checking the visibility state of an object, all deferred visibility states are resolved. For practical reasons, the recursion is transformed into an iteration. We do not use plain Tarjan's algorithm to avoid another pass over all nodes to create SCCs. Follow ups: 1. Adding wrapper nodes for cpp objects that are wrappables for V8 wrappers. 2. Adding detachedness information. Bug: chromium:1056170 Change-Id: Ib47df5c912c57d644d052f209276e9d926cece0f Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2480362 Commit-Queue: Michael Lippautz <mlippautz@chromium.org> Commit-Queue: Ulan Degenbaev <ulan@chromium.org> Auto-Submit: Michael Lippautz <mlippautz@chromium.org> Reviewed-by: Ulan Degenbaev <ulan@chromium.org> Cr-Commit-Position: refs/heads/master@{#70577}
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
}));
}
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) {
v8_object_.Reset(isolate, object);
}
void SetWrapperClassId(uint16_t class_id) {
v8_object_.SetWrapperClassId(class_id);
}
uint16_t WrapperClassId() const { return v8_object_.WrapperClassId(); }
TracedReference<v8::Object>& wrapper() { return v8_object_; }
private:
TracedReference<v8::Object> v8_object_;
};
constexpr const char GCedWithJSRef::kExpectedName[];
class V8_NODISCARD JsTestingScope {
public:
explicit JsTestingScope(v8::Isolate* isolate)
: isolate_(isolate),
handle_scope_(isolate),
context_(v8::Context::New(isolate)),
context_scope_(context_) {}
v8::Isolate* isolate() const { return isolate_; }
v8::Local<v8::Context> context() const { return context_; }
private:
v8::Isolate* isolate_;
v8::HandleScope handle_scope_;
v8::Local<v8::Context> context_;
v8::Context::Scope context_scope_;
};
cppgc::Persistent<GCedWithJSRef> SetupWrapperWrappablePair(
JsTestingScope& testing_scope, cppgc::AllocationHandle& allocation_handle,
const char* name) {
cppgc::Persistent<GCedWithJSRef> gc_w_js_ref =
cppgc::MakeGarbageCollected<GCedWithJSRef>(allocation_handle);
v8::Local<v8::Object> wrapper_object = WrapperHelper::CreateWrapper(
testing_scope.context(), gc_w_js_ref.Get(), name);
gc_w_js_ref->SetV8Object(testing_scope.isolate(), wrapper_object);
return std::move(gc_w_js_ref);
}
template <typename Callback>
void ForEachEntryWithName(const v8::HeapSnapshot* snapshot, const char* needle,
Callback callback) {
const HeapSnapshot* heap_snapshot =
reinterpret_cast<const HeapSnapshot*>(snapshot);
for (const HeapEntry& entry : heap_snapshot->entries()) {
if (strcmp(entry.name(), needle) == 0) {
callback(entry);
}
}
}
} // namespace
TEST_F(UnifiedHeapSnapshotTest, JSReferenceForcesVisibleObject) {
// Test ensures that a C++->JS reference forces an object to be visible in the
// snapshot.
JsTestingScope testing_scope(v8_isolate());
cppgc::Persistent<GCedWithJSRef> gc_w_js_ref = SetupWrapperWrappablePair(
testing_scope, allocation_handle(), "LeafJSObject");
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
EXPECT_TRUE(IsValidSnapshot(snapshot));
EXPECT_TRUE(
ContainsRetainingPath(*snapshot,
{
kExpectedCppRootsName, // NOLINT
GetExpectedName<GCedWithJSRef>(), // NOLINT
"LeafJSObject" // NOLINT
}));
}
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. In practice, the
// C++ node is merged into the existing JS node.
JsTestingScope testing_scope(v8_isolate());
cppgc::Persistent<GCedWithJSRef> gc_w_js_ref = SetupWrapperWrappablePair(
testing_scope, allocation_handle(), "MergedObject");
gc_w_js_ref->SetWrapperClassId(1); // Any class id will do.
v8::Local<v8::Object> next_object = WrapperHelper::CreateWrapper(
testing_scope.context(), nullptr, "NextObject");
v8::Local<v8::Object> wrapper_object =
gc_w_js_ref->wrapper().Get(v8_isolate());
// 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`.
wrapper_object
->Set(testing_scope.context(),
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
// GCedWithJSRef is merged into MergedObject, replacing its name.
"NextObject" // NOLINT
}));
const size_t cpp_size =
cppgc::internal::HeapObjectHeader::FromPayload(gc_w_js_ref.Get())
.GetSize();
const size_t js_size = Utils::OpenHandle(*wrapper_object)->Size();
ForEachEntryWithName(snapshot, GetExpectedName<GCedWithJSRef>(),
[cpp_size, js_size](const HeapEntry& entry) {
EXPECT_EQ(cpp_size + js_size, entry.self_size());
});
}
namespace {
constexpr uint16_t kClassIdForAttachedState = 0xAAAA;
class DetachednessHandler {
public:
static size_t callback_count;
static v8::EmbedderGraph::Node::Detachedness GetDetachedness(
v8::Isolate* isolate, const v8::Local<v8::Value>& v8_value,
uint16_t class_id, void* data) {
callback_count++;
return class_id == kClassIdForAttachedState
? v8::EmbedderGraph::Node::Detachedness::kAttached
: v8::EmbedderGraph::Node::Detachedness::kDetached;
}
static void Reset() { callback_count = 0; }
};
// static
size_t DetachednessHandler::callback_count = 0;
constexpr uint8_t kExpectedDetachedValueForUnknown =
static_cast<uint8_t>(v8::EmbedderGraph::Node::Detachedness::kUnknown);
constexpr uint8_t kExpectedDetachedValueForAttached =
static_cast<uint8_t>(v8::EmbedderGraph::Node::Detachedness::kAttached);
constexpr uint8_t kExpectedDetachedValueForDetached =
static_cast<uint8_t>(v8::EmbedderGraph::Node::Detachedness::kDetached);
} // namespace
TEST_F(UnifiedHeapSnapshotTest, NoTriggerForClassIdZero) {
// Test ensures that objects with JS references that have no class id set do
// not have their detachedness state queried.
JsTestingScope testing_scope(v8_isolate());
cppgc::Persistent<GCedWithJSRef> gc_w_js_ref = SetupWrapperWrappablePair(
testing_scope, allocation_handle(), "MergedObject");
DetachednessHandler::Reset();
v8_isolate()->GetHeapProfiler()->SetGetDetachednessCallback(
DetachednessHandler::GetDetachedness, nullptr);
gc_w_js_ref->SetWrapperClassId(0);
EXPECT_EQ(0u, gc_w_js_ref->WrapperClassId());
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
EXPECT_EQ(0u, DetachednessHandler::callback_count);
EXPECT_TRUE(IsValidSnapshot(snapshot));
EXPECT_TRUE(
ContainsRetainingPath(*snapshot,
{
kExpectedCppRootsName, // NOLINT
GetExpectedName<GCedWithJSRef>(), // NOLINT
}));
ForEachEntryWithName(
snapshot, GetExpectedName<GCedWithJSRef>(), [](const HeapEntry& entry) {
EXPECT_EQ(kExpectedDetachedValueForUnknown, entry.detachedness());
});
}
TEST_F(UnifiedHeapSnapshotTest, TriggerDetachednessCallbackSettingAttached) {
// Test ensures that objects with JS references that have a non-zero class id
// set do have their detachedness state queried and set (attached version).
JsTestingScope testing_scope(v8_isolate());
cppgc::Persistent<GCedWithJSRef> gc_w_js_ref = SetupWrapperWrappablePair(
testing_scope, allocation_handle(), "MergedObject");
DetachednessHandler::Reset();
v8_isolate()->GetHeapProfiler()->SetGetDetachednessCallback(
DetachednessHandler::GetDetachedness, nullptr);
gc_w_js_ref->SetWrapperClassId(kClassIdForAttachedState);
EXPECT_NE(0u, gc_w_js_ref->WrapperClassId());
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
EXPECT_EQ(1u, DetachednessHandler::callback_count);
EXPECT_TRUE(IsValidSnapshot(snapshot));
EXPECT_TRUE(
ContainsRetainingPath(*snapshot,
{
kExpectedCppRootsName, // NOLINT
GetExpectedName<GCedWithJSRef>(), // NOLINT
}));
ForEachEntryWithName(
snapshot, GetExpectedName<GCedWithJSRef>(), [](const HeapEntry& entry) {
EXPECT_EQ(kExpectedDetachedValueForAttached, entry.detachedness());
});
}
TEST_F(UnifiedHeapSnapshotTest, TriggerDetachednessCallbackSettingDetached) {
// Test ensures that objects with JS references that have a non-zero class id
// set do have their detachedness state queried and set (detached version).
JsTestingScope testing_scope(v8_isolate());
cppgc::Persistent<GCedWithJSRef> gc_w_js_ref = SetupWrapperWrappablePair(
testing_scope, allocation_handle(), "MergedObject");
DetachednessHandler::Reset();
v8_isolate()->GetHeapProfiler()->SetGetDetachednessCallback(
DetachednessHandler::GetDetachedness, nullptr);
gc_w_js_ref->SetWrapperClassId(kClassIdForAttachedState - 1);
EXPECT_NE(0u, gc_w_js_ref->WrapperClassId());
const v8::HeapSnapshot* snapshot = TakeHeapSnapshot();
EXPECT_EQ(1u, DetachednessHandler::callback_count);
EXPECT_TRUE(IsValidSnapshot(snapshot));
EXPECT_TRUE(
ContainsRetainingPath(*snapshot,
{
kExpectedCppRootsName, // NOLINT
GetExpectedName<GCedWithJSRef>(), // NOLINT
}));
ForEachEntryWithName(
snapshot, GetExpectedName<GCedWithJSRef>(), [](const HeapEntry& entry) {
EXPECT_EQ(kExpectedDetachedValueForDetached, entry.detachedness());
});
}
Reland "cppgc-js: Add snapshot for C++ objects" This reverts commit fba14bde5fa0ac26ee6732677acbc3f21e135c74. Reland fixes: - const vector<const string> -> const vector<string> Original message: The following implements a snapshotting algorithm for C++ objects that also filters strongly-connected components (SCCs) of only "hidden" objects that are not (transitively) referencing any non-hidden objects. C++ objects come in two versions. a. Named objects that have been assigned a name through NameProvider. b. Unnamed objects, that are potentially hidden if the build configuration requires Oilpan to hide such names. Hidden objects have their name set to NameProvider::kHiddenName. The main challenge for the algorithm is to avoid blowing up the final object graph with hidden nodes that do not carry information. For that reason, the algorithm filters SCCs of only hidden objects, e.g.: ... -> (object) -> (object) -> (hidden) -> (hidden) In this case the (hidden) objects are filtered from the graph. The trickiest part is maintaining visibility state for objects referencing other objects that are currently being processed. Main algorithm idea (two passes): 1. First pass marks all non-hidden objects and those that transitively reach non-hidden objects as visible. Details: - Iterate over all objects. - If object is non-hidden mark it as visible and also mark parent as visible if needed. - If object is hidden, traverse children as DFS to find non-hidden objects. Post-order process the objects and mark those objects as visible that have child nodes that are visible themselves. - Maintain an epoch counter (StateStorage::state_count_) to allow deferring the visibility decision to other objects in the same SCC. This is similar to the "lowlink" value in Tarjan's algorithm for SCC. - After the first pass it is guaranteed that all deferred visibility decisions can be resolved. 2. Second pass adds nodes and edges for all visible objects. - Upon first checking the visibility state of an object, all deferred visibility states are resolved. For practical reasons, the recursion is transformed into an iteration. We do not use plain Tarjan's algorithm to avoid another pass over all nodes to create SCCs. Follow ups: 1. Adding wrapper nodes for cpp objects that are wrappables for V8 wrappers. 2. Adding detachedness information. Bug: chromium:1056170 Change-Id: Ib47df5c912c57d644d052f209276e9d926cece0f Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2480362 Commit-Queue: Michael Lippautz <mlippautz@chromium.org> Commit-Queue: Ulan Degenbaev <ulan@chromium.org> Auto-Submit: Michael Lippautz <mlippautz@chromium.org> Reviewed-by: Ulan Degenbaev <ulan@chromium.org> Cr-Commit-Position: refs/heads/master@{#70577}
2020-10-16 15:05:08 +00:00
} // namespace internal
} // namespace v8