xo-gc: bugfix + mutation log test passes

This commit is contained in:
Roland Conybeare 2026-04-19 19:57:49 -04:00
commit 20d886d3d6
6 changed files with 310 additions and 117 deletions

View file

@ -35,6 +35,7 @@ namespace xo {
**/
void init_mlogs(std::size_t page_z);
const MutationLogConfig & config() const noexcept { return config_; }
MutationLog * get_mlog(Role r, Generation g) noexcept { return mlog_[r][g]; }
const MutationLog * get_mlog(Role r, Generation g) const noexcept { return mlog_[r][g]; }
/** reminder: abusing Role because we need one additional mlog **/

View file

@ -29,10 +29,10 @@ namespace xo {
for (std::uint32_t mlog_role = 0; mlog_role < c_n_role + 1; ++mlog_role) {
this->mlog_storage_[mlog_role][igen]
= _make_mlog(igen,
label_v[mlog_role],
config_.mutation_log_z_,
page_z);
= this->_make_mlog(igen,
label_v[mlog_role],
config_.mutation_log_z_,
page_z);
this->mlog_[mlog_role][igen]
= &(mlog_storage_[mlog_role][igen]);
@ -152,6 +152,8 @@ namespace xo {
*p_lhs = rhs;
if (!config_.enabled_flag_) {
log && log(xtag("msg", "noop b/c incremental gc disabled"));
// only need to log mutations when incremental gc is enabled
return;
}
@ -163,6 +165,8 @@ namespace xo {
Generation src_g = gco_store->generation_of(Role::to_space(), p_lhs);
if (src_g.is_sentinel()) {
log && log(xtag("msg", "noop because src not gc-owned"));
// only need mlog entries for gc-owned pointers.
// In this case pointer does not originate in gc-owned space
return;
@ -171,11 +175,15 @@ namespace xo {
Generation dest_g = gco_store->generation_of(Role::to_space(), rhs.data());
if (dest_g.is_sentinel()) {
log && log(xtag("msg", "noop because dest not gc-owned"));
// similarly, don't need mlog entry to non-gc-owned destination
return;
}
if (src_g < dest_g) {
log && log(xtag("msg", "noop because src gen younger than dest gen"));
// young-to-old pointers don't need to be remembered,
// since a GC cycle that collects an (old) generation is guarnatted
// to also collect all younger generations.
@ -194,6 +202,8 @@ namespace xo {
assert(src_hdr && dest_hdr);
if (gco_store->header2age(*src_hdr) <= gco_store->header2age(*dest_hdr)) {
log && log(xtag("msg", "noop because src age no older than dest age"));
// source and destination have the same age;
// therefore are always collected on the same set of GC cycles
// -> no need to remember separately.
@ -258,7 +268,7 @@ namespace xo {
// - to_mlog, triage_mlog are empty
for (Generation child_gen{0};
child_gen + 2 < config_.n_generation_;
child_gen + 1 < config_.n_generation_;
++child_gen) {
MutationLog * from_mlog = this->mlog_[Role::from_space()][child_gen];

View file

@ -275,7 +275,7 @@ namespace ut {
// construct, extend, and/or modify object graphs in {x1_v, x2_v}
GcosTestutil::gcos_construct_ab_object_graphs(nullptr /*cmd_seq*/,
GcosTestutil::gcos_construct_ab_object_graphs(TestSequence{} /*test_seq*/,
tc.obj_graph_type_,
tc.n_i0_test_obj_,
tc.n_i0_test_assign_,

View file

@ -24,6 +24,7 @@ namespace ut {
using xo::mm::ACollector;
using xo::mm::DMockCollector;
using xo::mm::X1VerifyStats;
using xo::mm::MutationLog;
using xo::mm::GCObjectStore;
using xo::mm::AGCObject;
using xo::mm::AAllocator;
@ -316,7 +317,7 @@ namespace ut {
* @p loop_index counts iteration with one gc-like phase.
**/
void
GcosTestutil::gcos_construct_ab_object_graphs(Step * cmd_seq,
GcosTestutil::gcos_construct_ab_object_graphs(TestSequence test_seq,
TestGraphType obj_graph_type,
uint32_t n_i0_test_obj,
uint32_t n_i0_test_assign,
@ -331,104 +332,185 @@ namespace ut {
std::vector<Recd> * p_x2_v,
xoshiro256ss * p_rgen)
{
if (cmd_seq && (loop_index == 0)) {
// do scripted sequence only
/** TestSequence memory layout
*
* test_seq
* |
* v
* TestSequence
* +-----------+ cmd_seq_[] (shared, sentinel-terminated)
* | cmd_seq_ |---> +-------+-------+-------+-------+-------+-------+
* +-----------+ | step0 | step1 | step2 | step3 | step4 | SENTL |
* | phases_ |-\ +-------+-------+-------+-------+-------+-------+
* +-----------+ | ix: 0 1 2 3 4 5
* |
* | phases_[] (sentinel-terminated)
* \-> +-----------+-----------+
* | Phase 0 | SENTINEL |
* | lo_ix_=0 | lo_ix_=-1 |
* | hi_ix_=5 | hi_ix_=-1 |
* | mlog_new_ | mlog_new_ |
* +-----------+-----------+
*
* Phase.lo_ix_ / hi_ix_ index into test_seq->cmd_seq_[].
* Phase 0 executes cmd_seq_[lo_ix_ .. hi_ix_), i.e. steps 0..4.
* Sentinel phase has lo_ix_ == -1.
*
* Each Step has {cmd_, arg0_ix_, arg1_ix_}.
* arg0_ix_ and arg1_ix_ index into x1_v[] / x2_v[],
* referring to objects created by earlier steps.
*
* Example (seq1):
* step0: {make_bool, 0, 0} -> x1_v[0] = #f
* step1: {make_bool, 1, 0} -> x1_v[1] = #t
* step2: {make_nil, 0, 0} -> x1_v[2] = ()
* step3: {make_cons, 0, 2} -> x1_v[3] = cons(x1_v[0], x1_v[2]) = (#f)
* step4: {assign_head, 3, 1} -> set-car!(x1_v[3], x1_v[1]) => (#t)
**/
auto alloc = obj<AAllocator,DArena>(p_gcos->new_space());
auto alloc2 = obj<AAllocator,DArena>(p_arena2);
DMockCollector mock(p_mls, p_gcos);
auto mockgc = obj<ACollector,DMockCollector>(&mock);
if (!test_seq.is_sentinel()) {
// Explicit command sequence.
// Each command creates a new node or modifies an existing one
while (cmd_seq->is_command()) {
bool is_alloc = false;
obj<AGCObject> xi;
obj<AGCObject> xi2;
uint64_t alloc_z = 0;
typeseq tseq;
// 1. Sequence of commands for this call.
// Will be phases[loop_index] if well-defined.
// 2. Expected effect on mutation log
//
Phase * phase_expect = nullptr;
{
Phase * p_phase = test_seq.phases_;
switch (cmd_seq->cmd_) {
case Step::Cmd::sentinel:
assert(false); // unreachable
break;
case Step::Cmd::make_nil:
// TODO combine with code in random_object_graph()
{
is_alloc = true;
xi = ListOps::nil();
alloc_z = 0; // not in gcos space
tseq = typeseq::id<DList>();
xi2 = ListOps::nil();
REQUIRE(xi._typeseq() == tseq);
REQUIRE(xi2._typeseq() == tseq);
if (test_seq.phases_) {
for (uint32_t i = 0; i < loop_index; ++i) {
if (!p_phase->is_sentinel())
++p_phase;
else
p_phase = nullptr;
}
break;
case Step::Cmd::make_cons:
// TODO combine with code in random_object_graph()
{
auto h1 = p_x1_v->at(cmd_seq->arg0_ix_).gco_;
auto r1 = obj<AGCObject,DList>::from(p_x1_v->at(cmd_seq->arg1_ix_).gco_);
auto h2 = p_x2_v->at(cmd_seq->arg0_ix_).gco_;
auto r2 = obj<AGCObject,DList>::from(p_x2_v->at(cmd_seq->arg1_ix_).gco_);
is_alloc = true;
xi = ListOps::cons(alloc, h1, r1);
alloc_z = sizeof(DList);
tseq = typeseq::id<DList>();
xi2 = ListOps::cons(alloc2, h2, r2);
}
break;
case Step::Cmd::make_bool:
// TODO combine with code in random_object_graph()
{
bool value = (cmd_seq->arg0_ix_ > 0);
is_alloc = true;
xi = DBoolean::box(alloc, value);
alloc_z = sizeof(DBoolean);
tseq = typeseq::id<DBoolean>();
xi2 = DBoolean::box(alloc2, value);
}
break;
case Step::Cmd::assign_head:
{
is_alloc = false;
auto lhs1 = obj<AGCObject,DList>::from(p_x1_v->at(cmd_seq->arg0_ix_).gco_);
auto rhs1 = p_x2_v->at(cmd_seq->arg1_ix_).gco_;
auto lhs2 = obj<AGCObject,DList>::from(p_x2_v->at(cmd_seq->arg0_ix_).gco_);
auto rhs2 = p_x2_v->at(cmd_seq->arg1_ix_).gco_;
assert(lhs1);
assert(!lhs1->is_empty());
assert(lhs2);
assert(!lhs2->is_empty());
assert(p_mls);
assert(mockgc);
lhs1->assign_head(mockgc, rhs1);
// alloc2 is ord arena -> no mlog
}
break;
}
if (is_alloc) {
p_x1_v->push_back(Recd(xi, alloc_z, tseq));
p_x2_v->push_back(Recd(xi2, alloc_z, tseq));
phase_expect = p_phase;
}
Step * cmd_seq = test_seq.cmd_seq_;
if (phase_expect && cmd_seq) {
// Do scripted sequence only.
// For this phases that is
// cmd_seq[ix]
// for
// phase_expect->lo_ix_ <= ix < phase_expect->hi_ix_
auto alloc = obj<AAllocator,DArena>(p_gcos->new_space());
auto alloc2 = obj<AAllocator,DArena>(p_arena2);
DMockCollector mock(p_mls, p_gcos);
auto mockgc = obj<ACollector,DMockCollector>(&mock);
for (int32_t ix = phase_expect->lo_ix_, hi = phase_expect->hi_ix_; ix < hi; ++ix) {
const Step & cmd = cmd_seq[ix];
bool is_alloc = false;
obj<AGCObject> xi;
obj<AGCObject> xi2;
uint64_t alloc_z = 0;
typeseq tseq;
switch (cmd.cmd_) {
case Step::Cmd::sentinel:
assert(false); // unreachable
break;
case Step::Cmd::make_nil:
// TODO combine with code in random_object_graph()
{
is_alloc = true;
xi = ListOps::nil();
alloc_z = 0; // not in gcos space
tseq = typeseq::id<DList>();
xi2 = ListOps::nil();
REQUIRE(xi._typeseq() == tseq);
REQUIRE(xi2._typeseq() == tseq);
}
break;
case Step::Cmd::make_cons:
// TODO combine with code in random_object_graph()
{
auto h1 = p_x1_v->at(cmd.arg0_ix_).gco_;
auto r1 = obj<AGCObject,DList>::from(p_x1_v->at(cmd.arg1_ix_).gco_);
auto h2 = p_x2_v->at(cmd.arg0_ix_).gco_;
auto r2 = obj<AGCObject,DList>::from(p_x2_v->at(cmd.arg1_ix_).gco_);
is_alloc = true;
xi = ListOps::cons(alloc, h1, r1);
alloc_z = sizeof(DList);
tseq = typeseq::id<DList>();
xi2 = ListOps::cons(alloc2, h2, r2);
}
break;
case Step::Cmd::make_bool:
// TODO combine with code in random_object_graph()
{
bool value = (cmd.arg0_ix_ > 0);
is_alloc = true;
xi = DBoolean::box(alloc, value);
alloc_z = sizeof(DBoolean);
tseq = typeseq::id<DBoolean>();
xi2 = DBoolean::box(alloc2, value);
}
break;
case Step::Cmd::assign_head:
{
is_alloc = false;
auto lhs1 = obj<AGCObject,DList>::from(p_x1_v->at(cmd.arg0_ix_).gco_);
auto rhs1 = p_x1_v->at(cmd.arg1_ix_).gco_;
auto lhs2 = obj<AGCObject,DList>::from(p_x2_v->at(cmd.arg0_ix_).gco_);
auto rhs2 = p_x2_v->at(cmd.arg1_ix_).gco_;
assert(lhs1);
assert(!lhs1->is_empty());
assert(lhs2);
assert(!lhs2->is_empty());
assert(p_mls);
assert(mockgc);
lhs1->assign_head(mockgc, rhs1);
// alloc2 is ord arena -> no mlog
}
break;
}
if (is_alloc) {
p_x1_v->push_back(Recd(xi, alloc_z, tseq));
p_x2_v->push_back(Recd(xi2, alloc_z, tseq));
}
}
++cmd_seq;
// check expected results
for (Generation gi{0}; gi + 1 < Generation(p_mls->config().n_generation_); ++gi) {
MutationLog * mlog = p_mls->get_mlog(Role::to_space(), gi);
REQUIRE(mlog);
REQUIRE(mlog->size() == phase_expect->mlog_new_z_[gi]);
}
}
} else {
switch (obj_graph_type) {
case TestGraphType::fixed:
assert(false); // unreachable
break;
case TestGraphType::selfcycle:
if (loop_index == 0) {
GcosTestutil::selfcycle_object_graph(p_x1_v,

View file

@ -11,6 +11,7 @@
#include <xo/alloc2/Generation.hpp>
#include <xo/arena/DArena.hpp>
#include <xo/randomgen/xoshiro256.hpp>
#include <array>
namespace ut {
using xo::mm::Generation;
@ -46,6 +47,54 @@ namespace ut {
uint32_t arg1_ix_;
};
/** a phase comprises:
* 1. start with {gcos,mls} in known + valid state.
* 2. perform a sequence of commands
* (in general a mix of allocs and mutations)
* command sequence in @ref cmd_seq_, null-terminated
* 3. verify mlog state after sequence
* 4. run instrumented collection phase
* 4a. swap roles (i.e. from- and to- spaces)
* 4b. move roots, see gcos_move_roots_and_verify()
* 4c. update mutation log, see forward_mutation_log()
* 4d. cleanup (reset from- spaces)
* 5. re-verify {gcos,mls} in valid state
**/
struct Phase {
bool is_sentinel() const noexcept { return lo_ix_ == -1; }
/** Command sequence for this phase.
* See TestSequence.cmd_seq_
* Phase comprises commands cmd_seq_[ix] for lo_ix <= ix < hi_ix
**/
int32_t lo_ix_ = -1;
int32_t hi_ix_ = -1;
/** expected number of new entries in
* to-space mutation log after executing @ref cmd_seq_
**/
std::array<uint32_t, xo::mm::c_max_generation - 1> mlog_new_z_;
};
struct TestSequence {
bool is_sentinel() const noexcept { return cmd_seq_ == nullptr; }
/** shared null-terminated command sequence.
* references are taken relative to cmd_seq_[0].
* A step
* {make_cons, x, y} -> make a cons cell.
* head from value ~ cmd_seq_[x]
* rest from value ~ cmd_seq_[y]
*
**/
Step * cmd_seq_ = nullptr;
/** array of phases.
* One gc cycle per phase.
* Sentinel phase has {lo_ix_ = -1, hi_ix_ = -1};
**/
Phase * phases_ = nullptr;
};
enum class TestGraphType {
/* spelled out sequence of Steps */
fixed,
@ -110,9 +159,11 @@ namespace ut {
const GCObjectStore & gcos);
/** sequence of steps. if non-null, ends with step s: s.cmd_ == Step::Cmd::Sentinel
*
* @p p_cmd_seq pointer to null-terminated array of Step[] arrays
**/
static void
gcos_construct_ab_object_graphs(Step * cmd_seq,
gcos_construct_ab_object_graphs(TestSequence test_seq,
TestGraphType obj_graph_type,
uint32_t n_i0_test_obj,
uint32_t n_i0_test_assign,

View file

@ -40,7 +40,7 @@ namespace ut {
size_t gc_z,
uint32_t type_z,
bool do_type_registration,
Step * cmd_seq,
TestSequence test_seq,
uint32_t mlog_z,
bool mlog_enabled_flag,
TestGraphType obj_graph_type,
@ -57,7 +57,7 @@ namespace ut {
do_type_registration_{do_type_registration},
mutation_log_z_{mlog_z},
mlog_enabled_flag_{mlog_enabled_flag},
cmd_seq_{cmd_seq},
test_seq_{test_seq},
obj_graph_type_{obj_graph_type},
n_gc_loop_{n_gc_loop},
n_i0_test_obj_{n_i0_test_obj},
@ -83,8 +83,8 @@ namespace ut {
* (load-bearing for incremental gc)
**/
bool mlog_enabled_flag_ = false;
/** first loop: explicit cell alloc/assign **/
Step * cmd_seq_ = nullptr;
/** 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 **/
@ -107,47 +107,96 @@ namespace ut {
using Cmd = Step::Cmd;
static Step seq0[] = {
static Step step_0[] = {
{Cmd::make_bool, 0, 0}, // #f
{Cmd::make_nil, 0, 0}, // #nil
{Cmd::make_nil, 0, 0}, // #nil
{Cmd::make_cons, 0, 1}, // cons(#f,#nil)
{Cmd::sentinel, 0, 0},
{Cmd::sentinel, 0, 0},
};
static Step seq1[] = {
{Cmd::make_bool, 0, 0}, // #f
{Cmd::make_bool, 1, 0}, // #t
{Cmd::make_nil, 0, 0}, // #nil
{Cmd::make_cons, 0, 2}, // cons(#f,#nil)
{Cmd::assign_head, 3, 1}, // set-car(cons(#f,#nil),#t)
{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)
{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
{ -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
* 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, nil, 0, F, c_random, 1, 0, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, nil, 0, F, c_selfcycle, 1, 1, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, seq0, 0, F, c_fixed, 1, 0, 0, 0, 0, F),
Testcase(2, 4, 16 * 1024, 8 * 128, T, seq1, 0, F, c_fixed, 1, 0, 0, 0, 0, T),
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, 2, 0, 0, 0, 0, T),
};
# undef T
@ -284,7 +333,7 @@ namespace ut {
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.cmd_seq_,
GcosTestutil::gcos_construct_ab_object_graphs(tc.test_seq_,
tc.obj_graph_type_,
tc.n_i0_test_obj_,
tc.n_i0_test_assign_,