xo-umbrella2/utest/MutationLogStore.test.cpp

414 lines
17 KiB
C++

/** @file MutationLogStore.test.cpp
*
* @author Roland Conybeare, Apr 2026
**/
#include "GcosTestutil.hpp"
#include "MlsTestutil.hpp"
#include <xo/object2/List.hpp>
#include <xo/object2/Boolean.hpp>
#include <xo/gc/GCObjectStore.hpp>
#include <xo/gc/GCObjectStoreVisitor.hpp>
#include <xo/gc/MutationLogStore.hpp>
#include <xo/gc/X1VerifyStats.hpp>
#include <xo/indentlog/scope.hpp>
#include <xo/randomgen/xoshiro256.hpp>
#include <xo/randomgen/random_seed.hpp>
#include <catch2/catch.hpp>
#include <unistd.h> // for ::getpagesize()
namespace ut {
using xo::scm::DList;
using xo::scm::DBoolean;
using xo::mm::MutationLogStore;
using xo::mm::MutationLogConfig;
using xo::mm::GCObjectStore;
using xo::mm::GCObjectStoreConfig;
using xo::mm::DGCObjectStoreVisitor;
using xo::mm::DArena;
using xo::mm::ArenaConfig;
using xo::mm::X1VerifyStats;
using xo::rng::xoshiro256ss;
using xo::rng::random_seed;
using xo::reflect::typeseq;
using xo::xtag;
using xo::scope;
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,
TestSequence test_seq,
uint32_t mlog_z,
bool mlog_enabled_flag,
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},
mutation_log_z_{mlog_z},
mlog_enabled_flag_{mlog_enabled_flag},
test_seq_{test_seq},
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}
{}
bool sanitize_flag() const noexcept { return true; }
/** number of generations in gco store **/
uint32_t n_gen_ = 0;
/** object prommotes 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 in bytes **/
uint32_t object_type_z_ = 0;
/** if true register types for gc-aware types used in unit test **/
bool do_type_registration_ = false;
/** storage for mutation log (mult by 3 x n_gen_) **/
uint32_t mutation_log_z_ = 0;
/** true if enabling mutation-log feature
* (load-bearing for incremental gc)
**/
bool mlog_enabled_flag_ = false;
/** if non-null; run contents of cmd_seq_[i] on loop #i **/
TestSequence test_seq_;
/** 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;
/** 2nd loop: #of cells in random object graph **/
uint32_t n_i0_test_obj_ = 0;
/** 2nd loop: #of random assignments to attempt **/
uint32_t n_i0_test_assign_ = 0;
/** 3rd+later loop: #of cells in random object graph **/
uint32_t n_i1_test_obj_ = 0;
/** 3rd+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 TestGraphType c_fixed = TestGraphType::fixed;
using Cmd = Step::Cmd;
static Step step_0[] = {
{Cmd::make_bool, 0, 0}, // #f
{Cmd::make_nil, 0, 0}, // #nil
{Cmd::make_cons, 0, 1}, // cons(#f,#nil)
{Cmd::sentinel, 0, 0},
};
static Phase phase_0[] = {
//
// lo hi mlog_new_z_[]
// v v v
{ 0, 3, {0} },
{ -1, -1, {0} },
};
static TestSequence seq_0 { step_0, phase_0 };
// seq1: side effect on head of cons cell.
// But no mlog entry b/c all object ages are equal
// -> no x-age pointers
//
static Step step_1[] = {
{Cmd::make_bool, 0, 0}, // [0]: #f
{Cmd::make_bool, 1, 0}, // [1]: #t
{Cmd::make_nil, 0, 0}, // [2]: #nil
{Cmd::make_cons, 0, 2}, // [3]: cons(#f,#nil)
{Cmd::assign_head, 3, 1}, // set-car(cons(#f,#nil),#t)
{Cmd::sentinel, 0, 0},
};
static Phase phase_1[] = {
//
// lo hi mlog_new_z_[]
// v v v
{ 0, 5, {0} },
{ -1, -1, {0} },
};
static TestSequence seq_1 { step_1, phase_1 };
static Step step_2[] = {
// ----- phase 0 -----
{Cmd::make_bool, 0, 0}, // [0]: #f
{Cmd::make_bool, 1, 0}, // [1]: #t
{Cmd::make_nil, 0, 0}, // [2]: #nil
{Cmd::make_cons, 0, 2}, // [3]: cons(#f,#nil)
// ----- phase 1 -----
{Cmd::make_bool, 1, 0}, // [4]: #t
{Cmd::assign_head, 3, 4}, // set-car(cons(#f,#nil),#t)
// ----- phase 2 -----
// ----- end -----
{Cmd::sentinel, 0, 0},
};
static Phase phase_2[] = {
//
// lo hi mlog_new_z_[]
// v v v
{ 0, 4, {0} }, // phase 0
{ 4, 6, {1} }, // phase 1. set-car makes 1x xgen ptr from g1->g0
{ 6, 6, {0} }, // phase 2. now both {src,dest} are in g1
{ -1, -1, {0} },
};
static TestSequence seq_2 { step_2, phase_2 };
# define seq_nil TestSequence{}
# define nil nullptr
# define T true
# define F false
static std::vector<Testcase> s_testcase_v = {
/**
* debug_flag
* n_i1_test_assign |
* n_i1_test_obj | |
* n_i0_test_assign | | |
* n_i0_test_obj | | | |
* n_gc_loop | | | | |
* obj_graph_type | | | | | |
* mlog_enabled_flag | | | | | | |
* mutation_log_z | | | | | | | |
* cmd_seq | | | | | | | | |
* do_type_registration | | | | | | | | | |
* n_survive object_type_z | | | | | | | | | | |
* n_gen | gc_size | | | | | | | | | | | |
* v v v v v v v v v v v v v v v
**/
Testcase(2, 4, 16 * 1024, 8 * 128, F, seq_nil, 0, F, c_random, 1, 0, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_nil, 0, F, c_selfcycle, 1, 1, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_0, 0, F, c_fixed, 1, 0, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_1, 0, F, c_fixed, 1, 0, 0, 0, 0, F),
Testcase(2, 1, 16 * 1024, 8 * 128, T, seq_2, 128, T, c_fixed, 3, 0, 0, 0, 0, T),
};
# undef T
# undef F
/** Fixture for MutationLogStore-1 test.
* Compare similar but not identical fixture in GCObjectStore.test.cpp
**/
class MlsFixture {
public:
explicit MlsFixture(const Testcase &);
/** configuration for @ref gcos_ **/
GCObjectStoreConfig gcos_config_;
/** configuration for @ref mls_ **/
MutationLogConfig mls_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.
* all objects are in one place
* 2. arena2 doesn't have concept of installed types.
* It doesn't have or require any builtin ability to traverse an object model,
* storage recovery strategy is O(1) "clear the whole arena".
**/
DArena arena2_;
/** statistics called by GCObjectStore.verify_ok() **/
X1VerifyStats verify_stats_;
/** holds objects in multiple generations.
**/
GCObjectStore gcos_;
/**
* mutation log store tracks pointers
* from older objects to younger objects,
* which can only be created by mutation
**/
MutationLogStore mls_;
};
MlsFixture::MlsFixture(const Testcase & tc)
: gcos_config_{(ArenaConfig()
.with_name("mlog-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_},
mls_config_{tc.n_gen_,
tc.mutation_log_z_,
tc.mlog_enabled_flag_,
tc.debug_flag_},
arena2_{DArena::map(ArenaConfig().with_name("arena2-ref")
.with_size(tc.gc_size_ * tc.n_gen_)
.with_store_header_flag(true))},
gcos_{gcos_config_, &verify_stats_},
mls_{mls_config_, &gcos_}
{}
}
TEST_CASE("MutationLogStore-1", "[MutationLogStore]")
{
constexpr bool c_debug_flag = true;
scope log0(XO_DEBUG(c_debug_flag), "MutationLogStore test");
std::uint64_t seed = 7988747704879432247ul;
//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) {
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)));
MlsFixture fixture(tc);
// unlike GCObjectStore, separate init.
//
// TODO: adopt GCObjectStore pattern
//
fixture.mls_.init_mlogs(getpagesize());
{
// updates counters in fixture.verify_stats_
fixture.gcos_.verify_ok();
fixture.mls_.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());
}
GCObjectStore & gcos = fixture.gcos_;
MutationLogStore & mls = fixture.mls_;
{
// gcos setup. parallels GCObjectStore.test.cpp
{
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);
}
}
/** mutator/collector loop **/
/** parallel {test,reference} object state.
*
**/
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));
GcosTestutil::gcos_construct_ab_object_graphs(tc.test_seq_,
tc.obj_graph_type_,
tc.n_i0_test_obj_,
tc.n_i0_test_assign_,
tc.n_i1_test_obj_,
tc.n_i1_test_assign_,
tc.debug_flag_,
&mls,
&gcos,
&fixture.arena2_,
loop_index,
&x1_v, &x2_v,
&rgen);
Generation gk = Generation::g1();
// no allocation errors
REQUIRE(gcos.last_error().error_ == xo::mm::error::ok);
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 for generations g < gk
gcos.swap_roles(gk);
mls.swap_roles(gk);
GcosTestutil::gcos_verify_gen0_fromspace_only_allocated(tc.n_gen_, gcos, loop_index,
gk, x1_v);
// gc core: move stuff
GcosTestutil::gcos_move_roots_and_verify(tc.do_type_registration_,
&gcos,
gk, x1_v, x2_v, tc.debug_flag_);
DGCObjectStoreVisitor visitor(&gcos, gk);
// after swapping roles only from-space mlog can be non-empty
MlsTestutil::verify_fromspace_only_logged(mls, gk);
// forward mutation log + mutation-rescued objects
mls.forward_mutation_log(visitor.ref(), gk);
// now only to-space mlog can be non-empty
MlsTestutil::verify_tospace_only_logged(mls, gk);
MlsTestutil::verify_mlog_load_bearing(mls, gk);
// Might expect scanning generation g >= gk to confirm each object refs only to-space.
//
// reset (+ perhaps clean) from-space
gcos.cleanup_phase(gk, tc.sanitize_flag());
// scan {gcos, mls} to collect counters in *gcos.verify_stats()
{
gcos.verify_stats()->clear();
gcos.verify_ok();
mls.verify_ok();
REQUIRE(gcos.verify_stats()->is_ok());
}
}
}
}
} /*namespace ut*/
/* end MutationLogStore.test.cpp */