diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 00bd9fb..1faebdc 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -476,7 +476,7 @@ namespace xo { void DX1Collector::execute_gc(Generation upto) noexcept { - scope log(XO_DEBUG(true), xtag("upto", upto)); + scope log(XO_DEBUG(config_.debug_flag_), xtag("upto", upto)); assert(!runstate_.is_running()); diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index f7ae549..7f21f5e 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -130,7 +130,6 @@ namespace xo { xtag("types.limit", object_types_.store()->limit_), xtag("types.hi", object_types_.store()->hi_)); - assert(false); return nullptr; } @@ -725,6 +724,9 @@ namespace xo { bool GCObjectStore::install_type(const AGCObject & meta) noexcept { + scope log(XO_DEBUG(config_.debug_flag_), + xtag("tseq", meta._typeseq())); + typeseq tseq = meta._typeseq(); assert(tseq.seqno() > 0); @@ -732,8 +734,11 @@ namespace xo { auto ix = static_cast(tseq.seqno()); if (ix >= object_types_.size()) { - if (!object_types_.resize(std::max(2 * object_types_.size(), ix + 1))) + if (!object_types_.resize(std::max(2 * object_types_.size(), ix + 1))) { + log && log("could not increase object_types_ size"); + return false; + } } assert(ix < object_types_.size()); @@ -843,16 +848,19 @@ namespace xo { /* TODO: AllocIterator pointing to free pointer */ GCMoveCheckpoint gray_lo_v = this->snap_move_checkpoint(upto); - //obj alloc(this); AGCObject * iface = this->lookup_type(tseq); - assert(iface->_has_null_vptr() == false); + //assert(iface && (iface->_has_null_vptr() == false)); - void * to_dest = this->_shallow_move(gc, iface, from_src); + void * to_dest = nullptr; - this->_forward_children_until_fixpoint(gc, upto, gray_lo_v); + if (iface) [[likely]] { + to_dest = this->_shallow_move(gc, iface, from_src); - log && log(xtag("to_dest", to_dest)); + this->_forward_children_until_fixpoint(gc, upto, gray_lo_v); + + log && log(xtag("to_dest", to_dest)); + } return to_dest; } /*_deep_move_gc_owned*/ @@ -866,8 +874,6 @@ namespace xo { AllocInfo info = this->alloc_info((std::byte *)from_src); - //obj gc_gco(gc); - void * to_dest = iface->gco_shallow_move(from_src, gc); log && log(xtag("from_src", from_src), xtag("to_dest", to_dest)); diff --git a/utest/DX1CollectorIterator.test.cpp b/utest/DX1CollectorIterator.test.cpp index 66a9e41..4d3ca6c 100644 --- a/utest/DX1CollectorIterator.test.cpp +++ b/utest/DX1CollectorIterator.test.cpp @@ -88,7 +88,7 @@ namespace xo { TEST_CASE("DX1CollectorIterator-2", "[alloc2][gc][DX1Collector]") { - scope log(XO_DEBUG(false)); + scope log(XO_DEBUG(false), "DX1CollectorIterator test"); ArenaConfig arena_cfg = { .name_ = "_test_unused", .size_ = 4*1024*1024, diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index 399a7ae..dd11503 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -3,34 +3,64 @@ * @author Roland Conybeare, Apr 2026 **/ +#include +#include "MockCollector.hpp" + +#include #include #include -#include +#include +#include #include +#include #include +#include +#include +#include #include namespace ut { + using xo::scm::ListOps; using xo::scm::DList; using xo::scm::DInteger; + using xo::scm::DBoolean; + using xo::mm::DMockCollector; using xo::mm::GCObjectStoreConfig; using xo::mm::GCObjectStore; using xo::mm::AGCObject; + using xo::mm::AGCObjectVisitor; + using xo::mm::Generation; + using xo::mm::Role; using xo::mm::ArenaConfig; + using xo::mm::AAllocator; + using xo::mm::DArena; + using xo::mm::AllocInfo; + using xo::mm::c_max_generation; + using xo::facet::obj; using xo::facet::typeseq; using xo::facet::impl_for; + using xo::rng::xoshiro256ss; + using xo::rng::random_seed; using xo::scope; + using xo::xtag; + using xo::tostr; using std::size_t; using std::uint32_t; namespace { struct Testcase { explicit Testcase(uint32_t n_gen, uint32_t n_survive, - size_t gc_z, uint32_t type_z) + size_t gc_z, uint32_t type_z, + bool do_type_registration, + uint32_t n_test_obj, + uint32_t n_test_assign) : n_gen_{n_gen}, n_survive_{n_survive}, gc_size_{gc_z}, - object_type_z_{type_z} + object_type_z_{type_z}, + do_type_registration_{do_type_registration}, + n_test_obj_{n_test_obj}, + n_test_assign_{n_test_assign} {} /** number of generations in gco store **/ @@ -44,22 +74,204 @@ namespace ut { **/ uint32_t object_type_z_ = 0; + /** if true, register types for + * gc-aware types used in unit test + * (i.e. DBoolean) + **/ + bool do_type_registration_ = false; + /** #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; }; static std::vector s_testcase_v = { - /** n_gen, n_survive, gc_size, object_type_z **/ - Testcase(2, 4, 16 * 1024, 8 * 128), + /** n_gen, n_survive, gc_size, object_type_z, do_type_registration, n_obj **/ + Testcase(2, 4, 16 * 1024, 8 * 128, false, 0, 0), + Testcase(2, 4, 16 * 1024, 8 * 128, true, 1, 0), + Testcase(2, 4, 16 * 1024, 8 * 128, true, 2, 0), + Testcase(2, 4, 16 * 1024, 8 * 128, true, 4, 0), + Testcase(2, 4, 16 * 1024, 8 * 128, true, 8, 4), + Testcase(2, 4, 16 * 1024, 8 * 128, true, 16, 7), }; - } + + /** record capturing some stats for a (randomly created) gc-aware object **/ + struct Recd { + Recd() = default; + Recd(obj value, uint32_t z, typeseq tseq) : gco_{value}, alloc_z_{z}, tseq_{tseq} {} + + // random gc-aware value + obj gco_; + // expected allocation size (lower bound) + uint32_t alloc_z_ = 0; + // representation + typeseq tseq_; + }; + + /** Create two isomorphic random object graphs containing @p n_obj nodes + * Using a few basic data types from xo-object2 + * DBoolean + * DList + * + * Generated objects stored in @p *p_gcos. + * Individual items pushed to @p *p_v. + * + * Isomorphic copy in @p *p_arena2, + * with individual items pushed to @p *p_v2. + * + * For each i in rance the node (*p_v)[i] is isomorphic to (*p_v2)[i] + * (*p_v)[i] allocated entirely from @p p_gcos->new_space() + * (*p_v2)[i] allocated entirely from @p p_arena2 + **/ + void + random_object_graph(uint32_t n_obj, + uint32_t n_assign, + xoshiro256ss * p_rgen, + std::vector * p_v, + GCObjectStore * p_gcos, + std::vector * p_v2, + DArena * p_arena2) + { + if (n_obj == 0) + return; + + for (uint32_t i_obj = 0; i_obj < n_obj; ++i_obj) { + auto alloc = obj(p_gcos->new_space()); + uint32_t sample = (*p_rgen)() % 100; + // randomly-constructed node in object graph + obj xi; + uint64_t alloc_z; + typeseq tseq; + + // 2nd allocator for copy of object model + auto alloc2 = obj(p_arena2); + // isomorphic node destined for arena2 + obj xi2; + + if (sample < 50) { + // create a DBoolean + bool value = ((*p_rgen)() % 2 == 0); + + xi = DBoolean::box(alloc, value); + alloc_z = sizeof(DBoolean); + tseq = typeseq::id(); + + xi2 = DBoolean::box(alloc2, value); + } else { + // create a DList cell, with random {car, cdr} + + obj car = ListOps::nil(); + obj cdr = ListOps::nil(); + + obj car2 = ListOps::nil(); + obj cdr2 = ListOps::nil(); + + auto z = p_v->size(); + + if (z > 0) { + // random car + { + uint32_t i = ((*p_rgen)() % z); + car = p_v->at(i).gco_; + + car2 = p_v2->at(i).gco_; + } + + // random cdr + { + uint32_t i = ((*p_rgen)() % z); + + // is v[i] a list cell? + { + auto tmp = obj::from(p_v->at(i).gco_); + if (tmp) + cdr = tmp; + } + + { + auto tmp2 = obj::from(p_v2->at(i).gco_); + if (tmp2) + cdr2 = tmp2; + } + } + } + + xi = ListOps::cons(alloc, car, cdr); + alloc_z = sizeof(DList); + tseq = typeseq::id(); + + xi2 = ListOps::cons(alloc2, car2, cdr2); + } + + p_v->push_back(Recd(xi, alloc_z, tseq)); + + // also save parallel copy + p_v2->push_back(Recd(xi2, alloc_z, tseq)); + } + + // also make some random modifications, + // so that it's possible to create cycles. + + for (uint32_t j = 0; j < n_assign; ++j) { + // choose an object at random + uint32_t sample = (*p_rgen)() % n_obj; + + assert(sample < p_v->size()); + + // is it a list cell? + auto xj = obj::from((*p_v)[sample].gco_); + auto xj2 = obj::from((*p_v2)[sample].gco_); + + if (xj) { + assert(xj2); + + // flip a coin -- try modifying one of {car, cdr} + 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. + + sample = (*p_rgen)() % n_obj; + auto rhs = obj::from((*p_v)[sample].gco_); + auto rhs2 = obj::from((*p_v2)[sample].gco_); + + if (rhs) { + // modify rest. skip usual gc write-barrier stuff + + assert(rhs2); + + xj->rest_ = rhs.data(); + xj2->rest_ = rhs2.data(); + } + } + } + } + } /*random_object_graph*/ + } /*namespace*/ TEST_CASE("GCObjectStore-1", "[GCObjectStore]") { - constexpr bool c_debug_flag = false; - scope log(XO_DEBUG(c_debug_flag)); + constexpr bool c_debug_flag = true; + scope log(XO_DEBUG(c_debug_flag), "GCObjectStore test"); + + std::uint64_t seed = 12168164826603821466ul; + //random_seed(&seed); + log && log(xtag("seed", seed)); + + auto rgen = xoshiro256ss(seed); for (size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { const Testcase & tc = s_testcase_v[i_tc]; + INFO(tostr(xtag("i_tc", i_tc), xtag("n_tc", n_tc))); + /** config for each half-space **/ ArenaConfig arena_config = (ArenaConfig() @@ -73,19 +285,306 @@ namespace ut { tc.object_type_z_, c_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)); + // 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); - //REQUIRE(nullptr == gcos.lookup_type(typeseq::id())); + Generation g0{0}; + Generation g1{1}; + Generation gn{tc.n_gen_}; -#ifdef NOT_YET + // 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())); + } + } + // verify basic arena partitioning + { + 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)); + + 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)); + } + } + + // verify we have non-zero space! + { + for (Generation gi = g0; gi < gn; ++gi) { + INFO(tostr(xtag("gi", gi))); + + REQUIRE(gcos.to_space(gi)->allocated() == 0); + REQUIRE(gcos.to_space(gi)->reserved() >= tc.gc_size_); + + REQUIRE(gcos.from_space(gi)->allocated() == 0); + REQUIRE(gcos.from_space(gi)->reserved() >= tc.gc_size_); + } + } + + // allocator + auto alloc = obj(gcos.new_space()); + + // create object(s). + // details depend on test case + + 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); + + //x1_v.push_back(Recd(DBoolean::box(alloc, true), + // sizeof(DBoolean), + // typeseq::id())); + } + + // someday: print the graph. Need a cycle-detecting printer + + 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_); + + REQUIRE(x1_v[i].gco_._typeseq() == x1_v[i].tseq_); + REQUIRE(x2_v[i].gco_._typeseq() == x2_v[i].tseq_); + } + + // new objects appear in to-space for generation 0 + 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_); + + REQUIRE(obj_info.payload().first == (std::byte *)x1.gco_.data()); + REQUIRE(obj_info.tseq() == x1.tseq_.seqno()); + + for (Generation gi = g0; gi < gn; ++gi) { + INFO(tostr(xtag("gi", gi))); + + if (gi == 0) + REQUIRE(gcos.to_space(gi)->allocated() > 0); + else + REQUIRE(gcos.to_space(gi)->allocated() == 0); + + REQUIRE(gcos.from_space(gi)->allocated() == 0); + } + } + + // swap_roles [but only for generation < g1, i.e. g0 + { + gcos.swap_roles(g1); + + for (size_t i = 0, n = x1_v.size(); i < n; ++i) { + const auto & x1 = x1_v.at(i); + + 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()); + + 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); + } + } + } + + // try moving everything to to-space. + // For this to week we must have registered the type, + // so gc knows how to traverse it + // + for (size_t i = 0, n = x1_v.size(); i < n; ++i) { + 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_))); + + if (tc.do_type_registration_) { + + /* Action of this loop iteration: + * + * gcos arena2 + * +------------+-----------+ +--------+ + * | from | to | | | + * | | | | | + * | +----+ | +-----+ | | +----+ | + * | | x1 |---->| x1p | | | | x2 | | + * | +----+ | +-----+ | | +----+ | + * | | | | | + * +------------+-----------+ +--------+ + * + * Before: + * x1, x2 have the same shape + * After + * x1 forward to x1p + * x1p and x2 have the same shape + */ + + // note: since members of x1_v[] can refer to each other, + // 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_); + REQUIRE(x1p_iface); + + auto x1p_data = gcos.deep_move_root(mock_gc_visitor, x1.gco_, g1); + REQUIRE(x1p_data); + + obj x1p_gco(x1p_iface, x1p_data); + + // obj has been replaced by forwarding pointer to obj2 + { + 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()); + } + + // 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())); + + // 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); + } + + } 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_, g1); + + // control here under normal GC use + // would represent a configuration fail + + REQUIRE(x1p_data == nullptr); + } + } + + // Things to test: + // - deep_move_interior() // used from MutationLogStore + // - forward_inplace_aux() // used from DX1Collector.visit_child + // - cleanup_phase() // used from DX1Collector._cleanup_phase -// // Usual path would be via ACollector interface; that's inconvenient here -// -#endif } } diff --git a/utest/Object2.test.cpp b/utest/Object2.test.cpp index 4bda353..5c259fb 100644 --- a/utest/Object2.test.cpp +++ b/utest/Object2.test.cpp @@ -64,7 +64,7 @@ namespace ut { TEST_CASE("printable1", "[pp][x1][list]") { constexpr bool c_debug_flag = false; - scope log(XO_DEBUG(c_debug_flag)); + scope log(XO_DEBUG(c_debug_flag), "Object2 printable1 test"); bool ok = SetupObject2::register_facets(); REQUIRE(ok); diff --git a/utest/X1Collector.test.cpp b/utest/X1Collector.test.cpp index 04e5c45..3bd4d4e 100644 --- a/utest/X1Collector.test.cpp +++ b/utest/X1Collector.test.cpp @@ -98,11 +98,11 @@ namespace ut { * This is a basic Collector test for xo-object2 data types **/ - constexpr bool c_debug_flag = true; - scope log(XO_DEBUG(c_debug_flag)); + constexpr bool c_debug_flag = false; + scope log(XO_DEBUG(c_debug_flag), "X1Collector test"); for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { - scope log(XO_DEBUG(true), xtag("i_tc", i_tc)); + scope log(XO_DEBUG(false), xtag("i_tc", i_tc)); try { const testcase_x1 & tc = s_testcase_v[i_tc]; diff --git a/utest/init_gc_utest.cpp b/utest/init_gc_utest.cpp index a88f604..c708c56 100644 --- a/utest/init_gc_utest.cpp +++ b/utest/init_gc_utest.cpp @@ -17,7 +17,7 @@ namespace xo { bool SetupGcUtest::register_facets() { - scope log(XO_DEBUG(true)); + scope log(XO_DEBUG(false)); FacetRegistry::register_impl();