// 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/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/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/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() { v8::HeapProfiler* heap_profiler = v8_isolate()->GetHeapProfiler(); return heap_profiler->TakeHeapSnapshot(nullptr, nullptr, false /* hide internals */); } }; 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[]; constexpr const char kExpectedCppRootsName[] = "C++ roots"; constexpr const char kExpectedCppCrossThreadRootsName[] = "C++ cross-thread 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()); CompactableGCed* original_pointer = gced->object; // Release the persistent reference to the other object. trash.Release(); // This first snapshot should not trigger compaction of the cppgc heap because // the heap is still very small. const v8::HeapSnapshot* snapshot1 = TakeHeapSnapshot(); EXPECT_TRUE(IsValidSnapshot(snapshot1)); EXPECT_EQ(original_pointer, gced->object); // PerformGarbageCollection, called during TakeHeapSnapshot above, reset the // heap's embedder_stack_state_, so we need to do this again so it has an // effect on the next GC. DisableConservativeStackScanningScopeForTesting no_css_again( isolate()->heap()); // Manually run a GC with compaction. The GCed object should move. CppHeap::From(isolate()->heap()->cpp_heap()) ->compactor() .EnableForNextGCForTesting(); isolate()->heap()->CollectAllGarbage(i::GCFlag::kReduceMemoryFootprint, i::GarbageCollectionReason::kTesting); 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(); 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, RetainingUnnamedType) { cppgc::Persistent base_without_name = cppgc::MakeGarbageCollected(allocation_handle()); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(); EXPECT_TRUE(IsValidSnapshot(snapshot)); if (!cppgc::NameProvider::SupportsCppClassNamesAsObjectNames()) { EXPECT_FALSE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, cppgc::NameProvider::kHiddenName})); } else { EXPECT_TRUE(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(); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, GetExpectedName(), 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(); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, GetExpectedName(), GetExpectedName(), 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(); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, GetExpectedName(), GetExpectedName(), GetExpectedName(), GetExpectedName()})); } namespace { class GCedWithJSRef : public cppgc::GarbageCollected { public: static uint16_t kWrappableType; 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); } void SetWrapperClassId(uint16_t class_id) { v8_object_.SetWrapperClassId(class_id); } uint16_t WrapperClassId() const { return v8_object_.WrapperClassId(); } TracedReference& wrapper() { return v8_object_; } private: TracedReference v8_object_; }; constexpr const char GCedWithJSRef::kExpectedName[]; // static uint16_t GCedWithJSRef::kWrappableType = WrapperHelper::kTracedEmbedderId; 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) { cppgc::Persistent gc_w_js_ref = cppgc::MakeGarbageCollected(allocation_handle); v8::Local wrapper_object = WrapperHelper::CreateWrapper( testing_scope.context(), &GCedWithJSRef::kWrappableType, gc_w_js_ref.Get(), name); gc_w_js_ref->SetV8Object(testing_scope.isolate(), wrapper_object); 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"); const v8::HeapSnapshot* snapshot = TakeHeapSnapshot(); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, GetExpectedName(), "LeafJSObject"})); } 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 gc_w_js_ref = SetupWrapperWrappablePair( testing_scope, allocation_handle(), "MergedObject"); gc_w_js_ref->SetWrapperClassId(1); // Any class id will do. v8::Local next_object = WrapperHelper::CreateWrapper( testing_scope.context(), nullptr, 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(); EXPECT_TRUE(IsValidSnapshot(snapshot)); EXPECT_TRUE(ContainsRetainingPath( *snapshot, {kExpectedCppRootsName, GetExpectedName(), // GCedWithJSRef is merged into MergedObject, replacing its name. "NextObject"})); const size_t js_size = Utils::OpenHandle(*wrapper_object)->Size(); #if CPPGC_SUPPORTS_OBJECT_NAMES const size_t cpp_size = cppgc::internal::HeapObjectHeader::FromObject(gc_w_js_ref.Get()) .AllocatedSize(); ForEachEntryWithName(snapshot, GetExpectedName(), [cpp_size, js_size](const HeapEntry& entry) { EXPECT_EQ(cpp_size + js_size, entry.self_size()); }); #else // !CPPGC_SUPPORTS_OBJECT_NAMES ForEachEntryWithName(snapshot, GetExpectedName(), [js_size](const HeapEntry& entry) { EXPECT_EQ(js_size, entry.self_size()); }); #endif // !CPPGC_SUPPORTS_OBJECT_NAMES } 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, 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(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, 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 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, 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 non-zero class id // 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"); 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, 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 non-zero class id // 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"); 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, GetExpectedName(), })); ForEachEntryWithName( snapshot, GetExpectedName(), [](const HeapEntry& entry) { EXPECT_EQ(kExpectedDetachedValueForDetached, entry.detachedness()); }); } } // namespace internal } // namespace v8