ability to inform allocator of gco->gco mutation, via AAllocator i/face.
432 lines
17 KiB
C++
432 lines
17 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/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::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::Generation;
|
|
using xo::mm::c_max_generation;
|
|
using xo::facet::with_facet;
|
|
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);
|
|
}
|
|
|
|
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_))
|
|
{}
|
|
|
|
# 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, F),
|
|
};
|
|
|
|
# undef T
|
|
# undef F
|
|
# undef nil
|
|
} /*namespace*/
|
|
|
|
// full collector test.
|
|
TEST_CASE("collector-x1-gc", "[alloc2][gc]")
|
|
{
|
|
scope log(XO_DEBUG(true),
|
|
"DX1Collector gc test");
|
|
|
|
//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>();
|
|
Generation g1{1};
|
|
{
|
|
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());
|
|
|
|
#ifdef NOT_YET
|
|
REQUIRE(roots->push_back(l1));
|
|
REQUIRE(mm->contains_allocated(Role::to_space(), x1.data()));
|
|
REQUIRE(mm->contains_allocated(Role::to_space(), l1.data()));
|
|
|
|
gc->add_gc_root_poly(&(*roots.operator->())[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()));
|
|
#endif
|
|
}
|
|
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 */
|