diff --git a/xo-gc/include/xo/gc/DGCObjectStoreVisitor.hpp b/xo-gc/include/xo/gc/DGCObjectStoreVisitor.hpp index 7ba65dad..4112ed40 100644 --- a/xo-gc/include/xo/gc/DGCObjectStoreVisitor.hpp +++ b/xo-gc/include/xo/gc/DGCObjectStoreVisitor.hpp @@ -26,7 +26,7 @@ namespace xo { class DGCObjectStoreVisitor { public: DGCObjectStoreVisitor(GCObjectStore * gcos, Generation upto); - + template obj ref() { return obj(this); } @@ -46,4 +46,4 @@ namespace xo { } /*namespace mm*/ } /*namespace xo*/ -/* end DGCObjectVisitor.hpp */ +/* end DGCObjectStoreVisitor.hpp */ diff --git a/xo-gc/include/xo/gc/GCObjectStore.hpp b/xo-gc/include/xo/gc/GCObjectStore.hpp index c318fcb2..f816e05b 100644 --- a/xo-gc/include/xo/gc/GCObjectStore.hpp +++ b/xo-gc/include/xo/gc/GCObjectStore.hpp @@ -60,6 +60,18 @@ namespace xo { **/ AGCObject * lookup_type(typeseq tseq) const noexcept; + /** report allocated memory for role r, generation g + **/ + size_type allocated(Generation g, Role r) const noexcept; + + /** report committed memory for role r, generation g + **/ + size_type committed(Generation g, Role r) const noexcept; + + /** report reserved memory for role r, generation g + **/ + size_type reserved(Generation g, Role r) const noexcept; + /** generation to which pointer @p addr belongs, given Role @p r; * sentinel if not found in this collector **/ diff --git a/xo-gc/include/xo/gc/MutationLogEntry.hpp b/xo-gc/include/xo/gc/MutationLogEntry.hpp index c76427a3..5d016730 100644 --- a/xo-gc/include/xo/gc/MutationLogEntry.hpp +++ b/xo-gc/include/xo/gc/MutationLogEntry.hpp @@ -35,7 +35,9 @@ namespace xo { void ** p_data() const { return p_data_; } obj snap() const { return snap_; } - /** true if child pointer has been altered since this mlog entry created **/ + /** true iff child pointer matches value when this mlog entry created **/ + bool is_active() const noexcept { return *p_data_ == snap_.data(); } + /** true iff child pointer has been altered since this mlog entry created **/ bool is_superseded() const noexcept { return *p_data_ != snap_.data(); } private: diff --git a/xo-gc/include/xo/gc/MutationLogStore.hpp b/xo-gc/include/xo/gc/MutationLogStore.hpp index 78042fac..fbe6a8b5 100644 --- a/xo-gc/include/xo/gc/MutationLogStore.hpp +++ b/xo-gc/include/xo/gc/MutationLogStore.hpp @@ -18,11 +18,12 @@ namespace xo { class DX1Collector; class X1VerifyStats; + using MutationLog = DArenaVector; + /** @brief container for X1 collector mutation logs **/ class MutationLogStore { public: - using MutationLog = DArenaVector; using size_type = DArena::size_type; public: @@ -34,6 +35,12 @@ namespace xo { **/ void init_mlogs(std::size_t page_z); + 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 **/ + MutationLog * triage_mlog(Generation g) noexcept { return mlog_[Role{c_n_role}][g]; } + const MutationLog * triage_mlog(Generation g) const noexcept { return mlog_[Role{c_n_role}][g]; } + /** total number of active mlog entries (across all generations) **/ size_type mutation_log_entries() const noexcept; @@ -41,12 +48,10 @@ namespace xo { void visit_pools(const MemorySizeVisitor & visitor) const; /** verify consistent mlog state, - * on behalf of gc-aware object store @p gc. * (using gc to identify location of objects). - * Update counters in @p *p_verify_stats. + * Update counters associated with gco_store_ **/ - void verify_ok(GCObjectStore * gc, - X1VerifyStats * p_verify_stats) noexcept; + void verify_ok() noexcept; /** on behalf of gc-aware object store @p gc, * change the value of a child pointer at @p p_lhs diff --git a/xo-gc/src/gc/DX1Collector.cpp b/xo-gc/src/gc/DX1Collector.cpp index 6232bb30..239f999e 100644 --- a/xo-gc/src/gc/DX1Collector.cpp +++ b/xo-gc/src/gc/DX1Collector.cpp @@ -420,8 +420,7 @@ namespace xo { gco_store_.verify_ok(); // 4. scan mutation logs - mlog_store_.verify_ok(&gco_store_, - &(this->verify_stats_)); + mlog_store_.verify_ok(); } // restore run state at end of verify cycle diff --git a/xo-gc/src/gc/GCObjectStore.cpp b/xo-gc/src/gc/GCObjectStore.cpp index 896f02e4..f8e8c479 100644 --- a/xo-gc/src/gc/GCObjectStore.cpp +++ b/xo-gc/src/gc/GCObjectStore.cpp @@ -154,6 +154,43 @@ namespace xo { return slot.iface(); } + namespace { + using size_type = GCObjectStore::size_type; + + size_type + stat_helper(const GCObjectStore & d, + size_type (DArena::* getter)() const, + Generation g, + Role r) + { + const DArena * arena = d.get_space(r, g); + + if (arena) [[likely]] { + return (arena->*getter)(); + } + + return 0; + } + } + + auto + GCObjectStore::allocated(Generation g, Role r) const noexcept -> size_type + { + return stat_helper(*this, &DArena::allocated, g, r); + } + + auto + GCObjectStore::committed(Generation g, Role r) const noexcept -> size_type + { + return stat_helper(*this, &DArena::committed, g, r); + } + + auto + GCObjectStore::reserved(Generation g, Role r) const noexcept -> size_type + { + return stat_helper(*this, &DArena::reserved, g, r); + } + Generation GCObjectStore::generation_of(Role r, const void * addr) const noexcept { @@ -258,7 +295,7 @@ namespace xo { obj error_mm, obj * p_output) const noexcept { - scope log(XO_DEBUG(true)); + scope log(XO_DEBUG(this->config_.debug_flag_)); (void)error_mm; @@ -369,7 +406,7 @@ namespace xo { obj error_mm, obj * p_output) const noexcept { - scope log(XO_DEBUG(true)); + scope log(XO_DEBUG(this->config_.debug_flag_)); (void)error_mm; @@ -883,7 +920,7 @@ namespace xo { std::byte * GCObjectStore::alloc_copy(void * src) noexcept { - scope log(XO_DEBUG(true)); + scope log(XO_DEBUG(config_.debug_flag_)); AllocInfo src_info = this->alloc_info((std::byte *)src); uint32_t age1p = std::min(src_info.age() + 1, diff --git a/xo-gc/src/gc/MutationLogStore.cpp b/xo-gc/src/gc/MutationLogStore.cpp index 4970aa1c..2a9d6ac3 100644 --- a/xo-gc/src/gc/MutationLogStore.cpp +++ b/xo-gc/src/gc/MutationLogStore.cpp @@ -90,13 +90,14 @@ namespace xo { } void - MutationLogStore::verify_ok(GCObjectStore * gco_store, - X1VerifyStats * p_verify_stats) noexcept + MutationLogStore::verify_ok() noexcept { + X1VerifyStats * p_verify_stats = gco_store_->verify_stats(); + // 4. scan mutation logs for (Generation g(0); g + 1 < config_.n_generation_; ++g) { - const DArena * space = gco_store->get_space(Role::to_space(), g); - const DArena * from = gco_store->get_space(Role::from_space(), g); + const DArena * space = gco_store_->get_space(Role::to_space(), g); + const DArena * from = gco_store_->get_space(Role::from_space(), g); // mutation log for generation g records *incoming* pointers // from more senior generations; includes objects from *this* diff --git a/xo-gc/utest/CMakeLists.txt b/xo-gc/utest/CMakeLists.txt index e2c623be..4f555807 100644 --- a/xo-gc/utest/CMakeLists.txt +++ b/xo-gc/utest/CMakeLists.txt @@ -11,12 +11,23 @@ set(UTEST_SRCS GCObjectStore.test.cpp Object2.test.cpp + DMockCollector.cpp + ICollector_DMockCollector.cpp + + MlsTestutil.cpp GcosTestutil.cpp init_gc_utest.cpp random_allocs.cpp ) +# note: manual target; generated code committed to git +xo_add_genfacetimpl( + TARGET xo-gc-facetimpl-collector-mockcollector + FACET_PKG xo_alloc2 + INPUT idl/ICollector_DMockCollector.json5 +) + if (ENABLE_TESTING) xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS}) xo_headeronly_dependency(${UTEST_EXE} randomgen) diff --git a/xo-gc/utest/DMockCollector.cpp b/xo-gc/utest/DMockCollector.cpp new file mode 100644 index 00000000..eb180685 --- /dev/null +++ b/xo-gc/utest/DMockCollector.cpp @@ -0,0 +1,106 @@ +/** @file DMockCollector.cpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#include "DMockCollector.hpp" + +namespace xo { + namespace mm { + + auto DMockCollector::allocated(Generation g, Role r) const noexcept -> size_type { + return gcos_->allocated(g, r); + } + + auto DMockCollector::committed(Generation g, Role r) const noexcept -> size_type { + return gcos_->committed(g, r); + } + + auto DMockCollector::reserved(Generation g, Role r) const noexcept -> size_type { + return gcos_->reserved(g, r); + } + + int32_t + DMockCollector::locate_address(const void * addr) const noexcept { + Generation g = gcos_->generation_of(Role::to_space(), addr); + + if (!g.is_sentinel()) + return g; + + return -1; + } + + bool + DMockCollector::contains(Role r, const void * addr) const noexcept { + return gcos_->contains(r, addr); + } + + bool + DMockCollector::is_type_installed(typeseq tseq) const noexcept { + return gcos_->is_type_installed(tseq); + } + + bool + DMockCollector::report_statistics(obj mm, + obj error_mm, + obj * p_output) const noexcept + { + return false; + } + + bool + DMockCollector::report_object_types(obj mm, + obj error_mm, + obj * p_output) const noexcept + { + return gcos_->report_object_types(mm, error_mm, p_output); + } + + bool + DMockCollector::report_object_ages(obj mm, + obj error_mm, + obj * p_output) const noexcept + { + return gcos_->report_object_ages(mm, error_mm, p_output); + } + + bool + DMockCollector::install_type(const AGCObject & meta) noexcept + { + return gcos_->install_type(meta); + } + + void + DMockCollector::add_gc_root_poly(obj * p_root) + { + assert(false); + } + + void + DMockCollector::remove_gc_root_poly(obj * p_root) + { + assert(false); + } + + void + DMockCollector::request_gc(Generation upto) + { + assert(false); + } + + void + DMockCollector::assign_member(void * parent, obj * p_lhs, obj & rhs) + { + mls_->assign_member(gcos_, parent, p_lhs, rhs); + } + + void * + DMockCollector::alloc_copy(std::byte * src) + { + return gcos_->alloc_copy(src); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DMockCollector.cpp */ diff --git a/xo-gc/utest/DMockCollector.hpp b/xo-gc/utest/DMockCollector.hpp new file mode 100644 index 00000000..69e9a223 --- /dev/null +++ b/xo-gc/utest/DMockCollector.hpp @@ -0,0 +1,65 @@ +/** @file DMockCollector.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace mm { + + class DMockCollector { + public: + using size_type = GCObjectStore::size_type; + using typeseq = xo::facet::typeseq; + + DMockCollector(MutationLogStore * mls, GCObjectStore * gcos) + : mls_{mls}, gcos_{gcos} {} + + size_type allocated(Generation g, Role r) const noexcept; + size_type committed(Generation g, Role r) const noexcept; + size_type reserved(Generation g, Role r) const noexcept; + + // like generation_fo(), but for ACollector api + int32_t locate_address(const void * addr) const noexcept; + + // true iff gcos contains address @p addr in @p role + bool contains(Role r, const void * addr) const noexcept; + + // true iff @p tseq has been installed in @p gcos_ + bool is_type_installed(typeseq tseq) const noexcept; + + bool report_statistics(obj mm, + obj error_mm, + obj * p_output) const noexcept; + + bool report_object_types(obj mm, + obj error_mm, + obj * p_output) const noexcept; + + bool report_object_ages(obj mm, + obj error_mm, + obj * p_output) const noexcept; + + bool install_type(const AGCObject & meta) noexcept; + + void add_gc_root_poly(obj * p_root); + void remove_gc_root_poly(obj * p_root); + void request_gc(Generation upto); + + // write barrier for assignment + void assign_member(void * parent, obj * p_lhs, obj & rhs); + + void * alloc_copy(std::byte * src); + + MutationLogStore * mls_ = nullptr; + GCObjectStore * gcos_ = nullptr; + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DMockCollector.hpp */ diff --git a/xo-gc/utest/GCObjectStore.test.cpp b/xo-gc/utest/GCObjectStore.test.cpp index 7c29dc85..19a6f92f 100644 --- a/xo-gc/utest/GCObjectStore.test.cpp +++ b/xo-gc/utest/GCObjectStore.test.cpp @@ -122,6 +122,7 @@ namespace ut { constexpr TestGraphType c_selfcycle = TestGraphType::selfcycle; constexpr TestGraphType c_random = TestGraphType::random; + /** arena size for object age/type reports **/ constexpr uint32_t c_report_z1 = 64 * 1024; constexpr uint32_t c_error_z1 = 16 * 1024; @@ -166,7 +167,9 @@ namespace ut { namespace { // aux functions specific to GCObjectStore-1 unit test below - // fixture for GCObjectStore-1 test + /** Fixture for GCObjectStore-1 test. + * Compare similar but not identical fixture in MutationLogStore.test.cpp + **/ class GcosFixture { public: explicit GcosFixture(const Testcase & tc); @@ -174,6 +177,7 @@ namespace ut { auto report_mm() { return obj(&report_arena_); } auto error_mm() { return obj(&error_arena_); } + /** configuration for @ref gcos_ **/ GCObjectStoreConfig gcos_config_; /** Parallel arena for reference @@ -186,6 +190,7 @@ namespace ut { * 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() **/ @@ -225,7 +230,7 @@ namespace ut { TEST_CASE("GCObjectStore-1", "[GCObjectStore]") { - constexpr bool c_debug_flag = true; + constexpr bool c_debug_flag = false; scope log0(XO_DEBUG(c_debug_flag), "GCObjectStore test"); std::uint64_t seed = 12168164826603821466ul; @@ -270,11 +275,14 @@ namespace ut { // construct, extend, and/or modify object graphs in {x1_v, x2_v} - GcosTestutil::gcos_construct_ab_object_graphs(tc.obj_graph_type_, + GcosTestutil::gcos_construct_ab_object_graphs(nullptr /*cmd_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_, + nullptr /*p_mls*/, &gcos, &fixture.arena2_, loop_index, diff --git a/xo-gc/utest/GcosTestutil.cpp b/xo-gc/utest/GcosTestutil.cpp index be2ad829..61739dd7 100644 --- a/xo-gc/utest/GcosTestutil.cpp +++ b/xo-gc/utest/GcosTestutil.cpp @@ -4,9 +4,11 @@ **/ #include "GcosTestutil.hpp" +#include "MockCollector.hpp" #include #include #include +#include #include #include #include @@ -19,6 +21,8 @@ namespace ut { using xo::scm::ListOps; using xo::scm::DList; using xo::scm::DBoolean; + using xo::mm::ACollector; + using xo::mm::DMockCollector; using xo::mm::X1VerifyStats; using xo::mm::GCObjectStore; using xo::mm::AGCObject; @@ -84,17 +88,20 @@ namespace ut { void GcosTestutil::random_object_graph(uint32_t n_new_obj, uint32_t n_assign, + bool debug_flag, xoshiro256ss * p_rgen, std::vector * p_v, GCObjectStore * p_gcos, std::vector * p_v2, DArena * p_arena2) { - scope log(XO_DEBUG(true)); + scope log(XO_DEBUG(debug_flag)); if (n_new_obj == 0 && n_assign == 0) return; + // TODO: combine // alloc setup w/ gco_construct_ab_object_graphs() bolierplate + for (uint32_t i_obj = 0; i_obj < n_new_obj; ++i_obj) { auto alloc = obj(p_gcos->new_space()); uint32_t sample = (*p_rgen)() % 100; @@ -309,11 +316,14 @@ namespace ut { * @p loop_index counts iteration with one gc-like phase. **/ void - GcosTestutil::gcos_construct_ab_object_graphs(TestGraphType obj_graph_type, + GcosTestutil::gcos_construct_ab_object_graphs(Step * cmd_seq, + TestGraphType obj_graph_type, 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, + MutationLogStore * p_mls, GCObjectStore * p_gcos, DArena * p_arena2, uint32_t loop_index, @@ -321,34 +331,133 @@ namespace ut { std::vector * p_x2_v, xoshiro256ss * p_rgen) { - switch (obj_graph_type) { - case TestGraphType::selfcycle: - if (loop_index == 0) { - GcosTestutil::selfcycle_object_graph(p_x1_v, - p_gcos, - p_x2_v, - p_arena2); - } - break; + if (cmd_seq && (loop_index == 0)) { + // do scripted sequence only - case TestGraphType::random: - { - uint32_t n_test_obj = ((loop_index == 0) - ? n_i0_test_obj - : n_i1_test_obj); - uint32_t n_test_assign = ((loop_index == 0) - ? n_i0_test_assign - : n_i1_test_assign); + auto alloc = obj(p_gcos->new_space()); + auto alloc2 = obj(p_arena2); + DMockCollector mock(p_mls, p_gcos); + auto mockgc = obj(&mock); - GcosTestutil::random_object_graph(n_test_obj, - n_test_assign, - p_rgen, - p_x1_v, - p_gcos, - p_x2_v, - p_arena2); + while (cmd_seq->is_command()) { + bool is_alloc = false; + obj xi; + obj xi2; + uint64_t alloc_z = 0; + typeseq tseq; + + 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(); + + 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_seq->arg0_ix_).gco_; + auto r1 = obj::from(p_x1_v->at(cmd_seq->arg1_ix_).gco_); + auto h2 = p_x2_v->at(cmd_seq->arg0_ix_).gco_; + auto r2 = obj::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(); + + 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(); + + xi2 = DBoolean::box(alloc2, value); + } + break; + case Step::Cmd::assign_head: + { + is_alloc = false; + + auto lhs1 = obj::from(p_x1_v->at(cmd_seq->arg0_ix_).gco_); + auto rhs1 = p_x2_v->at(cmd_seq->arg1_ix_).gco_; + auto lhs2 = obj::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)); + } + + ++cmd_seq; + } + } else { + switch (obj_graph_type) { + case TestGraphType::selfcycle: + if (loop_index == 0) { + GcosTestutil::selfcycle_object_graph(p_x1_v, + p_gcos, + p_x2_v, + p_arena2); + } + break; + + case TestGraphType::random: + { + uint32_t n_test_obj = ((loop_index == 0) + ? n_i0_test_obj + : n_i1_test_obj); + uint32_t n_test_assign = ((loop_index == 0) + ? n_i0_test_assign + : n_i1_test_assign); + + GcosTestutil::random_object_graph(n_test_obj, + n_test_assign, + debug_flag, + p_rgen, + p_x1_v, + p_gcos, + p_x2_v, + p_arena2); + } + break; } - break; } //x1_v.push_back(Recd(DBoolean::box(alloc, true), @@ -402,18 +511,24 @@ namespace ut { for (size_t i = 0, n = x1_v.size(); i < n; ++i) { const auto & x1 = x1_v.at(i); - REQUIRE(gcos.contains_allocated(Role::to_space(), x1.gco_.data())); - AllocInfo obj_info = gcos.alloc_info((std::byte *)x1.gco_.data()); - REQUIRE(obj_info.size() >= x1.alloc_z_); + // x1 could be a global, such as ListOps::nil() + if (x1.alloc_z_ > 0) { + REQUIRE(gcos.contains_allocated(Role::to_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()); + REQUIRE(obj_info.payload().first == (std::byte *)x1.gco_.data()); + REQUIRE(obj_info.tseq() == x1.tseq_.seqno()); - // also can use header2size / header2tseq convenience functions - REQUIRE(gcos.header2size(obj_info.header()) == obj_info.size()); - REQUIRE(gcos.header2age(obj_info.header()) <= object_age{loop_index}); - REQUIRE(gcos.header2tseq(obj_info.header()) == obj_info.tseq()); - REQUIRE(gcos.is_forwarding_header(obj_info.header()) == false); + // also can use header2size / header2tseq convenience functions + REQUIRE(gcos.header2size(obj_info.header()) == obj_info.size()); + REQUIRE(gcos.header2age(obj_info.header()) <= object_age{loop_index}); + REQUIRE(gcos.header2tseq(obj_info.header()) == obj_info.tseq()); + REQUIRE(gcos.is_forwarding_header(obj_info.header()) == false); + } else { + REQUIRE(!gcos.contains(Role::to_space(), x1.gco_.data())); + REQUIRE(!gcos.contains(Role::from_space(), x1.gco_.data())); + } } } @@ -431,10 +546,12 @@ namespace ut { INFO(tostr(xtag("gi", gi))); if (loop_index == 0) { - if ((gi == 0) && (x1_v.size() > 0)) - REQUIRE(gcos.to_space(gi)->allocated() > 0); - else + if ((gi == 0) && (x1_v.size() > 0)) { + // conceivable that x1_v[] only contains non-gco objects + //REQUIRE(gcos.to_space(gi)->allocated() > 0); + } else { REQUIRE(gcos.to_space(gi)->allocated() == 0); + } } REQUIRE(gcos.from_space(gi)->allocated() == 0); @@ -473,34 +590,39 @@ namespace ut { // x1 should be in gen g from-space (with g < upto) // or in gen g to-space (with g >= upto) - Generation g_from = gcos.generation_of(Role::from_space(), x1.gco_.data()); - Generation g_to = gcos.generation_of(Role::to_space(), x1.gco_.data()); + if (x1.alloc_z_ > 0) { + Generation g_from = gcos.generation_of(Role::from_space(), x1.gco_.data()); + Generation g_to = gcos.generation_of(Role::to_space(), x1.gco_.data()); - if (g_to.is_sentinel()) { - // if not in to-space, must be in from-space - REQUIRE(!g_from.is_sentinel()); + if (g_to.is_sentinel()) { + // if not in to-space, must be in from-space + REQUIRE(!g_from.is_sentinel()); - // + for some gen we're collecting - REQUIRE(g_from < upto); + // + for some gen we're collecting + REQUIRE(g_from < upto); - REQUIRE(gcos.contains(Role::from_space(), x1.gco_.data())); - REQUIRE(gcos.contains_allocated(Role::from_space(), x1.gco_.data())); + REQUIRE(gcos.contains(Role::from_space(), x1.gco_.data())); + REQUIRE(gcos.contains_allocated(Role::from_space(), x1.gco_.data())); + } else { + // if in to-space, must not be in from-space + REQUIRE(g_from.is_sentinel()); + + // + for some gen we're not collecting + REQUIRE(g_to >= upto); + + REQUIRE(gcos.contains(Role::to_space(), x1.gco_.data())); + REQUIRE(gcos.contains_allocated(Role::to_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()); } else { - // if in to-space, must not be in from-space - REQUIRE(g_from.is_sentinel()); - - // + for some gen we're not collecting - REQUIRE(g_to >= upto); - - REQUIRE(gcos.contains(Role::to_space(), x1.gco_.data())); - REQUIRE(gcos.contains_allocated(Role::to_space(), x1.gco_.data())); + REQUIRE(!gcos.contains(Role::to_space(), x1.gco_.data())); + REQUIRE(!gcos.contains(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()); } } @@ -603,31 +725,32 @@ namespace ut { const Recd & x1, obj x1_gco) { - REQUIRE((gcos.contains_allocated(Role::from_space(), x1_gco.data()) - || gcos.contains_allocated(Role::to_space(), x1_gco.data()))); - AllocInfo obj_info = gcos.alloc_info((std::byte *)x1_gco.data()); + if (x1.alloc_z_ > 0) { + REQUIRE((gcos.contains_allocated(Role::from_space(), x1_gco.data()) + || gcos.contains_allocated(Role::to_space(), x1_gco.data()))); + AllocInfo obj_info = gcos.alloc_info((std::byte *)x1_gco.data()); - INFO(tostr(xtag("obj_info.tseq", obj_info.tseq()), - xtag("obj_info.tname", TypeRegistry::id2name(typeseq(obj_info.tseq()))))); + INFO(tostr(xtag("obj_info.tseq", obj_info.tseq()), + xtag("obj_info.tname", TypeRegistry::id2name(typeseq(obj_info.tseq()))))); - REQUIRE(obj_info.size() >= x1.alloc_z_); - REQUIRE(obj_info.payload().first == (std::byte *)x1_gco.data()); + REQUIRE(obj_info.size() >= x1.alloc_z_); + REQUIRE(obj_info.payload().first == (std::byte *)x1_gco.data()); - if (obj_info.is_forwarding_tseq()) { - /* object was forwarded, so got collected */ - REQUIRE(obj_info.is_forwarding_tseq()); - } else { - /* not forwarded is ok iff in generation g >= upto */ + if (obj_info.is_forwarding_tseq()) { + /* object was forwarded, so got collected */ + REQUIRE(obj_info.is_forwarding_tseq()); + } else { + /* not forwarded is ok iff in generation g >= upto */ - Generation g = gcos.generation_of(Role::to_space(), x1_gco.data()); + Generation g = gcos.generation_of(Role::to_space(), x1_gco.data()); - REQUIRE(g >= upto); + REQUIRE(g >= upto); + } + + // if (!obj_info.is_forwarding_tseq()) + // print_backtrace_dwarf(true /*demangle*/); + // REQUIRE(obj_info.is_forwarding_tseq()); } - - // if (!obj_info.is_forwarding_tseq()) - // print_backtrace_dwarf(true /*demangle*/); - - // REQUIRE(obj_info.is_forwarding_tseq()); } void @@ -635,16 +758,18 @@ namespace ut { 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_); + if (x1.alloc_z_ > 0) { + 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(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())); + 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 diff --git a/xo-gc/utest/GcosTestutil.hpp b/xo-gc/utest/GcosTestutil.hpp index 66bc39e0..93e2c5bc 100644 --- a/xo-gc/utest/GcosTestutil.hpp +++ b/xo-gc/utest/GcosTestutil.hpp @@ -5,6 +5,7 @@ #pragma once +#include #include #include #include @@ -15,7 +16,39 @@ namespace ut { using xo::mm::Generation; using xo::facet::obj; + /** specify a step in scripted sequence + **/ + struct Step { + enum class Cmd { + /** sentinel for end of sequence **/ + sentinel, + /** refer to nil DList **/ + make_nil, + /** allocate DList w/ head x1_v[arg0_ix_], rest x1_v[arg1_ix_] **/ + make_cons, + /** allocate a boolean **/ + make_bool, + /** modify the head of a list x1_v[arg0_ix_]; replace with x1_v[arg1_ix_] **/ + assign_head, + + }; + + Step(Cmd cmd, uint32_t arg0, uint32_t arg1) + : cmd_{cmd}, arg0_ix_{arg0}, arg1_ix_{arg1} {} + + bool is_sentinel() const { return cmd_ == Cmd::sentinel; } + bool is_command() const { return cmd_ != Cmd::sentinel; } + + Cmd cmd_; + /** arg0 object index (index into x1_v[]) **/ + uint32_t arg0_ix_; + /** arg1 object index (index into x1_v[]) **/ + uint32_t arg1_ix_; + }; + enum class TestGraphType { + /* spelled out sequence of Steps */ + fixed, /* list cell pointing to itself */ selfcycle, /* random object graph */ @@ -40,6 +73,7 @@ namespace ut { }; struct GcosTestutil { + using MutationLogStore = xo::mm::MutationLogStore; using GCObjectStore = xo::mm::GCObjectStore; using AGCObject = xo::mm::AGCObject; using DArena = xo::mm::DArena; @@ -54,6 +88,7 @@ namespace ut { static void random_object_graph(uint32_t n_new_obj, uint32_t n_assign, + bool debug_flag, xoshiro256ss * p_rgen, std::vector * p_v, GCObjectStore * p_gcos, @@ -74,12 +109,17 @@ namespace ut { size_t gc_size, const GCObjectStore & gcos); + /** sequence of steps. if non-null, ends with step s: s.cmd_ == Step::Cmd::Sentinel + **/ static void - gcos_construct_ab_object_graphs(TestGraphType obj_graph_type, + gcos_construct_ab_object_graphs(Step * cmd_seq, + TestGraphType obj_graph_type, 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, + MutationLogStore * p_mls, GCObjectStore * p_gcos, DArena * p_arena2, uint32_t loop_index, diff --git a/xo-gc/utest/ICollector_DMockCollector.cpp b/xo-gc/utest/ICollector_DMockCollector.cpp new file mode 100644 index 00000000..bfb6b3a2 --- /dev/null +++ b/xo-gc/utest/ICollector_DMockCollector.cpp @@ -0,0 +1,106 @@ +/** @file ICollector_DMockCollector.cpp + * + * Generated automagically from ingredients: + * 1. code generator: + * [xo-facet/codegen/genfacet] + * arguments: + * --input [idl/ICollector_DMockCollector.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [iface_facet_any.hpp.j2] + * 3. idl for facet methods + * [idl/ICollector_DMockCollector.json5] +**/ + +#include "detail/ICollector_DMockCollector.hpp" + +namespace xo { + namespace mm { + auto + ICollector_DMockCollector::allocated(const DMockCollector & self, Generation g, Role r) noexcept -> size_type + { + return self.allocated(g, r); + } + + auto + ICollector_DMockCollector::committed(const DMockCollector & self, Generation g, Role r) noexcept -> size_type + { + return self.committed(g, r); + } + + auto + ICollector_DMockCollector::reserved(const DMockCollector & self, Generation g, Role r) noexcept -> size_type + { + return self.reserved(g, r); + } + + auto + ICollector_DMockCollector::locate_address(const DMockCollector & self, const void * addr) noexcept -> std::int32_t + { + return self.locate_address(addr); + } + + auto + ICollector_DMockCollector::contains(const DMockCollector & self, Role r, const void * addr) noexcept -> bool + { + return self.contains(r, addr); + } + + auto + ICollector_DMockCollector::is_type_installed(const DMockCollector & self, typeseq tseq) noexcept -> bool + { + return self.is_type_installed(tseq); + } + + auto + ICollector_DMockCollector::report_statistics(const DMockCollector & self, obj report_mm, obj error_mm, obj * output) noexcept -> bool + { + return self.report_statistics(report_mm, error_mm, output); + } + + auto + ICollector_DMockCollector::report_object_types(const DMockCollector & self, obj report_mm, obj error_mm, obj * output) noexcept -> bool + { + return self.report_object_types(report_mm, error_mm, output); + } + + auto + ICollector_DMockCollector::report_object_ages(const DMockCollector & self, obj report_mm, obj error_mm, obj * output) noexcept -> bool + { + return self.report_object_ages(report_mm, error_mm, output); + } + + auto + ICollector_DMockCollector::install_type(DMockCollector & self, const AGCObject & iface) -> bool + { + return self.install_type(iface); + } + auto + ICollector_DMockCollector::add_gc_root_poly(DMockCollector & self, obj * p_root) -> void + { + self.add_gc_root_poly(p_root); + } + auto + ICollector_DMockCollector::remove_gc_root_poly(DMockCollector & self, obj * p_root) -> void + { + self.remove_gc_root_poly(p_root); + } + auto + ICollector_DMockCollector::request_gc(DMockCollector & self, Generation upto) -> void + { + self.request_gc(upto); + } + auto + ICollector_DMockCollector::assign_member(DMockCollector & self, void * parent, obj * p_lhs, obj & rhs) -> void + { + self.assign_member(parent, p_lhs, rhs); + } + auto + ICollector_DMockCollector::alloc_copy(DMockCollector & self, std::byte * src) -> void * + { + return self.alloc_copy(src); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ICollector_DMockCollector.cpp */ diff --git a/xo-gc/utest/MlsTestutil.cpp b/xo-gc/utest/MlsTestutil.cpp new file mode 100644 index 00000000..125fcdf1 --- /dev/null +++ b/xo-gc/utest/MlsTestutil.cpp @@ -0,0 +1,110 @@ +/** @file MlsTestutil.cpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#include "MlsTestutil.hpp" +#include + +namespace ut { + using xo::mm::GCObjectStore; + using xo::mm::MutationLog; + using xo::mm::MutationLogEntry; + using xo::mm::AllocInfo; + using xo::mm::Role; + using xo::mm::Generation; + using xo::mm::c_max_generation; + + void + MlsTestutil::verify_fromspace_only_logged(const MutationLogStore & mls, + Generation upto) + { + for (Generation gi{0}; gi < std::min(upto, Generation(c_max_generation - 1)); ++gi) { + // from-space mlog may be empty or not + + // after swapping roles at beginning of GC, + // to-space mlog must be empty + { + const MutationLog * mlog = mls.get_mlog(Role::to_space(), gi); + REQUIRE(mlog->empty()); + } + + // triage mlog must be empty at beginning of GC phase + { + const MutationLog * mlog = mls.triage_mlog(gi); + REQUIRE(mlog->empty()); + } + } + } + + void + MlsTestutil::verify_tospace_only_logged(const MutationLogStore & mls, + Generation upto) + { + for (Generation gi{0}; gi < std::min(upto, Generation(c_max_generation - 1)); ++gi) { + // to-space mlog may be empty or not + + // from-space mlog must be empty in all generations. + // (only non-empty in GC phase, before GC completes) + { + const MutationLog * mlog = mls.get_mlog(Role::from_space(), gi); + REQUIRE(mlog->empty()); + } + + // traige mlog must be empty in all generations + // (only non-empty in GC phase, before GC completes) + { + const MutationLog * mlog = mls.triage_mlog(gi); + REQUIRE(mlog->empty()); + } + } + } + + void + MlsTestutil::verify_mlog_load_bearing(const MutationLogStore & mls, + Generation upto) + { + // reminders: + // - pointers from non-gc-owned objects permitted only from root objects. + // Such source objects are visited on every collection and don't need (or get) + // mlog entries. Exclude from consideration here. + // - Similarly pointers to non-gco-owned objects also don't need mlog entries. + + const GCObjectStore & gcos = *mls.gco_store_; + + for (Generation gi{0}; gi < std::min(upto, Generation(c_max_generation - 1)); ++gi) { + + const MutationLog * mlog = mls.get_mlog(Role::to_space(), gi); + + for (const MutationLogEntry & entry : *mlog) { + REQUIRE(entry.parent()); + REQUIRE(entry.p_data()); + REQUIRE(entry.snap()); + + if (entry.is_active()) { + AllocInfo src_info = gcos.alloc_info((std::byte *)entry.parent()); + void * dest = *entry.p_data(); + AllocInfo dest_info = gcos.alloc_info((std::byte *)*entry.p_data()); + + // source and destination must both be in to-space + REQUIRE(gcos.contains_allocated(Role::to_space(), entry.parent())); + REQUIRE(gcos.contains_allocated(Role::to_space(), *entry.p_data())); + + // either: + // 1. source in older generation than destination, + // (so destination may move under incremental collection, + // while parent generation stays put) + // 2. source may eventually promote to older generation, + // before destination. + // + // otherwise pointer does not require and should not have + // a mutation log entry + // + REQUIRE(src_info.age() > dest_info.age()); + } + } + } + } +} /*namespace ut*/ + +/* end MlsTestutil.cpp */ diff --git a/xo-gc/utest/MlsTestutil.hpp b/xo-gc/utest/MlsTestutil.hpp new file mode 100644 index 00000000..ae8c5e20 --- /dev/null +++ b/xo-gc/utest/MlsTestutil.hpp @@ -0,0 +1,27 @@ +/** @file MlsTestutil.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include + +namespace ut { + class MlsTestutil { + public: + using MutationLogStore = xo::mm::MutationLogStore; + using Generation = xo::mm::Generation; + + static void verify_fromspace_only_logged(const MutationLogStore & mls, + Generation upto); + static void verify_tospace_only_logged(const MutationLogStore & mls, + Generation upto); + /** verify that each mutation log entry is either: + * 1. invalid. cached destination no longer current + * 2. necessary: source age > dest age + **/ + static void verify_mlog_load_bearing(const MutationLogStore & mls, + Generation upto); + }; +} diff --git a/xo-gc/utest/MockCollector.hpp b/xo-gc/utest/MockCollector.hpp new file mode 100644 index 00000000..adc12f78 --- /dev/null +++ b/xo-gc/utest/MockCollector.hpp @@ -0,0 +1,11 @@ +/** @file MockCollector.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include "DMockCollector.hpp" +#include "detail/ICollector_DMockCollector.hpp" + +/* end MockCollector.hpp */ diff --git a/xo-gc/utest/MutationLogStore.test.cpp b/xo-gc/utest/MutationLogStore.test.cpp index 3f0f7228..c2c03c85 100644 --- a/xo-gc/utest/MutationLogStore.test.cpp +++ b/xo-gc/utest/MutationLogStore.test.cpp @@ -3,7 +3,12 @@ * @author Roland Conybeare, Apr 2026 **/ +#include "GcosTestutil.hpp" +#include "MlsTestutil.hpp" +#include +#include #include +#include #include #include #include @@ -12,28 +17,55 @@ #include 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 { - enum class TestGraphType { - /* list cell pointing to itself */ - selfcycle, - /* random object graph */ - random, - }; - struct Testcase { - explicit Testcase(bool debug_flag) : debug_flag_{debug_flag} {} + explicit Testcase(uint32_t n_gen, + uint32_t n_survive, + size_t gc_z, + uint32_t type_z, + bool do_type_registration, + Step * cmd_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}, + cmd_seq_{cmd_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} + {} /** number of generations in gco store **/ uint32_t n_gen_ = 0; @@ -51,17 +83,19 @@ namespace ut { * (load-bearing for incremental gc) **/ bool mlog_enabled_flag_ = false; + /** first loop: explicit cell alloc/assign **/ + Step * cmd_seq_ = nullptr; /** 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; - /** first loop: #of cells in random object graph **/ + /** 2nd loop: #of cells in random object graph **/ uint32_t n_i0_test_obj_ = 0; - /** first loop: #of random assignments to attempt **/ + /** 2nd loop: #of random assignments to attempt **/ uint32_t n_i0_test_assign_ = 0; - /** 2nd+later loop: #of cells in random object graph **/ + /** 3rd+later loop: #of cells in random object graph **/ uint32_t n_i1_test_obj_ = 0; - /** 2nd+later loop: #of random assignments to attempt **/ + /** 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_; @@ -69,28 +103,91 @@ namespace ut { 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; + constexpr TestGraphType c_fixed = TestGraphType::fixed; + using Cmd = Step::Cmd; + + static Step seq0[] = { + {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 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}, + }; + +# 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, 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), }; # 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_; }; @@ -107,6 +204,9 @@ namespace ut { 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_} {} @@ -134,17 +234,127 @@ namespace ut { MlsFixture fixture(tc); - // TODO: - // 1. move GCObjectStore.test.cpp - // shared code to separate .*pp files - // - gcos_testutil.*pp + // unlike GCObjectStore, separate init. // - // 2. add mutation log tests. Entry points - // - init_mlogs() - // - verify_ok() - // - assign_member() - // - swap_roles() - // - forward_mutation_log() + // 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); + + 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 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)); + + GcosTestutil::gcos_construct_ab_object_graphs(tc.cmd_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 + { + // TODO: consider moving sanitize_flag to Testcase + bool sanitize_flag = true; + gcos.cleanup_phase(gk, 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()); + } + } } } diff --git a/xo-gc/utest/detail/ICollector_DMockCollector.hpp b/xo-gc/utest/detail/ICollector_DMockCollector.hpp new file mode 100644 index 00000000..3607c2bd --- /dev/null +++ b/xo-gc/utest/detail/ICollector_DMockCollector.hpp @@ -0,0 +1,120 @@ +/** @file ICollector_DMockCollector.hpp + * + * Generated automagically from ingredients: + * 1. code generator: + * [xo-facet/codegen/genfacet] + * arguments: + * --input [idl/ICollector_DMockCollector.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [iface_facet_repr.hpp.j2] + * 3. idl for facet methods + * [idl/ICollector_DMockCollector.json5] + **/ + +#pragma once + +#include "Collector.hpp" +#include "../DMockCollector.hpp" + +namespace xo { namespace mm { class ICollector_DMockCollector; } } + +namespace xo { + namespace facet { + template <> + struct FacetImplementation + { + using ImplType = xo::mm::ICollector_Xfer + ; + }; + } +} + +namespace xo { + namespace mm { + /** @class ICollector_DMockCollector + **/ + class ICollector_DMockCollector { + public: + /** @defgroup mm-collector-dmockcollector-type-traits **/ + ///@{ + using size_type = xo::mm::ACollector::size_type; + using Copaque = xo::mm::ACollector::Copaque; + using Opaque = xo::mm::ACollector::Opaque; + using typeseq = xo::reflect::typeseq; + ///@} + /** @defgroup mm-collector-dmockcollector-methods **/ + ///@{ + // const methods + /** memory in use for this collector **/ + static size_type allocated(const DMockCollector & self, Generation g, Role r) noexcept; + /** memory committed for this collector **/ + static size_type committed(const DMockCollector & self, Generation g, Role r) noexcept; + /** address space reserved for this collector **/ + static size_type reserved(const DMockCollector & self, Generation g, Role r) noexcept; + /** Location of object in collector. -1 if not in collector memory. +Other negative values represent collector error states (good luck!). +Exact meaning of non-negative values up to collector implementation **/ + static std::int32_t locate_address(const DMockCollector & self, const void * addr) noexcept; + /** true if gc responsible for data at @p addr, and data belongs to Role @p r **/ + static bool contains(const DMockCollector & self, Role r, const void * addr) noexcept; + /** true iff gc-aware object of type @p tseq is installed in this collector **/ + static bool is_type_installed(const DMockCollector & self, typeseq tseq) noexcept; + /** Report gc statistics, at discretion of collector implementation. +Creates dictionary using memory from @p report_mm. +If unable to comply (e.g. oom), return runtime error allocated from @p error_mm. +Avoiding obj return type to avoid #include cycle **/ + static bool report_statistics(const DMockCollector & self, obj report_mm, obj error_mm, obj * output) noexcept; + /** Report gc object types, at discretion of collector implementation. +Creates dictionary using memory from @p report_mm. +If unable to comply (e.g. oom), return runtime error allocated from @p error_mm. +Avoiding obj return type to avoid #include cycle **/ + static bool report_object_types(const DMockCollector & self, obj report_mm, obj error_mm, obj * output) noexcept; + /** Report gc object ages, at discretion of collector implementation. +Creates array of dictionaries using memory from @p report_mm. +Each dictionary has keys n-live and bytes, indexed by object age. +If unable to comply (e.g. oom), return runtime error allocated from @p error_mm. +Avoiding obj return type to avoid #include cycle **/ + static bool report_object_ages(const DMockCollector & self, obj report_mm, obj error_mm, obj * output) noexcept; + + // non-const methods + /** install interface @p iface for representation with typeseq @p tseq +in collector @p d. + +The type AGCObject_Any here is misleading. +Will have been replaced by an instance of + @c AGCObject_Xfer for some @c DFoo +in which case calls through @c std::launder(&iface) +will properly act on @c DFoo. + +Return false if installation fails (e.g. memory exhausted) **/ + static bool install_type(DMockCollector & self, const AGCObject & iface); + /** add gc root with address @p p_root. gc will preserve subgraph at this address **/ + static void add_gc_root_poly(DMockCollector & self, obj * p_root); + /** remove gc root with address @p p_root. Reverse effect of prior add_gc_root_poly call **/ + static void remove_gc_root_poly(DMockCollector & self, obj * p_root); + /** Request immediate collection. + 1. if collection is enabled, immediately collect all generations + up to (but not including) g + 2. may nevertheless escalate to older generations, + depending on collector state. + 3. if collection is currently disabled, + collection will trigger the next time gc is enabled. + **/ + static void request_gc(DMockCollector & self, Generation upto); + /** Assign pointer @p p_lhs to destination @p rhs, within parent allocation @p parent + +Require: gc not in progress **/ + static void assign_member(DMockCollector & self, void * parent, obj * p_lhs, obj & rhs); + /** allocate copy of source object at address @p src. +Source must be owned by this collector. +Increments object age **/ + static void * alloc_copy(DMockCollector & self, std::byte * src); + ///@} + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end */ diff --git a/xo-gc/utest/idl/ICollector_DMockCollector.json5 b/xo-gc/utest/idl/ICollector_DMockCollector.json5 new file mode 100644 index 00000000..d0461090 --- /dev/null +++ b/xo-gc/utest/idl/ICollector_DMockCollector.json5 @@ -0,0 +1,24 @@ +{ + mode: "implementation", + output_cpp_dir: ".", + output_hpp_dir: ".", + output_impl_subdir: "detail", + includes: [ +// "", +// "" + ], + local_types: [ + { + name: "typeseq", + doc: ["identifies a c++ type"], + definition: "xo::reflect::typeseq" + }, + ], + namespace1: "xo", + namespace2: "mm", + facet_idl: "idl/Collector.json5", + brief: "provide ACollector interface for DMockCollector", + using_doxygen: true, + repr: "DMockCollector", + doc: [ "implement ACollector for DMockCollector" ], +} diff --git a/xo-gc/utest/init_gc_utest.cpp b/xo-gc/utest/init_gc_utest.cpp index 5f1958cf..f92c1f4b 100644 --- a/xo-gc/utest/init_gc_utest.cpp +++ b/xo-gc/utest/init_gc_utest.cpp @@ -4,7 +4,7 @@ **/ #include "init_gc_utest.hpp" -//#include "MockCollector.hpp" +#include "MockCollector.hpp" #include #include #include @@ -19,7 +19,7 @@ namespace xo { { scope log(XO_DEBUG(false)); - //FacetRegistry::register_impl(); + FacetRegistry::register_impl(); //log && log(xtag("DMockCollector.tseq", typeseq::id()));