From d1e2ae38f3bd6b780664e70c53c4bc5bd31c9e5f Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 11 Apr 2026 19:02:05 -0400 Subject: [PATCH] xo-gc: utest: refactor for modularity --- utest/GCObjectStore.test.cpp | 544 +++++++++++++++++++++-------------- 1 file changed, 323 insertions(+), 221 deletions(-) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index fa66d81..d258dba 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -389,94 +389,15 @@ namespace ut { REQUIRE(!gcos.from_space(gn)); } } - } - - 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))); - - /** config for each half-space **/ - ArenaConfig arena_config - = (ArenaConfig() - .with_name("arena-name-not-used") - .with_size(tc.gc_size_) - .with_store_header_flag(true)); - - GCObjectStoreConfig gcos_config(arena_config, - tc.n_gen_, - tc.n_survive_, - tc.object_type_z_, - tc.debug_flag_); - - /** 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 - = DArena::map(ArenaConfig().with_name("arena2-reference") - .with_size(tc.gc_size_ * tc.n_gen_) - .with_store_header_flag(true)); - - /** Arena for holding report output: - * See GCObjectStore methods .report_object_types(), .report_object_ages() - **/ - DArena report_arena - = DArena::map(ArenaConfig().with_name("report-arena") - .with_size(tc.report_size_) - .with_store_header_flag(true)); - obj report_mm(&report_arena); - - /** Arena for holding error messages **/ - DArena error_arena - = DArena::map(ArenaConfig().with_name("error-arena") - .with_size(tc.error_size_) - .with_store_header_flag(true)); - obj error_mm(&error_arena); - - X1VerifyStats verify_stats; - - // object type storage will be empty unless we install a type. - GCObjectStore gcos(gcos_config, &verify_stats); + void + gcos_verify_vacant(const Testcase & tc, + const GCObjectStore & gcos) + { Generation g0{0}; - Generation g1{1}; + //Generation g1{1}; Generation gn{tc.n_gen_}; - // scaffold mock collector doing incremental collection - DMockCollector mock_gc(&gcos, g1); - auto mock_gc_visitor = mock_gc.ref(); - - REQUIRE(gcos.is_type_installed(typeseq::id()) == false); - REQUIRE(gcos.is_type_installed(typeseq::id()) == false); - - gcos_install_test_types(tc, &gcos); - gcos_verify_arena_partitioning(tc, gcos); // verify we have non-zero space! { @@ -490,60 +411,65 @@ namespace ut { REQUIRE(gcos.from_space(gi)->reserved() >= tc.gc_size_); } } + } - // allocator api - auto alloc = obj(gcos.new_space()); - - // create object(s). - // details depend on test case - - std::vector x1_v; - std::vector x2_v; - { - switch (tc.obj_graph_type_) { - case TestGraphType::selfcycle: - selfcycle_object_graph(&x1_v, - &gcos, - &x2_v, - &arena2); - break; - case TestGraphType::random: - random_object_graph(tc.n_test_obj_, - tc.n_test_assign_, - &rgen, - &x1_v, - &gcos, - &x2_v, - &arena2); - break; - } - - //x1_v.push_back(Recd(DBoolean::box(alloc, true), - // sizeof(DBoolean), - // typeseq::id())); + void + gcos_construct_ab_object_graphs(const Testcase & tc, + GCObjectStore * p_gcos, + DArena * p_arena2, + std::vector * p_x1_v, + std::vector * p_x2_v, + xoshiro256ss * p_rgen) + { + switch (tc.obj_graph_type_) { + case TestGraphType::selfcycle: + selfcycle_object_graph(p_x1_v, + p_gcos, + p_x2_v, + p_arena2); + break; + case TestGraphType::random: + random_object_graph(tc.n_test_obj_, + tc.n_test_assign_, + p_rgen, + p_x1_v, + p_gcos, + p_x2_v, + p_arena2); + break; } - log1 && log1("verify before any gcos side effects"); + //x1_v.push_back(Recd(DBoolean::box(alloc, true), + // sizeof(DBoolean), + // typeseq::id())); + } - { - // traverses stored objects, updates counters - // in verify_stats (= gco.p_verify_stats_, via ctor) - // - gcos.verify_ok(mock_gc_visitor); + void + gcos_verify_consistency(obj mock_gc_visitor, + GCObjectStore * p_gcos, + const X1VerifyStats & verify_stats) + { + // traverses stored objects, updates counters + // in verify_stats (= gco.p_verify_stats_, via ctor) + // + p_gcos->verify_ok(mock_gc_visitor); - INFO(tostr(xtag("n_gc_root", verify_stats.n_gc_root_), - xtag("n_ext", verify_stats.n_ext_), - xtag("n_from", verify_stats.n_from_), - xtag("n_to", verify_stats.n_to_), - xtag("n_fwd", verify_stats.n_fwd_), - xtag("n_no_iface", verify_stats.n_no_iface_))); + INFO(tostr(xtag("n_gc_root", verify_stats.n_gc_root_), + xtag("n_ext", verify_stats.n_ext_), + xtag("n_from", verify_stats.n_from_), + xtag("n_to", verify_stats.n_to_), + xtag("n_fwd", verify_stats.n_fwd_), + xtag("n_no_iface", verify_stats.n_no_iface_))); - REQUIRE(verify_stats.is_ok()); - } - - // someday: print the graph. Need a cycle-detecting printer + REQUIRE(verify_stats.is_ok()); + } + void + gcos_verify_ab_equivalence(const std::vector & x1_v, + const std::vector & x2_v) + { REQUIRE(x1_v.size() == x2_v.size()); + for (size_t i = 0, n = x1_v.size(); i < n; ++i) { REQUIRE(x1_v[i].alloc_z_ == x2_v[i].alloc_z_); REQUIRE(x1_v[i].tseq_ == x2_v[i].tseq_); @@ -551,7 +477,12 @@ namespace ut { REQUIRE(x1_v[i].gco_._typeseq() == x1_v[i].tseq_); REQUIRE(x2_v[i].gco_._typeseq() == x2_v[i].tseq_); } + } + void + gcos_verify_allocinfo(const GCObjectStore & gcos, + const std::vector & x1_v) + { // gcos can reveal info about allocs for (size_t i = 0, n = x1_v.size(); i < n; ++i) { @@ -570,6 +501,15 @@ namespace ut { REQUIRE(gcos.header2tseq(obj_info.header()) == obj_info.tseq()); REQUIRE(gcos.is_forwarding_header(obj_info.header()) == false); } + } + + void + gcos_verify_gen0_only_allocated(const Testcase & tc, + const GCObjectStore & gcos, + const std::vector & x1_v) + { + Generation g0{0}; + Generation gn{tc.n_gen_}; // new objects appear in to-space for generation 0 for (Generation gi = g0; gi < gn; ++gi) { @@ -582,35 +522,128 @@ namespace ut { REQUIRE(gcos.from_space(gi)->allocated() == 0); } + } - // swap_roles [but only for generation < g1, i.e. g0 + void + gcos_verify_gen0_fromspace_only_allocated(const Testcase & tc, + const GCObjectStore & gcos, + const std::vector & x1_v) + { + for (size_t i = 0, n = x1_v.size(); i < n; ++i) { + const auto & x1 = x1_v.at(i); + + REQUIRE(gcos.contains(Role::from_space(), x1.gco_.data())); + REQUIRE(gcos.contains_allocated(Role::from_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()); + + Generation g0{0}; + Generation gn{tc.n_gen_}; + + for (Generation gi = g0; gi < gn; ++gi) { + INFO(tostr(xtag("gi", gi))); + + if (gi == 0) + REQUIRE(gcos.from_space(gi)->allocated() > 0); + else + REQUIRE(gcos.from_space(gi)->allocated() == 0); + + REQUIRE(gcos.to_space(gi)->allocated() == 0); + } + } + } + + void + gcos_verify_forwarding(const GCObjectStore & gcos, + const Recd & x1, + obj x1_gco) + { + REQUIRE(gcos.contains_allocated(Role::from_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.is_forwarding_tseq()); + } + + void + gcos_verify_forwarding_destination(const GCObjectStore & gcos, + const Recd & x1, + obj 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 x1p_gco, + obj x2_gco) + { + // written out polymorphic comparison + + // match DBoolean.. + bool match_attempted = false; { - gcos.swap_roles(g1); + auto x1p_b = obj::from(x1p_gco); + auto x2_b = obj::from(x2_gco); - for (size_t i = 0, n = x1_v.size(); i < n; ++i) { - const auto & x1 = x1_v.at(i); + if (x1p_b && x2_b) { + match_attempted = true; - REQUIRE(gcos.contains(Role::from_space(), x1.gco_.data())); - REQUIRE(gcos.contains_allocated(Role::from_space(), x1.gco_.data())); - AllocInfo obj_info = gcos.alloc_info((std::byte *)x1.gco_.data()); - REQUIRE(obj_info.size() >= x1.alloc_z_); + REQUIRE(x1p_b->value() == x2_b->value()); + } + } - REQUIRE(obj_info.payload().first == (std::byte *)x1.gco_.data()); - REQUIRE(obj_info.tseq() == x1.tseq_.seqno()); + // match DList.. + { + auto x1p_b = obj::from(x1p_gco); + auto x2_b = obj::from(x2_gco); - for (Generation gi = g0; gi < gn; ++gi) { - INFO(tostr(xtag("gi", gi))); + if (x1p_b && x2_b) { + match_attempted = true; - if (gi == 0) - REQUIRE(gcos.from_space(gi)->allocated() > 0); - else - REQUIRE(gcos.from_space(gi)->allocated() == 0); + // TODO: we could figure out the index in {x1_v[], x2_v[]} + // of x*_b {head, rest} respectively, + // and verify they're consistent. - REQUIRE(gcos.to_space(gi)->allocated() == 0); + 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, + obj mock_gc_visitor, + const std::vector & x1_v, + const std::vector & 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 @@ -619,8 +652,8 @@ namespace ut { const auto & x1 = x1_v.at(i); const auto & x2 = x2_v.at(i); - log1 && log1("moving roots"); - log1 && log1(xtag("i", 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_))); @@ -650,99 +683,40 @@ namespace ut { // it's possible that x1.gco_ is already a forwarding pointer // before we call deep_move_root(). - AGCObject * x1p_iface = gcos.lookup_type(x1.tseq_); + AGCObject * x1p_iface = p_gcos->lookup_type(x1.tseq_); REQUIRE(x1p_iface); + // snapshot root before moving obj x1_gco = x1.gco_; // modifies x1.gco_ in place - auto x1p_data = gcos.deep_move_root(mock_gc_visitor, - x1p_iface, (void **)&(x1.gco_.data_), - g1); + auto x1p_data = p_gcos->deep_move_root(mock_gc_visitor, + x1p_iface, (void **)&(x1.gco_.data_), + g1); REQUIRE(x1p_data); REQUIRE(x1p_data == x1.gco_.data_); obj x1p_gco(x1p_iface, x1p_data); - // obj (x1_gco) now forwarding pointer to x1p_gco = x1.gco_ - { - REQUIRE(gcos.contains_allocated(Role::from_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.is_forwarding_tseq()); - } + // obj (x1_gco) now forwarding pointer (to x1p_gco = x1.gco_) + gcos_verify_forwarding(*p_gcos, x1, x1_gco); // obj1p same contents as original obj - { - 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())); + gcos_verify_forwarding_destination(*p_gcos, x1, x1p_gco); // x1p_gco must look like x2.gco - REQUIRE(x1p_gco._typeseq() == x2.gco_._typeseq()); - // written out polymorphic comparison - { - // match DBoolean.. - bool match_attempted = false; - { - auto x1p_b = obj::from(x1p_gco); - auto x2_b = obj::from(x2.gco_); - - if (x1p_b && x2_b) { - match_attempted = true; - - REQUIRE(x1p_b->value() == x2_b->value()); - } - } - - // match DList.. - { - auto x1p_b = obj::from(x1p_gco); - auto x2_b = obj::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); - } - + 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 - = gcos.deep_move_root(mock_gc_visitor, - x1.gco_.iface(), - (void **)&(x1.gco_.data_), - g1); + = p_gcos->deep_move_root(mock_gc_visitor, + x1.gco_.iface(), + (void **)&(x1.gco_.data_), + g1); // control here under normal GC use // would represent a configuration fail @@ -750,6 +724,134 @@ namespace ut { REQUIRE(x1p_data == nullptr); } } + } + + // fixture for GCObjectStore-1 test + class GcosFixture { + public: + explicit GcosFixture(const Testcase & tc); + + 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_; + }; + + 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))} + {} + + } + + 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); + + obj report_mm(&fixture.report_arena_); + obj error_mm(&fixture.error_arena_); + + X1VerifyStats verify_stats; + + // object type storage will be empty unless we install a type. + GCObjectStore gcos(fixture.gcos_config_, &verify_stats); + + Generation g0{0}; + Generation g1{1}; + Generation gn{tc.n_gen_}; + + // scaffold mock collector doing incremental collection + DMockCollector mock_gc(&gcos, g1); + auto mock_gc_visitor = mock_gc.ref(); + + REQUIRE(gcos.is_type_installed(typeseq::id()) == false); + REQUIRE(gcos.is_type_installed(typeseq::id()) == false); + + gcos_install_test_types(tc, &gcos); + gcos_verify_arena_partitioning(tc, gcos); + gcos_verify_vacant(tc, gcos); + + // allocator api + auto alloc = obj(gcos.new_space()); + + // create object(s). + // details depend on test case + + std::vector x1_v; + std::vector x2_v; + + gcos_construct_ab_object_graphs(tc, &gcos, &fixture.arena2_, &x1_v, &x2_v, &rgen); + + log1 && log1("verify before any gcos side effects"); + + gcos_verify_consistency(mock_gc_visitor, + &gcos, + verify_stats); + + // someday: print the graph. Need a cycle-detecting printer + + gcos_verify_ab_equivalence(x1_v, x2_v); + gcos_verify_allocinfo(gcos, x1_v); + gcos_verify_gen0_only_allocated(tc, gcos, x1_v); + + // swap_roles [but only for generation < g1, i.e. g0 + gcos.swap_roles(g1); + + gcos_verify_gen0_fromspace_only_allocated(tc, gcos, x1_v); + + gcos_move_roots_and_verify(tc, &gcos, mock_gc_visitor, x1_v, x2_v, tc.debug_flag_); // Things to test: // - deep_move_interior() // used from MutationLogStore