// 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. #if defined(CPPGC_YOUNG_GENERATION) #include #include #include "include/cppgc/allocation.h" #include "include/cppgc/explicit-management.h" #include "include/cppgc/heap-consistency.h" #include "include/cppgc/internal/caged-heap-local-data.h" #include "include/cppgc/persistent.h" #include "src/heap/cppgc/heap-object-header.h" #include "src/heap/cppgc/heap-visitor.h" #include "src/heap/cppgc/heap.h" #include "test/unittests/heap/cppgc/tests.h" #include "testing/gtest/include/gtest/gtest.h" namespace cppgc { namespace internal { namespace { bool IsHeapObjectYoung(void* obj) { return HeapObjectHeader::FromObject(obj).IsYoung(); } bool IsHeapObjectOld(void* obj) { return !IsHeapObjectYoung(obj); } class SimpleGCedBase : public GarbageCollected { public: static size_t destructed_objects; virtual ~SimpleGCedBase() { ++destructed_objects; } virtual void Trace(Visitor* v) const { v->Trace(next); } Member next; }; size_t SimpleGCedBase::destructed_objects; template class SimpleGCed : public SimpleGCedBase { char array[Size]; }; using Small = SimpleGCed<64>; using Large = SimpleGCed; template struct OtherType; template <> struct OtherType { using Type = Large; }; template <> struct OtherType { using Type = Small; }; void ExpectPageYoung(BasePage& page) { EXPECT_TRUE(page.contains_young_objects()); auto& age_table = CagedHeapLocalData::Get().age_table; EXPECT_EQ(AgeTable::Age::kYoung, age_table.GetAgeForRange( CagedHeap::OffsetFromAddress(page.PayloadStart()), CagedHeap::OffsetFromAddress(page.PayloadEnd()))); } void ExpectPageMixed(BasePage& page) { EXPECT_TRUE(page.contains_young_objects()); auto& age_table = CagedHeapLocalData::Get().age_table; EXPECT_EQ(AgeTable::Age::kMixed, age_table.GetAgeForRange( CagedHeap::OffsetFromAddress(page.PayloadStart()), CagedHeap::OffsetFromAddress(page.PayloadEnd()))); } void ExpectPageOld(BasePage& page) { EXPECT_FALSE(page.contains_young_objects()); auto& age_table = CagedHeapLocalData::Get().age_table; EXPECT_EQ(AgeTable::Age::kOld, age_table.GetAgeForRange( CagedHeap::OffsetFromAddress(page.PayloadStart()), CagedHeap::OffsetFromAddress(page.PayloadEnd()))); } class RememberedSetExtractor : HeapVisitor { friend class HeapVisitor; public: static std::set Extract(cppgc::Heap* heap) { RememberedSetExtractor extractor; extractor.Traverse(Heap::From(heap)->raw_heap()); return std::move(extractor.slots_); } private: void VisitPage(BasePage& page) { auto* slot_set = page.slot_set(); if (!slot_set) return; const uintptr_t page_start = reinterpret_cast(&page); const size_t buckets_size = SlotSet::BucketsForSize(page.AllocatedSize()); slot_set->Iterate( page_start, 0, buckets_size, [this](SlotSet::Address slot) { slots_.insert(reinterpret_cast(slot)); return heap::base::KEEP_SLOT; }, SlotSet::EmptyBucketMode::FREE_EMPTY_BUCKETS); } bool VisitNormalPage(NormalPage& page) { VisitPage(page); return true; } bool VisitLargePage(LargePage& page) { VisitPage(page); return true; } std::set slots_; }; } // namespace class MinorGCTest : public testing::TestWithHeap { public: MinorGCTest() : testing::TestWithHeap() { // Enable young generation flag and run GC. After the first run the heap // will enable minor GC. Heap::From(GetHeap())->EnableGenerationalGC(); CollectMajor(); SimpleGCedBase::destructed_objects = 0; } ~MinorGCTest() override { Heap::From(GetHeap())->Terminate(); } static size_t DestructedObjects() { return SimpleGCedBase::destructed_objects; } void CollectMinor() { Heap::From(GetHeap())->CollectGarbage(GCConfig::MinorPreciseAtomicConfig()); } void CollectMinorWithStack() { Heap::From(GetHeap())->CollectGarbage( GCConfig::MinorConservativeAtomicConfig()); } void CollectMajor() { Heap::From(GetHeap())->CollectGarbage(GCConfig::PreciseAtomicConfig()); } void CollectMajorWithStack() { Heap::From(GetHeap())->CollectGarbage(GCConfig::ConservativeAtomicConfig()); } const auto& RememberedSourceObjects() const { return Heap::From(GetHeap())->remembered_set().remembered_source_objects_; } const auto& RememberedInConstructionObjects() const { return Heap::From(GetHeap()) ->remembered_set() .remembered_in_construction_objects_.previous; } }; template class MinorGCTestForType : public MinorGCTest { public: using Type = SmallOrLarge; }; using ObjectTypes = ::testing::Types; TYPED_TEST_SUITE(MinorGCTestForType, ObjectTypes); namespace { enum class GCType { kMinor, kMajor, }; enum class StackType { kWithout, kWith, }; template void RunGCAndExpectObjectsPromoted(MinorGCTest& test, Args*... args) { EXPECT_TRUE((IsHeapObjectYoung(args) && ...)); if constexpr (gc_type == GCType::kMajor) { if constexpr (stack_type == StackType::kWithout) { test.CollectMajor(); } else { test.CollectMajorWithStack(); } } else { if constexpr (stack_type == StackType::kWithout) { test.CollectMinor(); } else { test.CollectMinorWithStack(); } } EXPECT_TRUE((IsHeapObjectOld(args) && ...)); } struct ExpectRememberedSlotsAdded final { ExpectRememberedSlotsAdded( const MinorGCTest& test, std::initializer_list slots_expected_to_be_remembered) : test_(test), slots_expected_to_be_remembered_(slots_expected_to_be_remembered), initial_slots_(RememberedSetExtractor::Extract(test.GetHeap())) { // Check that the remembered set doesn't contain specified slots. EXPECT_FALSE(std::includes(initial_slots_.begin(), initial_slots_.end(), slots_expected_to_be_remembered_.begin(), slots_expected_to_be_remembered_.end())); } ~ExpectRememberedSlotsAdded() { const auto current_slots = RememberedSetExtractor::Extract(test_.GetHeap()); EXPECT_EQ(initial_slots_.size() + slots_expected_to_be_remembered_.size(), current_slots.size()); EXPECT_TRUE(std::includes(current_slots.begin(), current_slots.end(), slots_expected_to_be_remembered_.begin(), slots_expected_to_be_remembered_.end())); } private: const MinorGCTest& test_; std::set slots_expected_to_be_remembered_; std::set initial_slots_; }; struct ExpectRememberedSlotsRemoved final { ExpectRememberedSlotsRemoved( const MinorGCTest& test, std::initializer_list slots_expected_to_be_removed) : test_(test), slots_expected_to_be_removed_(slots_expected_to_be_removed), initial_slots_(RememberedSetExtractor::Extract(test.GetHeap())) { DCHECK_GE(initial_slots_.size(), slots_expected_to_be_removed_.size()); // Check that the remembered set does contain specified slots to be removed. EXPECT_TRUE(std::includes(initial_slots_.begin(), initial_slots_.end(), slots_expected_to_be_removed_.begin(), slots_expected_to_be_removed_.end())); } ~ExpectRememberedSlotsRemoved() { const auto current_slots = RememberedSetExtractor::Extract(test_.GetHeap()); EXPECT_EQ(initial_slots_.size() - slots_expected_to_be_removed_.size(), current_slots.size()); EXPECT_FALSE(std::includes(current_slots.begin(), current_slots.end(), slots_expected_to_be_removed_.begin(), slots_expected_to_be_removed_.end())); } private: const MinorGCTest& test_; std::set slots_expected_to_be_removed_; std::set initial_slots_; }; struct ExpectNoRememberedSlotsAdded final { explicit ExpectNoRememberedSlotsAdded(const MinorGCTest& test) : test_(test), initial_remembered_slots_( RememberedSetExtractor::Extract(test.GetHeap())) {} ~ExpectNoRememberedSlotsAdded() { EXPECT_EQ(initial_remembered_slots_, RememberedSetExtractor::Extract(test_.GetHeap())); } private: const MinorGCTest& test_; std::set initial_remembered_slots_; }; } // namespace TYPED_TEST(MinorGCTestForType, MinorCollection) { using Type = typename TestFixture::Type; MakeGarbageCollected(this->GetAllocationHandle()); EXPECT_EQ(0u, TestFixture::DestructedObjects()); MinorGCTest::CollectMinor(); EXPECT_EQ(1u, TestFixture::DestructedObjects()); { subtle::NoGarbageCollectionScope no_gc_scope(*Heap::From(this->GetHeap())); Type* prev = nullptr; for (size_t i = 0; i < 64; ++i) { auto* ptr = MakeGarbageCollected(this->GetAllocationHandle()); ptr->next = prev; prev = ptr; } } MinorGCTest::CollectMinor(); EXPECT_EQ(65u, TestFixture::DestructedObjects()); } TYPED_TEST(MinorGCTestForType, StickyBits) { using Type = typename TestFixture::Type; Persistent p1 = MakeGarbageCollected(this->GetAllocationHandle()); TestFixture::CollectMinor(); EXPECT_FALSE(HeapObjectHeader::FromObject(p1.Get()).IsYoung()); TestFixture::CollectMajor(); EXPECT_FALSE(HeapObjectHeader::FromObject(p1.Get()).IsYoung()); EXPECT_EQ(0u, TestFixture::DestructedObjects()); } TYPED_TEST(MinorGCTestForType, OldObjectIsNotVisited) { using Type = typename TestFixture::Type; Persistent p = MakeGarbageCollected(this->GetAllocationHandle()); TestFixture::CollectMinor(); EXPECT_EQ(0u, TestFixture::DestructedObjects()); EXPECT_FALSE(HeapObjectHeader::FromObject(p.Get()).IsYoung()); // Check that the old deleted object won't be visited during minor GC. Type* raw = p.Release(); TestFixture::CollectMinor(); EXPECT_EQ(0u, TestFixture::DestructedObjects()); EXPECT_FALSE(HeapObjectHeader::FromObject(raw).IsYoung()); EXPECT_FALSE(HeapObjectHeader::FromObject(raw).IsFree()); // Check that the old deleted object will be revisited in major GC. TestFixture::CollectMajor(); EXPECT_EQ(1u, TestFixture::DestructedObjects()); } template void InterGenerationalPointerTest(MinorGCTest* test, cppgc::Heap* heap) { Persistent old = MakeGarbageCollected(heap->GetAllocationHandle()); test->CollectMinor(); EXPECT_FALSE(HeapObjectHeader::FromObject(old.Get()).IsYoung()); Type2* young = nullptr; { subtle::NoGarbageCollectionScope no_gc_scope(*Heap::From(heap)); // Allocate young objects. for (size_t i = 0; i < 64; ++i) { auto* ptr = MakeGarbageCollected(heap->GetAllocationHandle()); ptr->next = young; young = ptr; EXPECT_TRUE(HeapObjectHeader::FromObject(young).IsYoung()); const uintptr_t offset = CagedHeap::OffsetFromAddress(young); // Age may be young or unknown. EXPECT_NE(AgeTable::Age::kOld, CagedHeapLocalData::Get().age_table.GetAge(offset)); } } auto remembered_set_size_before_barrier = RememberedSetExtractor::Extract(test->GetHeap()).size(); // Issue generational barrier. old->next = young; auto remembered_set_size_after_barrier = RememberedSetExtractor::Extract(test->GetHeap()).size(); EXPECT_EQ(remembered_set_size_before_barrier + 1u, remembered_set_size_after_barrier); // Check that the remembered set is visited. test->CollectMinor(); EXPECT_EQ(0u, MinorGCTest::DestructedObjects()); EXPECT_TRUE(RememberedSetExtractor::Extract(test->GetHeap()).empty()); for (size_t i = 0; i < 64; ++i) { EXPECT_FALSE(HeapObjectHeader::FromObject(young).IsFree()); EXPECT_FALSE(HeapObjectHeader::FromObject(young).IsYoung()); young = static_cast(young->next.Get()); } old.Release(); test->CollectMajor(); EXPECT_EQ(65u, MinorGCTest::DestructedObjects()); } TYPED_TEST(MinorGCTestForType, InterGenerationalPointerForSamePageTypes) { using Type = typename TestFixture::Type; InterGenerationalPointerTest(this, this->GetHeap()); } TYPED_TEST(MinorGCTestForType, InterGenerationalPointerForDifferentPageTypes) { using Type = typename TestFixture::Type; InterGenerationalPointerTest::Type>( this, this->GetHeap()); } TYPED_TEST(MinorGCTestForType, OmitGenerationalBarrierForOnStackObject) { using Type = typename TestFixture::Type; class StackAllocated : GarbageCollected { CPPGC_STACK_ALLOCATED(); public: Type* ptr = nullptr; } stack_object; // Try issuing generational barrier for on-stack object. stack_object.ptr = MakeGarbageCollected(this->GetAllocationHandle()); subtle::HeapConsistency::WriteBarrierParams params; EXPECT_EQ(subtle::HeapConsistency::WriteBarrierType::kNone, subtle::HeapConsistency::GetWriteBarrierType( reinterpret_cast(&stack_object.ptr), stack_object.ptr, params)); } TYPED_TEST(MinorGCTestForType, OmitGenerationalBarrierForSentinels) { using Type = typename TestFixture::Type; Persistent old = MakeGarbageCollected(this->GetAllocationHandle()); TestFixture::CollectMinor(); EXPECT_FALSE(HeapObjectHeader::FromObject(old.Get()).IsYoung()); { ExpectNoRememberedSlotsAdded _(*this); // Try issuing generational barrier for nullptr. old->next = static_cast(nullptr); } { ExpectNoRememberedSlotsAdded _(*this); // Try issuing generational barrier for sentinel. old->next = kSentinelPointer; } } template void TestRememberedSetInvalidation(MinorGCTest& test) { Persistent old = MakeGarbageCollected(test.GetAllocationHandle()); test.CollectMinor(); auto* young = MakeGarbageCollected(test.GetAllocationHandle()); { ExpectRememberedSlotsAdded _(test, {old->next.GetSlotForTesting()}); // Issue the generational barrier. old->next = young; } { ExpectRememberedSlotsRemoved _(test, {old->next.GetSlotForTesting()}); // Release the persistent and free the old object. auto* old_raw = old.Release(); subtle::FreeUnreferencedObject(test.GetHeapHandle(), *old_raw); } // Visiting remembered slots must not fail. test.CollectMinor(); } TYPED_TEST(MinorGCTestForType, RememberedSetInvalidationOnPromptlyFree) { using Type1 = typename TestFixture::Type; using Type2 = typename OtherType::Type; TestRememberedSetInvalidation(*this); TestRememberedSetInvalidation(*this); } TEST_F(MinorGCTest, RememberedSetInvalidationOnShrink) { using Member = Member; static constexpr size_t kTrailingMembers = 64; static constexpr size_t kBytesToAllocate = kTrailingMembers * sizeof(Member); static constexpr size_t kFirstMemberToInvalidate = kTrailingMembers / 2; static constexpr size_t kLastMemberToInvalidate = kTrailingMembers; // Create an object with additional kBytesToAllocate bytes. Persistent old = MakeGarbageCollected( this->GetAllocationHandle(), AdditionalBytes(kBytesToAllocate)); auto get_member = [&old](size_t i) -> Member& { return *reinterpret_cast(reinterpret_cast(old.Get()) + sizeof(Small) + i * sizeof(Member)); }; CollectMinor(); auto* young = MakeGarbageCollected(GetAllocationHandle()); const size_t remembered_set_size_before_barrier = RememberedSetExtractor::Extract(GetHeap()).size(); // Issue the generational barriers. for (size_t i = kFirstMemberToInvalidate; i < kLastMemberToInvalidate; ++i) { // Construct the member. new (&get_member(i)) Member; // Issue the barrier. get_member(i) = young; } const auto remembered_set_size_after_barrier = RememberedSetExtractor::Extract(GetHeap()).size(); // Check that barriers hit (kLastMemberToInvalidate - // kFirstMemberToInvalidate) times. EXPECT_EQ(remembered_set_size_before_barrier + (kLastMemberToInvalidate - kFirstMemberToInvalidate), remembered_set_size_after_barrier); // Shrink the buffer for old object. subtle::Resize(*old, AdditionalBytes(kBytesToAllocate / 2)); const auto remembered_set_after_shrink = RememberedSetExtractor::Extract(GetHeap()).size(); // Check that the reference was invalidated. EXPECT_EQ(remembered_set_size_before_barrier, remembered_set_after_shrink); // Visiting remembered slots must not fail. CollectMinor(); } namespace { template struct InlinedObject { struct Inner { Inner() = default; explicit Inner(AllocationHandle& handle) : ref(MakeGarbageCollected(handle)) {} void Trace(Visitor* v) const { v->Trace(ref); } double d = -1.; Member ref; }; InlinedObject() = default; explicit InlinedObject(AllocationHandle& handle) : ref(MakeGarbageCollected(handle)), inner(handle) {} void Trace(cppgc::Visitor* v) const { v->Trace(ref); v->Trace(inner); } int a_ = -1; Member ref; Inner inner; }; template class GCedWithInlinedArray : public GarbageCollected> { public: static constexpr size_t kNumObjects = 16; GCedWithInlinedArray(HeapHandle& heap_handle, AllocationHandle& alloc_handle) : heap_handle_(heap_handle), alloc_handle_(alloc_handle) {} using WriteBarrierParams = subtle::HeapConsistency::WriteBarrierParams; using HeapConsistency = subtle::HeapConsistency; void SetInPlaceRange(size_t from, size_t to) { DCHECK_GT(to, from); DCHECK_GT(kNumObjects, from); for (; from != to; ++from) new (&objects[from]) InlinedObject(alloc_handle_); GenerationalBarrierForSourceObject(&objects[from]); } void Trace(cppgc::Visitor* v) const { for (const auto& object : objects) v->Trace(object); } InlinedObject objects[kNumObjects]; private: void GenerationalBarrierForSourceObject(void* object) { DCHECK(object); WriteBarrierParams params; const auto barrier_type = HeapConsistency::GetWriteBarrierType( object, params, [this]() -> HeapHandle& { return heap_handle_; }); EXPECT_EQ(HeapConsistency::WriteBarrierType::kGenerational, barrier_type); HeapConsistency::GenerationalBarrierForSourceObject(params, object); } HeapHandle& heap_handle_; AllocationHandle& alloc_handle_; }; } // namespace TYPED_TEST(MinorGCTestForType, GenerationalBarrierDeferredTracing) { using Type = typename TestFixture::Type; Persistent> array = MakeGarbageCollected>( this->GetAllocationHandle(), this->GetHeapHandle(), this->GetAllocationHandle()); this->CollectMinor(); EXPECT_TRUE(IsHeapObjectOld(array.Get())); const auto& remembered_objects = this->RememberedSourceObjects(); { ExpectNoRememberedSlotsAdded _(*this); EXPECT_EQ(0u, remembered_objects.count( &HeapObjectHeader::FromObject(array->objects))); array->SetInPlaceRange(2, 4); EXPECT_EQ(1u, remembered_objects.count( &HeapObjectHeader::FromObject(array->objects))); } RunGCAndExpectObjectsPromoted( *this, array->objects[2].ref.Get(), array->objects[2].inner.ref.Get(), array->objects[3].ref.Get(), array->objects[3].inner.ref.Get()); EXPECT_EQ(0u, remembered_objects.size()); } namespace { class GCedWithCustomWeakCallback final : public GarbageCollected { public: static size_t custom_callback_called; void CustomWeakCallbackMethod(const LivenessBroker& broker) { custom_callback_called++; } void Trace(cppgc::Visitor* visitor) const { visitor->RegisterWeakCallbackMethod< GCedWithCustomWeakCallback, &GCedWithCustomWeakCallback::CustomWeakCallbackMethod>(this); } }; size_t GCedWithCustomWeakCallback::custom_callback_called = 0; } // namespace TEST_F(MinorGCTest, ReexecuteCustomCallback) { // Create an object with additional kBytesToAllocate bytes. Persistent old = MakeGarbageCollected(GetAllocationHandle()); CollectMinor(); EXPECT_EQ(1u, GCedWithCustomWeakCallback::custom_callback_called); CollectMinor(); EXPECT_EQ(2u, GCedWithCustomWeakCallback::custom_callback_called); CollectMinor(); EXPECT_EQ(3u, GCedWithCustomWeakCallback::custom_callback_called); CollectMajor(); // The callback must be called only once. EXPECT_EQ(4u, GCedWithCustomWeakCallback::custom_callback_called); } TEST_F(MinorGCTest, AgeTableIsReset) { using Type1 = SimpleGCed<16>; using Type2 = SimpleGCed<64>; using Type3 = SimpleGCed; Persistent p1 = MakeGarbageCollected(GetAllocationHandle()); Persistent p2 = MakeGarbageCollected(GetAllocationHandle()); Persistent p3 = MakeGarbageCollected(GetAllocationHandle()); auto* page1 = BasePage::FromPayload(p1.Get()); auto* page2 = BasePage::FromPayload(p2.Get()); auto* page3 = BasePage::FromPayload(p3.Get()); ASSERT_FALSE(page1->is_large()); ASSERT_FALSE(page2->is_large()); ASSERT_TRUE(page3->is_large()); ASSERT_NE(page1, page2); ASSERT_NE(page1, page3); ASSERT_NE(page2, page3); // First, expect all the pages to be young. ExpectPageYoung(*page1); ExpectPageYoung(*page2); ExpectPageYoung(*page3); CollectMinor(); // Expect pages to be promoted after the minor GC. ExpectPageOld(*page1); ExpectPageOld(*page2); ExpectPageOld(*page3); // Allocate another objects on the normal pages and a new large page. p1 = MakeGarbageCollected(GetAllocationHandle()); p2 = MakeGarbageCollected(GetAllocationHandle()); p3 = MakeGarbageCollected(GetAllocationHandle()); // Expect now the normal pages to be mixed. ExpectPageMixed(*page1); ExpectPageMixed(*page2); // The large page must remain old. ExpectPageOld(*page3); CollectMajor(); // After major GC all the pages must also become old. ExpectPageOld(*page1); ExpectPageOld(*page2); ExpectPageOld(*BasePage::FromPayload(p3.Get())); } namespace { template struct GCOnConstruction { explicit GCOnConstruction(MinorGCTest& test, size_t depth) { if constexpr (type == GCType::kMajor) { test.CollectMajorWithStack(); } else { test.CollectMinorWithStack(); } EXPECT_EQ(depth, test.RememberedInConstructionObjects().size()); } }; template struct InConstructionWithYoungRef : GarbageCollected> { using ValueType = SimpleGCed<64>; explicit InConstructionWithYoungRef(MinorGCTest& test) : call_gc(test, 1u), m(MakeGarbageCollected(test.GetAllocationHandle())) {} void Trace(Visitor* v) const { v->Trace(m); } GCOnConstruction call_gc; Member m; }; } // namespace TEST_F(MinorGCTest, RevisitInConstructionObjectsMinorMinorWithStack) { static constexpr auto kFirstGCType = GCType::kMinor; auto* gced = MakeGarbageCollected>( GetAllocationHandle(), *this); RunGCAndExpectObjectsPromoted( *this, gced->m.Get()); EXPECT_EQ(0u, RememberedInConstructionObjects().size()); } TEST_F(MinorGCTest, RevisitInConstructionObjectsMinorMinorWithoutStack) { static constexpr auto kFirstGCType = GCType::kMinor; Persistent> gced = MakeGarbageCollected>( GetAllocationHandle(), *this); RunGCAndExpectObjectsPromoted( *this, gced->m.Get()); EXPECT_EQ(0u, RememberedInConstructionObjects().size()); } TEST_F(MinorGCTest, RevisitInConstructionObjectsMajorMinorWithStack) { static constexpr auto kFirstGCType = GCType::kMajor; auto* gced = MakeGarbageCollected>( GetAllocationHandle(), *this); RunGCAndExpectObjectsPromoted( *this, gced->m.Get()); EXPECT_EQ(0u, RememberedInConstructionObjects().size()); } TEST_F(MinorGCTest, RevisitInConstructionObjectsMajorMinorWithoutStack) { static constexpr auto kFirstGCType = GCType::kMajor; Persistent> gced = MakeGarbageCollected>( GetAllocationHandle(), *this); RunGCAndExpectObjectsPromoted( *this, gced->m.Get()); EXPECT_EQ(0u, RememberedInConstructionObjects().size()); } TEST_F(MinorGCTest, PreviousInConstructionObjectsAreDroppedAfterFullGC) { MakeGarbageCollected>( GetAllocationHandle(), *this); EXPECT_EQ(1u, RememberedInConstructionObjects().size()); CollectMajor(); EXPECT_EQ(0u, RememberedInConstructionObjects().size()); } namespace { template struct NestedInConstructionWithYoungRef : GarbageCollected> { using ValueType = SimpleGCed<64>; NestedInConstructionWithYoungRef(MinorGCTest& test, size_t depth) : NestedInConstructionWithYoungRef(test, 1, depth) {} NestedInConstructionWithYoungRef(MinorGCTest& test, size_t current_depth, size_t max_depth) : current_depth(current_depth), max_depth(max_depth), next(current_depth != max_depth ? MakeGarbageCollected>( test.GetAllocationHandle(), test, current_depth + 1, max_depth) : nullptr), call_gc(test, current_depth), m(MakeGarbageCollected(test.GetAllocationHandle())) {} void Trace(Visitor* v) const { v->Trace(next); v->Trace(m); } size_t current_depth = 0; size_t max_depth = 0; Member> next; GCOnConstruction call_gc; Member m; }; } // namespace TEST_F(MinorGCTest, RevisitNestedInConstructionObjects) { static constexpr auto kFirstGCType = GCType::kMinor; Persistent> gced = MakeGarbageCollected>( GetAllocationHandle(), *this, 10); CollectMinor(); for (auto* p = gced.Get(); p; p = p->next.Get()) { EXPECT_TRUE(IsHeapObjectOld(p)); EXPECT_TRUE(IsHeapObjectOld(p->m)); } EXPECT_EQ(0u, RememberedInConstructionObjects().size()); } } // namespace internal } // namespace cppgc #endif