xo-gc/utest/Collector.test.cpp

496 lines
20 KiB
C++

/** @file Collector.test.cpp
*
* @author Roland Conybeare, Dec 2025
*
* NOTE: properly unit testing gc behavior requires
* xo-object2 dependency;
* see xo-object2/utest
**/
//#include <xo/alloc2/Allocator.hpp>
#include "random_allocs.hpp"
#include <xo/gc/X1Collector.hpp>
#include <xo/object2/Array.hpp>
#include <xo/object2/List.hpp>
#include <xo/object2/Integer.hpp>
#include <xo/alloc2/CollectorTypeRegistry.hpp>
#include <xo/alloc2/Allocator.hpp>
#include <xo/randomgen/xoshiro256.hpp>
#include <xo/randomgen/random_seed.hpp>
#include <xo/indentlog/scope.hpp>
#include <xo/indentlog/print/tag.hpp>
#include <xo/indentlog/print/array.hpp>
#include <catch2/catch.hpp>
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::ICollector_Any;
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<ACollector> constructible [ ]
// - obj<ACollector> truthy [ ]
// - obj<ACollector,DX1Collector> constructible [ ]
//
// - obj<AAllocator,DX1Collector> constructible [ ]
// - obj<AAllocator,DX1Collector> allocation
TEST_CASE("collector-any-null", "[alloc2][gc][ACollector]")
{
/* empty variant collector */
obj<ACollector> gc1;
REQUIRE(!gc1);
REQUIRE(gc1.iface() != nullptr);
REQUIRE(gc1.data() == nullptr);
REQUIRE(gc1._typeseq() == typeseq::id<ICollector_Any>());
}
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<ACollector, DX1Collector> 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<ACollector>::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<ACollector>::mkobj(&x1state);
auto x1alloc = with_facet<AAllocator>::mkobj(&x1state);
REQUIRE(x1gc.iface());
REQUIRE(x1gc.data());
REQUIRE(x1alloc.iface());
REQUIRE(x1alloc.data());
rng::Seed<rng::xoshiro256ss> 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<ACollector>::mkobj(&x1state);
/* typed allocator i/face */
auto x1alloc = with_facet<AAllocator>::mkobj(&x1state);
REQUIRE(x1gc.iface());
REQUIRE(x1gc.data());
REQUIRE(x1alloc.iface());
REQUIRE(x1alloc.data());
rng::Seed<rng::xoshiro256ss> 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<ACollector,DX1Collector>(&gc_);
CollectorTypeRegistry::instance().install_types(gc);
}
# define nil nullptr
# define T true
# define F false
static std::vector<Testcase> 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<AAllocator>();
auto gc = mm.to_facet<ACollector>();
REQUIRE(mm._typeseq() == typeseq::id<DX1Collector>());
REQUIRE(gc._typeseq() == typeseq::id<DX1Collector>());
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<AGCObject>();
REQUIRE(mm->contains_allocated(Role::to_space(), roots.data()));
gc.add_gc_root(&roots);
{
auto x1 = DInteger::box(mm, 42);
auto x1_gco = obj<AGCObject>(x1);
auto l1 = DList::cons(mm, x1, DList::_nil());
REQUIRE(l1._typeseq() == typeseq::id<DList>());
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<DList>());
// 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<AGCObject>));
{
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<ACollector>::mkobj(&x1state);
/* typed allocator i/face */
auto x1alloc = with_facet<AAllocator>::mkobj(&x1state);
REQUIRE(x1gc.iface());
REQUIRE(x1gc.data());
REQUIRE(x1alloc.iface());
REQUIRE(x1alloc.data());
rng::Seed<rng::xoshiro256ss> 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 */