xo-gc/utest/GCObjectStore.test.cpp

631 lines
26 KiB
C++

/** @file GCObjectStore.test.cpp
*
* @author Roland Conybeare, Apr 2026
**/
#include "GcosTestutil.hpp"
#include <xo/gc/GCObjectStore.hpp>
#include <xo/gc/X1VerifyStats.hpp>
#include <xo/object2/ListOps.hpp>
#include <xo/object2/List.hpp>
#include <xo/object2/Integer.hpp>
#include <xo/object2/Boolean.hpp>
#include <xo/alloc2/GCObjectVisitor.hpp>
#include <xo/alloc2/GCObject.hpp>
#include <xo/alloc2/Arena.hpp>
#include <xo/facet/TypeRegistry.hpp>
#include <xo/arena/print.hpp>
#include <xo/arena/backtrace.hpp>
#include <xo/indentlog/scope.hpp>
#include <xo/indentlog/print/tag.hpp>
#include <xo/randomgen/xoshiro256.hpp>
#include <xo/randomgen/random_seed.hpp>
#include <catch2/catch.hpp>
namespace ut {
using xo::scm::ListOps;
using xo::scm::DList;
using xo::scm::DInteger;
using xo::scm::DBoolean;
using xo::mm::GCObjectStoreConfig;
using xo::mm::GCObjectStore;
using xo::mm::X1VerifyStats;
using xo::mm::AGCObject;
using xo::mm::AGCObjectVisitor;
using xo::mm::Generation;
using xo::mm::Role;
using xo::mm::object_age;
using xo::mm::ArenaConfig;
using xo::mm::AAllocator;
using xo::mm::DArena;
using xo::mm::AllocInfo;
using xo::mm::c_max_generation;
using xo::print_backtrace_dwarf;
using xo::facet::obj;
using xo::facet::TypeRegistry;
using xo::facet::typeseq;
using xo::facet::impl_for;
using xo::rng::xoshiro256ss;
using xo::rng::random_seed;
using xo::scope;
using xo::xtag;
using xo::tostr;
using std::size_t;
using std::uint32_t;
namespace {
struct Testcase {
explicit Testcase(uint32_t n_gen, uint32_t n_survive,
size_t gc_z, uint32_t type_z,
bool do_type_registration,
size_t report_z,
size_t error_z,
TestGraphType obj_graph_type,
uint32_t n_gc_loop,
uint32_t n_i0_test_obj,
uint32_t n_i0_test_assign,
uint32_t n_i1_test_obj,
uint32_t n_i1_test_assign,
bool debug_flag)
: n_gen_{n_gen},
n_survive_{n_survive},
gc_size_{gc_z},
object_type_z_{type_z},
do_type_registration_{do_type_registration},
report_size_{report_z},
error_size_{error_z},
obj_graph_type_{obj_graph_type},
n_gc_loop_{n_gc_loop},
n_i0_test_obj_{n_i0_test_obj},
n_i0_test_assign_{n_i0_test_assign},
n_i1_test_obj_{n_i1_test_obj},
n_i1_test_assign_{n_i1_test_assign},
debug_flag_{debug_flag}
{}
/** number of generations in gco store **/
uint32_t n_gen_ = 0;
/** object promotes on surviving this many gc cycles **/
uint32_t n_survive_ = 0;
/** size of each generation's half-space, in bytes **/
size_t gc_size_ = 0;
/** Storage for object type array, in bytes.
* (need to allow 1 pointer per type)
**/
uint32_t object_type_z_ = 0;
/** if true, register types for
* gc-aware types used in unit test
* (i.e. DBoolean)
**/
bool do_type_registration_ = false;
/** size for report-output arena **/
size_t report_size_ = 0;
/** size for error-output arena **/
size_t error_size_ = 0;
/** object graph type **/
TestGraphType obj_graph_type_ = TestGraphType::random;
/** #of gc-like "move all the roots" phases to perform **/
uint32_t n_gc_loop_ = 0;
/** first loop: #of cells in random object graph **/
uint32_t n_i0_test_obj_ = 0;
/** first loop: #of random assignments to attempt (these may create cycles, for example) **/
uint32_t n_i0_test_assign_ = 0;
/** 2nd+later loop: #of cells in random object graph **/
uint32_t n_i1_test_obj_ = 0;
/** 2nd+later loop: #of random assignments to attempt **/
uint32_t n_i1_test_assign_ = 0;
/** true to enable debug when attempting this test case **/
bool debug_flag_ = false;
};
constexpr TestGraphType c_selfcycle = TestGraphType::selfcycle;
constexpr TestGraphType c_random = TestGraphType::random;
constexpr uint32_t c_report_z1 = 64 * 1024;
constexpr uint32_t c_error_z1 = 16 * 1024;
# define T true
# define F false
static std::vector<Testcase> s_testcase_v = {
// note: report_z: 64k not sufficient for report_object_ages()
/** n_gen, n_survive, gc_size, object_type_z, do_type_registration,
* report_z, error_z,
* n_gc_loop,
* n_i0_obj, n_i0_test_assign,
* n_i1_obj, n_i1_test_assign,
* debug_flag
* n_i1_obj
* n_i0_test_assign |
* n_i0_obj | |
* n_gc_loop | | |
* v v v v
**/
Testcase(2, 4, 16 * 1024, 8 * 128, F, c_report_z1, c_error_z1, c_random, 1, 0, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_selfcycle, 1, 1, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_selfcycle, 3, 1, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_selfcycle, 4, 1, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 1, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 2, 1, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 4, 1, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 8, 1, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 2, 13, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 2, 25, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 5, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 4, 2, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 50, 25, 0, 0, F),
};
# undef T
# undef F
} /*namespace*/
namespace {
// aux functions specific to GCObjectStore-1 unit test below
void
gcos_verify_gen0_fromspace_only_allocated(const Testcase & tc,
const GCObjectStore & gcos,
uint32_t loop_index,
Generation upto,
const std::vector<Recd> & x1_v)
{
Generation g0{0};
Generation gn{tc.n_gen_};
for (Generation gi = g0; gi < gn; ++gi) {
if (gi < upto) {
// we're collecting generation gi.
// Before we begin, to-space had better be empty
// (everthing in gi is in from-space)
REQUIRE(gcos.to_space(gi)->allocated() == 0);
} else {
// we're not collecting generation gi.
// from-space must be empty.
// May have content in to-space
REQUIRE(gcos.from_space(gi)->allocated() == 0);
}
}
for (size_t i = 0, n = x1_v.size(); i < n; ++i) {
const auto & x1 = x1_v.at(i);
// x1 should be in gen g from-space (with g < upto)
// or in gen g to-space (with g >= upto)
Generation g_from = gcos.generation_of(Role::from_space(), x1.gco_.data());
Generation g_to = gcos.generation_of(Role::to_space(), x1.gco_.data());
if (g_to.is_sentinel()) {
// if not in to-space, must be in from-space
REQUIRE(!g_from.is_sentinel());
// + for some gen we're collecting
REQUIRE(g_from < upto);
REQUIRE(gcos.contains(Role::from_space(), x1.gco_.data()));
REQUIRE(gcos.contains_allocated(Role::from_space(), x1.gco_.data()));
} else {
// if in to-space, must not be in from-space
REQUIRE(g_from.is_sentinel());
// + for some gen we're not collecting
REQUIRE(g_to >= upto);
REQUIRE(gcos.contains(Role::to_space(), x1.gco_.data()));
REQUIRE(gcos.contains_allocated(Role::to_space(), x1.gco_.data()));
}
AllocInfo obj_info = gcos.alloc_info((std::byte *)x1.gco_.data());
REQUIRE(obj_info.size() >= x1.alloc_z_);
REQUIRE(obj_info.payload().first == (std::byte *)x1.gco_.data());
REQUIRE(obj_info.tseq() == x1.tseq_.seqno());
}
}
void
gcos_verify_forwarding(const GCObjectStore & gcos,
Generation upto,
const Recd & x1,
obj<AGCObject> x1_gco)
{
REQUIRE((gcos.contains_allocated(Role::from_space(), x1_gco.data())
|| gcos.contains_allocated(Role::to_space(), x1_gco.data())));
AllocInfo obj_info = gcos.alloc_info((std::byte *)x1_gco.data());
INFO(tostr(xtag("obj_info.tseq", obj_info.tseq()),
xtag("obj_info.tname", TypeRegistry::id2name(typeseq(obj_info.tseq())))));
REQUIRE(obj_info.size() >= x1.alloc_z_);
REQUIRE(obj_info.payload().first == (std::byte *)x1_gco.data());
if (obj_info.is_forwarding_tseq()) {
/* object was forwarded, so got collected */
REQUIRE(obj_info.is_forwarding_tseq());
} else {
/* not forwarded is ok iff in generation g >= upto */
Generation g = gcos.generation_of(Role::to_space(), x1_gco.data());
REQUIRE(g >= upto);
}
// if (!obj_info.is_forwarding_tseq())
// print_backtrace_dwarf(true /*demangle*/);
// REQUIRE(obj_info.is_forwarding_tseq());
}
void
gcos_verify_forwarding_destination(const GCObjectStore & gcos,
const Recd & x1,
obj<AGCObject> x1p_gco)
{
REQUIRE(gcos.contains_allocated(Role::to_space(), x1p_gco.data()));
AllocInfo obj1p_info = gcos.alloc_info((std::byte *)x1p_gco.data());
REQUIRE(obj1p_info.size() >= x1.alloc_z_);
REQUIRE(obj1p_info.payload().first == (std::byte *)x1p_gco.data());
REQUIRE(obj1p_info.tseq() == x1.tseq_.seqno());
REQUIRE(x1p_gco.data() != nullptr);
REQUIRE(gcos.contains(Role::to_space(), x1p_gco.data()));
REQUIRE(gcos.contains_allocated(Role::to_space(), x1p_gco.data()));
}
void
gcos_verify_forwarded_ab_equivalence(obj<AGCObject> x1p_gco,
obj<AGCObject> x2_gco)
{
// written out polymorphic comparison
// match DBoolean..
bool match_attempted = false;
{
auto x1p_b = obj<AGCObject,DBoolean>::from(x1p_gco);
auto x2_b = obj<AGCObject,DBoolean>::from(x2_gco);
if (x1p_b && x2_b) {
match_attempted = true;
REQUIRE(x1p_b->value() == x2_b->value());
}
}
// match DList..
{
auto x1p_b = obj<AGCObject,DList>::from(x1p_gco);
auto x2_b = obj<AGCObject,DList>::from(x2_gco);
if (x1p_b && x2_b) {
match_attempted = true;
// TODO: we could figure out the index in {x1_v[], x2_v[]}
// of x*_b {head, rest} respectively,
// and verify they're consistent.
REQUIRE(x1p_b->head()._typeseq() == x2_b->head()._typeseq());
REQUIRE(x1p_b->size() == x2_b->size());
if (x1p_b->rest()) {
REQUIRE(x2_b->rest());
} else {
// unreachable, since using sentinel objectd for nil list
REQUIRE(x2_b->rest() == nullptr);
}
}
}
REQUIRE(match_attempted);
}
void
gcos_move_roots_and_verify(const Testcase & tc,
GCObjectStore * p_gcos,
Generation upto,
const std::vector<Recd> & x1_v,
const std::vector<Recd> & x2_v,
bool debug_flag)
{
scope log(XO_DEBUG(debug_flag));
Generation g1{1};
// try moving everything to to-space.
// For this to week we must have registered the type,
// so gc knows how to traverse it
//
for (size_t i = 0, n = x1_v.size(); i < n; ++i) {
const auto & x1 = x1_v.at(i);
const auto & x2 = x2_v.at(i);
log && log("moving roots");
log && log(xtag("i", i),
xtag("n", n),
xtag("x1.tseq_", x1.tseq_),
xtag("x1.tname", TypeRegistry::id2name(x1.tseq_)));
if (tc.do_type_registration_) {
/* Action of this loop iteration:
*
* gcos arena2
* +------------+-----------+ +--------+
* | from | to | | |
* | | | | |
* | +----+ | +-----+ | | +----+ |
* | | x1 |---->| x1p | | | | x2 | |
* | +----+ | +-----+ | | +----+ |
* | | | | |
* +------------+-----------+ +--------+
*
* Before:
* x1, x2 have the same shape
* After
* x1 forward to x1p
* x1p and x2 have the same shape
*/
// note: since members of x1_v[] can refer to each other,
// it's possible that x1.gco_ is already a forwarding pointer
// before we call deep_move_root().
AGCObject * x1p_iface = p_gcos->lookup_type(x1.tseq_);
REQUIRE(x1p_iface);
// snapshot root before moving
obj<AGCObject> x1_gco = x1.gco_;
// modifies x1.gco_ in place
auto x1p_data
= p_gcos->deep_move_root(x1p_iface, (void **)&(x1.gco_.data_), upto);
REQUIRE(x1p_data);
REQUIRE(x1p_data == x1.gco_.data_);
obj<AGCObject> x1p_gco(x1p_iface, x1p_data);
// obj (x1_gco) now forwarding pointer (to x1p_gco = x1.gco_)
gcos_verify_forwarding(*p_gcos, upto, x1, x1_gco);
// obj1p in to-space, same contents as original obj
gcos_verify_forwarding_destination(*p_gcos, x1, x1p_gco);
// x1p_gco must look like x2.gco
REQUIRE(x1p_gco._typeseq() == x2.gco_._typeseq());
gcos_verify_forwarded_ab_equivalence(x1p_gco, x2.gco_);
} else {
// can still try to move something.
// but will fail since type isn't registered
auto x1p_data
= p_gcos->deep_move_root(x1.gco_.iface(),
(void **)&(x1.gco_.data_),
g1);
// control here under normal GC use
// would represent a configuration fail
REQUIRE(x1p_data == nullptr);
}
}
}
// fixture for GCObjectStore-1 test
class GcosFixture {
public:
explicit GcosFixture(const Testcase & tc);
auto report_mm() { return obj<AAllocator,DArena>(&report_arena_); }
auto error_mm() { return obj<AAllocator,DArena>(&error_arena_); }
GCObjectStoreConfig gcos_config_;
/** Parallel arena for reference
*
* We will allocate parallel object model in this arena
* for reference; then compare with GCObjectStore behavior.
*
* 1. arena2 doesn't have any generation layer cake stuff
* 2. arena2 doesn't have concept of installed types.
* It doesn't have or require any builtin ability to traverse an object model
**/
DArena arena2_;
/** Arena for holding report output:
* See GCObjectStore methods .report_object_types(), .report_object_ages()
**/
DArena report_arena_;
/** Arena for holding error messages **/
DArena error_arena_;
/** statistics collected by GCObjectStore.verify_ok() **/
X1VerifyStats verify_stats_;
/** the thing we're exercising using this fixture **/
GCObjectStore gcos_;
};
GcosFixture::GcosFixture(const Testcase & tc)
: gcos_config_{ArenaConfig()
.with_name("gcos-fixture-arena-name-notused")
.with_size(tc.gc_size_)
.with_store_header_flag(true),
tc.n_gen_,
tc.n_survive_,
tc.object_type_z_,
tc.debug_flag_},
arena2_{DArena::map(ArenaConfig().with_name("arena2-ref")
.with_size(tc.gc_size_ * tc.n_gen_)
.with_store_header_flag(true))},
report_arena_{DArena::map(ArenaConfig().with_name("report-arena")
.with_size(tc.report_size_)
.with_store_header_flag(true))},
error_arena_{DArena::map(ArenaConfig().with_name("error-arena")
.with_size(tc.error_size_)
.with_store_header_flag(true))},
gcos_{gcos_config_, &verify_stats_}
{}
}
TEST_CASE("GCObjectStore-1", "[GCObjectStore]")
{
constexpr bool c_debug_flag = true;
scope log0(XO_DEBUG(c_debug_flag), "GCObjectStore test");
std::uint64_t seed = 12168164826603821466ul;
//random_seed(&seed);
log0 && log0(xtag("seed", seed));
for (size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
// Loop iterations here are independent.
// Could execute test cases in any order
// deterministic seed choice for each testcase
// -> individual cases preserve rng behavior
// regardless of testcase order and/or subsetting
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)));
GcosFixture fixture(tc);
GCObjectStore & gcos = fixture.gcos_;
REQUIRE(gcos.is_type_installed(typeseq::id<DList>()) == false);
REQUIRE(gcos.is_type_installed(typeseq::id<DBoolean>()) == false);
GcosTestutil::gcos_install_test_types(tc.do_type_registration_, &gcos);
GcosTestutil::gcos_verify_arena_partitioning(tc.n_gen_, tc.gc_size_, gcos);
GcosTestutil::gcos_verify_vacant(tc.n_gen_, tc.gc_size_, gcos);
// create object(s).
// details depend on test case
std::vector<Recd> x1_v;
std::vector<Recd> x2_v;
for(uint32_t loop_index = 0; loop_index < tc.n_gc_loop_; ++loop_index) {
scope log2(XO_DEBUG(tc.debug_flag_), "gc loop", xtag("loop_index", loop_index));
// construct, extend, and/or modify object graphs in {x1_v, x2_v}
GcosTestutil::gcos_construct_ab_object_graphs(tc.obj_graph_type_,
tc.n_i0_test_obj_,
tc.n_i0_test_assign_,
tc.n_i1_test_obj_,
tc.n_i1_test_assign_,
&gcos,
&fixture.arena2_,
loop_index,
&x1_v, &x2_v,
&rgen);
// no allocation errors
REQUIRE(gcos.last_error().error_ == xo::mm::error::ok);
log1 && log1("verify before any gcos side effects");
GcosTestutil::gcos_verify_consistency(&gcos);
// someday: print the graph. Need a cycle-detecting printer
GcosTestutil::gcos_verify_ab_equivalence(x1_v, x2_v);
GcosTestutil::gcos_verify_allocinfo(gcos, loop_index, x1_v);
GcosTestutil::gcos_verify_gen0_only_allocated(tc.n_gen_, gcos, loop_index, x1_v);
// swap_roles [but only for generation < g1, i.e. g0
gcos.swap_roles(Generation::g1());
gcos_verify_gen0_fromspace_only_allocated(tc, gcos, loop_index, Generation::g1(), x1_v);
gcos_move_roots_and_verify(tc, &gcos, Generation::g1(), x1_v, x2_v, tc.debug_flag_);
// Things to test:
// - deep_move_interior() // used from MutationLogStore
// - forward_inplace_aux() // used from DX1Collector.visit_child
{
bool sanitize_flag = true;
// swaps to- and from- spaces again
// Now from-space will be empty, all live objects in to-space
gcos.cleanup_phase(Generation::g1(), sanitize_flag);
}
{
fixture.verify_stats_.clear();
// traverses stored objects, updates counters
// in verify_stats (= gco.p_verify_stats_, via ctor)
//
gcos.verify_ok();
INFO(tostr(xtag("n_gc_root", fixture.verify_stats_.n_gc_root_),
xtag("n_ext", fixture.verify_stats_.n_ext_),
xtag("n_from", fixture.verify_stats_.n_from_),
xtag("n_to", fixture.verify_stats_.n_to_)));
INFO(tostr(xtag("n_fwd", fixture.verify_stats_.n_fwd_),
xtag("n_age_ok", fixture.verify_stats_.n_age_ok_),
xtag("n_age_bad", fixture.verify_stats_.n_age_bad_),
xtag("n_no_iface", fixture.verify_stats_.n_no_iface_)));
REQUIRE(fixture.verify_stats_.is_ok());
}
// report stats by type
{
obj<AGCObject> report_gco;
bool ok = gcos.report_object_types(fixture.report_mm(), fixture.error_mm(), &report_gco);
REQUIRE(ok);
REQUIRE(report_gco);
// TODO: print report_gco, verify output
// discard report
report_gco.reset();
fixture.report_mm()->clear();
}
// report stats by age
{
obj<AGCObject> report_gco;
bool ok = gcos.report_object_ages(fixture.report_mm(), fixture.error_mm(), &report_gco);
if (!ok) {
log1.retroactively_enable();
log1 && log1(xtag("error", fixture.report_mm().last_error()));
}
REQUIRE(ok);
REQUIRE(report_gco);
// TODO: print report_gco, verify output
// discard report
report_gco.reset();
fixture.report_mm()->clear();
}
}
} /* loop over test cases */
} /* TEST_CASE(GCObjectStore-1) */
} /*namespace ut*/
/* end GCObjectStore.test.cpp */