/** @file MutationLogStore.test.cpp * * @author Roland Conybeare, Apr 2026 **/ #include "GcosTestutil.hpp" #include "MlsTestutil.hpp" #include #include #include #include #include #include #include #include #include #include #include #include // for ::getpagesize() namespace ut { using xo::scm::DList; using xo::scm::DBoolean; using xo::scm::DInteger; 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 }; // ---------------------------------------------------------------- static Step step_3[] = { // ----- 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 ----- // ----- phase 3 ----- // ----- end ----- {Cmd::sentinel, 0, 0}, }; static Phase phase_3[] = { // // lo hi mlog_new_z_[] // v v v { 0, 4, {0} }, // phase 0 { 4, 6, {1} }, // phase 1. set-car makes 1x xage ptr { 6, 6, {1} }, // phase 2. now src in g1, dest in g0 { 6, 6, {0} }, // phase 3. now dest in g1 { -1, -1, {0} }, }; static TestSequence seq_3 { step_3, phase_3 }; // ---------------------------------------------------------------- static Step step_4[] = { // ----- 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) // 1st gc // ----- phase 1 ----- {Cmd::make_bool, 1, 0}, // [4]: #t {Cmd::assign_head, 3, 4}, // set-car([3],#t) // 2nd gc. [0]..[3] promote to g1 // [4] in g0 so [3]->[4] requires mlog entry // ----- phase 2 ----- {Cmd::make_bool, 0, 0}, // [5]: #f {Cmd::assign_head, 3, 5}, // set-car([3],#f) // 3rd gc. [4] promotes to g1, // [5] in g0 so [3]->[5] requires mlog entry // ----- phase 3 ----- // ----- phase 4 ----- // ----- end ----- {Cmd::sentinel, 0, 0}, }; static Phase phase_4[] = { // // lo hi mlog_new_z_[] // v v v { 0, 4, {0} }, // phase 0 gc { 4, 6, {1} }, // phase 1 gc. set-car makes 1x xage ptr { 6, 8, {2} }, // phase 2 gc. now src in g1, dest [4] in g0 { 8, 8, {1} }, // phase 3 gc. new dest [5] in g0 { 8, 8, {0} }, // phase 4 gc. now dest in g1 { -1, -1, {0} }, }; static TestSequence seq_4 { step_4, phase_4 }; // ---------------------------------------------------------------- static Step step_5[] = { // ----- phase 0 ----- {Cmd::make_int, 99, 0}, // [0]: 99 {Cmd::make_nil, 0, 0}, // [1]: #nil {Cmd::make_cons, 0, 1}, // [2]: cons([0],[1]) -> cons(99,#nil) // phase 0 gc (1st gc) // ----- phase 1 ----- {Cmd::make_int, 15, 0}, // [3]: 15 {Cmd::assign_head, 2, 3}, // set-car([2],[3]) -> set-car([2],15) // phase 1 gc (2nd gc) // [1]..[2] promote to g1 // [3] in g0 so [2]->[3] requires mlog entry // ----- phase 2 ----- {Cmd::make_int, 24, 0}, // [4]: 33 {Cmd::assign_head, 2, 4}, // set-car([2],[4]) -> set-car([2],33) // phase 2 gc (3rd gc) // ----- phase 3 ----- {Cmd::assign_root, 2, 0}, // [2] = [0] = 99 // o.g. [2] now garbage // ----- phase 4 ----- // ----- end ----- {Cmd::sentinel, 0, 0}, }; static Phase phase_5[] = { // // lo hi mlog_new_z_[] // v v v { 0, 3, {0} }, // phase 0 gc { 3, 5, {1} }, // phase 1 gc. set-car makes 1x xage ptr { 5, 7, {2} }, // phase 2 gc. now src in g1, dest [3] in g0 { 7, 8, {1} }, // phase 3 gc. new dest [4] in g0 { 8, 8, {0} }, // phase 4 gc. now dest [4] in g1 { -1, -1, {0} }, }; static TestSequence seq_5 { step_5, phase_5 }; // ---------------------------------------------------------------- # define seq_nil TestSequence{} # define nil nullptr # define T true # define F false static std::vector 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, F), Testcase(2, 2, 16 * 1024, 8 * 128, T, seq_3, 128, T, c_fixed, 4, 0, 0, 0, 0, F), Testcase(2, 2, 16 * 1024, 8 * 128, T, seq_4, 128, T, c_fixed, 4, 0, 0, 0, 0, F), Testcase(2, 2, 16 * 1024, 8 * 128, T, seq_5, 128, T, c_fixed, 4, 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()) == false); REQUIRE(gcos.is_type_installed(typeseq::id()) == false); REQUIRE(gcos.is_type_installed(typeseq::id()) == false); GcosTestutil::gcos_install_test_types(tc.do_type_registration_, &gcos); if (tc.do_type_registration_) { REQUIRE(gcos.is_type_installed(typeseq::id()) == true); REQUIRE(gcos.is_type_installed(typeseq::id()) == true); REQUIRE(gcos.is_type_installed(typeseq::id()) == true); } 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 x1_v; std::vector 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)); INFO(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()); } } /*one gc cycle per loop*/ } /*testcase loop*/ } } /*namespace ut*/ /* end MutationLogStore.test.cpp */