| // 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" |
| #include "include/v8-profiler.h" |
| #include "src/api/api-inl.h" |
| #include "src/heap/cppgc-js/cpp-heap.h" |
| #include "src/heap/cppgc/object-allocator.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" |
| #include "test/unittests/heap/unified-heap-utils.h" |
| |
| namespace v8 { |
| namespace internal { |
| |
| namespace { |
| |
| class UnifiedHeapSnapshotTest : public UnifiedHeapTest { |
| 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 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); |
| } |
| |
| } // 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 into the C++ |
| // node, i.e., the directly reachable JS object is merged into the C++ object. |
| 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 |
| // MergedObject is merged into GCedWithJSRef. |
| "NextObject" // NOLINT |
| })); |
| } |
| |
| 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; |
| |
| 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); |
| } |
| } |
| } |
| |
| 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()); |
| }); |
| } |
| |
| } // namespace internal |
| } // namespace v8 |