From 71397a06fb9595dae48cfdc956957b68caddbbac Mon Sep 17 00:00:00 2001 From: Alexander Shabalin Date: Fri, 2 Apr 2021 12:13:44 +0000 Subject: [PATCH] Add Stop-the-World Mark & Sweep GC for single-threaded programs. GC implementation for testing purposes * Only works for a single mutator * Runs on the same thread as the mutator * Runs every nth checkpoint and after every m bytes are allocated * Runs finalizers after sweeping on the mutator thread. --- .../backend.native/tests/build.gradle | 15 +- .../runtime/src/main/cpp/FinalizerHooks.hpp | 3 + .../src/main/cpp/ObjectTestSupport.hpp | 44 ++ .../runtime/src/mm/cpp/ExtraObjectData.cpp | 17 +- .../runtime/src/mm/cpp/ExtraObjectData.hpp | 4 +- kotlin-native/runtime/src/mm/cpp/GC.hpp | 3 +- .../runtime/src/mm/cpp/GlobalsRegistry.hpp | 2 + kotlin-native/runtime/src/mm/cpp/Memory.cpp | 7 +- .../runtime/src/mm/cpp/ObjectFactory.hpp | 78 ++- .../runtime/src/mm/cpp/ObjectFactoryTest.cpp | 35 +- .../src/mm/cpp/gc/MarkAndSweepUtils.hpp | 79 +++ .../mm/cpp/gc/MarkAndSweepUtilsMarkTest.cpp | 495 ++++++++++++++ .../mm/cpp/gc/MarkAndSweepUtilsSweepTest.cpp | 576 ++++++++++++++++ .../mm/cpp/gc/SingleThreadMarkAndSweep.cpp | 118 ++++ .../mm/cpp/gc/SingleThreadMarkAndSweep.hpp | 78 +++ .../cpp/gc/SingleThreadMarkAndSweepTest.cpp | 629 ++++++++++++++++++ 16 files changed, 2161 insertions(+), 22 deletions(-) create mode 100644 kotlin-native/runtime/src/mm/cpp/gc/MarkAndSweepUtils.hpp create mode 100644 kotlin-native/runtime/src/mm/cpp/gc/MarkAndSweepUtilsMarkTest.cpp create mode 100644 kotlin-native/runtime/src/mm/cpp/gc/MarkAndSweepUtilsSweepTest.cpp create mode 100644 kotlin-native/runtime/src/mm/cpp/gc/SingleThreadMarkAndSweep.cpp create mode 100644 kotlin-native/runtime/src/mm/cpp/gc/SingleThreadMarkAndSweep.hpp create mode 100644 kotlin-native/runtime/src/mm/cpp/gc/SingleThreadMarkAndSweepTest.cpp diff --git a/kotlin-native/backend.native/tests/build.gradle b/kotlin-native/backend.native/tests/build.gradle index 57e75a5b3b3..0201b349a85 100644 --- a/kotlin-native/backend.native/tests/build.gradle +++ b/kotlin-native/backend.native/tests/build.gradle @@ -1024,8 +1024,7 @@ task freeze5(type: KonanLocalTest) { } task freeze6(type: KonanLocalTest) { - enabled = (project.testTarget != 'wasm32') && // No exceptions on WASM. - !isExperimentalMM // Experimental MM does not have a GC yet. + enabled = (project.testTarget != 'wasm32') // No exceptions on WASM. goldValue = "OK\nOK\n" source = "runtime/workers/freeze6.kt" } @@ -1057,13 +1056,11 @@ task lazy1(type: KonanLocalTest) { } standaloneTest("lazy2") { - enabled = !isExperimentalMM // Experimental MM does not have a GC yet. goldValue = "123\nOK\n" source = "runtime/workers/lazy2.kt" } standaloneTest("lazy3") { - enabled = !isExperimentalMM // Experimental MM does not have a GC yet. source = "runtime/workers/lazy3.kt" } @@ -2876,13 +2873,11 @@ task memory_escape1(type: KonanLocalTest) { } task memory_cycles0(type: KonanLocalTest) { - enabled = !isExperimentalMM // Experimental MM does not have a GC yet. goldValue = "42\n" source = "runtime/memory/cycles0.kt" } task memory_cycles1(type: KonanLocalTest) { - enabled = !isExperimentalMM // Experimental MM does not have a GC yet. source = "runtime/memory/cycles1.kt" } @@ -2896,7 +2891,6 @@ task memory_escape2(type: KonanLocalTest) { } task memory_weak0(type: KonanLocalTest) { - enabled = !isExperimentalMM // Experimental MM does not have a GC yet. goldValue = "Data(s=Hello)\nnull\nOK\n" source = "runtime/memory/weak0.kt" } @@ -2907,7 +2901,6 @@ task memory_weak1(type: KonanLocalTest) { } standaloneTest("memory_only_gc") { - enabled = !isExperimentalMM // Experimental MM does not have a GC yet. source = "runtime/memory/only_gc.kt" } @@ -4207,7 +4200,6 @@ standaloneTest("interop_opengl_teapot") { if (PlatformInfo.isAppleTarget(project)) { interopTest("interop_objc_smoke") { - enabled = !isExperimentalMM // Experimental MM does not have a GC yet. goldValue = "84\nFoo\nDeallocated\n" + "Hello, World!\nKotlin says: Hello, everybody!\nHello from Kotlin\n2, 1\n" + "true\ntrue\n" + @@ -4242,7 +4234,7 @@ if (PlatformInfo.isAppleTarget(project)) { } interopTestMultifile("interop_objc_tests") { - enabled = !isExperimentalMM // Experimental MM does not have a GC yet. + enabled = !isExperimentalMM // Experimental MM does not support Obj-C weaks yet. source = "interop/objc/tests/" interop = 'objcTests' flags = ['-tr', '-e', 'main'] @@ -4492,7 +4484,7 @@ dynamicTest("interop_kt42397") { dynamicTest("interop_cleaners_main_thread") { disabled = (project.target.name != project.hostName) || - isExperimentalMM // Experimental MM does not have a GC yet. + isExperimentalMM // Experimental MM doesn't support multiple mutators yet. source = "interop/cleaners/cleaners.kt" cSource = "$projectDir/interop/cleaners/main_thread.cpp" clangTool = "clang++" @@ -4826,7 +4818,6 @@ if (isAppleTarget(project)) { } frameworkTest("testStdlibFramework") { - enabled = !isExperimentalMM // Experimental MM does not have GC yet. framework('Stdlib') { sources = ['framework/stdlib'] bitcode = true diff --git a/kotlin-native/runtime/src/main/cpp/FinalizerHooks.hpp b/kotlin-native/runtime/src/main/cpp/FinalizerHooks.hpp index 135dfa9b621..9571efbdc0a 100644 --- a/kotlin-native/runtime/src/main/cpp/FinalizerHooks.hpp +++ b/kotlin-native/runtime/src/main/cpp/FinalizerHooks.hpp @@ -10,6 +10,9 @@ struct ObjHeader; namespace kotlin { +// Note: when finalizer is run, object's `ObjHeader*` fields might already be freed, +// finalizer must never try to reference them. + bool HasFinalizers(ObjHeader* object) noexcept; void RunFinalizers(ObjHeader* object) noexcept; diff --git a/kotlin-native/runtime/src/main/cpp/ObjectTestSupport.hpp b/kotlin-native/runtime/src/main/cpp/ObjectTestSupport.hpp index 7a4c54e9e47..4a42459e89f 100644 --- a/kotlin-native/runtime/src/main/cpp/ObjectTestSupport.hpp +++ b/kotlin-native/runtime/src/main/cpp/ObjectTestSupport.hpp @@ -209,66 +209,110 @@ private: template class ObjectArray : public internal::Array { public: + static ObjectArray& FromArrayHeader(ArrayHeader* arr) noexcept { + return static_cast&>(internal::Array::FromArrayHeader(arr)); + } + ObjectArray() noexcept : internal::Array(theArrayTypeInfo) {} }; template class BooleanArray : public internal::Array { public: + static BooleanArray& FromArrayHeader(ArrayHeader* arr) noexcept { + return static_cast&>(internal::Array::FromArrayHeader(arr)); + } + BooleanArray() noexcept : internal::Array(theBooleanArrayTypeInfo) {} }; template class ByteArray : public internal::Array { public: + static ByteArray& FromArrayHeader(ArrayHeader* arr) noexcept { + return static_cast&>(internal::Array::FromArrayHeader(arr)); + } + ByteArray() noexcept : internal::Array(theByteArrayTypeInfo) {} }; template class CharArray : public internal::Array { public: + static CharArray& FromArrayHeader(ArrayHeader* arr) noexcept { + return static_cast&>(internal::Array::FromArrayHeader(arr)); + } + CharArray() noexcept : internal::Array(theCharArrayTypeInfo) {} }; template class DoubleArray : public internal::Array { public: + static DoubleArray& FromArrayHeader(ArrayHeader* arr) noexcept { + return static_cast&>(internal::Array::FromArrayHeader(arr)); + } + DoubleArray() noexcept : internal::Array(theDoubleArrayTypeInfo) {} }; template class FloatArray : public internal::Array { public: + static FloatArray& FromArrayHeader(ArrayHeader* arr) noexcept { + return static_cast&>(internal::Array::FromArrayHeader(arr)); + } + FloatArray() noexcept : internal::Array(theFloatArrayTypeInfo) {} }; template class IntArray : public internal::Array { public: + static IntArray& FromArrayHeader(ArrayHeader* arr) noexcept { + return static_cast&>(internal::Array::FromArrayHeader(arr)); + } + IntArray() noexcept : internal::Array(theIntArrayTypeInfo) {} }; template class LongArray : public internal::Array { public: + static LongArray& FromArrayHeader(ArrayHeader* arr) noexcept { + return static_cast&>(internal::Array::FromArrayHeader(arr)); + } + LongArray() noexcept : internal::Array(theLongArrayTypeInfo) {} }; template class NativePtrArray : public internal::Array { public: + static NativePtrArray& FromArrayHeader(ArrayHeader* arr) noexcept { + return static_cast&>(internal::Array::FromArrayHeader(arr)); + } + NativePtrArray() noexcept : internal::Array(theNativePtrArrayTypeInfo) {} }; template class ShortArray : public internal::Array { public: + static ShortArray& FromArrayHeader(ArrayHeader* arr) noexcept { + return static_cast&>(internal::Array::FromArrayHeader(arr)); + } + ShortArray() noexcept : internal::Array(theShortArrayTypeInfo) {} }; template class String : public internal::Array { public: + static String& FromArrayHeader(ArrayHeader* arr) noexcept { + return static_cast&>(internal::Array::FromArrayHeader(arr)); + } + String() noexcept : internal::Array(theStringTypeInfo) {} }; diff --git a/kotlin-native/runtime/src/mm/cpp/ExtraObjectData.cpp b/kotlin-native/runtime/src/mm/cpp/ExtraObjectData.cpp index 61959c6a32c..b0086d7e3b2 100644 --- a/kotlin-native/runtime/src/mm/cpp/ExtraObjectData.cpp +++ b/kotlin-native/runtime/src/mm/cpp/ExtraObjectData.cpp @@ -5,6 +5,7 @@ #include "ExtraObjectData.hpp" +#include "ObjectOps.hpp" #include "PointerBits.h" #include "Weak.h" @@ -64,11 +65,19 @@ void mm::ExtraObjectData::Uninstall(ObjHeader* object) noexcept { delete &data; } +bool mm::ExtraObjectData::HasWeakReferenceCounter() noexcept { + return weakReferenceCounter_ != nullptr; +} + +void mm::ExtraObjectData::ClearWeakReferenceCounter() noexcept { + if (!HasWeakReferenceCounter()) return; + + WeakReferenceCounterClear(weakReferenceCounter_); + mm::SetHeapRef(&weakReferenceCounter_, nullptr); +} + mm::ExtraObjectData::~ExtraObjectData() { - if (weakReferenceCounter_) { - WeakReferenceCounterClear(weakReferenceCounter_); - ZeroHeapRef(&weakReferenceCounter_); - } + RuntimeAssert(!HasWeakReferenceCounter(), "Object must have cleared weak references"); #ifdef KONAN_OBJC_INTEROP Kotlin_ObjCExport_releaseAssociatedObject(associatedObject_); diff --git a/kotlin-native/runtime/src/mm/cpp/ExtraObjectData.hpp b/kotlin-native/runtime/src/mm/cpp/ExtraObjectData.hpp index 1bf84344670..b4693ed0305 100644 --- a/kotlin-native/runtime/src/mm/cpp/ExtraObjectData.hpp +++ b/kotlin-native/runtime/src/mm/cpp/ExtraObjectData.hpp @@ -47,6 +47,9 @@ public: std::atomic& flags() noexcept { return flags_; } + bool HasWeakReferenceCounter() noexcept; + void ClearWeakReferenceCounter() noexcept; + private: explicit ExtraObjectData(const TypeInfo* typeInfo) noexcept : typeInfo_(typeInfo) {} ~ExtraObjectData(); @@ -60,7 +63,6 @@ private: void* associatedObject_ = nullptr; #endif - // TODO: Need to respect when marking. ObjHeader* weakReferenceCounter_ = nullptr; }; diff --git a/kotlin-native/runtime/src/mm/cpp/GC.hpp b/kotlin-native/runtime/src/mm/cpp/GC.hpp index 83e1c3b2649..2556c829d87 100644 --- a/kotlin-native/runtime/src/mm/cpp/GC.hpp +++ b/kotlin-native/runtime/src/mm/cpp/GC.hpp @@ -6,6 +6,7 @@ #ifndef RUNTIME_MM_GC_H #define RUNTIME_MM_GC_H +#include "gc/SingleThreadMarkAndSweep.hpp" #include "gc/NoOpGC.hpp" namespace kotlin { @@ -14,7 +15,7 @@ namespace mm { // TODO: GC should be extracted into a separate module, so that we can do different GCs without // the need to redo the entire MM. For now changing GCs can be done by modifying `using` below. -using GC = NoOpGC; +using GC = SingleThreadMarkAndSweep; } // namespace mm } // namespace kotlin diff --git a/kotlin-native/runtime/src/mm/cpp/GlobalsRegistry.hpp b/kotlin-native/runtime/src/mm/cpp/GlobalsRegistry.hpp index 8e6c9754e52..ce0177eea0f 100644 --- a/kotlin-native/runtime/src/mm/cpp/GlobalsRegistry.hpp +++ b/kotlin-native/runtime/src/mm/cpp/GlobalsRegistry.hpp @@ -43,6 +43,8 @@ public: // much of a problem is it. Iterable Iter() noexcept { return globals_.Iter(); } + void ClearForTests() { globals_.ClearForTests(); } + private: // TODO: Add-only MultiSourceQueue can be made more efficient. Measure, if it's a problem. MultiSourceQueue globals_; diff --git a/kotlin-native/runtime/src/mm/cpp/Memory.cpp b/kotlin-native/runtime/src/mm/cpp/Memory.cpp index c466945c71d..265fa5ff51d 100644 --- a/kotlin-native/runtime/src/mm/cpp/Memory.cpp +++ b/kotlin-native/runtime/src/mm/cpp/Memory.cpp @@ -92,7 +92,12 @@ extern "C" MemoryState* InitMemory(bool firstRuntime) { } extern "C" void DeinitMemory(MemoryState* state, bool destroyRuntime) { - mm::ThreadRegistry::Instance().Unregister(mm::FromMemoryState(state)); + auto* node = mm::FromMemoryState(state); + if (destroyRuntime) { + node->Get()->gc().PerformFullGC(); + // TODO: Also make sure that finalizers are run. + } + mm::ThreadRegistry::Instance().Unregister(node); } extern "C" void RestoreMemory(MemoryState*) { diff --git a/kotlin-native/runtime/src/mm/cpp/ObjectFactory.hpp b/kotlin-native/runtime/src/mm/cpp/ObjectFactory.hpp index 58b1d763516..d7777567d0a 100644 --- a/kotlin-native/runtime/src/mm/cpp/ObjectFactory.hpp +++ b/kotlin-native/runtime/src/mm/cpp/ObjectFactory.hpp @@ -13,6 +13,7 @@ #include "Alignment.hpp" #include "Alloc.h" +#include "FinalizerHooks.hpp" #include "Memory.h" #include "Mutex.hpp" #include "Types.h" @@ -101,6 +102,26 @@ public: class Producer : private MoveOnly { public: + class Iterator { + public: + Node& operator*() noexcept { return *node_; } + Node* operator->() noexcept { return node_; } + + Iterator& operator++() noexcept { + node_ = node_->next_.get(); + return *this; + } + + bool operator==(const Iterator& rhs) const noexcept { return node_ == rhs.node_; } + bool operator!=(const Iterator& rhs) const noexcept { return node_ != rhs.node_; } + + private: + friend class Producer; + explicit Iterator(Node* node) noexcept : node_(node) {} + + Node* node_; + }; + Producer(ObjectFactoryStorage& owner, Allocator allocator) noexcept : owner_(owner), allocator_(std::move(allocator)) {} ~Producer() { Publish(); } @@ -158,6 +179,9 @@ public: owner_.AssertCorrectUnsafe(); } + Iterator begin() noexcept { return Iterator(root_.get()); } + Iterator end() noexcept { return Iterator(nullptr); } + void ClearForTests() noexcept { // Since it's only for tests, no need to worry about stack overflows. root_.reset(); @@ -302,6 +326,11 @@ public: // Lock `ObjectFactoryStorage` for safe iteration. Iterable Iter() noexcept { return Iterable(*this); } + void ClearForTests() { + root_.reset(); + last_ = nullptr; + } + private: // Expects `mutex_` to be held by the current thread. std::pair, Node*> ExtractUnsafe(Node* previousNode) noexcept { @@ -454,6 +483,27 @@ public: class ThreadQueue : private MoveOnly { public: + class Iterator { + public: + NodeRef operator*() noexcept { return NodeRef(*iterator_); } + NodeRef operator->() noexcept { return NodeRef(*iterator_); } + + Iterator& operator++() noexcept { + ++iterator_; + return *this; + } + + bool operator==(const Iterator& rhs) const noexcept { return iterator_ == rhs.iterator_; } + bool operator!=(const Iterator& rhs) const noexcept { return iterator_ != rhs.iterator_; } + + private: + friend class ObjectFactory; + + explicit Iterator(typename Storage::Producer::Iterator iterator) noexcept : iterator_(std::move(iterator)) {} + + typename Storage::Producer::Iterator iterator_; + }; + ThreadQueue(ObjectFactory& owner, GCThreadData& gc) noexcept : producer_(owner.storage_, internal::AllocatorWithGC(internal::SimpleAllocator(), gc)) {} @@ -483,6 +533,9 @@ public: void Publish() noexcept { producer_.Publish(); } + Iterator begin() noexcept { return Iterator(producer_.begin()); } + Iterator end() noexcept { return Iterator(producer_.end()); } + void ClearForTests() noexcept { producer_.ClearForTests(); } private: @@ -534,8 +587,27 @@ public: typename Storage::Consumer::Iterator iterator_; }; - Iterator begin() noexcept { return Iterator(consumer_.begin()); } - Iterator end() noexcept { return Iterator(consumer_.end()); } + class Iterable { + public: + Iterator begin() noexcept { return Iterator(owner_.consumer_.begin()); } + Iterator end() noexcept { return Iterator(owner_.consumer_.end()); } + + private: + friend class FinalizerQueue; + + explicit Iterable(FinalizerQueue& owner) : owner_(owner) {} + + FinalizerQueue& owner_; + }; + + // TODO: Consider running it in the destructor instead. + void Finalize() noexcept { + for (auto node : Iterable(*this)) { + RunFinalizers(node->IsArray() ? node->GetArrayHeader()->obj() : node->GetObjHeader()); + } + } + + Iterable IterForTests() noexcept { return Iterable(*this); } private: friend class ObjectFactory; @@ -565,6 +637,8 @@ public: Iterable Iter() noexcept { return Iterable(*this); } + void ClearForTests() { storage_.ClearForTests(); } + private: Storage storage_; }; diff --git a/kotlin-native/runtime/src/mm/cpp/ObjectFactoryTest.cpp b/kotlin-native/runtime/src/mm/cpp/ObjectFactoryTest.cpp index 3129b78a304..25aaa5e4670 100644 --- a/kotlin-native/runtime/src/mm/cpp/ObjectFactoryTest.cpp +++ b/kotlin-native/runtime/src/mm/cpp/ObjectFactoryTest.cpp @@ -12,6 +12,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "FinalizerHooksTestSupport.hpp" #include "GC.hpp" #include "ObjectTestSupport.hpp" #include "TestSupport.hpp" @@ -889,13 +890,45 @@ TEST(ObjectFactoryTest, Move) { { int count = 0; - for (auto it = finalizerQueue.begin(); it != finalizerQueue.end(); ++it, ++count) { + auto iter = finalizerQueue.IterForTests(); + for (auto it = iter.begin(); it != iter.end(); ++it, ++count) { EXPECT_TRUE(it->IsArray()); } EXPECT_THAT(count, 10); } } +TEST(ObjectFactoryTest, RunFinalizers) { + FinalizerHooksTestSupport finalizerHooks; + + test_support::TypeInfoHolder objectType{test_support::TypeInfoHolder::ObjectBuilder().addFlag(TF_HAS_FINALIZER)}; + GC::ThreadData gc; + ObjectFactory objectFactory; + ObjectFactory::ThreadQueue threadQueue(objectFactory, gc); + ObjectFactory::FinalizerQueue finalizerQueue; + + KStdVector objects; + for (int i = 0; i < 10; ++i) { + objects.push_back(threadQueue.CreateObject(objectType.typeInfo())); + } + + threadQueue.Publish(); + + { + auto iter = objectFactory.Iter(); + for (auto it = iter.begin(); it != iter.end();) { + iter.MoveAndAdvance(finalizerQueue, it); + } + } + + for (auto& object : objects) { + EXPECT_CALL(finalizerHooks.finalizerHook(), Call(object)); + } + finalizerQueue.Finalize(); + // Hooks called before `FinalizerQueue` destructor. + testing::Mock::VerifyAndClearExpectations(&finalizerHooks.finalizerHook()); +} + TEST(ObjectFactoryTest, ConcurrentPublish) { test_support::TypeInfoHolder type{test_support::TypeInfoHolder::ObjectBuilder()}; ObjectFactory objectFactory; diff --git a/kotlin-native/runtime/src/mm/cpp/gc/MarkAndSweepUtils.hpp b/kotlin-native/runtime/src/mm/cpp/gc/MarkAndSweepUtils.hpp new file mode 100644 index 00000000000..a028e413ec1 --- /dev/null +++ b/kotlin-native/runtime/src/mm/cpp/gc/MarkAndSweepUtils.hpp @@ -0,0 +1,79 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#ifndef RUNTIME_MM_MARK_AND_SWEEP_UTILS_H +#define RUNTIME_MM_MARK_AND_SWEEP_UTILS_H + +#include "../ExtraObjectData.hpp" +#include "FinalizerHooks.hpp" +#include "Memory.h" +#include "ObjectTraversal.hpp" +#include "Runtime.h" +#include "Types.h" + +namespace kotlin { +namespace mm { + +// TODO: Because of `graySet` this implementation may allocate heap memory during GC. +template +void Mark(KStdVector graySet) noexcept { + while (!graySet.empty()) { + ObjHeader* top = graySet.back(); + graySet.pop_back(); + + RuntimeAssert(!isNullOrMarker(top), "Got invalid reference %p in gray set", top); + RuntimeAssert(!top->local(), "TODO: Stack objects are not supported yet, top=%p", top); + + if (top->heap()) { + if (!Traits::TryMark(top)) { + continue; + } + } + + if (!top->permanent()) { + traverseReferredObjects(top, [&graySet](ObjHeader* field) noexcept { + if (!isNullOrMarker(field) && !field->permanent() && !Traits::IsMarked(field)) { + graySet.push_back(field); + } + }); + } + + if (auto* extraObjectData = mm::ExtraObjectData::Get(top)) { + auto* weakCounter = *extraObjectData->GetWeakCounterLocation(); + if (!isNullOrMarker(weakCounter)) { + graySet.push_back(weakCounter); + } + } + } +} + +template +typename Traits::ObjectFactory::FinalizerQueue Sweep(typename Traits::ObjectFactory& objectFactory) noexcept { + typename Traits::ObjectFactory::FinalizerQueue finalizerQueue; + + auto iter = objectFactory.Iter(); + for (auto it = iter.begin(); it != iter.end();) { + if (Traits::TryResetMark(*it)) { + ++it; + continue; + } + auto* objHeader = it->IsArray() ? it->GetArrayHeader()->obj() : it->GetObjHeader(); + if (auto* extraObject = mm::ExtraObjectData::Get(objHeader)) { + extraObject->ClearWeakReferenceCounter(); + } + if (HasFinalizers(objHeader)) { + iter.MoveAndAdvance(finalizerQueue, it); + } else { + iter.EraseAndAdvance(it); + } + } + + return finalizerQueue; +} + +} // namespace mm +} // namespace kotlin + +#endif // RUNTIME_MM_MARK_AND_SWEEP_UTILS_H diff --git a/kotlin-native/runtime/src/mm/cpp/gc/MarkAndSweepUtilsMarkTest.cpp b/kotlin-native/runtime/src/mm/cpp/gc/MarkAndSweepUtilsMarkTest.cpp new file mode 100644 index 00000000000..d4fc71a11cd --- /dev/null +++ b/kotlin-native/runtime/src/mm/cpp/gc/MarkAndSweepUtilsMarkTest.cpp @@ -0,0 +1,495 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#include "MarkAndSweepUtils.hpp" + +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "FinalizerHooks.hpp" +#include "ObjectTestSupport.hpp" +#include "Utils.hpp" + +using namespace kotlin; + +namespace { + +struct Payload { + ObjHeader* field1; + ObjHeader* field2; + ObjHeader* field3; + + static constexpr std::array kFields = { + &Payload::field1, + &Payload::field2, + &Payload::field3, + }; +}; + +test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder()}; + +// TODO: This base might belong in `test_support` +class BaseObject { +public: + enum class Kind { + kPermanent, + kHeapLike // Treated as heap object for the purposes of the test. + }; + + virtual ObjHeader* GetObjHeader() = 0; + + void InstallExtraData() { mm::ExtraObjectData::Install(GetObjHeader()); } + + void InstallWeakCounter(BaseObject& counter) { + auto& extraObjectData = mm::ExtraObjectData::GetOrInstall(GetObjHeader()); + *extraObjectData.GetWeakCounterLocation() = counter.GetObjHeader(); + } + +protected: + void SetKind(Kind kind) { + switch (kind) { + case Kind::kPermanent: + GetObjHeader()->typeInfoOrMeta_ = setPointerBits(GetObjHeader()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + RuntimeAssert(GetObjHeader()->permanent(), "Must be permanent"); + break; + case Kind::kHeapLike: + RuntimeAssert(GetObjHeader()->heap(), "Must be heap"); + break; + } + } + + void Finalize() { + if (auto* extraObjectData = mm::ExtraObjectData::Get(GetObjHeader())) { + extraObjectData->ClearWeakReferenceCounter(); + } + RunFinalizers(GetObjHeader()); + } +}; + +class Object : public BaseObject, public test_support::Object { +public: + explicit Object(Kind kind = Kind::kHeapLike) : test_support::Object(typeHolder.typeInfo()) { SetKind(kind); } + + ~Object() { Finalize(); } + + ObjHeader* GetObjHeader() override { return header(); } +}; + +class ObjectArray : public BaseObject, public test_support::ObjectArray<3> { +public: + explicit ObjectArray(Kind kind = Kind::kHeapLike) : test_support::ObjectArray<3>() { SetKind(kind); } + + ~ObjectArray() { Finalize(); } + + ObjHeader* GetObjHeader() override { return header(); } +}; + +class CharArray : public BaseObject, public test_support::CharArray<3> { +public: + explicit CharArray(Kind kind = Kind::kHeapLike) : test_support::CharArray<3>() { SetKind(kind); } + + ~CharArray() { Finalize(); } + + ObjHeader* GetObjHeader() override { return header(); } +}; + +class ScopedMarkTraits : private Pinned { +public: + ScopedMarkTraits() { + RuntimeAssert(instance_ == nullptr, "Only one ScopedMarkTraits is allowed"); + instance_ = this; + } + + ~ScopedMarkTraits() { + RuntimeAssert(instance_ == this, "ScopedMarkTraits instance broke"); + instance_ = nullptr; + } + + const KStdUnorderedSet& marked() const { return marked_; } + + static bool TryMark(ObjHeader* object) noexcept { return instance_->marked_.insert(object).second; } + static bool IsMarked(ObjHeader* object) noexcept { return instance_->marked_.find(object) != instance_->marked_.end(); } + +private: + static ScopedMarkTraits* instance_; + + KStdUnorderedSet marked_; +}; + +// static +ScopedMarkTraits* ScopedMarkTraits::instance_ = nullptr; + +class MarkAndSweepUtilsMarkTest : public ::testing::Test { +public: + const KStdUnorderedSet& marked() const { return markTraits_.marked(); } + + auto MarkedMatcher(std::initializer_list> expected) { + KStdVector objects; + for (auto& object : expected) { + objects.push_back(object.get().GetObjHeader()); + } + return testing::UnorderedElementsAreArray(objects); + } + + void Mark(std::initializer_list> graySet) { + KStdVector objects; + for (auto& object : graySet) objects.push_back(object.get().GetObjHeader()); + mm::Mark(std::move(objects)); + } + +private: + ScopedMarkTraits markTraits_; +}; + +#define EXPECT_MARKED(...) EXPECT_THAT(marked(), MarkedMatcher({__VA_ARGS__})) + +} // namespace + +TEST_F(MarkAndSweepUtilsMarkTest, MarkNothing) { + Mark({}); + + EXPECT_MARKED(); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleObject) { + Object object; + + Mark({object}); + + EXPECT_MARKED(object); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleObjectArray) { + ObjectArray array; + + Mark({array}); + + EXPECT_MARKED(array); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleCharArray) { + CharArray array; + + Mark({array}); + + EXPECT_MARKED(array); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSinglePermanentObject) { + Object object{BaseObject::Kind::kPermanent}; + + Mark({object}); + + EXPECT_MARKED(); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSinglePermanentObjectArray) { + ObjectArray array{BaseObject::Kind::kPermanent}; + + Mark({array}); + + EXPECT_MARKED(); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSinglePermanentCharArray) { + CharArray array{BaseObject::Kind::kPermanent}; + + Mark({array}); + + EXPECT_MARKED(); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleObjectWithInvalidFields) { + Object object; + object->field1 = kInitializingSingleton; + + Mark({object}); + + EXPECT_MARKED(object); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleObjectArrayWithInvalidFields) { + ObjectArray array; + array.elements()[0] = kInitializingSingleton; + + Mark({array}); + + EXPECT_MARKED(array); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleCharArrayWithSomeData) { + CharArray array; + array.elements()[0] = 'a'; + array.elements()[1] = 'b'; + array.elements()[2] = 'c'; + + Mark({array}); + + EXPECT_MARKED(array); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleObjectWithExtraData) { + Object object; + object.InstallExtraData(); + + Mark({object}); + + EXPECT_MARKED(object); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleObjectArrayWithExtraData) { + ObjectArray array; + array.InstallExtraData(); + + Mark({array}); + + EXPECT_MARKED(array); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleCharArrayWithExtraData) { + CharArray array; + array.InstallExtraData(); + + Mark({array}); + + EXPECT_MARKED(array); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleObjectWithWeakCounter) { + Object weakCounter; + Object object; + object.InstallWeakCounter(weakCounter); + + Mark({object}); + + EXPECT_MARKED(object, weakCounter); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleObjectArrayWithWeakCounter) { + Object weakCounter; + ObjectArray array; + array.InstallWeakCounter(weakCounter); + + Mark({array}); + + EXPECT_MARKED(array, weakCounter); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleCharArrayWithWeakCounter) { + Object weakCounter; + CharArray array; + array.InstallWeakCounter(weakCounter); + + Mark({array}); + + EXPECT_MARKED(array, weakCounter); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleObjectWithInvalidFieldsWithWeakCounter) { + Object weakCounter; + Object object; + object->field1 = kInitializingSingleton; + object.InstallWeakCounter(weakCounter); + + Mark({object}); + + EXPECT_MARKED(object, weakCounter); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleObjectArrayWithInvalidFieldsWithWeakCounter) { + Object weakCounter; + ObjectArray array; + array.elements()[0] = kInitializingSingleton; + array.InstallWeakCounter(weakCounter); + + Mark({array}); + + EXPECT_MARKED(array, weakCounter); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkSingleCharArrayWithSomeDataWithWeakCounter) { + Object weakCounter; + CharArray array; + array.elements()[0] = 'a'; + array.elements()[1] = 'b'; + array.elements()[2] = 'c'; + array.InstallWeakCounter(weakCounter); + + Mark({array}); + + EXPECT_MARKED(array, weakCounter); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkTree) { + Object root; + Object root_field1; + Object root_field1_field1; + Object root_field1_field2; + ObjectArray root_field3; + Object root_field3_element1; + ObjectArray root_field3_element2; + CharArray root_field3_element3; + root->field1 = root_field1.header(); + root_field1->field1 = root_field1_field1.header(); + root_field1->field2 = root_field1_field2.header(); + root->field3 = root_field3.header(); + root_field3.elements()[0] = root_field3_element1.header(); + root_field3.elements()[1] = root_field3_element2.header(); + root_field3.elements()[2] = root_field3_element3.header(); + + Mark({root}); + + EXPECT_MARKED( + root, root_field1, root_field1_field1, root_field1_field2, root_field3, root_field3_element1, root_field3_element2, + root_field3_element3); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkTreeWithPermanentRoot) { + Object root{BaseObject::Kind::kPermanent}; + Object root_field1{BaseObject::Kind::kPermanent}; + Object root_field1_field1{BaseObject::Kind::kPermanent}; + Object root_field1_field2{BaseObject::Kind::kPermanent}; + ObjectArray root_field3{BaseObject::Kind::kPermanent}; + Object root_field3_element1{BaseObject::Kind::kPermanent}; + ObjectArray root_field3_element2{BaseObject::Kind::kPermanent}; + CharArray root_field3_element3{BaseObject::Kind::kPermanent}; + root->field1 = root_field1.header(); + root_field1->field1 = root_field1_field1.header(); + root_field1->field2 = root_field1_field2.header(); + root->field3 = root_field3.header(); + root_field3.elements()[0] = root_field3_element1.header(); + root_field3.elements()[1] = root_field3_element2.header(); + root_field3.elements()[2] = root_field3_element3.header(); + + Mark({root}); + + EXPECT_MARKED(); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkTreeWithPermanentMiddle) { + Object root; + Object root_field1{BaseObject::Kind::kPermanent}; + Object root_field1_field1{BaseObject::Kind::kPermanent}; + Object root_field1_field2{BaseObject::Kind::kPermanent}; + ObjectArray root_field3; + Object root_field3_element1; + ObjectArray root_field3_element2; + CharArray root_field3_element3; + root->field1 = root_field1.header(); + root_field1->field1 = root_field1_field1.header(); + root_field1->field2 = root_field1_field2.header(); + root->field3 = root_field3.header(); + root_field3.elements()[0] = root_field3_element1.header(); + root_field3.elements()[1] = root_field3_element2.header(); + root_field3.elements()[2] = root_field3_element3.header(); + + Mark({root}); + + EXPECT_MARKED(root, root_field3, root_field3_element1, root_field3_element2, root_field3_element3); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkTreeWithPermanentLeaf) { + Object root; + Object root_field1; + Object root_field1_field1{BaseObject::Kind::kPermanent}; + Object root_field1_field2; + ObjectArray root_field3; + Object root_field3_element1; + ObjectArray root_field3_element2; + CharArray root_field3_element3; + root->field1 = root_field1.header(); + root_field1->field1 = root_field1_field1.header(); + root_field1->field2 = root_field1_field2.header(); + root->field3 = root_field3.header(); + root_field3.elements()[0] = root_field3_element1.header(); + root_field3.elements()[1] = root_field3_element2.header(); + root_field3.elements()[2] = root_field3_element3.header(); + + Mark({root}); + + EXPECT_MARKED(root, root_field1, root_field1_field2, root_field3, root_field3_element1, root_field3_element2, root_field3_element3); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkRecursiveTree) { + Object root; + Object inner1; + ObjectArray inner2; + root->field1 = inner1.header(); + inner1->field1 = inner2.header(); + inner2.elements()[0] = root.header(); + + Mark({root}); + + EXPECT_MARKED(root, inner1, inner2); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkRecursiveTreeWithPermanentRoot) { + Object root{BaseObject::Kind::kPermanent}; + Object inner1{BaseObject::Kind::kPermanent}; + ObjectArray inner2{BaseObject::Kind::kPermanent}; + root->field1 = inner1.header(); + inner1->field1 = inner2.header(); + inner2.elements()[0] = root.header(); + + Mark({root}); + + EXPECT_MARKED(); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkForest) { + Object root1; + ObjectArray root2; + Object root3; + + Mark({root1, root2, root3}); + + EXPECT_MARKED(root1, root2, root3); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkForestWithPermanentFirst) { + Object root1{BaseObject::Kind::kPermanent}; + ObjectArray root2; + Object root3; + + Mark({root1, root2, root3}); + + EXPECT_MARKED(root2, root3); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkForestWithPermanentSecond) { + Object root1; + ObjectArray root2{BaseObject::Kind::kPermanent}; + Object root3; + + Mark({root1, root2, root3}); + + EXPECT_MARKED(root1, root3); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkForestWithPermanentThird) { + Object root1; + ObjectArray root2; + Object root3{BaseObject::Kind::kPermanent}; + + Mark({root1, root2, root3}); + + EXPECT_MARKED(root1, root2); +} + +TEST_F(MarkAndSweepUtilsMarkTest, MarkForestWithInterconnectedRoots) { + Object root1; + ObjectArray root2; + Object root3; + + root1->field1 = root2.header(); + root2.elements()[0] = root3.header(); + root3->field1 = root1.header(); + + Mark({root1, root2, root3}); + + EXPECT_MARKED(root1, root2, root3); +} diff --git a/kotlin-native/runtime/src/mm/cpp/gc/MarkAndSweepUtilsSweepTest.cpp b/kotlin-native/runtime/src/mm/cpp/gc/MarkAndSweepUtilsSweepTest.cpp new file mode 100644 index 00000000000..d991efb5dc0 --- /dev/null +++ b/kotlin-native/runtime/src/mm/cpp/gc/MarkAndSweepUtilsSweepTest.cpp @@ -0,0 +1,576 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#include "MarkAndSweepUtils.hpp" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "../ObjectFactory.hpp" +#include "FinalizerHooksTestSupport.hpp" +#include "ObjectTestSupport.hpp" + +using namespace kotlin; + +namespace { + +struct Payload { + ObjHeader* field1; + ObjHeader* field2; + ObjHeader* field3; + + static constexpr std::array kFields = { + &Payload::field1, + &Payload::field2, + &Payload::field3, + }; +}; + +// TODO: This should go into test support for weak references. +struct WeakCounterPayload { + void* referred; + KInt lock; + KInt cookie; + + static constexpr std::array kFields{}; +}; + +test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder()}; +test_support::TypeInfoHolder typeHolderWithFinalizer{test_support::TypeInfoHolder::ObjectBuilder().addFlag(TF_HAS_FINALIZER)}; +test_support::TypeInfoHolder typeHolderWeakCounter{test_support::TypeInfoHolder::ObjectBuilder()}; + +struct GC { + struct ObjectData { + enum class State { + kUnmarked, + kMarked, + kMarkReset, + }; + State state = State::kUnmarked; + }; + + struct ThreadData { + void SafePointAllocation(size_t) {} + void OnOOM(size_t) {} + }; +}; + +using ObjectFactory = mm::ObjectFactory; + +class Object : public test_support::Object { +public: + // No way to directly create or destroy it. + Object() = delete; + ~Object() = delete; + + static Object& FromObjHeader(ObjHeader* obj) { return static_cast(test_support::Object::FromObjHeader(obj)); } + + void InstallExtraData() { mm::ExtraObjectData::Install(header()); } + + bool HasWeakCounter() { + if (auto* extraObjectData = mm::ExtraObjectData::Get(header())) { + return extraObjectData->HasWeakReferenceCounter(); + } + return false; + } + + void Mark() { objectData().state = GC::ObjectData::State::kMarked; } + + GC::ObjectData::State state() { return objectData().state; } + +private: + GC::ObjectData& objectData() { return ObjectFactory::NodeRef::From(header()).GCObjectData(); } +}; + +class ObjectArray : public test_support::ObjectArray<3> { +public: + // No way to directly create or destroy it. + ObjectArray() = delete; + ~ObjectArray() = delete; + + static ObjectArray& FromArrayHeader(ArrayHeader* array) { + return static_cast(test_support::ObjectArray<3>::FromArrayHeader(array)); + } + + void InstallExtraData() { mm::ExtraObjectData::Install(header()); } + + bool HasWeakCounter() { + if (auto* extraObjectData = mm::ExtraObjectData::Get(header())) { + return extraObjectData->HasWeakReferenceCounter(); + } + return false; + } + + void Mark() { objectData().state = GC::ObjectData::State::kMarked; } + + GC::ObjectData::State state() { return objectData().state; } + +private: + GC::ObjectData& objectData() { return ObjectFactory::NodeRef::From(header()).GCObjectData(); } +}; + +class CharArray : public test_support::CharArray<3> { +public: + // No way to directly create or destroy it. + CharArray() = delete; + ~CharArray() = delete; + + static CharArray& FromArrayHeader(ArrayHeader* array) { + return static_cast(test_support::CharArray<3>::FromArrayHeader(array)); + } + + void InstallExtraData() { mm::ExtraObjectData::Install(header()); } + + bool HasWeakCounter() { + if (auto* extraObjectData = mm::ExtraObjectData::Get(header())) { + return extraObjectData->HasWeakReferenceCounter(); + } + return false; + } + + void Mark() { objectData().state = GC::ObjectData::State::kMarked; } + + GC::ObjectData::State state() { return objectData().state; } + +private: + GC::ObjectData& objectData() { return ObjectFactory::NodeRef::From(header()).GCObjectData(); } +}; + +using WeakCounter = test_support::Object; + +void MarkWeakCounter(WeakCounter& counter) { + ObjectFactory::NodeRef::From(counter.header()).GCObjectData().state = GC::ObjectData::State::kMarked; +} + +GC::ObjectData::State GetWeakCounterState(WeakCounter& counter) { + return ObjectFactory::NodeRef::From(counter.header()).GCObjectData().state; +} + +struct SweepTraits { + using ObjectFactory = ObjectFactory; + + static bool TryResetMark(ObjectFactory::NodeRef node) { + GC::ObjectData& objectData = node.GCObjectData(); + switch (objectData.state) { + case GC::ObjectData::State::kUnmarked: + return false; + case GC::ObjectData::State::kMarked: + objectData.state = GC::ObjectData::State::kMarkReset; + return true; + case GC::ObjectData::State::kMarkReset: + RuntimeFail("Trying to reset mark twice."); + } + } +}; + +class MarkAndSweepUtilsSweepTest : public ::testing::Test { +public: + ~MarkAndSweepUtilsSweepTest() override { + for (auto& finalizerQueue : finalizers_) { + finalizerQueue.Finalize(); + } + testing::Mock::VerifyAndClear(&finalizerHook()); + // TODO: Figure out a better way to clear up the stuff. + EXPECT_CALL(finalizerHook(), Call(testing::_)).Times(testing::AnyNumber()); + for (auto node : objectFactory_.Iter()) { + auto* obj = node->IsArray() ? node->GetArrayHeader()->obj() : node->GetObjHeader(); + if (auto* extraObject = mm::ExtraObjectData::Get(obj)) { + extraObject->ClearWeakReferenceCounter(); + } + RunFinalizers(obj); + } + } + + KStdVector Sweep() { + auto finalizers = mm::Sweep(objectFactory_); + KStdVector objects; + for (auto node : finalizers.IterForTests()) { + objects.push_back(node.IsArray() ? node.GetArrayHeader()->obj() : node.GetObjHeader()); + } + finalizers_.push_back(std::move(finalizers)); + return objects; + } + + KStdVector Alive() { + KStdVector objects; + for (auto node : objectFactory_.Iter()) { + objects.push_back(node.IsArray() ? node.GetArrayHeader()->obj() : node.GetObjHeader()); + } + return objects; + } + + Object& AllocateObject(const TypeInfo* typeInfo = typeHolder.typeInfo()) { + auto* object = objectFactoryThreadQueue_.CreateObject(typeInfo); + objectFactoryThreadQueue_.Publish(); + return Object::FromObjHeader(object); + } + + ObjectArray& AllocateObjectArray() { + auto* array = objectFactoryThreadQueue_.CreateArray(theArrayTypeInfo, 3); + objectFactoryThreadQueue_.Publish(); + return ObjectArray::FromArrayHeader(array); + } + + CharArray& AllocateCharArray() { + auto* array = objectFactoryThreadQueue_.CreateArray(theCharArrayTypeInfo, 3); + objectFactoryThreadQueue_.Publish(); + return CharArray::FromArrayHeader(array); + } + + WeakCounter& InstallWeakCounter(ObjHeader* objHeader) { + auto* weakCounterHeader = objectFactoryThreadQueue_.CreateObject(typeHolderWeakCounter.typeInfo()); + objectFactoryThreadQueue_.Publish(); + auto& weakCounter = WeakCounter::FromObjHeader(weakCounterHeader); + auto& extraObjectData = mm::ExtraObjectData::GetOrInstall(objHeader); + *extraObjectData.GetWeakCounterLocation() = weakCounter.header(); + weakCounter->referred = objHeader; + return weakCounter; + } + + testing::MockFunction& finalizerHook() { return finalizerHooks_.finalizerHook(); } + +private: + FinalizerHooksTestSupport finalizerHooks_; + GC::ThreadData gcThreadData_; + ObjectFactory objectFactory_; + ObjectFactory::ThreadQueue objectFactoryThreadQueue_{objectFactory_, gcThreadData_}; + KStdVector finalizers_; +}; + +} // namespace + +TEST_F(MarkAndSweepUtilsSweepTest, SweepEmpty) { + ASSERT_THAT(Alive(), testing::UnorderedElementsAre()); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre()); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleObject) { + auto& object = AllocateObject(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre()); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleObjectArray) { + auto& array = AllocateObjectArray(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(array.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre()); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleCharArray) { + auto& array = AllocateCharArray(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(array.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre()); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleMarkedObject) { + auto& object = AllocateObject(); + object.Mark(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(object.header())); + EXPECT_THAT(object.state(), GC::ObjectData::State::kMarkReset); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleMarkedObjectArray) { + auto& array = AllocateObjectArray(); + array.Mark(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(array.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(array.header())); + EXPECT_THAT(array.state(), GC::ObjectData::State::kMarkReset); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleMarkedCharArray) { + auto& array = AllocateCharArray(); + array.Mark(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(array.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(array.header())); + EXPECT_THAT(array.state(), GC::ObjectData::State::kMarkReset); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleObjectWithExtraData) { + auto& object = AllocateObject(); + object.InstallExtraData(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre(object.header())); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre()); + EXPECT_THAT(object.state(), GC::ObjectData::State::kUnmarked); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleObjectArrayWithExtraData) { + auto& array = AllocateObjectArray(); + array.InstallExtraData(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(array.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre(array.header())); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre()); + EXPECT_THAT(array.state(), GC::ObjectData::State::kUnmarked); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleCharArrayWithExtraData) { + auto& array = AllocateCharArray(); + array.InstallExtraData(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(array.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre(array.header())); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre()); + EXPECT_THAT(array.state(), GC::ObjectData::State::kUnmarked); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleMarkedObjectWithExtraData) { + auto& object = AllocateObject(); + object.InstallExtraData(); + object.Mark(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(object.header())); + EXPECT_THAT(object.state(), GC::ObjectData::State::kMarkReset); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleMarkedObjectArrayWithExtraData) { + auto& array = AllocateObjectArray(); + array.InstallExtraData(); + array.Mark(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(array.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(array.header())); + EXPECT_THAT(array.state(), GC::ObjectData::State::kMarkReset); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleMarkedCharArrayWithExtraData) { + auto& array = AllocateCharArray(); + array.InstallExtraData(); + array.Mark(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(array.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(array.header())); + EXPECT_THAT(array.state(), GC::ObjectData::State::kMarkReset); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleObjectWithFinalizerHook) { + auto& object = AllocateObject(typeHolderWithFinalizer.typeInfo()); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre(object.header())); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre()); + EXPECT_THAT(object.state(), GC::ObjectData::State::kUnmarked); + + EXPECT_CALL(finalizerHook(), Call(object.header())); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleMarkedObjectWithFinalizerHook) { + auto& object = AllocateObject(typeHolderWithFinalizer.typeInfo()); + object.Mark(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(object.header())); + EXPECT_THAT(object.state(), GC::ObjectData::State::kMarkReset); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleObjectWithWeakCounter) { + auto& object = AllocateObject(); + auto& weakCounter = InstallWeakCounter(object.header()); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object.header(), weakCounter.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre(object.header())); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre()); + EXPECT_THAT(object.state(), GC::ObjectData::State::kUnmarked); + EXPECT_FALSE(object.HasWeakCounter()); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleObjectArrayWithWeakCounter) { + auto& array = AllocateObjectArray(); + auto& weakCounter = InstallWeakCounter(array.header()); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(array.header(), weakCounter.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre(array.header())); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre()); + EXPECT_THAT(array.state(), GC::ObjectData::State::kUnmarked); + EXPECT_FALSE(array.HasWeakCounter()); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleCharArrayWithWeakCounter) { + auto& array = AllocateCharArray(); + auto& weakCounter = InstallWeakCounter(array.header()); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(array.header(), weakCounter.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre(array.header())); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre()); + EXPECT_THAT(array.state(), GC::ObjectData::State::kUnmarked); + EXPECT_FALSE(array.HasWeakCounter()); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleMarkedObjectWithWeakCounter) { + auto& object = AllocateObject(); + auto& weakCounter = InstallWeakCounter(object.header()); + object.Mark(); + MarkWeakCounter(weakCounter); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object.header(), weakCounter.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(object.header(), weakCounter.header())); + EXPECT_THAT(object.state(), GC::ObjectData::State::kMarkReset); + EXPECT_THAT(GetWeakCounterState(weakCounter), GC::ObjectData::State::kMarkReset); + EXPECT_TRUE(object.HasWeakCounter()); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleMarkedObjectArrayWithWeakCounter) { + auto& array = AllocateObjectArray(); + auto& weakCounter = InstallWeakCounter(array.header()); + array.Mark(); + MarkWeakCounter(weakCounter); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(array.header(), weakCounter.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(array.header(), weakCounter.header())); + EXPECT_THAT(array.state(), GC::ObjectData::State::kMarkReset); + EXPECT_THAT(GetWeakCounterState(weakCounter), GC::ObjectData::State::kMarkReset); + EXPECT_TRUE(array.HasWeakCounter()); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleMarkedCharArrayWithWeakCounter) { + auto& array = AllocateCharArray(); + auto& weakCounter = InstallWeakCounter(array.header()); + array.Mark(); + MarkWeakCounter(weakCounter); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(array.header(), weakCounter.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(array.header(), weakCounter.header())); + EXPECT_THAT(array.state(), GC::ObjectData::State::kMarkReset); + EXPECT_THAT(GetWeakCounterState(weakCounter), GC::ObjectData::State::kMarkReset); + EXPECT_TRUE(array.HasWeakCounter()); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepObjects) { + auto& object1 = AllocateObject(); + auto& object2 = AllocateObject(typeHolderWithFinalizer.typeInfo()); + auto& object3 = AllocateObject(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object1.header(), object2.header(), object3.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre(object2.header())); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre()); + + EXPECT_CALL(finalizerHook(), Call(object2.header())); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepObjectsMarkAll) { + auto& object1 = AllocateObject(); + object1.Mark(); + auto& object2 = AllocateObject(typeHolderWithFinalizer.typeInfo()); + object2.Mark(); + auto& object3 = AllocateObject(); + object3.Mark(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object1.header(), object2.header(), object3.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(object1.header(), object2.header(), object3.header())); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepObjectsMarkFirst) { + auto& object1 = AllocateObject(); + object1.Mark(); + auto& object2 = AllocateObject(typeHolderWithFinalizer.typeInfo()); + auto& object3 = AllocateObject(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object1.header(), object2.header(), object3.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre(object2.header())); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(object1.header())); + + EXPECT_CALL(finalizerHook(), Call(object2.header())); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepObjectsMarkSecond) { + auto& object1 = AllocateObject(); + auto& object2 = AllocateObject(typeHolderWithFinalizer.typeInfo()); + object2.Mark(); + auto& object3 = AllocateObject(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object1.header(), object2.header(), object3.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre()); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(object2.header())); +} + +TEST_F(MarkAndSweepUtilsSweepTest, SweepObjectsMarkThird) { + auto& object1 = AllocateObject(); + auto& object2 = AllocateObject(typeHolderWithFinalizer.typeInfo()); + auto& object3 = AllocateObject(); + object3.Mark(); + ASSERT_THAT(Alive(), testing::UnorderedElementsAre(object1.header(), object2.header(), object3.header())); + + auto finalizers = Sweep(); + + EXPECT_THAT(finalizers, testing::UnorderedElementsAre(object2.header())); + EXPECT_THAT(Alive(), testing::UnorderedElementsAre(object3.header())); + + EXPECT_CALL(finalizerHook(), Call(object2.header())); +} diff --git a/kotlin-native/runtime/src/mm/cpp/gc/SingleThreadMarkAndSweep.cpp b/kotlin-native/runtime/src/mm/cpp/gc/SingleThreadMarkAndSweep.cpp new file mode 100644 index 00000000000..1d1912e6a2b --- /dev/null +++ b/kotlin-native/runtime/src/mm/cpp/gc/SingleThreadMarkAndSweep.cpp @@ -0,0 +1,118 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#include "SingleThreadMarkAndSweep.hpp" + +#include "../GlobalData.hpp" +#include "../RootSet.hpp" +#include "../ThreadData.hpp" +#include "../ThreadRegistry.hpp" +#include "MarkAndSweepUtils.hpp" +#include "Memory.h" +#include "Runtime.h" + +using namespace kotlin; + +namespace { + +struct MarkTraits { + static bool IsMarked(ObjHeader* object) noexcept { + auto& objectData = mm::ObjectFactory::NodeRef::From(object).GCObjectData(); + return objectData.color() == mm::SingleThreadMarkAndSweep::ObjectData::Color::kBlack; + } + + static bool TryMark(ObjHeader* object) noexcept { + auto& objectData = mm::ObjectFactory::NodeRef::From(object).GCObjectData(); + if (objectData.color() == mm::SingleThreadMarkAndSweep::ObjectData::Color::kBlack) return false; + objectData.setColor(mm::SingleThreadMarkAndSweep::ObjectData::Color::kBlack); + return true; + }; +}; + +struct SweepTraits { + using ObjectFactory = mm::ObjectFactory; + + static bool TryResetMark(ObjectFactory::NodeRef node) noexcept { + auto& objectData = node.GCObjectData(); + if (objectData.color() == mm::SingleThreadMarkAndSweep::ObjectData::Color::kWhite) return false; + objectData.setColor(mm::SingleThreadMarkAndSweep::ObjectData::Color::kWhite); + return true; + } +}; + +struct FinalizeTraits { + using ObjectFactory = mm::ObjectFactory; +}; + +} // namespace + +void mm::SingleThreadMarkAndSweep::ThreadData::SafePointFunctionEpilogue() noexcept { + if (gc_.GetThreshold() == 0 || safePointsCounter_ % gc_.GetThreshold() == 0) { + PerformFullGC(); + } + ++safePointsCounter_; +} + +void mm::SingleThreadMarkAndSweep::ThreadData::SafePointLoopBody() noexcept { + if (gc_.GetThreshold() == 0 || safePointsCounter_ % gc_.GetThreshold() == 0) { + PerformFullGC(); + } + ++safePointsCounter_; +} + +void mm::SingleThreadMarkAndSweep::ThreadData::SafePointExceptionUnwind() noexcept { + if (gc_.GetThreshold() == 0 || safePointsCounter_ % gc_.GetThreshold() == 0) { + PerformFullGC(); + } + ++safePointsCounter_; +} + +void mm::SingleThreadMarkAndSweep::ThreadData::SafePointAllocation(size_t size) noexcept { + size_t allocationOverhead = + gc_.GetAllocationThresholdBytes() == 0 ? allocatedBytes_ : allocatedBytes_ % gc_.GetAllocationThresholdBytes(); + if (allocationOverhead + size >= gc_.GetAllocationThresholdBytes()) { + PerformFullGC(); + } + allocatedBytes_ += size; +} + +void mm::SingleThreadMarkAndSweep::ThreadData::PerformFullGC() noexcept { + gc_.PerformFullGC(); +} + +void mm::SingleThreadMarkAndSweep::ThreadData::OnOOM(size_t size) noexcept { + PerformFullGC(); +} + +void mm::SingleThreadMarkAndSweep::PerformFullGC() noexcept { + RuntimeAssert(running_ == false, "Cannot have been called during another collection"); + running_ = true; + + KStdVector graySet; + for (auto& thread : mm::GlobalData::Instance().threadRegistry().Iter()) { + thread.Publish(); + for (auto* object : mm::ThreadRootSet(thread)) { + if (!isNullOrMarker(object)) { + graySet.push_back(object); + } + } + } + mm::StableRefRegistry::Instance().ProcessDeletions(); + for (auto* object : mm::GlobalRootSet()) { + if (!isNullOrMarker(object)) { + graySet.push_back(object); + } + } + + mm::Mark(std::move(graySet)); + auto finalizerQueue = mm::Sweep(mm::GlobalData::Instance().objectFactory()); + + running_ = false; + + // TODO: These will actually need to be run on a separate thread. + // TODO: This probably should check for the existence of runtime itself, but unit tests initialize only memory. + RuntimeAssert(mm::ThreadRegistry::Instance().CurrentThreadData() != nullptr, "Finalizers need a Kotlin runtime"); + finalizerQueue.Finalize(); +} diff --git a/kotlin-native/runtime/src/mm/cpp/gc/SingleThreadMarkAndSweep.hpp b/kotlin-native/runtime/src/mm/cpp/gc/SingleThreadMarkAndSweep.hpp new file mode 100644 index 00000000000..8c0f77ec96c --- /dev/null +++ b/kotlin-native/runtime/src/mm/cpp/gc/SingleThreadMarkAndSweep.hpp @@ -0,0 +1,78 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#ifndef RUNTIME_MM_GC_SINGLE_THREAD_MARK_AND_SWEEP_H +#define RUNTIME_MM_GC_SINGLE_THREAD_MARK_AND_SWEEP_H + +#include + +#include "Types.h" +#include "Utils.hpp" + +namespace kotlin { +namespace mm { + +// Stop-the-world Mark-and-Sweep for a single mutator +class SingleThreadMarkAndSweep : private Pinned { +public: + class ObjectData { + public: + enum class Color { + kWhite = 0, // Initial color at the start of collection cycles. Objects with this color at the end of GC cycle are collected. + // All new objects are allocated with this color. + kBlack, // Objects encountered during mark phase. + }; + + Color color() const noexcept { return color_; } + void setColor(Color color) noexcept { color_ = color; } + + private: + Color color_ = Color::kWhite; + }; + + class ThreadData : private Pinned { + public: + using ObjectData = SingleThreadMarkAndSweep::ObjectData; + + explicit ThreadData(SingleThreadMarkAndSweep& gc) noexcept : gc_(gc) {} + ~ThreadData() = default; + + void SafePointFunctionEpilogue() noexcept; + void SafePointLoopBody() noexcept; + void SafePointExceptionUnwind() noexcept; + void SafePointAllocation(size_t size) noexcept; + + void PerformFullGC() noexcept; + + void OnOOM(size_t size) noexcept; + + private: + SingleThreadMarkAndSweep& gc_; + size_t allocatedBytes_ = 0; + size_t safePointsCounter_ = 0; + }; + + SingleThreadMarkAndSweep() noexcept {} + ~SingleThreadMarkAndSweep() = default; + + void SetThreshold(size_t value) noexcept { threshold_ = value; } + size_t GetThreshold() noexcept { return threshold_; } + + void SetAllocationThresholdBytes(size_t value) noexcept { allocationThresholdBytes_ = value; } + size_t GetAllocationThresholdBytes() noexcept { return allocationThresholdBytes_; } + +private: + void PerformFullGC() noexcept; + + bool running_ = false; + + size_t threshold_ = 1000; + size_t allocationThresholdBytes_ = 10000; +}; + +} // namespace mm +} // namespace kotlin + +#endif // RUNTIME_MM_GC_SINGLE_THREAD_MARK_AND_SWEEP_H diff --git a/kotlin-native/runtime/src/mm/cpp/gc/SingleThreadMarkAndSweepTest.cpp b/kotlin-native/runtime/src/mm/cpp/gc/SingleThreadMarkAndSweepTest.cpp new file mode 100644 index 00000000000..779bf35716c --- /dev/null +++ b/kotlin-native/runtime/src/mm/cpp/gc/SingleThreadMarkAndSweepTest.cpp @@ -0,0 +1,629 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#include "SingleThreadMarkAndSweep.hpp" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "../ExtraObjectData.hpp" +#include "../GlobalData.hpp" +#include "../ObjectOps.hpp" +#include "../TestSupport.hpp" +#include "FinalizerHooksTestSupport.hpp" +#include "ObjectTestSupport.hpp" + +using namespace kotlin; + +// These tests can only work if `GC` is `SingleThreadMarkAndSweep`. +// TODO: Extracting GC into a separate module will help with this. + +namespace { + +struct Payload { + ObjHeader* field1; + ObjHeader* field2; + ObjHeader* field3; + + static constexpr std::array kFields = { + &Payload::field1, + &Payload::field2, + &Payload::field3, + }; +}; + +// TODO: This should go into test support for weak references. +struct WeakCounterPayload { + void* referred; + KInt lock; + KInt cookie; + + static constexpr std::array kFields{}; +}; + +using WeakCounter = test_support::Object; + +test_support::TypeInfoHolder typeHolder{test_support::TypeInfoHolder::ObjectBuilder()}; +test_support::TypeInfoHolder typeHolderWithFinalizer{test_support::TypeInfoHolder::ObjectBuilder().addFlag(TF_HAS_FINALIZER)}; +test_support::TypeInfoHolder typeHolderWeakCounter{test_support::TypeInfoHolder::ObjectBuilder()}; + +// TODO: Clean GlobalObjectHolder after it's gone. +class GlobalObjectHolder : private Pinned { +public: + explicit GlobalObjectHolder(mm::ThreadData& threadData) { + mm::GlobalsRegistry::Instance().RegisterStorageForGlobal(&threadData, &location_); + mm::AllocateObject(&threadData, typeHolder.typeInfo(), &location_); + } + + ObjHeader* header() { return location_; } + + test_support::Object& operator*() { return test_support::Object::FromObjHeader(location_); } + test_support::Object& operator->() { return test_support::Object::FromObjHeader(location_); } + +private: + ObjHeader* location_; +}; + +// TODO: Clean GlobalPermanentObjectHolder after it's gone. +class GlobalPermanentObjectHolder : private Pinned { +public: + explicit GlobalPermanentObjectHolder(mm::ThreadData& threadData) { + mm::GlobalsRegistry::Instance().RegisterStorageForGlobal(&threadData, &global_); + global_->typeInfoOrMeta_ = setPointerBits(global_->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + RuntimeAssert(global_->permanent(), "Must be permanent"); + } + + ObjHeader* header() { return global_; } + + test_support::Object& operator*() { return object_; } + test_support::Object& operator->() { return object_; } + +private: + test_support::Object object_{typeHolder.typeInfo()}; + ObjHeader* global_{object_.header()}; +}; + +// TODO: Clean GlobalObjectArrayHolder after it's gone. +class GlobalObjectArrayHolder : private Pinned { +public: + explicit GlobalObjectArrayHolder(mm::ThreadData& threadData) { + mm::GlobalsRegistry::Instance().RegisterStorageForGlobal(&threadData, &location_); + mm::AllocateArray(&threadData, theArrayTypeInfo, 3, &location_); + } + + ObjHeader* header() { return location_; } + + test_support::ObjectArray<3>& operator*() { return test_support::ObjectArray<3>::FromArrayHeader(location_->array()); } + test_support::ObjectArray<3>& operator->() { return test_support::ObjectArray<3>::FromArrayHeader(location_->array()); } + + ObjHeader*& operator[](size_t index) noexcept { return (**this).elements()[index]; } + +private: + ObjHeader* location_; +}; + +// TODO: Clean GlobalCharArrayHolder after it's gone. +class GlobalCharArrayHolder : private Pinned { +public: + explicit GlobalCharArrayHolder(mm::ThreadData& threadData) { + mm::GlobalsRegistry::Instance().RegisterStorageForGlobal(&threadData, &location_); + mm::AllocateArray(&threadData, theCharArrayTypeInfo, 3, &location_); + } + + ObjHeader* header() { return location_; } + + test_support::CharArray<3>& operator*() { return test_support::CharArray<3>::FromArrayHeader(location_->array()); } + test_support::CharArray<3>& operator->() { return test_support::CharArray<3>::FromArrayHeader(location_->array()); } + +private: + ObjHeader* location_; +}; + +class StackObjectHolder : private Pinned { +public: + explicit StackObjectHolder(mm::ThreadData& threadData) { mm::AllocateObject(&threadData, typeHolder.typeInfo(), holder_.slot()); } + explicit StackObjectHolder(test_support::Object& object) : holder_(object.header()) {} + + ObjHeader* header() { return holder_.obj(); } + + test_support::Object& operator*() { return test_support::Object::FromObjHeader(holder_.obj()); } + test_support::Object& operator->() { return test_support::Object::FromObjHeader(holder_.obj()); } + +private: + ObjHolder holder_; +}; + +class StackObjectArrayHolder : private Pinned { +public: + explicit StackObjectArrayHolder(mm::ThreadData& threadData) { mm::AllocateArray(&threadData, theArrayTypeInfo, 3, holder_.slot()); } + + ObjHeader* header() { return holder_.obj(); } + + test_support::ObjectArray<3>& operator*() { return test_support::ObjectArray<3>::FromArrayHeader(holder_.obj()->array()); } + test_support::ObjectArray<3>& operator->() { return test_support::ObjectArray<3>::FromArrayHeader(holder_.obj()->array()); } + + ObjHeader*& operator[](size_t index) noexcept { return (**this).elements()[index]; } + +private: + ObjHolder holder_; +}; + +class StackCharArrayHolder : private Pinned { +public: + explicit StackCharArrayHolder(mm::ThreadData& threadData) { mm::AllocateArray(&threadData, theCharArrayTypeInfo, 3, holder_.slot()); } + + ObjHeader* header() { return holder_.obj(); } + + test_support::CharArray<3>& operator*() { return test_support::CharArray<3>::FromArrayHeader(holder_.obj()->array()); } + test_support::CharArray<3>& operator->() { return test_support::CharArray<3>::FromArrayHeader(holder_.obj()->array()); } + +private: + ObjHolder holder_; +}; + +test_support::Object& AllocateObject(mm::ThreadData& threadData) { + ObjHolder holder; + mm::AllocateObject(&threadData, typeHolder.typeInfo(), holder.slot()); + return test_support::Object::FromObjHeader(holder.obj()); +} + +test_support::Object& AllocateObjectWithFinalizer(mm::ThreadData& threadData) { + ObjHolder holder; + mm::AllocateObject(&threadData, typeHolderWithFinalizer.typeInfo(), holder.slot()); + return test_support::Object::FromObjHeader(holder.obj()); +} + +KStdVector Alive(mm::ThreadData& threadData) { + KStdVector objects; + for (auto node : threadData.objectFactoryThreadQueue()) { + objects.push_back(node.IsArray() ? node.GetArrayHeader()->obj() : node.GetObjHeader()); + } + for (auto node : mm::GlobalData::Instance().objectFactory().Iter()) { + objects.push_back(node.IsArray() ? node.GetArrayHeader()->obj() : node.GetObjHeader()); + } + return objects; +} + +using Color = mm::SingleThreadMarkAndSweep::ObjectData::Color; + +Color GetColor(ObjHeader* objHeader) { + auto nodeRef = mm::ObjectFactory::NodeRef::From(objHeader); + return nodeRef.GCObjectData().color(); +} + +WeakCounter& InstallWeakCounter(mm::ThreadData& threadData, ObjHeader* objHeader, ObjHeader** location) { + mm::AllocateObject(&threadData, typeHolderWeakCounter.typeInfo(), location); + auto& weakCounter = WeakCounter::FromObjHeader(*location); + auto& extraObjectData = mm::ExtraObjectData::GetOrInstall(objHeader); + *extraObjectData.GetWeakCounterLocation() = weakCounter.header(); + weakCounter->referred = objHeader; + return weakCounter; +} + +class SingleThreadMarkAndSweepTest : public testing::Test { +public: + ~SingleThreadMarkAndSweepTest() { + mm::GlobalsRegistry::Instance().ClearForTests(); + mm::GlobalData::Instance().objectFactory().ClearForTests(); + } + + testing::MockFunction& finalizerHook() { return finalizerHooks_.finalizerHook(); } + +private: + FinalizerHooksTestSupport finalizerHooks_; +}; + +} // namespace + +TEST_F(SingleThreadMarkAndSweepTest, RootSet) { + RunInNewThread([](mm::ThreadData& threadData) { + GlobalObjectHolder global1{threadData}; + GlobalObjectArrayHolder global2{threadData}; + GlobalCharArrayHolder global3{threadData}; + StackObjectHolder stack1{threadData}; + StackObjectArrayHolder stack2{threadData}; + StackCharArrayHolder stack3{threadData}; + + ASSERT_THAT( + Alive(threadData), + testing::UnorderedElementsAre( + global1.header(), global2.header(), global3.header(), stack1.header(), stack2.header(), stack3.header())); + ASSERT_THAT(GetColor(global1.header()), Color::kWhite); + ASSERT_THAT(GetColor(global2.header()), Color::kWhite); + ASSERT_THAT(GetColor(global3.header()), Color::kWhite); + ASSERT_THAT(GetColor(stack1.header()), Color::kWhite); + ASSERT_THAT(GetColor(stack2.header()), Color::kWhite); + ASSERT_THAT(GetColor(stack3.header()), Color::kWhite); + + threadData.gc().PerformFullGC(); + + EXPECT_THAT( + Alive(threadData), + testing::UnorderedElementsAre( + global1.header(), global2.header(), global3.header(), stack1.header(), stack2.header(), stack3.header())); + EXPECT_THAT(GetColor(global1.header()), Color::kWhite); + EXPECT_THAT(GetColor(global2.header()), Color::kWhite); + EXPECT_THAT(GetColor(global3.header()), Color::kWhite); + EXPECT_THAT(GetColor(stack1.header()), Color::kWhite); + EXPECT_THAT(GetColor(stack2.header()), Color::kWhite); + EXPECT_THAT(GetColor(stack3.header()), Color::kWhite); + }); +} + +TEST_F(SingleThreadMarkAndSweepTest, InterconnectedRootSet) { + RunInNewThread([](mm::ThreadData& threadData) { + GlobalObjectHolder global1{threadData}; + GlobalObjectArrayHolder global2{threadData}; + GlobalCharArrayHolder global3{threadData}; + StackObjectHolder stack1{threadData}; + StackObjectArrayHolder stack2{threadData}; + StackCharArrayHolder stack3{threadData}; + + global1->field1 = stack1.header(); + global1->field2 = global1.header(); + global1->field3 = global2.header(); + global2[0] = global1.header(); + global2[1] = global3.header(); + stack1->field1 = global1.header(); + stack1->field2 = stack1.header(); + stack1->field3 = stack2.header(); + stack2[0] = stack1.header(); + stack2[1] = stack3.header(); + + ASSERT_THAT( + Alive(threadData), + testing::UnorderedElementsAre( + global1.header(), global2.header(), global3.header(), stack1.header(), stack2.header(), stack3.header())); + ASSERT_THAT(GetColor(global1.header()), Color::kWhite); + ASSERT_THAT(GetColor(global2.header()), Color::kWhite); + ASSERT_THAT(GetColor(global3.header()), Color::kWhite); + ASSERT_THAT(GetColor(stack1.header()), Color::kWhite); + ASSERT_THAT(GetColor(stack2.header()), Color::kWhite); + ASSERT_THAT(GetColor(stack3.header()), Color::kWhite); + + threadData.gc().PerformFullGC(); + + EXPECT_THAT( + Alive(threadData), + testing::UnorderedElementsAre( + global1.header(), global2.header(), global3.header(), stack1.header(), stack2.header(), stack3.header())); + EXPECT_THAT(GetColor(global1.header()), Color::kWhite); + EXPECT_THAT(GetColor(global2.header()), Color::kWhite); + EXPECT_THAT(GetColor(global3.header()), Color::kWhite); + EXPECT_THAT(GetColor(stack1.header()), Color::kWhite); + EXPECT_THAT(GetColor(stack2.header()), Color::kWhite); + EXPECT_THAT(GetColor(stack3.header()), Color::kWhite); + }); +} + +TEST_F(SingleThreadMarkAndSweepTest, FreeObjects) { + RunInNewThread([](mm::ThreadData& threadData) { + auto& object1 = AllocateObject(threadData); + auto& object2 = AllocateObject(threadData); + + ASSERT_THAT(Alive(threadData), testing::UnorderedElementsAre(object1.header(), object2.header())); + ASSERT_THAT(GetColor(object1.header()), Color::kWhite); + ASSERT_THAT(GetColor(object2.header()), Color::kWhite); + + threadData.gc().PerformFullGC(); + + EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre()); + }); +} + +TEST_F(SingleThreadMarkAndSweepTest, FreeObjectsWithFinalizers) { + RunInNewThread([this](mm::ThreadData& threadData) { + auto& object1 = AllocateObjectWithFinalizer(threadData); + auto& object2 = AllocateObjectWithFinalizer(threadData); + + ASSERT_THAT(Alive(threadData), testing::UnorderedElementsAre(object1.header(), object2.header())); + ASSERT_THAT(GetColor(object1.header()), Color::kWhite); + ASSERT_THAT(GetColor(object2.header()), Color::kWhite); + + EXPECT_CALL(finalizerHook(), Call(object1.header())); + EXPECT_CALL(finalizerHook(), Call(object2.header())); + threadData.gc().PerformFullGC(); + + EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre()); + }); +} + +TEST_F(SingleThreadMarkAndSweepTest, FreeObjectWithFreeWeak) { + RunInNewThread([](mm::ThreadData& threadData) { + auto& object1 = AllocateObject(threadData); + auto& weak1 = ([&threadData, &object1]() -> WeakCounter& { + ObjHolder holder; + return InstallWeakCounter(threadData, object1.header(), holder.slot()); + })(); + + ASSERT_THAT(Alive(threadData), testing::UnorderedElementsAre(object1.header(), weak1.header())); + ASSERT_THAT(GetColor(object1.header()), Color::kWhite); + ASSERT_THAT(GetColor(weak1.header()), Color::kWhite); + ASSERT_THAT(weak1->referred, object1.header()); + + threadData.gc().PerformFullGC(); + + EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre()); + }); +} + +TEST_F(SingleThreadMarkAndSweepTest, FreeObjectWithHoldedWeak) { + RunInNewThread([](mm::ThreadData& threadData) { + auto& object1 = AllocateObject(threadData); + StackObjectHolder stack{threadData}; + auto& weak1 = InstallWeakCounter(threadData, object1.header(), &stack->field1); + + ASSERT_THAT(Alive(threadData), testing::UnorderedElementsAre(object1.header(), weak1.header(), stack.header())); + ASSERT_THAT(GetColor(object1.header()), Color::kWhite); + ASSERT_THAT(GetColor(weak1.header()), Color::kWhite); + ASSERT_THAT(weak1->referred, object1.header()); + + threadData.gc().PerformFullGC(); + + EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(weak1.header(), stack.header())); + EXPECT_THAT(GetColor(weak1.header()), Color::kWhite); + EXPECT_THAT(weak1->referred, nullptr); + }); +} + +TEST_F(SingleThreadMarkAndSweepTest, ObjectReferencedFromRootSet) { + RunInNewThread([](mm::ThreadData& threadData) { + GlobalObjectHolder global{threadData}; + StackObjectHolder stack{threadData}; + auto& object1 = AllocateObject(threadData); + auto& object2 = AllocateObject(threadData); + auto& object3 = AllocateObject(threadData); + auto& object4 = AllocateObject(threadData); + + global->field1 = object1.header(); + object1->field1 = object2.header(); + stack->field1 = object3.header(); + object3->field1 = object4.header(); + + ASSERT_THAT( + Alive(threadData), + testing::UnorderedElementsAre( + global.header(), stack.header(), object1.header(), object2.header(), object3.header(), object4.header())); + ASSERT_THAT(GetColor(global.header()), Color::kWhite); + ASSERT_THAT(GetColor(stack.header()), Color::kWhite); + ASSERT_THAT(GetColor(object1.header()), Color::kWhite); + ASSERT_THAT(GetColor(object2.header()), Color::kWhite); + ASSERT_THAT(GetColor(object3.header()), Color::kWhite); + ASSERT_THAT(GetColor(object4.header()), Color::kWhite); + + threadData.gc().PerformFullGC(); + + EXPECT_THAT( + Alive(threadData), + testing::UnorderedElementsAre( + global.header(), stack.header(), object1.header(), object2.header(), object3.header(), object4.header())); + EXPECT_THAT(GetColor(global.header()), Color::kWhite); + EXPECT_THAT(GetColor(stack.header()), Color::kWhite); + EXPECT_THAT(GetColor(object1.header()), Color::kWhite); + EXPECT_THAT(GetColor(object2.header()), Color::kWhite); + EXPECT_THAT(GetColor(object3.header()), Color::kWhite); + EXPECT_THAT(GetColor(object4.header()), Color::kWhite); + }); +} + +TEST_F(SingleThreadMarkAndSweepTest, ObjectsWithCycles) { + RunInNewThread([](mm::ThreadData& threadData) { + GlobalObjectHolder global{threadData}; + StackObjectHolder stack{threadData}; + auto& object1 = AllocateObject(threadData); + auto& object2 = AllocateObject(threadData); + auto& object3 = AllocateObject(threadData); + auto& object4 = AllocateObject(threadData); + auto& object5 = AllocateObject(threadData); + auto& object6 = AllocateObject(threadData); + + global->field1 = object1.header(); + object1->field1 = object2.header(); + object2->field1 = object1.header(); + stack->field1 = object3.header(); + object3->field1 = object4.header(); + object4->field1 = object3.header(); + object5->field1 = object6.header(); + object6->field1 = object5.header(); + + ASSERT_THAT( + Alive(threadData), + testing::UnorderedElementsAre( + global.header(), stack.header(), object1.header(), object2.header(), object3.header(), object4.header(), + object5.header(), object6.header())); + ASSERT_THAT(GetColor(global.header()), Color::kWhite); + ASSERT_THAT(GetColor(stack.header()), Color::kWhite); + ASSERT_THAT(GetColor(object1.header()), Color::kWhite); + ASSERT_THAT(GetColor(object2.header()), Color::kWhite); + ASSERT_THAT(GetColor(object3.header()), Color::kWhite); + ASSERT_THAT(GetColor(object4.header()), Color::kWhite); + ASSERT_THAT(GetColor(object5.header()), Color::kWhite); + ASSERT_THAT(GetColor(object6.header()), Color::kWhite); + + threadData.gc().PerformFullGC(); + + EXPECT_THAT( + Alive(threadData), + testing::UnorderedElementsAre( + global.header(), stack.header(), object1.header(), object2.header(), object3.header(), object4.header())); + EXPECT_THAT(GetColor(global.header()), Color::kWhite); + EXPECT_THAT(GetColor(stack.header()), Color::kWhite); + EXPECT_THAT(GetColor(object1.header()), Color::kWhite); + EXPECT_THAT(GetColor(object2.header()), Color::kWhite); + EXPECT_THAT(GetColor(object3.header()), Color::kWhite); + EXPECT_THAT(GetColor(object4.header()), Color::kWhite); + }); +} + +TEST_F(SingleThreadMarkAndSweepTest, ObjectsWithCyclesAndFinalizers) { + RunInNewThread([this](mm::ThreadData& threadData) { + GlobalObjectHolder global{threadData}; + StackObjectHolder stack{threadData}; + auto& object1 = AllocateObjectWithFinalizer(threadData); + auto& object2 = AllocateObjectWithFinalizer(threadData); + auto& object3 = AllocateObjectWithFinalizer(threadData); + auto& object4 = AllocateObjectWithFinalizer(threadData); + auto& object5 = AllocateObjectWithFinalizer(threadData); + auto& object6 = AllocateObjectWithFinalizer(threadData); + + global->field1 = object1.header(); + object1->field1 = object2.header(); + object2->field1 = object1.header(); + stack->field1 = object3.header(); + object3->field1 = object4.header(); + object4->field1 = object3.header(); + object5->field1 = object6.header(); + object6->field1 = object5.header(); + + ASSERT_THAT( + Alive(threadData), + testing::UnorderedElementsAre( + global.header(), stack.header(), object1.header(), object2.header(), object3.header(), object4.header(), + object5.header(), object6.header())); + ASSERT_THAT(GetColor(global.header()), Color::kWhite); + ASSERT_THAT(GetColor(stack.header()), Color::kWhite); + ASSERT_THAT(GetColor(object1.header()), Color::kWhite); + ASSERT_THAT(GetColor(object2.header()), Color::kWhite); + ASSERT_THAT(GetColor(object3.header()), Color::kWhite); + ASSERT_THAT(GetColor(object4.header()), Color::kWhite); + ASSERT_THAT(GetColor(object5.header()), Color::kWhite); + ASSERT_THAT(GetColor(object6.header()), Color::kWhite); + + EXPECT_CALL(finalizerHook(), Call(object5.header())); + EXPECT_CALL(finalizerHook(), Call(object6.header())); + threadData.gc().PerformFullGC(); + + EXPECT_THAT( + Alive(threadData), + testing::UnorderedElementsAre( + global.header(), stack.header(), object1.header(), object2.header(), object3.header(), object4.header())); + EXPECT_THAT(GetColor(global.header()), Color::kWhite); + EXPECT_THAT(GetColor(stack.header()), Color::kWhite); + EXPECT_THAT(GetColor(object1.header()), Color::kWhite); + EXPECT_THAT(GetColor(object2.header()), Color::kWhite); + EXPECT_THAT(GetColor(object3.header()), Color::kWhite); + EXPECT_THAT(GetColor(object4.header()), Color::kWhite); + }); +} + +TEST_F(SingleThreadMarkAndSweepTest, ObjectsWithCyclesIntoRootSet) { + RunInNewThread([](mm::ThreadData& threadData) { + GlobalObjectHolder global{threadData}; + StackObjectHolder stack{threadData}; + auto& object1 = AllocateObject(threadData); + auto& object2 = AllocateObject(threadData); + + global->field1 = object1.header(); + object1->field1 = global.header(); + stack->field1 = object2.header(); + object2->field1 = stack.header(); + + ASSERT_THAT(Alive(threadData), testing::UnorderedElementsAre(global.header(), stack.header(), object1.header(), object2.header())); + ASSERT_THAT(GetColor(global.header()), Color::kWhite); + ASSERT_THAT(GetColor(stack.header()), Color::kWhite); + ASSERT_THAT(GetColor(object1.header()), Color::kWhite); + ASSERT_THAT(GetColor(object2.header()), Color::kWhite); + + threadData.gc().PerformFullGC(); + + EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global.header(), stack.header(), object1.header(), object2.header())); + EXPECT_THAT(GetColor(global.header()), Color::kWhite); + EXPECT_THAT(GetColor(stack.header()), Color::kWhite); + EXPECT_THAT(GetColor(object1.header()), Color::kWhite); + EXPECT_THAT(GetColor(object2.header()), Color::kWhite); + }); +} + +TEST_F(SingleThreadMarkAndSweepTest, RunGCTwice) { + RunInNewThread([](mm::ThreadData& threadData) { + GlobalObjectHolder global{threadData}; + StackObjectHolder stack{threadData}; + auto& object1 = AllocateObject(threadData); + auto& object2 = AllocateObject(threadData); + auto& object3 = AllocateObject(threadData); + auto& object4 = AllocateObject(threadData); + auto& object5 = AllocateObject(threadData); + auto& object6 = AllocateObject(threadData); + + global->field1 = object1.header(); + object1->field1 = object2.header(); + object2->field1 = object1.header(); + stack->field1 = object3.header(); + object3->field1 = object4.header(); + object4->field1 = object3.header(); + object5->field1 = object6.header(); + object6->field1 = object5.header(); + + ASSERT_THAT( + Alive(threadData), + testing::UnorderedElementsAre( + global.header(), stack.header(), object1.header(), object2.header(), object3.header(), object4.header(), + object5.header(), object6.header())); + ASSERT_THAT(GetColor(global.header()), Color::kWhite); + ASSERT_THAT(GetColor(stack.header()), Color::kWhite); + ASSERT_THAT(GetColor(object1.header()), Color::kWhite); + ASSERT_THAT(GetColor(object2.header()), Color::kWhite); + ASSERT_THAT(GetColor(object3.header()), Color::kWhite); + ASSERT_THAT(GetColor(object4.header()), Color::kWhite); + ASSERT_THAT(GetColor(object5.header()), Color::kWhite); + ASSERT_THAT(GetColor(object6.header()), Color::kWhite); + + threadData.gc().PerformFullGC(); + threadData.gc().PerformFullGC(); + + EXPECT_THAT( + Alive(threadData), + testing::UnorderedElementsAre( + global.header(), stack.header(), object1.header(), object2.header(), object3.header(), object4.header())); + EXPECT_THAT(GetColor(global.header()), Color::kWhite); + EXPECT_THAT(GetColor(stack.header()), Color::kWhite); + EXPECT_THAT(GetColor(object1.header()), Color::kWhite); + EXPECT_THAT(GetColor(object2.header()), Color::kWhite); + EXPECT_THAT(GetColor(object3.header()), Color::kWhite); + EXPECT_THAT(GetColor(object4.header()), Color::kWhite); + }); +} + +TEST_F(SingleThreadMarkAndSweepTest, PermanentObjects) { + RunInNewThread([](mm::ThreadData& threadData) { + GlobalPermanentObjectHolder global1{threadData}; + GlobalObjectHolder global2{threadData}; + test_support::Object permanentObject{typeHolder.typeInfo()}; + permanentObject.header()->typeInfoOrMeta_ = setPointerBits(permanentObject.header()->typeInfoOrMeta_, OBJECT_TAG_PERMANENT_CONTAINER); + RuntimeAssert(permanentObject.header()->permanent(), "Must be permanent"); + + global1->field1 = permanentObject.header(); + global2->field1 = global1.header(); + + ASSERT_THAT(Alive(threadData), testing::UnorderedElementsAre(global2.header())); + EXPECT_THAT(GetColor(global2.header()), Color::kWhite); + + threadData.gc().PerformFullGC(); + + EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global2.header())); + EXPECT_THAT(GetColor(global2.header()), Color::kWhite); + }); +} + +TEST_F(SingleThreadMarkAndSweepTest, SameObjectInRootSet) { + RunInNewThread([](mm::ThreadData& threadData) { + GlobalObjectHolder global{threadData}; + StackObjectHolder stack(*global); + auto& object = AllocateObject(threadData); + + global->field1 = object.header(); + + ASSERT_THAT(global.header(), stack.header()); + ASSERT_THAT(Alive(threadData), testing::UnorderedElementsAre(global.header(), object.header())); + EXPECT_THAT(GetColor(global.header()), Color::kWhite); + EXPECT_THAT(GetColor(object.header()), Color::kWhite); + + threadData.gc().PerformFullGC(); + + EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global.header(), object.header())); + EXPECT_THAT(GetColor(global.header()), Color::kWhite); + EXPECT_THAT(GetColor(object.header()), Color::kWhite); + }); +}