/** @file Collector.test.cpp * * @author Roland Conybeare, Dec 2025 * * NOTE: properly unit testing gc behavior requires * xo-object2 dependency; * see xo-object2/utest **/ //#include #include "random_allocs.hpp" #include #include #include #include #include #include #include #include #include #include #include #include namespace xo { using xo::scm::DList; using xo::scm::DArray; using xo::scm::DInteger; using xo::mm::CollectorTypeRegistry; using xo::mm::AAllocator; using xo::mm::ACollector; using xo::mm::AGCObject; using xo::mm::X1CollectorConfig; using xo::mm::DX1Collector; using xo::mm::Role; using xo::mm::ArenaConfig; using xo::mm::AllocHeaderConfig; using xo::mm::AllocHeader; using xo::mm::Generation; using xo::mm::c_max_generation; using xo::facet::with_facet; using xo::reflect::typeseq; using xo::scope; namespace ut { // checklist // - obj constructible [ ] // - obj truthy [ ] // - obj constructible [ ] // // - obj constructible [ ] // - obj allocation TEST_CASE("collector-any-null", "[alloc2][gc][ACollector]") { /* empty variant collector */ obj gc1; REQUIRE(!gc1); REQUIRE(gc1.iface() != nullptr); REQUIRE(gc1.data() == nullptr); } TEST_CASE("DX1Collector-1", "[alloc2][gc][DX1Collector]") { ArenaConfig arena_cfg = { .name_ = "_test_unused", .size_ = 4*1024*1024, .store_header_flag_ = true, .header_ = AllocHeaderConfig(0 /*guard_z*/, 0xfd /*guard_byte*/, 0 /*tseq_bits*/, 0 /*age_bits*/, 16 /*size_bits*/), }; X1CollectorConfig cfg = { .arena_config_ = arena_cfg, .n_generation_ = 2, .gc_trigger_v_ = {{64*1024, 1024*1024, #ifdef NOPE 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 #endif }} }; DX1Collector gc = DX1Collector{cfg}; Generation g0 = Generation{0}; REQUIRE(gc.to_space(g0)); REQUIRE(gc.from_space(g0)); REQUIRE(gc.to_space(g0)->is_mapped()); REQUIRE(gc.from_space(g0)->is_mapped()); Generation g1 = Generation{1}; REQUIRE(gc.to_space(g1)); REQUIRE(gc.from_space(g1)); REQUIRE(gc.to_space(g1)->is_mapped()); REQUIRE(gc.from_space(g1)->is_mapped()); /* verify from/to x N/T are unique */ REQUIRE(gc.to_space(g0) != gc.from_space(g0)); REQUIRE(gc.to_space(g1) != gc.to_space(g0)); REQUIRE(gc.from_space(g1) != gc.from_space(g0)); REQUIRE(gc.to_space(g0) != gc.from_space(g1)); REQUIRE(gc.to_space(g1) != gc.from_space(g1)); for (Generation gi{0}; gi < 2; ++gi) { INFO(xtag("gi", gi)); REQUIRE(gc.to_space(gi)); REQUIRE(gc.from_space(gi)); REQUIRE(gc.from_space(gi)->is_mapped()); REQUIRE(gc.to_space(gi)->is_mapped()); } for (Generation gi = Generation(2); gi < c_max_generation; ++gi) { INFO(xtag("gi", gi)); REQUIRE(!gc.to_space(gi)); REQUIRE(!gc.from_space(gi)); } } TEST_CASE("collector-x1-obj", "[alloc2][gc]") { ArenaConfig arena_cfg = { .name_ = "_test_unused", .size_ = 4*1024*1024, .store_header_flag_ = true, .header_ = AllocHeaderConfig(0 /*guard_z*/, 0xfd /*guard_byte*/, 0 /*tseq_bits*/, 0 /*age_bits*/, 16 /*size_bits*/), }; X1CollectorConfig cfg = { .arena_config_ = arena_cfg, .n_generation_ = 2, .gc_trigger_v_ = {{64*1024, 1024*1024, #ifdef NOPE 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 #endif }} }; DX1Collector gc = DX1Collector{cfg}; /* typed collector -- repr known at compile time */ obj x1(&gc); REQUIRE(x1.iface()); REQUIRE(x1.data()); } TEST_CASE("collector-x1-facet-mkobj", "[alloc2][gc]") { ArenaConfig arena_cfg = { .name_ = "_test_unused", .size_ = 4*1024*1024, .store_header_flag_ = true, .header_ = AllocHeaderConfig(0 /*guard_z*/, 0xfd /*guard_byte*/, 0 /*tseq-bits*/, 0 /*age-bits*/, 16 /*size-bits*/), }; X1CollectorConfig cfg = { .arena_config_ = arena_cfg, .n_generation_ = 2, .gc_trigger_v_ = {{64*1024, 1024*1024, #ifdef NOPE 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 #endif }} }; DX1Collector gc = DX1Collector{cfg}; /* typed collector -- repr inferred at compile time */ auto x1 = with_facet::mkobj(&gc); REQUIRE(x1.iface()); REQUIRE(x1.data()); } TEST_CASE("collector-x1-alloc", "[alloc2][gc]") { scope log(XO_DEBUG(false), "DX1Collector alloc test"); ArenaConfig arena_cfg = { .name_ = "_test_unused", .size_ = 4*1024*1024, .store_header_flag_ = true, .header_ = AllocHeaderConfig(0 /*guard_z*/, 0xfd /*guard-byte*/, 0 /*tseq-bits*/, 0 /*age-bits*/, 16 /*size-bits*/), }; /* collector with one generation collapses to a non-generational copying collector */ X1CollectorConfig cfg = { .arena_config_ = arena_cfg, .n_generation_ = 1, .gc_trigger_v_ = {{64*1024, 0, #ifdef NOPE 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 #endif }} }; DX1Collector x1state = DX1Collector{cfg}; /* typed collector */ auto x1gc = with_facet::mkobj(&x1state); auto x1alloc = with_facet::mkobj(&x1state); REQUIRE(x1gc.iface()); REQUIRE(x1gc.data()); REQUIRE(x1alloc.iface()); REQUIRE(x1alloc.data()); rng::Seed seed; log && log(xtag("seed", seed)); auto rng = rng::xoshiro256ss(seed); bool catch_flag = false; REQUIRE(utest::AllocUtil::random_allocs(25, catch_flag, &rng, x1alloc)); } TEST_CASE("collector-x1-alloc2", "[alloc2][gc]") { scope log(XO_DEBUG(false), "DX1Collector alloc test2"); ArenaConfig arena_cfg = { .name_ = "_test_unused", .size_ = 4*1024*1024, .store_header_flag_ = true, .header_ = AllocHeaderConfig(8 /*guard_z*/, 0xfd /*guard-byte*/, 0 /*tseq-bits*/, 0 /*age-bits*/, 16 /*size-bits*/), }; /* collector with one generation collapses to a non-generational copying collector */ X1CollectorConfig cfg = { .arena_config_ = arena_cfg, .n_generation_ = 1, .gc_trigger_v_ = {{64*1024, 0, #ifdef NOPE 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 #endif }} }; /* X1 allocator+collector */ DX1Collector x1state = DX1Collector{cfg}; /* typed collector i/face */ auto x1gc = with_facet::mkobj(&x1state); /* typed allocator i/face */ auto x1alloc = with_facet::mkobj(&x1state); REQUIRE(x1gc.iface()); REQUIRE(x1gc.data()); REQUIRE(x1alloc.iface()); REQUIRE(x1alloc.data()); rng::Seed seed; log && log("ratio: seed=", seed); auto rng = rng::xoshiro256ss(seed); // these are not gc-aware objects. // just testing ability to work as a low-level allocator REQUIRE(utest::AllocUtil::random_allocs(25, false, &rng, x1alloc)); } namespace { class Testcase { public: Testcase(uint32_t ng, uint32_t ns, size_t gcz, uint32_t otz, bool dbg_flag) : n_gen_{ng}, n_survive_{ns}, gc_halfspace_z_{gcz}, object_type_z_{otz}, debug_flag_{dbg_flag} {} public: /** number of generations in gco store **/ uint32_t n_gen_ = 0; /** promote to next gen on surviving this number of gc cycles **/ uint32_t n_survive_ = 0; /** size of each generations' half-space, in bytes **/ size_t gc_halfspace_z_ = 0; /** storage for object type array, in bytes * one 8-byte facet pointer per type **/ uint32_t object_type_z_; #ifdef NOT_YET /** size for error output arena **/ size_t error_size_ = 0; #endif /** true to enable debug output for this test case **/ bool debug_flag_ = false; }; class X1Fixture { public: explicit X1Fixture(uint32_t i_tc, const Testcase & tc); #ifdef NOT_IN_USE DArena error_arena_; #endif DX1Collector gc_; }; X1Fixture::X1Fixture(uint32_t i_tc, const Testcase & tc) : gc_(X1CollectorConfig() .with_name("collector-x1-gc-" + std::to_string(i_tc)) .with_n_gen(tc.n_gen_) .with_n_survive(tc.n_survive_) .with_size(tc.gc_halfspace_z_) .with_debug_flag(tc.debug_flag_)) { auto gc = obj(&gc_); CollectorTypeRegistry::instance().install_types(gc); } # define nil nullptr # define T true # define F false static std::vector s_testcase_v = { /** * debug_flag * object_type_z | * gc_halfspace_z | | * n_survive | | | * n_gen | | | | * v v v v v **/ Testcase(1, 2, 16 * 1024, 128, T), }; # undef T # undef F # undef nil } /*namespace*/ // full collector test. // // PLAN: // eventually: make generative // // Setup ( // 1. gc_utest_main.cpp Subsystem::initialize_all() // invokes per-module plugin init. Gets types registered // with FacetRegistry, CollectorTypeRegistry etc. // 2. per-utest collector setup (fixture) // calls CollectorTypeRegistry::instance().install_types(gc) // to establish the set of types that collector knows. // TEST_CASE("collector-x1-gc", "[alloc2][gc]") { const auto & testname = Catch::getResultCapture().getCurrentTestName(); scope log(XO_DEBUG(true), xtag("test", testname)); //std::uint64_t seed = 7988747704879432247ul; //random_seed(&seed); for (size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { // auto rgen = xoshiro256ss(seed + i_tc); const Testcase & tc = s_testcase_v[i_tc]; scope log1(XO_DEBUG(tc.debug_flag_), "testcase loop", xtag("i_tc", i_tc)); INFO(tostr(xtag("i_tc", i_tc), xtag("n_tc", n_tc))); X1Fixture fixture(i_tc, tc); auto & x1 = fixture.gc_; REQUIRE(x1.verify_ok()); auto mm = x1.ref(); auto gc = mm.to_facet(); REQUIRE(mm._typeseq() == typeseq::id()); REQUIRE(gc._typeseq() == typeseq::id()); Generation g0 = Generation::g0(); REQUIRE(mm.allocated() == tc.object_type_z_); REQUIRE(gc.allocated(g0, Role::to_space()) == 0); REQUIRE(gc.allocated(g0, Role::from_space()) == 0); Generation g1 = Generation::g1(); { auto roots = DArray::_empty(mm, 1)->ref(); REQUIRE(mm->contains_allocated(Role::to_space(), roots.data())); gc.add_gc_root(&roots); { auto x1 = DInteger::box(mm, 42); auto x1_gco = obj(x1); auto l1 = DList::cons(mm, x1, DList::_nil()); REQUIRE(l1._typeseq() == typeseq::id()); REQUIRE(roots->push_back(mm, l1)); REQUIRE(mm->contains_allocated(Role::to_space(), x1.data())); REQUIRE(mm->contains_allocated(Role::to_space(), l1.data())); REQUIRE(roots->at(0) == l1); REQUIRE(roots->at(0)._typeseq() == typeseq::id()); // z: total allocated so far // 3x 8-byte header // sizeof(DInteger) // sizeof(DList) // sizeof(DArray(1)) // auto z = (3 * sizeof(AllocHeader) + sizeof(DInteger) + sizeof(DList) + sizeof(DArray) + sizeof(obj)); { REQUIRE(z == 80); REQUIRE(mm.allocated() == tc.object_type_z_ + z); REQUIRE(gc.allocated(g0, Role::to_space()) == z); REQUIRE(gc.allocated(g1, Role::to_space()) == 0); REQUIRE(gc.allocated(g0, Role::from_space()) == 0); REQUIRE(gc.allocated(g1, Role::from_space()) == 0); } gc->request_gc(g1); // 1st GC // x1 target got moved, og locn now relabeled from-space REQUIRE(mm->contains(Role::from_space(), x1.data())); REQUIRE(!mm->contains_allocated(Role::from_space(), x1.data())); // l1 target got moved, og locn now relabeled from-space REQUIRE(mm->contains(Role::from_space(), l1.data())); REQUIRE(!mm->contains_allocated(Role::from_space(), l1.data())); REQUIRE(mm.allocated() == tc.object_type_z_ + z); REQUIRE(gc.allocated(g0, Role::to_space()) == z); REQUIRE(gc.allocated(g1, Role::to_space()) == 0); REQUIRE(gc.allocated(g0, Role::from_space()) == 0); REQUIRE(gc.allocated(g1, Role::from_space()) == 0); } // NOTE: if this fails: // look for preceding GCObjectStore::lookup_type out-of-bounds. // May need to add to CollectorTypeRegistry // REQUIRE(mm->contains_allocated(Role::to_space(), roots.data())); } } #ifdef NOT_YET /* typed collector i/face */ auto x1gc = with_facet::mkobj(&x1state); /* typed allocator i/face */ auto x1alloc = with_facet::mkobj(&x1state); REQUIRE(x1gc.iface()); REQUIRE(x1gc.data()); REQUIRE(x1alloc.iface()); REQUIRE(x1alloc.data()); rng::Seed seed; log && log("ratio: seed=", seed); auto rng = rng::xoshiro256ss(seed); // these are not gc-aware objects. // just testing ability to work as a low-level allocator REQUIRE(utest::AllocUtil::random_allocs(25, false, &rng, x1alloc)); #endif } } } /* end Collector.test.cpp */