xo-gc stack: fix mutation setup + xo-reader2 utest

This commit is contained in:
Roland Conybeare 2026-05-07 23:44:32 -04:00
commit 50d87d3371
9 changed files with 224 additions and 23 deletions

View file

@ -57,6 +57,24 @@ namespace xo {
Generation gc_upto_;
};
/** @class GcStatistics
**/
struct GCStatistics {
public:
GCStatistics() = default;
explicit GCStatistics(uint32_t n_gc) : n_gc_{n_gc} {};
uint32_t n_gc() const noexcept { return n_gc_; }
void include_gc() {
++n_gc_;
}
private:
/** count #gc **/
uint32_t n_gc_ = 0;
};
struct DX1CollectorIterator;
/** @brief GC root struct
@ -116,6 +134,8 @@ namespace xo {
std::string_view name() const noexcept { return config_.name_; }
GCRunState runstate() const noexcept { return runstate_; }
const GCStatistics & gc_stats() const noexcept { return gc_stats_; }
const ObjectTypeTable * get_object_types() const noexcept { return gco_store_.get_object_types(); }
const RootSet * get_root_set() const noexcept { return &root_set_; }
const DArena * get_space(Role r, Generation g) const noexcept { return gco_store_.get_space(r, g); }
@ -405,6 +425,9 @@ namespace xo {
**/
MutationLogStore mlog_store_;
/** counters collected across GC phases **/
GCStatistics gc_stats_;
/** counters collected during @ref verify_ok call **/
X1VerifyStats verify_stats_;
};

View file

@ -528,6 +528,9 @@ namespace xo {
log && log("step 5 : cleanup");
this->_cleanup_phase(upto);
log && log("step 6 : update gc statistics");
gc_stats_.include_gc();
if (config_.sanitize_flag_) {
log && log("step 5b : verify");
bool ok = this->verify_ok();
@ -550,7 +553,7 @@ namespace xo {
void
DX1Collector::_swap_roles(Generation upto) noexcept
{
scope log(XO_DEBUG(true), xtag("upto", upto));
scope log(XO_DEBUG(config_.debug_flag_), xtag("upto", upto));
gco_store_.swap_roles(upto);
mlog_store_.swap_roles(upto);
@ -559,7 +562,7 @@ namespace xo {
void
DX1Collector::_cleanup_phase(Generation upto)
{
scope log(XO_DEBUG(true), xtag("upto", upto));
scope log(XO_DEBUG(config_.debug_flag_), xtag("upto", upto));
this->gco_store_.cleanup_phase(upto, config_.sanitize_flag_);
this->runstate_ = GCRunState::idle();
@ -568,7 +571,7 @@ namespace xo {
void
DX1Collector::_copy_roots(Generation upto) noexcept
{
scope log(XO_DEBUG(true));
scope log(XO_DEBUG(config_.debug_flag_));
for (RootSet::size_type i = 0, n = root_set_.size(); i < n; ++i) {
GCRoot & slot = root_set_[i];

View file

@ -705,7 +705,7 @@ namespace xo {
void
GCObjectStore::_verify_aux(AGCObject * iface, void * data)
{
scope log(XO_DEBUG(config_.debug_flag_));
scope log(XO_DEBUG(false));
(void)iface;
@ -719,6 +719,7 @@ namespace xo {
if (!g2.is_sentinel()) {
// verify failure - live pointer still refers to from-space
log.retroactively_enable();
print_backtrace_dwarf(true /*demangle*/);
++(p_verify_stats_->n_from_);
@ -1020,13 +1021,17 @@ namespace xo {
AGCObject * iface,
void * from_src)
{
scope log(XO_DEBUG(config_.debug_flag_));
scope log(XO_DEBUG(config_.debug_flag_),
xtag("iface", iface),
xtag("from_src", from_src));
assert(!iface->_has_null_vptr());
AllocInfo info = this->alloc_info((std::byte *)from_src);
void * to_dest = iface->gco_shallow_move(from_src, gc);
log && log(xtag("from_src", from_src), xtag("to_dest", to_dest));
log && log(xtag("to_dest", to_dest));
log && log(xtag("tseq", info.tseq()),
xtag("tname", TypeRegistry::id2name(typeseq(info.tseq()))),
xtag("age", info.age()),

View file

@ -160,8 +160,14 @@ namespace xo {
assert(rhs_iface);
assert(rhs_data);
if (lhs_iface)
*lhs_iface = *rhs_iface;
if (lhs_iface) {
// memcpy (not assignment): lhs_iface points to AGCObject storage
// whose vptr was set at construction (e.g. IGCObject_Any from
// a default-constructed obj<AGCObject>). Polymorphic copy-assignment
// copies AGCObject's data members but NOT the vptr, so it would
// leave the slot dispatching to the wrong (often fatal) iface.
::memcpy((void *)lhs_iface, (void *)rhs_iface, sizeof(AGCObject));
}
*lhs_addr = rhs_data;
@ -195,6 +201,13 @@ namespace xo {
return;
}
if (dest_g + 1 == config_.n_generation_) {
log && log(xtag("msg", "noop because dest in last gen"));
// don't need mlog entry to final gen
return;
}
if (src_g < dest_g) {
log && log(xtag("msg", "noop because src gen younger than dest gen"));
@ -259,7 +272,7 @@ namespace xo {
void
MutationLogStore::swap_roles(Generation upto) noexcept
{
scope log(XO_DEBUG(true), xtag("upto", upto));
scope log(XO_DEBUG(config_.debug_flag_), xtag("upto", upto));
for (Generation g = Generation{0}; g < upto; ++g) {
log && log("swap roles", xtag("g", g));

View file

@ -9,6 +9,7 @@ set(UTEST_SRCS
DX1CollectorIterator.test.cpp
MutationLogStore.test.cpp
GCObjectStore.test.cpp
GCObjectConversion.test.cpp
Object2.test.cpp
DMockCollector.cpp

View file

@ -13,6 +13,7 @@
#include <xo/object2/Array.hpp>
#include <xo/object2/List.hpp>
#include <xo/object2/Integer.hpp>
#include <xo/alloc2/CollectorTypeRegistry.hpp>
#include <xo/alloc2/Allocator.hpp>
#include <xo/randomgen/xoshiro256.hpp>
#include <xo/randomgen/random_seed.hpp>
@ -25,6 +26,7 @@ namespace xo {
using xo::scm::DList;
using xo::scm::DArray;
using xo::scm::DInteger;
using xo::mm::CollectorTypeRegistry;
using xo::mm::AAllocator;
using xo::mm::ACollector;
using xo::mm::AGCObject;
@ -33,9 +35,11 @@ namespace xo {
using xo::mm::Role;
using xo::mm::ArenaConfig;
using xo::mm::AllocHeaderConfig;
using xo::mm::AllocHeader;
using xo::mm::Generation;
using xo::mm::c_max_generation;
using xo::facet::with_facet;
using xo::reflect::typeseq;
using xo::scope;
namespace ut {
@ -321,7 +325,11 @@ namespace xo {
.with_n_survive(tc.n_survive_)
.with_size(tc.gc_halfspace_z_)
.with_debug_flag(tc.debug_flag_))
{}
{
auto gc = obj<ACollector,DX1Collector>(&gc_);
CollectorTypeRegistry::instance().install_types(gc);
}
# define nil nullptr
# define T true
@ -332,11 +340,11 @@ namespace xo {
* debug_flag
* object_type_z |
* gc_halfspace_z | |
* n_survive | | |
* n_gen | | | |
* v v v v v
* n_survive | | |
* n_gen | | | |
* v v v v v
**/
Testcase(1, 2, 16 * 1024, 128, F),
Testcase(1, 2, 16 * 1024, 128, T),
};
# undef T
@ -345,10 +353,23 @@ namespace xo {
} /*namespace*/
// full collector test.
//
// PLAN:
// eventually: make generative
//
// Setup (
// 1. gc_utest_main.cpp Subsystem::initialize_all()
// invokes per-module plugin init. Gets types registered
// with FacetRegistry, CollectorTypeRegistry etc.
// 2. per-utest collector setup (fixture)
// calls CollectorTypeRegistry::instance().install_types(gc)
// to establish the set of types that collector knows.
//
TEST_CASE("collector-x1-gc", "[alloc2][gc]")
{
scope log(XO_DEBUG(true),
"DX1Collector gc test");
const auto & testname = Catch::getResultCapture().getCurrentTestName();
scope log(XO_DEBUG(true), xtag("test", testname));
//std::uint64_t seed = 7988747704879432247ul;
//random_seed(&seed);
@ -372,7 +393,17 @@ namespace xo {
auto mm = x1.ref<AAllocator>();
auto gc = mm.to_facet<ACollector>();
Generation g1{1};
REQUIRE(mm._typeseq() == typeseq::id<DX1Collector>());
REQUIRE(gc._typeseq() == typeseq::id<DX1Collector>());
Generation g0 = Generation::g0();
REQUIRE(mm.allocated() == tc.object_type_z_);
REQUIRE(gc.allocated(g0, Role::to_space()) == 0);
REQUIRE(gc.allocated(g0, Role::from_space()) == 0);
Generation g1 = Generation::g1();
{
auto roots = DArray::_empty(mm, 1)->ref<AGCObject>();
REQUIRE(mm->contains_allocated(Role::to_space(), roots.data()));
@ -383,12 +414,32 @@ namespace xo {
auto x1_gco = obj<AGCObject>(x1);
auto l1 = DList::cons(mm, x1, DList::_nil());
#ifdef NOT_YET
REQUIRE(roots->push_back(l1));
REQUIRE(l1._typeseq() == typeseq::id<DList>());
REQUIRE(roots->push_back(mm, l1));
REQUIRE(mm->contains_allocated(Role::to_space(), x1.data()));
REQUIRE(mm->contains_allocated(Role::to_space(), l1.data()));
gc->add_gc_root_poly(&(*roots.operator->())[0]);
REQUIRE(roots->at(0) == l1);
REQUIRE(roots->at(0)._typeseq() == typeseq::id<DList>());
// z: total allocated so far
// 3x 8-byte header
// sizeof(DInteger)
// sizeof(DList)
// sizeof(DArray(1))
//
auto z = (3 * sizeof(AllocHeader)
+ sizeof(DInteger)
+ sizeof(DList)
+ sizeof(DArray) + sizeof(obj<AGCObject>));
{
REQUIRE(z == 80);
REQUIRE(mm.allocated() == tc.object_type_z_ + z);
REQUIRE(gc.allocated(g0, Role::to_space()) == z);
REQUIRE(gc.allocated(g1, Role::to_space()) == 0);
REQUIRE(gc.allocated(g0, Role::from_space()) == 0);
REQUIRE(gc.allocated(g1, Role::from_space()) == 0);
}
gc->request_gc(g1); // 1st GC
@ -398,9 +449,20 @@ namespace xo {
// l1 target got moved, og locn now relabeled from-space
REQUIRE(mm->contains(Role::from_space(), l1.data()));
REQUIRE(!mm->contains_allocated(Role::from_space(), l1.data()));
#endif
REQUIRE(mm.allocated() == tc.object_type_z_ + z);
REQUIRE(gc.allocated(g0, Role::to_space()) == z);
REQUIRE(gc.allocated(g1, Role::to_space()) == 0);
REQUIRE(gc.allocated(g0, Role::from_space()) == 0);
REQUIRE(gc.allocated(g1, Role::from_space()) == 0);
}
// NOTE: if this fails:
// look for preceding GCObjectStore::lookup_type out-of-bounds.
// May need to add to CollectorTypeRegistry
//
REQUIRE(mm->contains_allocated(Role::to_space(), roots.data()));
}
}

View file

@ -0,0 +1,95 @@
/** @file GCObjectConversion.test.cpp
*
* @author Roland Conybeare, May 2026
**/
#include "GCObjectConversion.hpp"
#include <xo/object2/ListOps.hpp>
#include <xo/object2/List.hpp>
#include <xo/object2/Array.hpp>
#include <xo/object2/Integer.hpp>
#include <xo/alloc2/Arena.hpp>
#include <catch2/catch.hpp>
namespace xo {
//using xo::scm::ASequence;
using xo::scm::ListOps;
using xo::scm::DArray;
using xo::scm::DList;
using xo::scm::DInteger;
using xo::scm::GCObjectConversion;
using xo::mm::AGCObject;
using xo::mm::ArenaConfig;
using xo::mm::AAllocator;
using xo::mm::DArena;
using xo::facet::obj;
namespace ut {
TEST_CASE("GCObjectConversion-1", "[GCObjectConversion]")
{
scope log(XO_DEBUG(true), "GCObjectConversion-1");
ArenaConfig cfg {
.name_ = "testarena",
.size_ = 128
};
DArena arena = DArena::map(cfg);
auto mm = obj<AAllocator,DArena>(&arena);
auto v1 = DArray::empty(mm, 3);
REQUIRE(v1);
REQUIRE(v1->size() == 0);
{
obj v1_seq
= GCObjectConversion<obj<AGCObject,DArray>>::from_gco(mm /*not used*/, v1);
REQUIRE(v1_seq);
REQUIRE(v1_seq == v1);
REQUIRE(v1_seq->size() == 0);
}
{
obj l1_seq
= GCObjectConversion<obj<AGCObject,DList>>::from_gco(mm /*not used*/, v1);
REQUIRE(!l1_seq);
}
}
TEST_CASE("GCObjectConversion-2", "[GCObjectConversion]")
{
scope log(XO_DEBUG(true), "GCObjectConversion-2");
ArenaConfig cfg {
.name_ = "testarena",
.size_ = 128
};
DArena arena = DArena::map(cfg);
auto mm = obj<AAllocator,DArena>(&arena);
auto l1 = ListOps::cons(mm, DInteger::box(mm, 42), ListOps::nil());
REQUIRE(l1);
REQUIRE(l1->size() == 1);
{
// will fail; source is DArena
obj l1_seq
= GCObjectConversion<obj<AGCObject,DList>>::from_gco(mm /*not used*/, l1);
REQUIRE(l1_seq);
}
{
obj v1_seq
= GCObjectConversion<obj<AGCObject,DArray>>::from_gco(mm /*not used*/, l1);
REQUIRE(!v1_seq);
}
}
} /*namespace ut*/
} /*namespace xo*/
/* end GCObjectConversion.cpp */

View file

@ -341,7 +341,7 @@ namespace ut {
Testcase(2, 1, 16 * 1024, 8 * 128, T, seq_2, 128, T, c_fixed, 3, 0, 0, 0, 0, F),
Testcase(2, 2, 16 * 1024, 8 * 128, T, seq_3, 128, T, c_fixed, 4, 0, 0, 0, 0, F),
Testcase(2, 2, 16 * 1024, 8 * 128, T, seq_4, 128, T, c_fixed, 4, 0, 0, 0, 0, F),
Testcase(2, 2, 16 * 1024, 8 * 128, T, seq_5, 128, T, c_fixed, 4, 0, 0, 0, 0, T),
Testcase(2, 2, 16 * 1024, 8 * 128, T, seq_5, 128, T, c_fixed, 4, 0, 0, 0, 0, F),
};
# undef T

View file

@ -13,7 +13,6 @@
#include <xo/object2/Float.hpp>
#include <xo/object2/Integer.hpp>
#include <xo/object2/List.hpp>
//#include "list/IGCObject_DList.hpp"
#include <xo/gc/X1Collector.hpp>
//#include <xo/alloc2/Collector.hpp>