// 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 #include "include/cppgc/allocation.h" #include "include/cppgc/common.h" #include "include/cppgc/cross-thread-persistent.h" #include "include/cppgc/custom-space.h" #include "include/cppgc/garbage-collected.h" #include "include/cppgc/name-provider.h" #include "include/cppgc/persistent.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/heap-object-header.h" #include "src/heap/cppgc/object-allocator.h" #include "src/objects/heap-object.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/cppgc-js/unified-heap-utils.h" #include "test/unittests/heap/heap-utils.h" namespace cppgc { class CompactableCustomSpace : public CustomSpace { public: static constexpr size_t kSpaceIndex = 0; static constexpr bool kSupportsCompaction = true; }; } // namespace cppgc namespace v8::internal { struct CompactableGCed : public cppgc::GarbageCollected, public cppgc::NameProvider { public: static constexpr const char kExpectedName[] = "CompactableGCed"; void Trace(cppgc::Visitor* v) const {} const char* GetHumanReadableName() const final { return "CompactableGCed"; } size_t data = 0; }; struct CompactableHolder : public cppgc::GarbageCollected { public: explicit CompactableHolder(cppgc::AllocationHandle& allocation_handle) { object = cppgc::MakeGarbageCollected(allocation_handle); } void Trace(cppgc::Visitor* visitor) const { cppgc::internal::VisitorBase::TraceRawForTesting( visitor, const_cast(object)); visitor->RegisterMovableReference( const_cast(&object)); } CompactableGCed* object = nullptr; }; } // namespace v8::internal namespace cppgc { template <> struct SpaceTrait { using Space = CompactableCustomSpace; }; } // namespace cppgc namespace v8 { namespace internal { namespace { class UnifiedHeapSnapshotTest : public UnifiedHeapTest { public: UnifiedHeapSnapshotTest() = default; explicit UnifiedHeapSnapshotTest( std::vector> custom_spaces) : UnifiedHeapTest(std::move(custom_spaces)) {} const v8::HeapSnapshot* TakeHeapSnapshot( cppgc::EmbedderStackState stack_state = cppgc::EmbedderStackState::kMayContainHeapPointers, v8::HeapProfiler::HeapSnapshotMode snapshot_mode = v8::HeapProfiler::HeapSnapshotMode::kExposeInternals) { v8::HeapProfiler* heap_profiler = v8_isolate()->GetHeapProfiler(); v8::HeapProfiler::HeapSnapshotOptions options; options.control = nullptr; options.global_object_name_resolver = nullptr; options.snapshot_mode = snapshot_mode; options.numerics_mode = v8::HeapProfiler::NumericsMode::kHideNumericValues; options.stack_state = stack_state; return heap_profiler->TakeHeapSnapshot(options); } protected: void TestMergedWrapperNode(v8::HeapProfiler::HeapSnapshotMode snapshot_mode); }; bool IsValidSnapshot(const v8::HeapSnapshot* snapshot, int depth = 3) { const HeapSnapshot* heap_snapshot = reinterpret_cast(snapshot); std::unordered_set 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; } // Returns the IDs of all entries in the snapshot with the given name. std::vector GetIds(const v8::HeapSnapshot& snapshot, std::string name) { const HeapSnapshot& heap_snapshot = reinterpret_cast(snapshot); std::vector result; for (const HeapEntry& entry : heap_snapshot.entries()) { if (entry.name() == name) { result.push_back(entry.id()); } } return result; } bool ContainsRetainingPath(const v8::HeapSnapshot& snapshot, const std::vector retaining_path, bool debug_retaining_path = false) { const HeapSnapshot& heap_snapshot = reinterpret_cast(snapshot); std::vector haystack = {heap_snapshot.root()}; for (size_t i = 0; i < retaining_path.size(); ++i) { const std::string& needle = retaining_path[i]; std::vector 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 { 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 next; cppgc::Member 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* GetHumanReadableName() const final { return "GCed"; } }; // static constexpr const char GCed::kExpectedName[]; static constexpr const char kExpectedCppRootsName[] = "C++ Persistent roots"; static constexpr const char kExpectedCppCrossThreadRootsName[] = "C++ CrossThreadPersistent roots"; static constexpr const char kExpectedCppStackRootsName[] = "C++ native stack roots"; template constexpr const char* GetExpectedName() { if (std::is_base_of::value || cppgc::NameProvider::SupportsCppClassNamesAsObjectNames()) { 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 = cppgc::MakeGarbageCollected(allocation_handle()); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, GetExpectedName()})); } TEST_F(UnifiedHeapSnapshotTest, ConsistentId) { cppgc::Persistent gced = cppgc::MakeGarbageCollected(allocation_handle()); const v8::HeapSnapshot* snapshot1 = TakeHeapSnapshot(); EXPECT_TRUE(IsValidSnapshot(snapshot1)); const v8::HeapSnapshot* snapshot2 = TakeHeapSnapshot(); EXPECT_TRUE(IsValidSnapshot(snapshot2)); std::vector ids1 = GetIds(*snapshot1, GetExpectedName()); std::vector ids2 = GetIds(*snapshot2, GetExpectedName()); EXPECT_EQ(ids1.size(), size_t{1}); EXPECT_EQ(ids2.size(), size_t{1}); EXPECT_EQ(ids1[0], ids2[0]); } class UnifiedHeapWithCustomSpaceSnapshotTest : public UnifiedHeapSnapshotTest { public: static std::vector> GetCustomSpaces() { std::vector> custom_spaces; custom_spaces.emplace_back( std::make_unique()); return custom_spaces; } UnifiedHeapWithCustomSpaceSnapshotTest() : UnifiedHeapSnapshotTest(GetCustomSpaces()) {} }; TEST_F(UnifiedHeapWithCustomSpaceSnapshotTest, ConsistentIdAfterCompaction) { // Ensure that only things held by Persistent handles will remain after GC. DisableConservativeStackScanningScopeForTesting no_css(isolate()->heap()); // Allocate an object that will be thrown away by the GC, so that there's // somewhere for the compactor to move stuff to. cppgc::Persistent trash = cppgc::MakeGarbageCollected(allocation_handle()); // Create the object which we'll actually test. cppgc::Persistent gced = cppgc::MakeGarbageCollected(allocation_handle(), allocation_handle()); // Release the persistent reference to the other object. trash.Release(); void* original_pointer = gced->object; // This first snapshot should not trigger compaction of the cppgc heap because // the heap is still very small. const v8::HeapSnapshot* snapshot1 = TakeHeapSnapshot(cppgc::EmbedderStackState::kNoHeapPointers); EXPECT_TRUE(IsValidSnapshot(snapshot1)); EXPECT_EQ(original_pointer, gced->object); // Manually run a GC with compaction. The GCed object should move. CppHeap::From(isolate()->heap()->cpp_heap()) ->compactor() .EnableForNextGCForTesting(); i::InvokeMajorGC(isolate(), i::GCFlag::kReduceMemoryFootprint); EXPECT_NE(original_pointer, gced->object); // In the second heap snapshot, the moved object should still have the same // ID. const v8::HeapSnapshot* snapshot2 = TakeHeapSnapshot(cppgc::EmbedderStackState::kNoHeapPointers); EXPECT_TRUE(IsValidSnapshot(snapshot2)); std::vector ids1 = GetIds(*snapshot1, GetExpectedName()); std::vector ids2 = GetIds(*snapshot2, GetExpectedName()); // Depending on build config, GetIds might have returned only the ID for the // CompactableGCed instance or it might have also returned the ID for the // CompactableHolder. EXPECT_TRUE(ids1.size() == 1 || ids1.size() == 2); std::sort(ids1.begin(), ids1.end()); std::sort(ids2.begin(), ids2.end()); EXPECT_EQ(ids1, ids2); } TEST_F(UnifiedHeapSnapshotTest, RetainedByCppCrossThreadRoot) { cppgc::subtle::CrossThreadPersistent gced = cppgc::MakeGarbageCollected(allocation_handle()); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppCrossThreadRootsName, GetExpectedName()})); } TEST_F(UnifiedHeapSnapshotTest, RetainedByStackRoots) { auto* volatile gced = cppgc::MakeGarbageCollected(allocation_handle()); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(cppgc::EmbedderStackState::kMayContainHeapPointers); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppStackRootsName, GetExpectedName()})); EXPECT_STREQ(gced->GetHumanReadableName(), GetExpectedName()); } TEST_F(UnifiedHeapSnapshotTest, RetainingUnnamedTypeWithInternalDetails) { cppgc::Persistent base_without_name = cppgc::MakeGarbageCollected(allocation_handle()); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, GetExpectedName()})); } TEST_F(UnifiedHeapSnapshotTest, RetainingUnnamedTypeWithoutInternalDetails) { cppgc::Persistent base_without_name = cppgc::MakeGarbageCollected(allocation_handle()); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(cppgc::EmbedderStackState::kMayContainHeapPointers, v8::HeapProfiler::HeapSnapshotMode::kRegular); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_FALSE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, cppgc::NameProvider::kHiddenName})); EXPECT_FALSE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, GetExpectedName()})); } TEST_F(UnifiedHeapSnapshotTest, RetainingNamedThroughUnnamed) { cppgc::Persistent base_without_name = cppgc::MakeGarbageCollected(allocation_handle()); base_without_name->next = cppgc::MakeGarbageCollected(allocation_handle()); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(cppgc::EmbedderStackState::kMayContainHeapPointers, v8::HeapProfiler::HeapSnapshotMode::kRegular); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, cppgc::NameProvider::kHiddenName, GetExpectedName()})); } 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(allocation_handle()); auto* second = cppgc::MakeGarbageCollected(allocation_handle()); first->next = second; first->next->next = first; auto* third = cppgc::MakeGarbageCollected(allocation_handle()); first->next2 = third; cppgc::Persistent holder(second); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(cppgc::EmbedderStackState::kMayContainHeapPointers, v8::HeapProfiler::HeapSnapshotMode::kRegular); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, cppgc::NameProvider::kHiddenName, cppgc::NameProvider::kHiddenName, GetExpectedName()})); } 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 hidden_holder( cppgc::MakeGarbageCollected(allocation_handle())); auto* first = cppgc::MakeGarbageCollected(allocation_handle()); auto* second = cppgc::MakeGarbageCollected(allocation_handle()); first->next = second; second->next = *hidden_holder; second->next2 = first; first->next2 = cppgc::MakeGarbageCollected(allocation_handle()); cppgc::Persistent holder(first); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(cppgc::EmbedderStackState::kMayContainHeapPointers, v8::HeapProfiler::HeapSnapshotMode::kRegular); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, cppgc::NameProvider::kHiddenName, cppgc::NameProvider::kHiddenName, cppgc::NameProvider::kHiddenName, GetExpectedName()})); } namespace { class GCedWithJSRef : public cppgc::GarbageCollected { 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 object) { v8_object_.Reset(isolate, object); } TracedReference& wrapper() { return v8_object_; } void set_detachedness(v8::EmbedderGraph::Node::Detachedness detachedness) { detachedness_ = detachedness; } v8::EmbedderGraph::Node::Detachedness detachedness() const { return detachedness_; } private: TracedReference v8_object_; v8::EmbedderGraph::Node::Detachedness detachedness_ = v8::EmbedderGraph::Node::Detachedness ::kUnknown; }; 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 context() const { return context_; } private: v8::Isolate* isolate_; v8::HandleScope handle_scope_; v8::Local context_; v8::Context::Scope context_scope_; }; cppgc::Persistent SetupWrapperWrappablePair( JsTestingScope& testing_scope, cppgc::AllocationHandle& allocation_handle, const char* name, v8::EmbedderGraph::Node::Detachedness detachedness = v8::EmbedderGraph::Node::Detachedness::kUnknown) { cppgc::Persistent gc_w_js_ref = cppgc::MakeGarbageCollected(allocation_handle); v8::Local wrapper_object = WrapperHelper::CreateWrapper( testing_scope.context(), gc_w_js_ref.Get(), name); gc_w_js_ref->SetV8Object(testing_scope.isolate(), wrapper_object); gc_w_js_ref->set_detachedness(detachedness); return gc_w_js_ref; } template void ForEachEntryWithName(const v8::HeapSnapshot* snapshot, const char* needle, Callback callback) { const HeapSnapshot* heap_snapshot = reinterpret_cast(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 gc_w_js_ref = SetupWrapperWrappablePair( testing_scope, allocation_handle(), "LeafJSObject"); // Reset the JS->C++ ref or otherwise the nodes would be merged. WrapperHelper::ResetWrappableConnection( v8_isolate(), gc_w_js_ref->wrapper().Get(v8_isolate())); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(cppgc::EmbedderStackState::kMayContainHeapPointers, v8::HeapProfiler::HeapSnapshotMode::kRegular); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, cppgc::NameProvider::kHiddenName, "LeafJSObject"}, true)); } void UnifiedHeapSnapshotTest::TestMergedWrapperNode( v8::HeapProfiler::HeapSnapshotMode snapshot_mode) { // Test ensures that the snapshot sets a wrapper node for C++->JS references // that have a valid back reference 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 gc_w_js_ref = SetupWrapperWrappablePair( testing_scope, allocation_handle(), "MergedObject"); v8::Local next_object = WrapperHelper::CreateWrapper( testing_scope.context(), nullptr, "NextObject"); v8::Local 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( cppgc::EmbedderStackState::kMayContainHeapPointers, snapshot_mode); EXPECT_TRUE(IsValidSnapshot(snapshot)); const char* kExpectedName = snapshot_mode == v8::HeapProfiler::HeapSnapshotMode::kExposeInternals ? GetExpectedName() : cppgc::NameProvider::kHiddenName; EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, kExpectedName, // GCedWithJSRef is merged into MergedObject, replacing its name. "NextObject"})); const size_t js_size = Utils::OpenDirectHandle(*wrapper_object)->Size(); if (snapshot_mode == v8::HeapProfiler::HeapSnapshotMode::kExposeInternals) { const size_t cpp_size = cppgc::internal::HeapObjectHeader::FromObject(gc_w_js_ref.Get()) .AllocatedSize(); ForEachEntryWithName(snapshot, kExpectedName, [cpp_size, js_size](const HeapEntry& entry) { EXPECT_EQ(cpp_size + js_size, entry.self_size()); }); } else { ForEachEntryWithName(snapshot, kExpectedName, [js_size](const HeapEntry& entry) { EXPECT_EQ(js_size, entry.self_size()); }); } } TEST_F(UnifiedHeapSnapshotTest, MergedWrapperNodeWithInternalDetails) { TestMergedWrapperNode(v8::HeapProfiler::HeapSnapshotMode::kExposeInternals); } TEST_F(UnifiedHeapSnapshotTest, MergedWrapperNodeWithoutInternalDetails) { TestMergedWrapperNode(v8::HeapProfiler::HeapSnapshotMode::kRegular); } namespace { class DetachednessHandler { public: static size_t callback_count; static v8::EmbedderGraph::Node::Detachedness GetDetachedness( v8::Isolate* isolate, const v8::Local& v8_value, uint16_t, void*) { callback_count++; return WrapperHelper::UnwrapAs(isolate, v8_value.As()) ->detachedness(); } static void Reset() { callback_count = 0; } }; // static size_t DetachednessHandler::callback_count = 0; constexpr uint8_t kExpectedDetachedValueForUnknown = static_cast(v8::EmbedderGraph::Node::Detachedness::kUnknown); constexpr uint8_t kExpectedDetachedValueForAttached = static_cast(v8::EmbedderGraph::Node::Detachedness::kAttached); constexpr uint8_t kExpectedDetachedValueForDetached = static_cast(v8::EmbedderGraph::Node::Detachedness::kDetached); } // namespace TEST_F(UnifiedHeapSnapshotTest, NoTriggerForStandAloneTracedReference) { // Test ensures that C++ objects with TracedReference have their V8 objects // not merged and queried for detachedness if the backreference is invalid. JsTestingScope testing_scope(v8_isolate()); // Marking the object as attached. The check below queries for unknown, making // sure that the state is not propagated. cppgc::Persistent gc_w_js_ref = SetupWrapperWrappablePair( testing_scope, allocation_handle(), "MergedObject", v8::EmbedderGraph::Node::Detachedness::kAttached); DetachednessHandler::Reset(); v8_isolate()->GetHeapProfiler()->SetGetDetachednessCallback( DetachednessHandler::GetDetachedness, nullptr); WrapperHelper::ResetWrappableConnection( v8_isolate(), gc_w_js_ref->wrapper().Get(v8_isolate())); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(); EXPECT_EQ(0u, DetachednessHandler::callback_count); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE( ContainsRetainingPath(*snapshot, { kExpectedCppRootsName, GetExpectedName(), })); ForEachEntryWithName( snapshot, GetExpectedName(), [](const HeapEntry& entry) { EXPECT_EQ(kExpectedDetachedValueForUnknown, entry.detachedness()); }); } TEST_F(UnifiedHeapSnapshotTest, TriggerDetachednessCallbackSettingAttached) { // Test ensures that objects with JS references that have a valid back // reference set do have their detachedness state queried and set (attached // version). JsTestingScope testing_scope(v8_isolate()); cppgc::Persistent gc_w_js_ref = SetupWrapperWrappablePair( testing_scope, allocation_handle(), "MergedObject", v8::EmbedderGraph::Node::Detachedness::kAttached); DetachednessHandler::Reset(); v8_isolate()->GetHeapProfiler()->SetGetDetachednessCallback( DetachednessHandler::GetDetachedness, nullptr); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(); EXPECT_EQ(1u, DetachednessHandler::callback_count); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE( ContainsRetainingPath(*snapshot, { kExpectedCppRootsName, GetExpectedName(), })); ForEachEntryWithName( snapshot, GetExpectedName(), [](const HeapEntry& entry) { EXPECT_EQ(kExpectedDetachedValueForAttached, entry.detachedness()); }); } TEST_F(UnifiedHeapSnapshotTest, TriggerDetachednessCallbackSettingDetached) { // Test ensures that objects with JS references that have a valid back // reference set do have their detachedness state queried and set (detached // version). JsTestingScope testing_scope(v8_isolate()); cppgc::Persistent gc_w_js_ref = SetupWrapperWrappablePair( testing_scope, allocation_handle(), "MergedObject", v8::EmbedderGraph::Node::Detachedness ::kDetached); DetachednessHandler::Reset(); v8_isolate()->GetHeapProfiler()->SetGetDetachednessCallback( DetachednessHandler::GetDetachedness, nullptr); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(); EXPECT_EQ(1u, DetachednessHandler::callback_count); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE( ContainsRetainingPath(*snapshot, { kExpectedCppRootsName, GetExpectedName(), })); ForEachEntryWithName( snapshot, GetExpectedName(), [](const HeapEntry& entry) { EXPECT_EQ(kExpectedDetachedValueForDetached, entry.detachedness()); }); } namespace { class WrappedContext : public cppgc::GarbageCollected, public cppgc::NameProvider { public: static constexpr const char kExpectedName[] = "cppgc WrappedContext"; // Cycle: // Context -> EmbdderData -> WrappedContext JS object -> WrappedContext cppgc // object -> Context static cppgc::Persistent New(v8::Isolate* isolate) { v8::Local context = v8::Context::New(isolate); v8::Local obj = WrapperHelper::CreateWrapper(context, nullptr, "js WrappedContext"); context->SetEmbedderData(kContextDataIndex, obj); cppgc::Persistent ref = cppgc::MakeGarbageCollected( isolate->GetCppHeap()->GetAllocationHandle(), isolate, obj, context); WrapperHelper::SetWrappableConnection(isolate, obj, ref.Get()); return ref; } static v8::EmbedderGraph::Node::Detachedness GetDetachedness( v8::Isolate* isolate, const v8::Local& v8_value, uint16_t, void*) { return WrapperHelper::UnwrapAs(isolate, v8_value.As()) ->detachedness(); } const char* GetHumanReadableName() const final { return kExpectedName; } virtual void Trace(cppgc::Visitor* v) const { v->Trace(object_); v->Trace(context_); } WrappedContext(v8::Isolate* isolate, v8::Local object, v8::Local context) { object_.Reset(isolate, object); context_.Reset(isolate, context); } v8::Local context(v8::Isolate* isolate) { return context_.Get(isolate); } void set_detachedness(v8::EmbedderGraph::Node::Detachedness detachedness) { detachedness_ = detachedness; } v8::EmbedderGraph::Node::Detachedness detachedness() const { return detachedness_; } private: static constexpr int kContextDataIndex = 0; // This is needed to merge the nodes in the heap snapshot. TracedReference object_; TracedReference context_; v8::EmbedderGraph::Node::Detachedness detachedness_ = v8::EmbedderGraph::Node::Detachedness::kUnknown; }; } // anonymous namespace TEST_F(UnifiedHeapSnapshotTest, WrappedContext) { JsTestingScope testing_scope(v8_isolate()); v8_isolate()->GetHeapProfiler()->SetGetDetachednessCallback( WrappedContext::GetDetachedness, nullptr); cppgc::Persistent wrapped = WrappedContext::New(v8_isolate()); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, wrapped->GetHumanReadableName(), "system / NativeContext", "system / EmbedderDataArray", wrapped->GetHumanReadableName()}, true)); wrapped->set_detachedness(v8::EmbedderGraph::Node::Detachedness::kDetached); v8_isolate()->GetHeapProfiler()->DeleteAllHeapSnapshots(); snapshot = TakeHeapSnapshot(); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, wrapped->GetHumanReadableName(), "system / NativeContext", "system / EmbedderDataArray", wrapped->GetHumanReadableName()}, true)); ForEachEntryWithName( snapshot, wrapped->GetHumanReadableName(), [](const HeapEntry& entry) { EXPECT_EQ(kExpectedDetachedValueForDetached, entry.detachedness()); }); } namespace { class GCedWithDynamicName : public cppgc::GarbageCollected, public cppgc::NameProvider { public: virtual void Trace(cppgc::Visitor* v) const {} void SetValue(int value) { value_ = value; } const char* GetHumanReadableName() const final { v8::HeapProfiler* heap_profiler = v8::Isolate::GetCurrent()->GetHeapProfiler(); if (heap_profiler->IsTakingSnapshot()) { std::string name = "dynamic name " + std::to_string(value_); return heap_profiler->CopyNameForHeapSnapshot(name.c_str()); } return "static name"; } private: int value_ = 0; }; } // namespace TEST_F(UnifiedHeapSnapshotTest, DynamicName) { cppgc::Persistent object_zero = cppgc::MakeGarbageCollected(allocation_handle()); cppgc::Persistent object_one = cppgc::MakeGarbageCollected(allocation_handle()); object_one->SetValue(1); std::string static_name = cppgc::internal::HeapObjectHeader::FromObject(object_one.Get()) .GetName() .value; EXPECT_EQ(static_name, std::string("static name")); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath(*snapshot, {kExpectedCppRootsName, "dynamic name 0"})); EXPECT_TRUE(ContainsRetainingPath(*snapshot, {kExpectedCppRootsName, "dynamic name 1"})); EXPECT_FALSE( ContainsRetainingPath(*snapshot, {kExpectedCppRootsName, "static name"})); } } // namespace internal } // namespace v8