From c4eb58f6ffa4a8374f22b5897af1f9f9a9564a93 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 11 Apr 2026 16:51:52 -0400 Subject: [PATCH] xo-gc: bugfixes for GCObjectStore, unit test exapnded In particular: drop casual assignment to DList.rest_, will break acyclic assumption of DList.size() --- include/xo/gc/GCObjectStore.hpp | 45 +++- src/gc/DX1Collector.cpp | 41 +--- src/gc/GCObjectStore.cpp | 89 +++++--- utest/DMockCollector.cpp | 15 +- utest/GCObjectStore.test.cpp | 368 ++++++++++++++++++++++---------- 5 files changed, 366 insertions(+), 192 deletions(-) diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index e1acf0cb..ba925f9b 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -16,6 +16,11 @@ namespace xo { class X1VerifyStats; /** @brief container to hold gc-aware objects for X1 collector + * + * Note: X1VerifyStats are in DX1Collector. + * They need to be there, since also interact with MutationLogStore. + * This is reason for DX1Collector to invoke .verify_aux() + * so it can supply X1VerifyStats location **/ class GCObjectStore { public: @@ -28,16 +33,21 @@ namespace xo { using typeseq = xo::reflect::typeseq; public: - explicit GCObjectStore(const GCObjectStoreConfig & cfg); + explicit GCObjectStore(const GCObjectStoreConfig & cfg, X1VerifyStats * p_verify_stats); const GCObjectStoreConfig & config() const noexcept { return config_; } const ObjectTypeTable * get_object_types() const noexcept { return &object_types_; } const DArena * get_space(Role r, Generation g) const noexcept { return space_[r][g]; } + const DArena * from_space(Generation g) const noexcept { return this->get_space(Role::from_space(), g); } + const DArena * to_space(Generation g) const noexcept { return this->get_space(Role::to_space(), g); } + const DArena * new_space() const noexcept { return this->get_space(Role::to_space(), Generation{0}); } + DArena * get_space(Role r, Generation g) noexcept { return space_[r][g]; } - DArena * from_space(Generation g) noexcept { return get_space(Role::from_space(), g); } - DArena * to_space(Generation g) noexcept { return get_space(Role::to_space(), g); } - DArena * new_space() noexcept { return to_space(Generation{0}); } + DArena * from_space(Generation g) noexcept { return this->get_space(Role::from_space(), g); } + DArena * to_space(Generation g) noexcept { return this->get_space(Role::to_space(), g); } + DArena * new_space() noexcept { return this->get_space(Role::to_space(), Generation{0}); } + X1VerifyStats * verify_stats() noexcept { return p_verify_stats_; } /** true iff type with id @p tseq has known metadata * (i.e. has appeared in preceding call to install_type @@ -124,8 +134,7 @@ namespace xo { * to call AGCObject visitor method (forward_children()) on each * object stored here. **/ - void verify_ok(obj gc, - X1VerifyStats * p_verify_stats) noexcept; + void verify_ok(obj gc) noexcept; /** Register object type with this collector. * Provides shallow copy and pointer forwarding for instances of this @@ -142,11 +151,14 @@ namespace xo { /** move subgraph at @p root to to-space on behalf of collector @p gc * Special behavior relative to @ref _deep_move_interior : * If @p root is not in gc-space, visit immediate children and move them in place (!). - + * + * @return new address for @p from_src + * * Require: runstate_.is_running() **/ void * deep_move_root(obj gc, - obj from_src, + const AGCObject * root_iface, + void ** root_data, Generation upto); /** move interior subgraph at @p from_src to to-space. @@ -158,6 +170,16 @@ namespace xo { void * from_src, Generation upto); +#ifdef NOT_YET + /** Target for GCObjectVisitor facet + * During gc phase (@p reason is 'forward') + * 1. evacuate object at @p *lhs_data to to-space. + * 2. replace @p *lhs_data with forwarding pointer + * to new location. + **/ + void visit_child(VisitReason reason, AGCObject * lhs_iface, void ** lhs_data); +#endif + /** Evacuate object at @p *lhs_data to to-space, during collection phase * acting on generations g in [0 ,.., upto). * Need @p gc to pass to invoke AGCObject methods shallow_copy() and @@ -172,12 +194,11 @@ namespace xo { /** categorize fop {@p lhs_iface, @p lhs_data} * based on location of @p lhs_data. - * Update @p *p_verify_stats based on the result: + * Update @ref p_verify_stats_ based on the result: * increment exactly one of {n_from_, n_to_, n_ext_} **/ void verify_aux(AGCObject * lhs_iface, - void * lhs_data, - X1VerifyStats * p_verify_stats); + void * lhs_data); /** Cleanup at the end of a gc cycle. * Reset from-space @@ -248,6 +269,8 @@ namespace xo { **/ std::array space_[c_n_role]; + /** dedicated counters. updated by .verify_aux() **/ + X1VerifyStats * p_verify_stats_ = nullptr; }; } /*namespace mm*/ diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index a358e68d..cf079781 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -69,7 +69,7 @@ namespace xo { DX1Collector::DX1Collector(const X1CollectorConfig & cfg) : config_{cfg}, - gco_store_{cfg.gco_store_config()}, + gco_store_{cfg.gco_store_config(), &verify_stats_}, mlog_store_{cfg.mlog_config(), &gco_store_} { assert(config_.arena_config_.header_.size_bits_ + @@ -417,7 +417,7 @@ namespace xo { } // 3. scan to-space for each generation - gco_store_.verify_ok(this->ref(), &(this->verify_stats_)); + gco_store_.verify_ok(this->ref()); // 4. scan mutation logs mlog_store_.verify_ok(&gco_store_, @@ -576,7 +576,9 @@ namespace xo { xtag("slot.root()", slot.root()), xtag("slot.root()->data_", slot.root()->data_)); - void * root_to = gco_store_.deep_move_root(this->ref(), *slot.root(), upto); + void * root_to = gco_store_.deep_move_root(this->ref(), + slot.root()->iface(), + (void **)&(slot.root()->data_), upto); slot.root()->reset_opaque(root_to); @@ -604,7 +606,7 @@ namespace xo { } case VisitReason::code::verify: // called during verify_ok - gco_store_.verify_aux(lhs_iface, *lhs_data, &verify_stats_); + gco_store_.verify_aux(lhs_iface, *lhs_data); break; default: // should be unreachable @@ -612,37 +614,6 @@ namespace xo { } } -#ifdef OBSOLETE - void - DX1Collector::_verify_aux(AGCObject * iface, void * data) - { - //scope log(XO_DEBUG(config_.debug_flag_), xtag("data", data)); - - (void)iface; - (void)data; - - Generation g1 = this->generation_of(Role::to_space(), data); - - if (g1.is_sentinel()) { - assert(this->contains(Role::to_space(), data) == false); - - Generation g2 = this->generation_of(Role::from_space(), data); - - if (!g2.is_sentinel()) { - // verify failure - live pointer still refers to from-space - - ++(verify_stats_.n_from_); - } else { - ++(verify_stats_.n_ext_); - } - } else { - assert(this->contains(Role::to_space(), data)); - - ++(verify_stats_.n_to_); - } - } -#endif - auto DX1Collector::alloc(typeseq t, size_type z) noexcept -> value_type { diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index e20f92ab..b76801d2 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -31,8 +32,9 @@ namespace xo { namespace mm { - GCObjectStore::GCObjectStore(const GCObjectStoreConfig & cfg) - : config_{cfg} + GCObjectStore::GCObjectStore(const GCObjectStoreConfig & cfg, + X1VerifyStats * p_verify_stats) + : config_{cfg}, p_verify_stats_{p_verify_stats} { assert(config_.arena_config_.header_.size_bits_ + config_.arena_config_.header_.age_bits_ + @@ -473,6 +475,36 @@ namespace xo { return (g < upto); } +#ifdef NOT_YET + void + GCObjectStore::visit_child(VisitReason reason, + AGCObject * lhs_iface, + void ** lhs_data) + { + // MAYBE: adapter distinct from DX1Collector that supports GCObjectVisitor facet, + // calls DX1Collector::_verify_aux() + + switch (reason.code()) { + case VisitReason::code::forward: + { + Generation upto = runstate_.gc_upto(); + + // called during collection phase + this->forward_inplace_aux + (this->ref(), lhs_iface, lhs_data, upto); + break; + } + case VisitReason::code::verify: + // called during verify_ok + gco_store_.verify_aux(lhs_iface, *lhs_data); + break; + default: + // should be unreachable + assert(false); + } + } +#endif + void GCObjectStore::forward_inplace_aux(obj gc, AGCObject * lhs_iface, @@ -558,7 +590,9 @@ namespace xo { * * This is guaranteed anyway, by alignment rules */ - assert(alloc_z >= sizeof(uintptr_t)); + if (alloc_z < sizeof(uintptr_t)) { + assert(false); + } if (this->is_forwarding_header(alloc_hdr)) { /* *lhs_data already refers to a forwarding pointer */ @@ -661,10 +695,9 @@ namespace xo { void GCObjectStore::verify_aux(AGCObject * iface, - void * data, - X1VerifyStats * p_verify_stats) + void * data) { - //scope log(XO_DEBUG(config_.debug_flag_), xtag("data", data)); + scope log(XO_DEBUG(config_.debug_flag_)); (void)iface; @@ -678,14 +711,16 @@ namespace xo { if (!g2.is_sentinel()) { // verify failure - live pointer still refers to from-space - ++(p_verify_stats->n_from_); + print_backtrace_dwarf(true /*demangle*/); + + ++(p_verify_stats_->n_from_); } else { - ++(p_verify_stats->n_ext_); + ++(p_verify_stats_->n_ext_); } } else { assert(this->contains(Role::to_space(), data)); - ++(p_verify_stats->n_to_); + ++(p_verify_stats_->n_to_); } } @@ -733,8 +768,7 @@ namespace xo { } void - GCObjectStore::verify_ok(obj gc, - X1VerifyStats * p_verify_stats) noexcept + GCObjectStore::verify_ok(obj gc) noexcept { for (Generation g(0); g < config_.n_generation_; ++g) { const DArena * space = this->get_space(Role::to_space(), g); @@ -742,7 +776,7 @@ namespace xo { for (const AllocInfo & info : *space) { if (info.is_forwarding_tseq()) { - ++(p_verify_stats->n_fwd_); + ++(p_verify_stats_->n_fwd_); } else { typeseq tseq(info.tseq()); @@ -755,15 +789,9 @@ namespace xo { // assembled fop for gc-aware object obj gco(iface, const_cast(data)); - // forward_children is hijacked here to verify - // child pointer validity. - // - // Nested control reenters - // X1Collector::forward_inplace() -> _verify_aux() - // - gco.visit_gco_children(VisitReason::forward(), gc); + gco.visit_gco_children(VisitReason::verify(), gc); } else { - ++(p_verify_stats->n_no_iface_); + ++(p_verify_stats_->n_no_iface_); continue; } } @@ -803,7 +831,8 @@ namespace xo { void * GCObjectStore::deep_move_root(obj gc, - obj from_src, + const AGCObject * root_iface, + void ** root_data, Generation upto) { // NOTE: @@ -815,22 +844,23 @@ namespace xo { scope log(XO_DEBUG(config_.debug_flag_)); - if (!from_src) + if (!root_data || !*root_data) return nullptr; - bool src_in_from_space = this->contains(Role::from_space(), - from_src.data()); + bool src_in_from_space = this->contains(Role::from_space(), *root_data); if (src_in_from_space) { - return this->_deep_move_gc_owned(gc, from_src.data(), upto); + *root_data = this->_deep_move_gc_owned(gc, *root_data, upto); } else { // we aren't moving from_src, it's not gc-owned. - // However weare moving all its gc-owned children + // However we are moving all its gc-owned children GCMoveCheckpoint gray_lo_v = this->snap_move_checkpoint(upto); - from_src.visit_gco_children(VisitReason::forward(), gc); + auto root = obj(root_iface, *root_data); + + root.visit_gco_children(VisitReason::forward(), gc); // For each generation g: // traverse objects newer than gray_lo_v[g], to make sure children @@ -840,8 +870,11 @@ namespace xo { // this->_forward_children_until_fixpoint(gc, upto, gray_lo_v); - return from_src.data(); + // reminder: *root_data preserved + } + + return *root_data; } void * diff --git a/utest/DMockCollector.cpp b/utest/DMockCollector.cpp index 6b6e9204..833d32b2 100644 --- a/utest/DMockCollector.cpp +++ b/utest/DMockCollector.cpp @@ -23,10 +23,17 @@ namespace xo { void DMockCollector::visit_child(VisitReason reason, AGCObject * lhs_iface, void ** lhs_data) { - (void)reason; - - p_gco_store_->forward_inplace_aux - (this->ref(), lhs_iface, lhs_data, upto_); + switch (reason.code()) { + case VisitReason::code::forward: + p_gco_store_->forward_inplace_aux + (this->ref(), lhs_iface, lhs_data, upto_); + break; + case VisitReason::code::verify: + p_gco_store_->verify_aux(lhs_iface, *lhs_data); + break; + default: + assert(false); + } } std::byte * diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index 198c0219..fa66d812 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -4,6 +4,7 @@ **/ #include +#include #include "MockCollector.hpp" #include @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +30,7 @@ namespace ut { using xo::mm::DMockCollector; using xo::mm::GCObjectStoreConfig; using xo::mm::GCObjectStore; + using xo::mm::X1VerifyStats; using xo::mm::AGCObject; using xo::mm::AGCObjectVisitor; using xo::mm::Generation; @@ -39,6 +42,7 @@ namespace ut { using xo::mm::AllocInfo; using xo::mm::c_max_generation; using xo::facet::obj; + using xo::facet::TypeRegistry; using xo::facet::typeseq; using xo::facet::impl_for; using xo::rng::xoshiro256ss; @@ -50,14 +54,23 @@ namespace ut { using std::uint32_t; namespace { + enum class TestGraphType { + /* list cell pointing to itself */ + selfcycle, + /* random object graph */ + random, + }; + 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_test_obj, - uint32_t n_test_assign) + uint32_t n_test_assign, + bool debug_flag) : n_gen_{n_gen}, n_survive_{n_survive}, gc_size_{gc_z}, @@ -65,8 +78,10 @@ namespace ut { do_type_registration_{do_type_registration}, report_size_{report_z}, error_size_{error_z}, + obj_graph_type_{obj_graph_type}, n_test_obj_{n_test_obj}, - n_test_assign_{n_test_assign} + n_test_assign_{n_test_assign}, + debug_flag_{debug_flag} {} /** number of generations in gco store **/ @@ -89,12 +104,19 @@ namespace ut { 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 cells in random object graph **/ uint32_t n_test_obj_ = 0; /** #of random assignments to attempt (these may create cycles, for example) **/ uint32_t n_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; @@ -102,12 +124,14 @@ namespace ut { // 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_obj, n_test_assign **/ - Testcase(2, 4, 16 * 1024, 8 * 128, false, c_report_z1, c_error_z1, 0, 0), - Testcase(2, 4, 16 * 1024, 8 * 128, true, c_report_z1, c_error_z1, 1, 0), - Testcase(2, 4, 16 * 1024, 8 * 128, true, c_report_z1, c_error_z1, 2, 0), - Testcase(2, 4, 16 * 1024, 8 * 128, true, c_report_z1, c_error_z1, 4, 0), - Testcase(2, 4, 16 * 1024, 8 * 128, true, c_report_z1, c_error_z1, 8, 4), - Testcase(2, 4, 16 * 1024, 8 * 128, true, c_report_z1, c_error_z1, 16, 7), + Testcase(2, 4, 16 * 1024, 8 * 128, false, c_report_z1, c_error_z1, c_random, 0, 0, false), + Testcase(2, 4, 16 * 1024, 8 * 128, true, c_report_z1, c_error_z1, c_selfcycle, 1, 0, false), + Testcase(2, 4, 16 * 1024, 8 * 128, true, c_report_z1, c_error_z1, c_random, 1, 0, false), + Testcase(2, 4, 16 * 1024, 8 * 128, true, c_report_z1, c_error_z1, c_random, 2, 13, false), + Testcase(2, 4, 16 * 1024, 8 * 128, true, c_report_z1, c_error_z1, c_random, 2, 25, false), + Testcase(2, 4, 16 * 1024, 8 * 128, true, c_report_z1, c_error_z1, c_random, 5, 0, false), + Testcase(2, 4, 16 * 1024, 8 * 128, true, c_report_z1, c_error_z1, c_random, 4, 2, false), + Testcase(2, 4, 16 * 1024, 8 * 128, true, c_report_z1, c_error_z1, c_random, 50, 25, false), }; /** record capturing some stats for a (randomly created) gc-aware object **/ @@ -123,6 +147,35 @@ namespace ut { typeseq tseq_; }; + /** Create two isomorphic object graphs. + * Each graph comprises a single DList cell + * that points to itself + **/ + void + selfcycle_object_graph(std::vector * p_v1, + GCObjectStore * p_gcos, + std::vector * p_v2, + DArena * arena2) + { + auto alloc1 = obj(p_gcos->new_space()); + auto alloc2 = obj(arena2); + + auto t1 = DBoolean::box(alloc1, true); + auto t2 = DBoolean::box(alloc2, true); + + auto l1 = ListOps::cons(alloc1, t1, ListOps::nil()); + auto l2 = ListOps::cons(alloc2, t2, ListOps::nil()); + + // shortcut. Can get away with skipping mm_do_assign(), + // because we know lhs of assignment is in the youngest generation + + l1->head_ = l1; // l1->assign_head(gc, l1); // need collector facet + l2->head_ = l2; // l2->assign_head(gc, l2); // need collector facet + + p_v1->push_back(Recd(l1, sizeof(DList), typeseq::id())); + p_v2->push_back(Recd(l2, sizeof(DList), typeseq::id())); + } + /** Create two isomorphic random object graphs containing @p n_obj nodes * Using a few basic data types from xo-object2 * DBoolean @@ -147,6 +200,8 @@ namespace ut { std::vector * p_v2, DArena * p_arena2) { + scope log(XO_DEBUG(true)); + if (n_obj == 0) return; @@ -229,62 +284,136 @@ namespace ut { for (uint32_t j = 0; j < n_assign; ++j) { // choose an object at random - uint32_t sample = (*p_rgen)() % n_obj; + uint32_t lhs_ix = (*p_rgen)() % n_obj; - assert(sample < p_v->size()); + assert(lhs_ix < p_v->size()); // is it a list cell? - auto xj = obj::from((*p_v)[sample].gco_); - auto xj2 = obj::from((*p_v2)[sample].gco_); + auto xj1 = obj::from((*p_v)[lhs_ix].gco_); + auto xj2 = obj::from((*p_v2)[lhs_ix].gco_); - if (xj) { + if (xj1) { assert(xj2); // flip a coin -- try modifying one of {car, cdr} - sample = (*p_rgen)() % 100; + uint32_t sample = (*p_rgen)() % 100; if (sample < 50) { // modify head. skip usual gc write-barrier stuff - sample = (*p_rgen)() % n_obj; - // rhs could even be xj itself - xj->head_ = (*p_v)[sample].gco_; - xj2->head_ = (*p_v2)[sample].gco_; - } else { - // modify rest, maybe. + uint32_t rhs_ix = (*p_rgen)() % n_obj; - sample = (*p_rgen)() % n_obj; - auto rhs = obj::from((*p_v)[sample].gco_); - auto rhs2 = obj::from((*p_v2)[sample].gco_); + auto rhs1 = (*p_v)[rhs_ix].gco_; + auto rhs2 = (*p_v2)[rhs_ix].gco_; - if (rhs) { - // modify rest. skip usual gc write-barrier stuff - - assert(rhs2); - - xj->rest_ = rhs.data(); - xj2->rest_ = rhs2.data(); + if (log) { + log("replacing edge in random object graph"); + log(xtag("n-obj", n_obj)); + log(xtag("lhs-ix", lhs_ix)); + log(xtag("rhs-ix", rhs_ix)); + log(xtag("rhs.tname", TypeRegistry::id2name(rhs1._typeseq()))); } + + // rhs1 could even be xj1 itself (in which case rhs2 is xj2) + xj1->head_ = rhs1; + xj2->head_ = rhs2; + } else { + // don't modify DList.rest_, risks losing acyclic propertly. + // GCObjectStore handles this, but DList.size() assumes + // list is acyclic } } } } /*random_object_graph*/ } /*namespace*/ + namespace { + // aux functions specific to GCObjectStore-1 unit test below + + void + gcos_install_test_types(const Testcase & tc, + GCObjectStore * p_gcos) + { + // verify that GCOS recongnizes as registered, + // the types we intend using for unit test + + if (tc.do_type_registration_) { + { + REQUIRE(p_gcos->install_type(impl_for())); + REQUIRE(p_gcos->is_type_installed(typeseq::id())); + } + { + REQUIRE(p_gcos->install_type(impl_for())); + REQUIRE(p_gcos->is_type_installed(typeseq::id())); + } + } + } + + void + gcos_verify_arena_partitioning(const Testcase & tc, + const GCObjectStore & gcos) + { + Generation g0{0}; + Generation g1{1}; + Generation gn{tc.n_gen_}; + + // verify basic arena partitioning + sizing + + REQUIRE(g0 != g1); + REQUIRE(gcos.new_space()); + REQUIRE(gcos.new_space() == gcos.get_space(Role::to_space(), g0)); + REQUIRE(gcos.new_space()->reserved() >= tc.gc_size_); + REQUIRE(gcos.from_space(g0)); + + for (Generation gi = g1; gi < tc.n_gen_; ++gi) { + // all configured generations exist + REQUIRE(gcos.to_space(gi)); + REQUIRE(gcos.from_space(gi)); + + // to- and from- space are distinct + REQUIRE(gcos.to_space(gi) != gcos.from_space(gi)); + + // arenas for different generations are distinct + for (Generation gj = g0; gj < gi; ++gj) { + REQUIRE(gcos.to_space(gi) != gcos.to_space(gj)); + REQUIRE(gcos.from_space(gi) != gcos.to_space(gj)); + + REQUIRE(gcos.to_space(gi) != gcos.from_space(gj)); + REQUIRE(gcos.from_space(gi) != gcos.to_space(gj)); + } + } + + // generations that weren't requested, don't exist + if (gn < c_max_generation) { + REQUIRE(!gcos.to_space(gn)); + REQUIRE(!gcos.from_space(gn)); + } + } + } + TEST_CASE("GCObjectStore-1", "[GCObjectStore]") { constexpr bool c_debug_flag = true; - scope log(XO_DEBUG(c_debug_flag), "GCObjectStore test"); + scope log0(XO_DEBUG(c_debug_flag), "GCObjectStore test"); std::uint64_t seed = 12168164826603821466ul; //random_seed(&seed); - log && log(xtag("seed", seed)); - - auto rgen = xoshiro256ss(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 **/ @@ -298,7 +427,7 @@ namespace ut { tc.n_gen_, tc.n_survive_, tc.object_type_z_, - c_debug_flag); + tc.debug_flag_); /** Parallel arena for reference * @@ -330,64 +459,24 @@ namespace ut { .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); - - // scaffold mock collector doing incremental collection - DMockCollector mock_gc(&gcos, Generation{0}); - auto mock_gc_visitor = mock_gc.ref(); - - REQUIRE(gcos.is_type_installed(typeseq::id()) == false); - REQUIRE(gcos.is_type_installed(typeseq::id()) == false); + GCObjectStore gcos(gcos_config, &verify_stats); Generation g0{0}; Generation g1{1}; Generation gn{tc.n_gen_}; - // install gc-aware types that we intend using in unit test - if (tc.do_type_registration_) { - { - REQUIRE(gcos.install_type(impl_for())); - REQUIRE(gcos.is_type_installed(typeseq::id())); - } - { - REQUIRE(gcos.install_type(impl_for())); - REQUIRE(gcos.is_type_installed(typeseq::id())); - } - } + // scaffold mock collector doing incremental collection + DMockCollector mock_gc(&gcos, g1); + auto mock_gc_visitor = mock_gc.ref(); - // verify basic arena partitioning + sizing - { - REQUIRE(g0 != g1); - REQUIRE(gcos.new_space()); - REQUIRE(gcos.new_space() == gcos.to_space(g0)); - REQUIRE(gcos.new_space()->reserved() >= tc.gc_size_); - REQUIRE(gcos.from_space(g0)); + REQUIRE(gcos.is_type_installed(typeseq::id()) == false); + REQUIRE(gcos.is_type_installed(typeseq::id()) == false); - for (Generation gi = g1; gi < tc.n_gen_; ++gi) { - // all configured generations exist - REQUIRE(gcos.to_space(gi)); - REQUIRE(gcos.from_space(gi)); - - // to- and from- space are distinct - REQUIRE(gcos.to_space(gi) != gcos.from_space(gi)); - - // arenas for different generations are distinct - for (Generation gj = g0; gj < gi; ++gj) { - REQUIRE(gcos.to_space(gi) != gcos.to_space(gj)); - REQUIRE(gcos.from_space(gi) != gcos.to_space(gj)); - - REQUIRE(gcos.to_space(gi) != gcos.from_space(gj)); - REQUIRE(gcos.from_space(gi) != gcos.to_space(gj)); - } - } - - // generations that weren't requested, don't exist - if (gn < c_max_generation) { - REQUIRE(!gcos.to_space(gn)); - REQUIRE(!gcos.from_space(gn)); - } - } + gcos_install_test_types(tc, &gcos); + gcos_verify_arena_partitioning(tc, gcos); // verify we have non-zero space! { @@ -411,19 +500,47 @@ namespace ut { std::vector x1_v; std::vector x2_v; { - random_object_graph(tc.n_test_obj_, - tc.n_test_assign_, - &rgen, - &x1_v, - &gcos, - &x2_v, - &arena2); + 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())); } + log1 && log1("verify before any gcos side effects"); + + { + // traverses stored objects, updates counters + // in verify_stats (= gco.p_verify_stats_, via ctor) + // + 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_))); + + REQUIRE(verify_stats.is_ok()); + } + // someday: print the graph. Need a cycle-detecting printer REQUIRE(x1_v.size() == x2_v.size()); @@ -502,7 +619,11 @@ namespace ut { const auto & x1 = x1_v.at(i); const auto & x2 = x2_v.at(i); - INFO(tostr(xtag("i", i), xtag("n", n), xtag("x1.tseq_", x1.tseq_))); + log1 && log1("moving roots"); + log1 && log1(xtag("i", i), + xtag("n", n), + xtag("x1.tseq_", x1.tseq_), + xtag("x1.tname", TypeRegistry::id2name(x1.tseq_))); if (tc.do_type_registration_) { @@ -532,18 +653,24 @@ namespace ut { AGCObject * x1p_iface = gcos.lookup_type(x1.tseq_); REQUIRE(x1p_iface); - auto x1p_data = gcos.deep_move_root(mock_gc_visitor, x1.gco_, g1); + 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); REQUIRE(x1p_data); + REQUIRE(x1p_data == x1.gco_.data_); obj x1p_gco(x1p_iface, x1p_data); - // obj has been replaced by forwarding pointer to obj2 + // 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(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.payload().first == (std::byte *)x1_gco.data()); REQUIRE(obj_info.is_forwarding_tseq()); } @@ -612,7 +739,10 @@ namespace ut { // but will fail since type isn't registered auto x1p_data - = gcos.deep_move_root(mock_gc_visitor, x1.gco_, g1); + = 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 @@ -625,22 +755,30 @@ namespace ut { // - deep_move_interior() // used from MutationLogStore // - forward_inplace_aux() // used from DX1Collector.visit_child - // - report_object_types - // - report_object_ages() + { + bool sanitize_flag = true; - bool sanitize_flag = true; + // swaps to- and from- spaces again + // Now from-space will be empty, all live objects in to-space - // swaps to- and from- spaces again - // Now from-space will be empty, all live objects in to-space + gcos.cleanup_phase(g1, sanitize_flag); + } - gcos.cleanup_phase(g1, sanitize_flag); + { + // traverses stored objects, updates counters + // in verify_stats (= gco.p_verify_stats_, via ctor) + // + gcos.verify_ok(mock_gc_visitor); -#ifdef NOT_YET - gcos.verify_ok(xxx objectvisitor, - xxx &verify_stats); -#endif + 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_))); - // - verify_ok + REQUIRE(verify_stats.is_ok()); + } { obj report_gco; @@ -662,8 +800,8 @@ namespace ut { bool ok = gcos.report_object_ages(report_mm, error_mm, &report_gco); if (!ok) { - log.retroactively_enable(); - log && log(xtag("error", report_mm.last_error())); + log1.retroactively_enable(); + log1 && log1(xtag("error", report_mm.last_error())); } REQUIRE(ok); @@ -671,11 +809,13 @@ namespace ut { // TODO: print report_gco, verify output + // discard report + report_gco.reset(); report_mm->clear(); } - } - } + } /* loop over test cases */ + } /* TEST_CASE(GCObjectStore-1) */ } /*namespace ut*/