From 49ac2200a2a05b2417e5dccda248efc9518dbf95 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 22 Dec 2025 22:24:10 -0500 Subject: [PATCH 001/174] xo-gc: refactor to extract DX1Collector etc from xo-alloc2/ --- CMakeLists.txt | 32 ++ cmake/xo-bootstrap-macros.cmake | 35 +++ cmake/xo-gcConfig.cmake.in | 7 + include/xo/gc/Collector.hpp | 13 + include/xo/gc/DX1Collector.hpp | 261 ++++++++++++++++ include/xo/gc/DX1CollectorIterator.hpp | 95 ++++++ include/xo/gc/GCObject.hpp | 13 + include/xo/gc/detail/ACollector.hpp | 60 ++++ .../IAllocIterator_DX1CollectorIterator.hpp | 36 +++ .../xo/gc/detail/IAllocator_DX1Collector.hpp | 77 +++++ include/xo/gc/detail/ICollector_Any.hpp | 51 ++++ .../xo/gc/detail/ICollector_DX1Collector.hpp | 55 ++++ include/xo/gc/detail/ICollector_Xfer.hpp | 75 +++++ include/xo/gc/detail/RCollector.hpp | 51 ++++ include/xo/gc/gcobject/AGCObject.hpp | 53 ++++ include/xo/gc/gcobject/IGCObject_Any.hpp | 48 +++ include/xo/gc/gcobject/IGCObject_Xfer.hpp | 62 ++++ include/xo/gc/gcobject/RGCObject.hpp | 47 +++ include/xo/gc/generation.hpp | 36 +++ include/xo/gc/object_age.hpp | 32 ++ include/xo/gc/role.hpp | 35 +++ src/gc/CMakeLists.txt | 21 ++ src/gc/DX1Collector.cpp | 289 ++++++++++++++++++ src/gc/DX1CollectorIterator.cpp | 126 ++++++++ .../IAllocIterator_DX1CollectorIterator.cpp | 41 +++ src/gc/IAllocator_DX1Collector.cpp | 127 ++++++++ src/gc/ICollector_Any.cpp | 39 +++ src/gc/ICollector_DX1Collector.cpp | 191 ++++++++++++ src/gc/IGCObject_Any.cpp | 34 +++ utest/CMakeLists.txt | 21 ++ utest/Collector.test.cpp | 243 +++++++++++++++ utest/DX1CollectorIterator.test.cpp | 221 ++++++++++++++ utest/gc_utest_main.cpp | 6 + utest/random_allocs.cpp | 217 +++++++++++++ utest/random_allocs.hpp | 45 +++ 35 files changed, 2795 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 cmake/xo-bootstrap-macros.cmake create mode 100644 cmake/xo-gcConfig.cmake.in create mode 100644 include/xo/gc/Collector.hpp create mode 100644 include/xo/gc/DX1Collector.hpp create mode 100644 include/xo/gc/DX1CollectorIterator.hpp create mode 100644 include/xo/gc/GCObject.hpp create mode 100644 include/xo/gc/detail/ACollector.hpp create mode 100644 include/xo/gc/detail/IAllocIterator_DX1CollectorIterator.hpp create mode 100644 include/xo/gc/detail/IAllocator_DX1Collector.hpp create mode 100644 include/xo/gc/detail/ICollector_Any.hpp create mode 100644 include/xo/gc/detail/ICollector_DX1Collector.hpp create mode 100644 include/xo/gc/detail/ICollector_Xfer.hpp create mode 100644 include/xo/gc/detail/RCollector.hpp create mode 100644 include/xo/gc/gcobject/AGCObject.hpp create mode 100644 include/xo/gc/gcobject/IGCObject_Any.hpp create mode 100644 include/xo/gc/gcobject/IGCObject_Xfer.hpp create mode 100644 include/xo/gc/gcobject/RGCObject.hpp create mode 100644 include/xo/gc/generation.hpp create mode 100644 include/xo/gc/object_age.hpp create mode 100644 include/xo/gc/role.hpp create mode 100644 src/gc/CMakeLists.txt create mode 100644 src/gc/DX1Collector.cpp create mode 100644 src/gc/DX1CollectorIterator.cpp create mode 100644 src/gc/IAllocIterator_DX1CollectorIterator.cpp create mode 100644 src/gc/IAllocator_DX1Collector.cpp create mode 100644 src/gc/ICollector_Any.cpp create mode 100644 src/gc/ICollector_DX1Collector.cpp create mode 100644 src/gc/IGCObject_Any.cpp create mode 100644 utest/CMakeLists.txt create mode 100644 utest/Collector.test.cpp create mode 100644 utest/DX1CollectorIterator.test.cpp create mode 100644 utest/gc_utest_main.cpp create mode 100644 utest/random_allocs.cpp create mode 100644 utest/random_allocs.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..4480a1ad --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,32 @@ +# xo-gc/CMakeLists.txt + +cmake_minimum_required(VERSION 3.10) + +project(xo_gc VERSION 0.1) + +include(GNUInstallDirs) +include(cmake/xo-bootstrap-macros.cmake) + +xo_cxx_toplevel_options3() + +# ---------------------------------------------------------------- +# c++ settings + +set(PROJECT_CXX_FLAGS "") +#set(PROJECT_CXX_FLAGS "-fconcepts-diagnostics-depth=2") # gcc-only! +add_definitions(${PROJECT_CXX_FLAGS}) + +# ---------------------------------------------------------------- + +# must complete definition of expression lib before configuring examples +add_subdirectory(src/gc) +add_subdirectory(utest) +#xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +# ---------------------------------------------------------------- +# docs targets depend on other library/utest/exec targets above, +# --> must come after them. +# +#add_subdirectory(docs) + +# end CMakeLists.txt diff --git a/cmake/xo-bootstrap-macros.cmake b/cmake/xo-bootstrap-macros.cmake new file mode 100644 index 00000000..aba31169 --- /dev/null +++ b/cmake/xo-bootstrap-macros.cmake @@ -0,0 +1,35 @@ +# ---------------------------------------------------------------- +# for example: +# $ PREFIX=/usr/local # for example +# $ cmake -DCMAKE_MODULE_PATH=prefix -DCMAKE_INSTALL_PREFIX=$PREFIX -B .build +# +# will get +# CMAKE_MODULE_PATH +# from xo-cmake-config --cmake-module-path +# +# and expect .cmake macros in +# CMAKE_MODULE_PATH/xo_macros/xo_cxx.cmake +# ---------------------------------------------------------------- + +find_program(XO_CMAKE_CONFIG_EXECUTABLE NAMES xo-cmake-config REQUIRED) + +if ("${XO_CMAKE_CONFIG_EXECUTABLE}" STREQUAL "XO_CMAKE_CONFIG_EXECUTABLE-NOT_FOUND") + message(FATAL "could not find xo-cmake-config executable") +endif() + +message(STATUS "XO_CMAKE_CONFIG_EXECUTABLE=${XO_CMAKE_CONFIG_EXECUTABLE}") + +if (NOT XO_SUBMODULE_BUILD) + if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix)) + # default to typical install location for xo-project-macros + execute_process(COMMAND ${XO_CMAKE_CONFIG_EXECUTABLE} --cmake-module-path OUTPUT_VARIABLE CMAKE_MODULE_PATH) + message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}") + endif() +endif() + +# needs to have been installed somewhere on CMAKE_MODULE_PATH, +# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX) +# +include(xo_macros/xo_cxx) + +xo_cxx_bootstrap_message() diff --git a/cmake/xo-gcConfig.cmake.in b/cmake/xo-gcConfig.cmake.in new file mode 100644 index 00000000..c32a8368 --- /dev/null +++ b/cmake/xo-gcConfig.cmake.in @@ -0,0 +1,7 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +#find_dependency(indentlog) +find_dependency(xo_alloc2) +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +check_required_components("@PROJECT_NAME@") diff --git a/include/xo/gc/Collector.hpp b/include/xo/gc/Collector.hpp new file mode 100644 index 00000000..b4214036 --- /dev/null +++ b/include/xo/gc/Collector.hpp @@ -0,0 +1,13 @@ +/** @file Collector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "detail/ACollector.hpp" +#include "detail/ICollector_Any.hpp" +#include "detail/ICollector_Xfer.hpp" +#include "detail/RCollector.hpp" + +/* end Collector.hpp */ diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp new file mode 100644 index 00000000..778f0518 --- /dev/null +++ b/include/xo/gc/DX1Collector.hpp @@ -0,0 +1,261 @@ +/** @file DX1Collector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "arena/ArenaConfig.hpp" +#include "arena/DArena.hpp" +#include "generation.hpp" +#include "object_age.hpp" +#include "role.hpp" +#include +#include + +namespace xo { + namespace mm { + template + using up = std::unique_ptr; + +#ifdef NOT_YET + /** State associated with a single DX1Collector generation + **/ + struct Generation { + Generation(uint8_t gen_id, up from_space, up to_space); + ~Generation() = default; + + /** identity of this generation. Generations are numbered from + * 0 (youngest) to N (oldest), with N <= c_max_generation + **/ + uint8_t gen_id_; + /** from-space. empty between collection episodes. + * During collection holds former to-space + **/ + up from_space_; + /** to-space. New allocations occur here **/ + up to_space_; + }; +#endif + + struct CollectorConfig { + using size_type = std::size_t; + +#ifdef OBSOLETE // get from arena_config_.header_ + /* + * alloc header + * TTTTTTTTTTTTGGGGGZZZZZZZZZZZZ + * < tseq >< size > + * + * masking + * + * ..432107654321076543210 bit + * + * > < .gen_bits + * 0..............01111111 gen_mask_unshifted + * 0..011111110..........0 gen_mask_shifted + * > < gen_shift + */ + //constexpr std::uint64_t gen_mult() const; + constexpr std::uint64_t gen_shift() const; + constexpr std::uint64_t gen_mask_unshifted() const; + constexpr std::uint64_t gen_mask_shifted() const; + + //constexpr std::uint64_t tseq_mult() const; + constexpr std::uint64_t tseq_shift() const; + constexpr std::uint64_t tseq_mask_unshifted() const; + constexpr std::uint64_t tseq_mask_shifted() const; +#endif + + generation age2gen(object_age age) const noexcept { + return generation(age % n_survive_threshold_); + } + + public: + // ----- Instance Variables ----- + + /** optional name, for diagnostics **/ + std::string name_; + + /** Configuration for collector spaces. + * Will have at least {nursery,tenured} x {from,to} spaces. + * Not using name_ member. + * + * REQUIRE: + * - arena_config_.store_header_flag_ must be true + **/ + ArenaConfig arena_config_; + + /** number of bits to represent generation **/ + std::uint64_t gen_bits_ = 8; + + /** number of bits to represent tseq **/ + std::uint64_t tseq_bits_ = 24; + + /** Number of generations. + * Must be at least 2. + **/ + uint32_t n_generation_ = 2; + + /** Number of promotion steps. + * An object that survives this number of collections + * advances to the next generation. + **/ + uint32_t n_survive_threshold_ = 2; + + /** Trigger garbage collection when to-space allocation for + * generation g reaches gc_trigger_v_[g] + **/ + std::array gc_trigger_v_; + + /** true -> enable incremental collection. + * false -> only do full collection. + * + * Incremental collection requires mutation logs. + **/ + bool allow_incremental_gc_ = true; + + /** If non-zero remember statistics for + * the last @p stats_history_z_ collections. + **/ + uint32_t stats_history_z_ = false; + + /** true to enable debug logging **/ + bool debug_flag_ = false; + }; + + // ----- GCRunState ----- + + /** @class GCRunState + * @brief encapsulate state needed while GC is running + **/ + struct GCRunState { + GCRunState() : gc_upto_{0} {} + explicit GCRunState(generation gc_upto); + + static GCRunState gc_not_running(); + static GCRunState gc_upto(generation g); + + bool is_running() const { return gc_upto_ > 0; } + + generation gc_upto() const { return gc_upto_; } + + private: + /** running gc collecting all generations gi < gc_upto **/ + generation gc_upto_; + }; + + struct DX1CollectorIterator; + + // ----- DX1Collector ----- + + struct DX1Collector { + using size_type = DArena::size_type; + using value_type = DArena::value_type; + using header_type = DArena::header_type; + + explicit DX1Collector(const CollectorConfig & cfg); + + const DArena * get_space(role r, generation g) const noexcept { return space_[r][g]; } + 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}); } + + /** total reserved memory in bytes, across all {role, generation} **/ + size_type reserved_total() const noexcept; + /** total size in bytes (same as committed_total()) **/ + size_type size_total() const noexcept; + /** total committed memory in bytes, across all {role, generation} **/ + size_type committed_total() const noexcept; + /** total available memory in bytes, across all {role, generation} **/ + size_type available_total() const noexcept; + /** total allocated memory in bytes, across all {role, generation} **/ + size_type allocated_total() const noexcept; + + /** true iff address @p addr allocated from this collector + * in role @p r (according to current GC state) + **/ + bool contains(role r, const void * addr) const noexcept; + + /** return details from last error (will be in gen0 to-space) **/ + AllocError last_error() const noexcept; + + /** get allocation size from header **/ + std::size_t header2size(header_type hdr) const noexcept; + /** get generation counter from alloc header **/ + object_age header2age(header_type hdr) const noexcept; + /** get tseq from alloc header **/ + uint32_t header2tseq(header_type hdr) const noexcept; + + /** true iff original alloc has been replaced by a forwarding pointer **/ + bool is_forwarding_header(header_type hdr) const noexcept; + + /** Retreive bookkeeping info for allocation at @p mem. **/ + AllocInfo alloc_info(value_type mem) const noexcept; + + // ----- allocation ----- + + /** simple allocation. new allocs always in gen0 to-space **/ + value_type alloc(size_type z) noexcept; + /** compound allocation. To be followed immediately by: + * 1. zero or more calls to sub_alloc(zi, complete=false), then + * 2. exactly one call to sub_alloc(zi, complete=true) + * all the allocs in a compound allocation share the same + * allocation header. End state is equivalent to a single + * allocation with size z + sum(zi). + * New allocs always in gen0 to-space + **/ + value_type super_alloc(size_type z) noexcept; + /** sub-allocation with preceding compound allocation. + * New allocs always in gen0 to-space + **/ + value_type sub_alloc(size_type z, bool complete) noexcept; + /** expand gen0 committed size to at least @p z. + **/ + bool expand(size_type z) noexcept; + + // ----- iteration ----- + + /** alloc iterator at begin position **/ + DX1CollectorIterator begin() const noexcept; + /** alloc iterator at end position + * (valid, but cannot be dereferenced) + **/ + DX1CollectorIterator end() const noexcept; + + // ----- book-keeping ----- + + /** reverse to-space and from-space roles for generation g **/ + void reverse_roles(generation g) noexcept; + + /** discard all allocated memory **/ + void clear() noexcept; + + public: + /** garbage collector configuration **/ + CollectorConfig config_; + + /** current gc state **/ + GCRunState runstate_; + + /** collector-managed memory here. + * - space_[1] is from-space + * - space_[0] is to-space + * coordinates with role ingc/role.hpp, see also. + **/ + + /** arena objects for collector managed memory + * 1:1 with roles, but polarity reverses for each collection + **/ + std::array space_storage_[c_n_role]; + + /** arena pointers. The roles of space_storage_[0][g] and space_storage_[1][g] + * are reversed each time generation g gets collected. + **/ + std::array space_[c_n_role]; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DX1Collector.hpp */ diff --git a/include/xo/gc/DX1CollectorIterator.hpp b/include/xo/gc/DX1CollectorIterator.hpp new file mode 100644 index 00000000..af1e867a --- /dev/null +++ b/include/xo/gc/DX1CollectorIterator.hpp @@ -0,0 +1,95 @@ +/** @file DX1CollectorIterator.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AllocInfo.hpp" +#include "generation.hpp" +#include "arena/DArenaIterator.hpp" +#include "cmpresult.hpp" + +namespace xo { + namespace mm { + struct DX1Collector; + + /** @class DX1CollectorIterator + * @brief Representation for alloc iterator over X1 collector + * + * Will iterate across all allocs in all generations + **/ + struct DX1CollectorIterator { + DX1CollectorIterator() = default; + DX1CollectorIterator(const DX1Collector * gc, + generation gen_ix, + generation gen_hi, + DArenaIterator arena_ix, + DArenaIterator arena_hi); + + /** Invalid iterator. Does not compare equal to anything, including itself **/ + static DX1CollectorIterator invalid() { return DX1CollectorIterator(); } + /** Create iterator pointing to the beginning of @p gc. + * Iterator cannot modify payload memory + **/ + static DX1CollectorIterator begin(DX1Collector * gc); + /** Create iterator pointing to the end of @p gc. + * Iterator cannot modify payload memory. + **/ + static DX1CollectorIterator end(DX1Collector * gc); + + /** true if iterator is invalid. invalid iterators are not comparable **/ + bool is_valid() const noexcept { return (gc_ != nullptr); } + bool is_invalid() const noexcept { return !is_valid(); } + + generation gen_ix() const { return gen_ix_; } + generation gen_hi() const { return gen_hi_; } + DArenaIterator arena_ix() const { return arena_ix_; } + DArenaIterator arena_hi() const { return arena_hi_; } + + /** fetch contents at current iterator position **/ + AllocInfo deref() const noexcept; + /** compare two iterators. To be comparable, + * iterators must refer to the same collector + **/ + cmpresult compare(const DX1CollectorIterator & other) const noexcept; + /** advance iterator to next allocation **/ + void next() noexcept; + + /** for *ix synonym for ix.deref() **/ + AllocInfo operator*() const noexcept { return this->deref(); } + + private: + /** if non-empty, normalize to state with arena_ix_ != arena_hi_ **/ + void normalize() noexcept; + + private: + /** Iterator visits allocations from this collector **/ + const DX1Collector * gc_ = nullptr; + /** Iterating over generations in [@p gen_ix_, @p gen_hi_). + * Current position is within arena for @p gen_ix_ to-space, + * Provided @p gen_ix_ < @p gen_hi_ + **/ + generation gen_ix_; + generation gen_hi_; + /** Iterating over allocs in [@p arena_ix_, @p arena_hi_). + * Current position is at @p arena_ix_ + **/ + DArenaIterator arena_ix_; + DArenaIterator arena_hi_; + }; + + inline bool + operator==(const DX1CollectorIterator & x, const DX1CollectorIterator & y) { + return x.compare(y).is_equal(); + } + + inline bool + operator!=(const DX1CollectorIterator & x, const DX1CollectorIterator & y) { + return !x.compare(y).is_equal(); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DX1CollectorIterator.hpp */ diff --git a/include/xo/gc/GCObject.hpp b/include/xo/gc/GCObject.hpp new file mode 100644 index 00000000..edc89c32 --- /dev/null +++ b/include/xo/gc/GCObject.hpp @@ -0,0 +1,13 @@ +/** @file GCObject.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "gcobject/AGCObject.hpp" +#include "gcobject/IGCObject_Any.hpp" +#include "gcobject/IGCObject_Xfer.hpp" +#include "gcobject/RGCObject.hpp" + +/* end GCObject.hpp */ diff --git a/include/xo/gc/detail/ACollector.hpp b/include/xo/gc/detail/ACollector.hpp new file mode 100644 index 00000000..9c32125d --- /dev/null +++ b/include/xo/gc/detail/ACollector.hpp @@ -0,0 +1,60 @@ +/** @file ACollector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "gcobject/IGCObject_Any.hpp" + +#include +#include +#include + +#include "generation.hpp" +#include "role.hpp" + +#include +#include + +namespace xo { + namespace mm { + using Copaque = const void *; + using Opaque = void *; + + struct IGCObject_Any; // see IGCObject_Any.hpp + + /** @class ACollector + * @brief Abstract facet for the XO garbage collector + * + * Collector also supports the @ref AAllocator facet, see also + **/ + struct ACollector { + using size_type = std::size_t; + + virtual int32_t _typeseq() const noexcept = 0; + + virtual size_type allocated(Copaque d, generation g, role r) const noexcept = 0; + virtual size_type reserved(Copaque d, generation g, role r) const noexcept = 0; + virtual size_type committed(Copaque d, generation g, role r) const noexcept = 0; + + /** 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. + **/ + virtual void install_type(Opaque d, int32_t tseq, IGCObject_Any & iface) = 0; + virtual void add_gc_root(Opaque d, int32_t tseq, Opaque * root) = 0; + + /** evacuate @p *lhs to to-space and replace with forwarding pointer + * Require: gc in progress + **/ + virtual void forward_inplace(Opaque d, obj * lhs) = 0; + }; + } + +} /*namespace xo*/ diff --git a/include/xo/gc/detail/IAllocIterator_DX1CollectorIterator.hpp b/include/xo/gc/detail/IAllocIterator_DX1CollectorIterator.hpp new file mode 100644 index 00000000..c221b580 --- /dev/null +++ b/include/xo/gc/detail/IAllocIterator_DX1CollectorIterator.hpp @@ -0,0 +1,36 @@ +/** @file IAllocIterator_DX1Collector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include +#include "xo/gc/DX1CollectorIterator.hpp" + +namespace xo { + namespace mm { struct IAllocIterator_DX1CollectorIterator; } + + namespace facet { + template <> + struct FacetImplementation { + using ImplType = xo::mm::IAllocIterator_Xfer; + }; + } + + namespace mm { + /** @class IAllocIterator_DX1Collector + * @brief alloc iteration for the DX1Collector allocator + **/ + struct IAllocIterator_DX1CollectorIterator { + static AllocInfo deref(const DX1CollectorIterator &) noexcept; + static cmpresult compare(const DX1CollectorIterator &, + const obj & other) noexcept; + static void next(DX1CollectorIterator &) noexcept; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IAllocIterator_DX1Collector.hpp */ diff --git a/include/xo/gc/detail/IAllocator_DX1Collector.hpp b/include/xo/gc/detail/IAllocator_DX1Collector.hpp new file mode 100644 index 00000000..c525dadb --- /dev/null +++ b/include/xo/gc/detail/IAllocator_DX1Collector.hpp @@ -0,0 +1,77 @@ +/** @file IAllocator_DX1Collector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "Allocator.hpp" +#include "DX1Collector.hpp" + +namespace xo { + namespace mm { struct IAllocator_DX1Collector; } + + namespace facet { + template <> + struct FacetImplementation + { + using ImplType = xo::mm::IAllocator_Xfer; + }; + } + + namespace mm { + /* changes here coordinate with + * AAllocator AAllocator.hpp + * IAllocator_Any IAllocator_Any.hpp + * IAllocator_Xfer IAllocator_Xfer.hpp + * RAllocator RCollector.hpp + */ + struct IAllocator_DX1Collector { + using size_type = std::size_t; + using value_type = std::byte *; + using range_type = AAllocator::range_type; + + // todo: available() + + static std::string_view name(const DX1Collector &) noexcept; + /** reserved memory across all {roles, generations} **/ + static size_type reserved(const DX1Collector &) noexcept; + /** 'size'. synonym for committed size **/ + static size_type size(const DX1Collector &) noexcept; + /** committed size accross all {roles, generations} **/ + static size_type committed(const DX1Collector &) noexcept; + /** available (committed but unused) space across all {roles, generations} **/ + static size_type available(const DX1Collector &) noexcept; + /** space used by @p d across all {roles, generations}. **/ + static size_type allocated(const DX1Collector &) noexcept; + /** true iff address @p p comes from collector @p d **/ + static bool contains(const DX1Collector & d, const void * p) noexcept; + /** report last error, if any, for collector @p d **/ + static AllocError last_error(const DX1Collector &) noexcept; + /** fetch allocation bookkeeping info **/ + static AllocInfo alloc_info(const DX1Collector & d, value_type mem) noexcept; + /** create alloc-iterator range over allocs in @d, + * with iterator working storage obtained from @p ialloc + **/ + static range_type alloc_range(const DX1Collector & d, DArena & ialloc) noexcept; + + /** always alloc in gen0 to-space **/ + static value_type alloc(DX1Collector & d, size_type z) noexcept; + static value_type super_alloc(DX1Collector & d, size_type z) noexcept; + static value_type sub_alloc(DX1Collector & d, size_type z, bool complete) noexcept; + /** expand gen0 spaces (both from-space and to-space) **/ + static bool expand(DX1Collector & d, size_type z) noexcept; + + /** reset to empty state; clears all generations **/ + static void clear(DX1Collector & d); + /** invoke destructor **/ + static void destruct_data(DX1Collector & d); + }; + + } /*namespace mm*/ +} /*namespace xo*/ + + +/* end IAllocator_DX1Collector.hpp */ diff --git a/include/xo/gc/detail/ICollector_Any.hpp b/include/xo/gc/detail/ICollector_Any.hpp new file mode 100644 index 00000000..c9779687 --- /dev/null +++ b/include/xo/gc/detail/ICollector_Any.hpp @@ -0,0 +1,51 @@ +/** @file ICollector_Any.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "ACollector.hpp" +//#include + +namespace xo { + namespace mm { struct ICollector_Any; } + + namespace facet { + template <> + struct FacetImplementation { + using ImplType = xo::mm::ICollector_Any; + }; + } + + namespace mm { + /** @class ICollector_Any + * @brief Stub Collector Implementation for empty variant instance + **/ + struct ICollector_Any : public ACollector { + using size_type = std::size_t; + + // from ACollector + int32_t _typeseq() const noexcept override { return s_typeseq; } + + // const methods + [[noreturn]] size_type allocated(Copaque, generation, role) const noexcept override { _fatal(); } + [[noreturn]] size_type reserved(Copaque, generation, role) const noexcept override { _fatal(); } + [[noreturn]] size_type committed(Copaque, generation, role) const noexcept override { _fatal(); } + + // non-const methods + [[noreturn]] void install_type(Opaque, int32_t, IGCObject_Any &) noexcept override { _fatal(); } + [[noreturn]] void add_gc_root(Opaque, int32_t, Opaque *) override { _fatal(); } + [[noreturn]] void forward_inplace(Opaque, obj *) override { _fatal(); } + + private: + [[noreturn]] static void _fatal(); + + public: + static int32_t s_typeseq; + static bool _valid; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ICollector_Any.hpp */ diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp new file mode 100644 index 00000000..9bc6e129 --- /dev/null +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -0,0 +1,55 @@ +/** @file ICollector_DX1Collector.hpp +* + * @author Roland Conybeare, Dec 2025 + **/ + +#include "ACollector.hpp" +#include "ICollector_Xfer.hpp" +#include "DX1Collector.hpp" + +namespace xo { + namespace mm { struct ICollector_DX1Collector; } + + namespace facet { + template <> + struct FacetImplementation + { + using ImplType = xo::mm::ICollector_Xfer; + }; + } + + namespace mm { + /* changes here coordinate with + * ACollector ACollector.hpp + * ICollector_Any ICollector_Any.hpp + * ICollector_Xfer ICollector_Xfer.hpp + * RCollector RCollector.hpp + */ + struct ICollector_DX1Collector { + using size_type = std::size_t; + using header_type = DArena::header_type; + + static bool check_move_policy(const DX1Collector & d, + header_type alloc_hdr, + void * object_data); + + // todo: available() + + static size_type allocated(const DX1Collector & d, generation g, role r); + static size_type reserved(const DX1Collector & d, generation g, role r); + static size_type committed(const DX1Collector & d, generation g, role r); + + static void install_type(DX1Collector & d, + int32_t seq, IGCObject_Any & iface); + static void add_gc_root(DX1Collector & d, int32_t tseq, Opaque * root); + static void forward_inplace(DX1Collector & d, obj * lhs); + + static int32_t s_typeseq; + static bool _valid; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ICollector_DX1_Collector.hpp */ diff --git a/include/xo/gc/detail/ICollector_Xfer.hpp b/include/xo/gc/detail/ICollector_Xfer.hpp new file mode 100644 index 00000000..2177b63a --- /dev/null +++ b/include/xo/gc/detail/ICollector_Xfer.hpp @@ -0,0 +1,75 @@ +/** @file ICollector_Xfer.hpp + * + * @author Roland Conybeare, 2025 + **/ + +#pragma once + +#include "ACollector.hpp" + +namespace xo { + namespace mm { + /** @class ICollector_Xfer + * + * Adapts typed ACollector implementation @tparam ICollector_DRepr + * to type-erased @ref ACollector interface + * + * See for example + * @ref ICollector_DX1Collector + **/ + template + struct ICollector_Xfer : public ACollector { + public: + using Impl = ICollector_DRepr; + using size_type = ACollector::size_type; + + static const DRepr & _dcast(Copaque d) { return *(const DRepr *)d; } + static DRepr & _dcast(Opaque d) { return *(DRepr *)d; } + + // from ACollector + + // const methods + + int32_t _typeseq() const noexcept override { return s_typeseq; } + size_type allocated(Copaque d, generation g, role r) const noexcept override { + return I::allocated(_dcast(d), g, r); + } + size_type reserved(Copaque d, generation g, role r) const noexcept override { + return I::reserved(_dcast(d), g, r); + } + size_type committed(Copaque d, generation g, role r) const noexcept override { + return I::committed(_dcast(d), g, r); + } + + // non-const methods + + void install_type(Opaque d, int32_t tseq, IGCObject_Any & iface) override { + I::install_type(_dcast(d), tseq, iface); + } + void add_gc_root(Opaque d, int32_t tseq, Opaque * root) override { + I::add_gc_root(_dcast(d), tseq, root); + } + void forward_inplace(Opaque d, obj * lhs) override { + I::forward_inplace(_dcast(d), lhs); + } + + private: + using I = Impl; + + public: + static int32_t s_typeseq; + static bool _valid; + }; + + template + int32_t + ICollector_Xfer::s_typeseq = facet::typeseq::id(); + + template + bool + ICollector_Xfer::_valid = facet::valid_facet_implementation(); + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ICollector_Xfer.hpp */ diff --git a/include/xo/gc/detail/RCollector.hpp b/include/xo/gc/detail/RCollector.hpp new file mode 100644 index 00000000..753af3c7 --- /dev/null +++ b/include/xo/gc/detail/RCollector.hpp @@ -0,0 +1,51 @@ +/** @file RCollector.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "ACollector.hpp" +#include + +namespace xo { + namespace mm { + /** @class RCollector **/ + template + struct RCollector : public Object { + private: + using O = Object; + public: + using ObjectType = Object; + using DataPtr = Object::DataPtr; + using size_type = std::size_t; + //using value_type = std::byte *; + + RCollector() = default; + RCollector(DataPtr data) : Object{std::move(data)} {} + + int32_t _typeseq() const noexcept { return O::iface()->_typeseq(); } + size_type allocated(generation g, role r) const noexcept { return O::iface()->allocated(O::data(), g, r); } + size_type reserved(generation g, role r) const noexcept { return O::iface()->reserved(O::data(), g, r); } + size_type committed(generation g, role r) const noexcept { return O::iface()->committed(O::data(), g, r); } + + void install_type(int32_t tseq, IGCObject_Any & iface) { return O::iface()->install_type(O::data(), tseq, iface); } + void add_gc_root(int32_t tseq, Opaque * root) { O::iface()->add_gc_root(O::data(), tseq, root); } + + void forward_inplace(obj * lhs) { O::iface()->forward_inplace(O::data(), lhs); } + + static bool _valid; + }; + + template + bool + RCollector::_valid = facet::valid_object_router(); + } /*namespace mm*/ + + namespace facet { + template + struct RoutingFor { + using RoutingType = xo::mm::RCollector; + }; + } +} /*namespace xo*/ + +/* end RCollector.hpp */ diff --git a/include/xo/gc/gcobject/AGCObject.hpp b/include/xo/gc/gcobject/AGCObject.hpp new file mode 100644 index 00000000..1e0a6d45 --- /dev/null +++ b/include/xo/gc/gcobject/AGCObject.hpp @@ -0,0 +1,53 @@ +/** @file AGCObject.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "Allocator.hpp" +#include "xo/facet/facet_implementation.hpp" +#include "xo/facet/typeseq.hpp" +#include "xo/facet/obj.hpp" // for obj in shallow_copy +#include +#include + +namespace xo { + namespace mm { + using Copaque = const void *; + using Opaque = void *; + + /** @class AObject + * @brief Abstract facet for collector-eligible data + * + * Data that supports AGCObject can have memory managed + * by ACollector + **/ + struct AGCObject { + using size_type = std::size_t; + + /** RTTI: unique id# for actual runtime data representation **/ + virtual int32_t _typeseq() const noexcept = 0; + + virtual size_type shallow_size(Copaque d) const noexcept = 0; + virtual Opaque * shallow_copy(Copaque d, + obj mm) const noexcept = 0; + virtual size_type forward_children(Opaque d) const noexcept = 0; + }; + + // implementation IGCObject_DRepr of AGCObject for state DRepr + // should provide a specialization: + // + // template <> + // struct xo::facet::FacetImplementation { + // using ImplType = IGCObject_DRepr; + // }; + // + // then IGCObject_ImplType --> IGCObject_DRepr + // + template + using IGCObject_ImplType = xo::facet::FacetImplType; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end AGCObject.hpp */ diff --git a/include/xo/gc/gcobject/IGCObject_Any.hpp b/include/xo/gc/gcobject/IGCObject_Any.hpp new file mode 100644 index 00000000..039a1259 --- /dev/null +++ b/include/xo/gc/gcobject/IGCObject_Any.hpp @@ -0,0 +1,48 @@ +/** @file IGCObject_Any.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AGCObject.hpp" +#include + +namespace xo { + namespace mm { struct IGCObject_Any; } + + namespace facet { + template <> + struct FacetImplementation { + using ImplType = xo::mm::IGCObject_Any; + }; + } + + namespace mm { + /** @class IGCObject_Any + * @brief AGCObject implementation for empty variant instance + **/ + struct IGCObject_Any : public AGCObject { + using size_type = std::size_t; + + const AGCObject * iface() const { return std::launder(this); } + + // from AGCObject + int32_t _typeseq() const noexcept override { return s_typeseq; } + + [[noreturn]] size_type shallow_size(Copaque) const noexcept override { _fatal(); } + [[noreturn]] Opaque * shallow_copy(Copaque, + obj) const noexcept override { _fatal(); } + [[noreturn]] size_type forward_children(Opaque) const noexcept override { _fatal(); } + + private: + [[noreturn]] static void _fatal(); + + public: + static int32_t s_typeseq; + static bool _valid; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IGCObject_Any.hpp */ diff --git a/include/xo/gc/gcobject/IGCObject_Xfer.hpp b/include/xo/gc/gcobject/IGCObject_Xfer.hpp new file mode 100644 index 00000000..75f78fd6 --- /dev/null +++ b/include/xo/gc/gcobject/IGCObject_Xfer.hpp @@ -0,0 +1,62 @@ +/** @file IGCObject_Xfer.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AGCObject.hpp" + +namespace xo { + namespace mm { + /** @class IGCObject_Xfer + * + * Adapts typed GC object implementation @tparam IGCObject_DRepr + * to type-erased @ref AGCObject interface + **/ + template + struct IGCObject_Xfer : public AGCObject { + using Impl = IGCObject_DRepr; + using size_type = AGCObject::size_type; + + static const DRepr & _dcast(Copaque d) { return *(const DRepr *)d; } + static DRepr & _dcast(Opaque d) { return *(DRepr *)d; } + + // from AGCObject + + // const methods + + int32_t _typeseq() const noexcept override { return s_typeseq; } + size_type shallow_size(Copaque d) const noexcept override { + return I::shallow_copy(_dcast(d)); + } + Opaque * shallow_copy(Copaque d, obj mm) const noexcept override { + return I::shallow_size(_dcast(d), mm); + } + + // non-const methods + + size_type forward_children(Opaque d) const noexcept override { + return I::forward_children(d); + } + + private: + using I = Impl; + + public: + static int32_t s_typeseq; + static bool _valid; + }; + + template + int32_t + IGCObject_Xfer::s_typeseq = facet::typeseq::id(); + + template + bool + IGCObject_Xfer::_valid = facet::valid_facet_implementation(); + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IGCObject_Xfer.hpp */ diff --git a/include/xo/gc/gcobject/RGCObject.hpp b/include/xo/gc/gcobject/RGCObject.hpp new file mode 100644 index 00000000..054b995e --- /dev/null +++ b/include/xo/gc/gcobject/RGCObject.hpp @@ -0,0 +1,47 @@ +/** @file RGCObject.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "AGCObject.hpp" +#include + +namespace xo { + namespace mm { + /** @class RGCObject **/ + template + struct RGCObject : public Object { + private: + using O = Object; + public: + using ObjectType = Object; + using DataPtr = Object::DataPtr; + using size_type = std::size_t; + + RGCObject() = default; + RGCObject(Object::DataPtr data) : Object{std::move(data)} {} + + int32_t _typeseq() const noexcept { return O::iface()->_typeseq(); } + size_type shallow_size() const noexcept { O::iface()->shallow_size(O::data()); } + Opaque * shallow_copy(obj mm) const noexcept { O::iface()->shallow_copy(O::data(), mm); } + size_type forward_children() noexcept { O::iface()->forward_children(O::data()); } + + static bool _valid; + }; + + template + bool + RGCObject::_valid = facet::valid_object_router(); + } /*namespace mm*/ + + namespace facet { + template + struct RoutingFor { + using RoutingType = xo::mm::RGCObject; + }; + } +} /*namespace xo*/ + +/* end RGCObject.hpp */ diff --git a/include/xo/gc/generation.hpp b/include/xo/gc/generation.hpp new file mode 100644 index 00000000..12b36d06 --- /dev/null +++ b/include/xo/gc/generation.hpp @@ -0,0 +1,36 @@ +/** @file generation.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace mm { + /** hard maximum number of generations **/ + static constexpr uint32_t c_max_generation = 16; + + /** @class generation + * @brief type-safe generation number + **/ + struct generation { + using value_type = std::uint32_t; + + constexpr generation() = default; + explicit constexpr generation(value_type x) : value_{x} {} + + static generation nursery() { return generation{0}; } + + constexpr operator value_type() const { return value_; } + + generation & operator++() { ++value_; return *this; } + + std::uint32_t value_ = 0; + }; + } +} + +/* end generation.hpp */ diff --git a/include/xo/gc/object_age.hpp b/include/xo/gc/object_age.hpp new file mode 100644 index 00000000..ae876bda --- /dev/null +++ b/include/xo/gc/object_age.hpp @@ -0,0 +1,32 @@ +/** @file object_age.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include + +namespace xo { + namespace mm { + /** hard maximum remembered object age **/ + static constexpr uint32_t c_max_object_age = 127; + + /** @class object_age + * @brief type-safe object age + * + * Object age measured in number of garbage collections survived. + **/ + struct object_age { + using value_type = std::uint32_t; + + explicit object_age(value_type x) : value_{x} {} + + operator value_type() const { return value_; } + + std::uint32_t value_; + }; + } +} + +/* end object_age.hpp */ diff --git a/include/xo/gc/role.hpp b/include/xo/gc/role.hpp new file mode 100644 index 00000000..58fc72a2 --- /dev/null +++ b/include/xo/gc/role.hpp @@ -0,0 +1,35 @@ +/** @file role.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace mm { + static constexpr uint32_t c_n_role = 2; + + struct role { + using value_type = std::uint32_t; + + explicit constexpr role(value_type x) : role_{x} {} + + static constexpr role to_space() { return role{0}; } + static constexpr role from_space() { return role{1}; } + + static constexpr std::array all() { return {{to_space(), from_space()}}; } + + static constexpr role begin() { return role{0}; } + static constexpr role end() { return role{2}; } + + operator value_type() const { return role_; } + + value_type role_ = 0; + }; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end role.hpp */ diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt new file mode 100644 index 00000000..2d42c4c9 --- /dev/null +++ b/src/gc/CMakeLists.txt @@ -0,0 +1,21 @@ +# gc/CMakeLists.txt + +set(SELF_LIB xo_gc) +set(SELF_SRCS + + ICollector_Any.cpp + IGCObject_Any.cpp + IAllocator_DX1Collector.cpp + ICollector_DX1Collector.cpp + IAllocIterator_DX1CollectorIterator.cpp + + DX1Collector.cpp + DX1CollectorIterator.cpp + + ) + +xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) +# note: deps here must also appear in cmake/xo_alloc2Config.cmake.in +xo_dependency(${SELF_LIB} xo_alloc2) +xo_dependency(${SELF_LIB} xo_facet) +xo_dependency(${SELF_LIB} indentlog) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp new file mode 100644 index 00000000..7e91ddbd --- /dev/null +++ b/src/gc/DX1Collector.cpp @@ -0,0 +1,289 @@ +/** @file DX1Collector.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "Allocator.hpp" +#include "arena/IAllocator_DArena.hpp" +#include "xo/gc/DX1Collector.hpp" +#include "xo/gc/DX1CollectorIterator.hpp" +#include "generation.hpp" +#include "object_age.hpp" +#include +#include +#include +#include + +namespace xo { + using xo::mm::AAllocator; + using xo::facet::with_facet; + + namespace mm { +#ifdef NOT_USING + constexpr std::uint64_t + CollectorConfig::gen_mult() const { + return 1ul << arena_config_.header_size_bits_; + } +#endif + +#ifdef NOT_USING + constexpr std::uint64_t + CollectorConfig::tseq_mult() const { + return 1ul << (gen_bits_ + arena_config_.header_size_bits_); + } +#endif + + // ----- GCRunState ----- + + GCRunState::GCRunState(generation gc_upto) + : gc_upto_{gc_upto} + {} + + GCRunState + GCRunState::gc_not_running() + { + return GCRunState(generation(0)); + } + + GCRunState + GCRunState::gc_upto(generation g) + { + return GCRunState(generation(g + 1)); + } + + // ----- DX1Collector ----- + + using size_type = xo::mm::DX1Collector::size_type; + + DX1Collector::DX1Collector(const CollectorConfig & cfg) : config_{cfg} + { + assert(config_.arena_config_.header_.size_bits_ + + config_.arena_config_.header_.age_bits_ + + config_.arena_config_.header_.tseq_bits_ <= 64); + + for (uint32_t igen = 0, ngen = cfg.n_generation_; igen < ngen; ++igen) { + space_storage_[0][igen] = DArena::map(cfg.arena_config_); + space_storage_[1][igen] = DArena::map(cfg.arena_config_); + + space_[role::to_space()][igen] = &space_storage_[0][igen]; + space_[role::from_space()][igen] = &space_storage_[1][igen]; + } + + for (uint32_t igen = cfg.n_generation_; igen < c_max_generation; ++igen) { + space_[role::to_space()][igen] = nullptr; + space_[role::from_space()][igen] = nullptr; + } + } + + bool + DX1Collector::contains(role r, const void * addr) const noexcept + { + for (generation gi{0}; gi < config_.n_generation_; ++gi) { + const DArena * arena = get_space(r, gi); + + if (arena->contains(addr)) + return true; + } + + return false; + } + + AllocError + DX1Collector::last_error() const noexcept + { + // TODO: + // need to adjust here if runtime errors + // encountered during gc. + + return get_space(role::to_space(), generation::nursery())->last_error_; + } + + namespace { + size_type + accumulate_total_aux(const DX1Collector & d, + size_t (DArena::* get_stat_fn)() const) noexcept + { + size_t z = 0; + + for (role ri : role::all()) { + for (generation gj{0}; gj < d.config_.n_generation_; ++gj) { + const DArena * arena = d.get_space(ri, gj); + + assert(arena); + + z += (arena->*get_stat_fn)(); + } + } + + return z; + } + } + + size_type + DX1Collector::reserved_total() const noexcept + { + return accumulate_total_aux(*this, &DArena::reserved); + } + + size_type + DX1Collector::size_total() const noexcept + { + return committed_total(); + } + + size_type + DX1Collector::committed_total() const noexcept + { + return accumulate_total_aux(*this, &DArena::committed); + } + + size_type + DX1Collector::available_total() const noexcept + { + return accumulate_total_aux(*this, &DArena::available); + } + + size_type + DX1Collector::allocated_total() const noexcept + { + return accumulate_total_aux(*this, &DArena::allocated); + } + + size_type + DX1Collector::header2size(header_type hdr) const noexcept + { + uint32_t z = config_.arena_config_.header_.size(hdr); + + return z; + } + + object_age + DX1Collector::header2age(header_type hdr) const noexcept + { + uint32_t age = config_.arena_config_.header_.age(hdr); + + assert(age < c_max_object_age); + + return object_age(age); + } + + uint32_t + DX1Collector::header2tseq(header_type hdr) const noexcept + { + uint32_t tseq = config_.arena_config_.header_.tseq(hdr); + + return tseq; + } + + bool + DX1Collector::is_forwarding_header(header_type hdr) const noexcept + { + /** forwarding pointer encoded as sentinel tseq **/ + return config_.arena_config_.header_.is_forwarding_tseq(hdr); + } + + auto + DX1Collector::alloc(size_type z) noexcept -> value_type + { + return with_facet::mkobj(new_space()).alloc(z); + } + + auto + DX1Collector::super_alloc(size_type z) noexcept -> value_type { + return with_facet::mkobj(new_space()).super_alloc(z); + } + + auto + DX1Collector::sub_alloc(size_type z, bool complete) noexcept -> value_type { + return with_facet::mkobj(new_space()).sub_alloc(z, complete); + } + + bool + DX1Collector::expand(size_type z) noexcept + { + if (with_facet::mkobj(to_space(generation{0})).expand(z)) + return with_facet::mkobj(from_space(generation{0})).expand(z); + + return false; + } + + AllocInfo + DX1Collector::alloc_info(value_type mem) const noexcept { + for (role ri : role::all()) { + for (generation gj{0}; gj < config_.n_generation_; ++gj) { + const DArena * arena = this->get_space(ri, gj); + + assert(arena); + + if (arena->contains(mem)) { + return arena->alloc_info(mem); + } + } + } + + // deliberately attempt on nursery to-space, to capture error info + return sentinel + return this->get_space(role::to_space(), generation{0})->alloc_info(mem); + } + + DX1CollectorIterator + DX1Collector::begin() const noexcept + { + scope log(XO_DEBUG(false)); + + const DArena * arena + = get_space(role::to_space(), + generation{0}); + + return DX1CollectorIterator(this, + generation{0}, + generation{config_.n_generation_}, + arena->begin(), + arena->end()); + } + + DX1CollectorIterator + DX1Collector::end() const noexcept { + scope log(XO_DEBUG(false)); + + generation gen_hi = generation{config_.n_generation_}; + + /** valid iterator for end points to end of last DArena. + * otherwise will interfere with working compare + * (since invalid iterators are incomparable) + **/ + + const DArena * arena + = get_space(role::to_space(), + generation(config_.n_generation_ - 1)); + DArenaIterator arena_end = arena->end(); + + return DX1CollectorIterator(this, + gen_hi, + gen_hi, + arena_end, + arena_end); + } + + void + DX1Collector::reverse_roles(generation g) noexcept { + assert(g < config_.n_generation_); + + std::swap(space_[0][g], space_[1][g]); + } + + void + DX1Collector::clear() noexcept { + for (role ri : role::all()) { + for (generation gj{0}; gj < config_.n_generation_; ++gj) { + DArena * arena = this->get_space(ri, gj); + + assert(arena); + + arena->clear(); + } + } + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DX1Collector.cpp */ diff --git a/src/gc/DX1CollectorIterator.cpp b/src/gc/DX1CollectorIterator.cpp new file mode 100644 index 00000000..3942468b --- /dev/null +++ b/src/gc/DX1CollectorIterator.cpp @@ -0,0 +1,126 @@ +/** @file DX1CollectorIterator.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "xo/gc/DX1CollectorIterator.hpp" +#include "xo/gc/DX1Collector.hpp" +#include +#include + +namespace xo { + namespace mm { + DX1CollectorIterator::DX1CollectorIterator(const DX1Collector * gc, + generation gen_ix, + generation gen_hi, + DArenaIterator arena_ix, + DArenaIterator arena_hi) : gc_{gc}, + gen_ix_{gen_ix}, + gen_hi_{gen_hi}, + arena_ix_{arena_ix}, + arena_hi_{arena_hi} + { + this->normalize(); + } + + void + DX1CollectorIterator::normalize() noexcept + { + scope log(XO_DEBUG(false), + xtag("gen_ix", gen_ix_), + xtag("gen_hi", gen_hi_), + xtag("arena_ix.pos", arena_ix_.pos_), + xtag("arena_hi.pos", arena_hi_.pos_)); + + /* normalize: find lowest generation with non-empty to-space */ + if (arena_ix_.pos_ == arena_hi_.pos_) { + log && log(xtag("action", "look-lub-nonempty-gen")); + + if (gen_ix_ < gen_hi_) + ++gen_ix_; + + for (; gen_ix_ < gen_hi_; ++gen_ix_) { + const DArena * arena + = gc_->get_space(role::to_space(), gen_ix_); + + assert(arena); + + arena_ix_ = arena->begin(); + arena_hi_ = arena->end(); + + if (arena_ix_ != arena_hi_) { + // normalization achieved! + break; + } + } + + log && log(xtag("gen_ix", gen_ix_), + xtag("arena_ix.pos", arena_ix_.pos_), + xtag("arena_hi.pos", arena_hi_.pos_)); + } else { + log && log(xtag("action", "noop")); + } + } + + AllocInfo + DX1CollectorIterator::deref() const noexcept + { + return arena_ix_.deref(); + } + + cmpresult + DX1CollectorIterator::compare(const DX1CollectorIterator & other_ix) const noexcept + { + scope log(XO_DEBUG(false), + xtag("is_valid", is_valid()), + xtag("other_ix.is_valid", other_ix.is_valid()) ); + + if (is_invalid() || (gc_ != other_ix.gc_)) { + log && log("incomparable!"); + return cmpresult::incomparable(); + } + + if (gen_ix_ != other_ix.gen_ix_) { + log && log(xtag("gen", gen_ix_), xtag("other.gen", other_ix.gen_ix_)); + + /* same collector, different arenas -> compare based on gen# */ + + return cmpresult::from_cmp(gen_ix_, other_ix.gen_ix_); + } + + /* both iterators refer to the same arena, + * so can compare their arena iterators directly + */ + cmpresult retval = arena_ix_.compare(other_ix.arena_ix_); + + log && log(xtag("retval", retval)); + + return retval; + } + + void + DX1CollectorIterator::next() noexcept + { + scope log(XO_DEBUG(false), + xtag("arena_ix.arena", arena_ix_.arena_), + xtag("arena_ix.pos", arena_ix_.pos_), + xtag("arena_hi.arena", arena_hi_.arena_), + xtag("arena_hi.pos", arena_hi_.pos_)); + + if (arena_ix_ != arena_hi_) { + ++arena_ix_; + + log && log(xtag("++arena_ix.pos", arena_ix_.pos_)); + + this->normalize(); + + log && log(xtag("arena_ix.arena", arena_ix_.arena_), + xtag("arena_ix.pos", arena_ix_.pos_)); + } else { + log && log(xtag("action", "arena-at-end")); + } + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DX1CollectorIterator.cpp */ diff --git a/src/gc/IAllocIterator_DX1CollectorIterator.cpp b/src/gc/IAllocIterator_DX1CollectorIterator.cpp new file mode 100644 index 00000000..9746870a --- /dev/null +++ b/src/gc/IAllocIterator_DX1CollectorIterator.cpp @@ -0,0 +1,41 @@ +/** @file IAllocIterator_DX1CollectorIterator.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "xo/gc/detail/IAllocIterator_DX1CollectorIterator.hpp" +#include "AllocIterator.hpp" +//#include + +namespace xo { + namespace mm { + AllocInfo + IAllocIterator_DX1CollectorIterator::deref(const DX1CollectorIterator & ix) noexcept + { + return ix.deref(); + } + + cmpresult + IAllocIterator_DX1CollectorIterator::compare(const DX1CollectorIterator & ix, + const obj & other_arg) noexcept + { + /* downcast from variant */ + auto other = obj::from(other_arg); + + if (!other) + return cmpresult::incomparable(); + + DX1CollectorIterator & other_ix = *other.data(); + + return ix.compare(other_ix); + } + + void + IAllocIterator_DX1CollectorIterator::next(DX1CollectorIterator & ix) noexcept + { + ix.next(); + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IAllocIterator_DX1CollectorIterator.cpp */ diff --git a/src/gc/IAllocator_DX1Collector.cpp b/src/gc/IAllocator_DX1Collector.cpp new file mode 100644 index 00000000..d3e172fc --- /dev/null +++ b/src/gc/IAllocator_DX1Collector.cpp @@ -0,0 +1,127 @@ +/** @file IAllocator_DX1Collector.cpp + * + * @author Roland Conybeare, Dec 2025 + * + * See also ICollector_DX1Collector.cpp for collector facet + **/ + +#include "detail/IAllocator_DX1Collector.hpp" +#include "detail/IAllocIterator_DX1CollectorIterator.hpp" +#include "DX1CollectorIterator.hpp" +#include + +namespace xo { + using xo::facet::with_facet; + using std::size_t; + using std::byte; + + namespace mm { + using value_type = IAllocator_DX1Collector::value_type; + + std::string_view + IAllocator_DX1Collector::name(const DX1Collector & d) noexcept + { + return d.config_.name_; + } + + auto + IAllocator_DX1Collector::reserved(const DX1Collector & d) noexcept -> size_type + { + return d.reserved_total(); + } + + auto + IAllocator_DX1Collector::size(const DX1Collector & d) noexcept -> size_type + { + return d.size_total(); + } + + auto + IAllocator_DX1Collector::committed(const DX1Collector & d) noexcept -> size_type + { + return d.committed_total(); + } + + auto + IAllocator_DX1Collector::available(const DX1Collector & d) noexcept -> size_type + { + return d.available_total(); + } + + auto + IAllocator_DX1Collector::allocated(const DX1Collector & d) noexcept -> size_type + { + return d.allocated_total(); + } + + bool + IAllocator_DX1Collector::contains(const DX1Collector & d, const void * addr) noexcept + { + return d.contains(role::to_space(), addr); + } + + AllocError + IAllocator_DX1Collector::last_error(const DX1Collector & d) noexcept + { + return d.last_error(); + } + + auto + IAllocator_DX1Collector::alloc_range(const DX1Collector & d, + DArena & ialloc) noexcept -> range_type + { + DX1CollectorIterator * begin_ix = construct_with(ialloc, d.begin()); + DX1CollectorIterator * end_ix = construct_with(ialloc, d.end()); + + obj begin_obj = with_facet::mkobj(begin_ix); + obj end_obj = with_facet::mkobj( end_ix); + + return AllocRange(std::make_pair(begin_obj, end_obj)); + } + + auto + IAllocator_DX1Collector::alloc(DX1Collector & d, size_type z) noexcept -> value_type + { + return d.alloc(z); + } + + auto + IAllocator_DX1Collector::super_alloc(DX1Collector & d, size_type z) noexcept -> value_type + { + return d.super_alloc(z); + } + + auto + IAllocator_DX1Collector::sub_alloc(DX1Collector & d, size_type z, bool complete) noexcept -> value_type + { + return d.sub_alloc(z, complete); + } + + bool + IAllocator_DX1Collector::expand(DX1Collector & d, size_type z) noexcept + { + return d.expand(z); + } + + AllocInfo + IAllocator_DX1Collector::alloc_info(const DX1Collector & d, value_type mem) noexcept + { + return d.alloc_info(mem); + } + + void + IAllocator_DX1Collector::clear(DX1Collector & d) + { + d.clear(); + } + + void + IAllocator_DX1Collector::destruct_data(DX1Collector & d) + { + d.~DX1Collector(); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IAllocator_DX1Collector.cpp */ diff --git a/src/gc/ICollector_Any.cpp b/src/gc/ICollector_Any.cpp new file mode 100644 index 00000000..d3ea1fb8 --- /dev/null +++ b/src/gc/ICollector_Any.cpp @@ -0,0 +1,39 @@ +/** @file ICollector_Any.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "detail/ICollector_Any.hpp" +#include + +namespace xo { + using xo::facet::DVariantPlaceholder; + using xo::facet::typeseq; + using xo::facet::valid_facet_implementation; + + namespace mm { + + void + ICollector_Any::_fatal() { + /* control here on uninitialized ICollector_Any. + * Initialized instance will have specific implementation type + * e.g. ICollector_Xfer + */ + + std::cerr << "fatal" + << ": attempt to call uninitialized" + << " ICollector_Any method" + << std::endl; + std::terminate(); + } + + int32_t + ICollector_Any::s_typeseq = typeseq::id(); + + bool + ICollector_Any::_valid = valid_facet_implementation(); + + } /*namespace mm*/ +} /*namespace xo*/ + +/** end ICollector_Any.cpp */ diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp new file mode 100644 index 00000000..41e05f9d --- /dev/null +++ b/src/gc/ICollector_DX1Collector.cpp @@ -0,0 +1,191 @@ +/** @file ICollector_DX1Collector.cpp + * + * @author Roland Conybeare, Dec 2025 + * + * See also IAllocator_DX1Collector.cpp for allocator facet + **/ + +#include "xo/gc/detail/ICollector_DX1Collector.hpp" +#include "GCObject.hpp" + +namespace xo { + namespace mm { + using size_type = ICollector_DX1Collector::size_type; + using std::byte; + + namespace { + size_type + stat_helper(const DX1Collector & 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; + } + } + + size_type + ICollector_DX1Collector::reserved(const DX1Collector & d, generation g, role r) + { + return stat_helper(d, &DArena::reserved, g, r); + } + + size_type + ICollector_DX1Collector::allocated(const DX1Collector & d, generation g, role r) + { + return stat_helper(d, &DArena::allocated, g, r); + } + + size_type + ICollector_DX1Collector::committed(const DX1Collector & d, generation g, role r) + { + return stat_helper(d, &DArena::committed, g, r); + } + + void + ICollector_DX1Collector::install_type(DX1Collector & d, + std::int32_t tseq, + IGCObject_Any & iface) + { + (void)d; + (void)tseq; + (void)iface; + + assert(false); + } + + void + ICollector_DX1Collector::add_gc_root(DX1Collector & d, + int32_t tseq, + Opaque * root) + { + (void)d; + (void)tseq; + (void)root; + + assert(false); + } + + void + ICollector_DX1Collector::forward_inplace(DX1Collector & d, + obj * lhs) + { + assert(d.runstate_.is_running()); + + /* + * lhs obj + * | +---------+ +---+-+----+ + * \--->| .iface | | T |G|size| header + * +---------+ object_data +---+-+----+ + * | .data x----------------->| alloc | + * +---------+ | data | + * | for | + * | instance | + * | ... | + * +----------+ + */ + + void * object_data = (byte *)(*lhs).opaque_data(); + + if (!d.contains(role::from_space(), object_data)) { + /* *lhs isn't in GC-allocated space. + * + * This happens for a modest number of global + * constant, for example DBoolean {true, false}. + * + * It's important we recognize these up front. + * Since not allocated from GC, they don't have + * an alloc-header. + */ + return; + } + + /** NOTE: for form's sake: + * better to lookup actual arena that + * allocated object data. + * + **/ + DArena * some_arena = d.to_space(generation(0)); + + DArena::header_type * p_header + = some_arena->obj2hdr(object_data); + + DArena::header_type alloc_hdr = *p_header; + + /* recover allocation size */ + std::size_t alloc_z = some_arena->config_.header_.size(alloc_hdr); + + /* need to be able to fit forwarding pointer + * in place of forwarded object. + * + * This is guaranteed anyway, by alignment rules + */ + assert(alloc_z > sizeof(uintptr_t)); + + if (d.is_forwarding_header(alloc_hdr)) { + /* *lhs already refers to a forwarding pointer */ + + /* + * lhs obj + * | +---------+ +---+-+----+ + * \--->| .iface | |FWD|G|size| alloc_hdr + * +---------+ object_data +---+-+----+ + * | .data x----------------->| x--------> + * +---------+ | | dest + * | | + * +----------+ + */ + void * dest = *(void**)object_data; + + /* update *lhs in-place */ + (*lhs).reset_opaque(dest); + } else if (check_move_policy(d, alloc_hdr, object_data)) { + /* copy object *lhs + replace with forwarding pointer */ + + /* which arena are we writing to? need allocator interface */ + + assert(false); + +#ifdef NOT_YET + // to do this need IAllocator_DX1Collector fully implemented + + void * copy = (*lhs).shallow_copy(xxx); + + xxx mm xxx; +#endif + } else { + /* object doesn't need to move. + * e.g. incremental collection + object is tenured + */ + } + } + + bool + ICollector_DX1Collector::check_move_policy(const DX1Collector & d, + header_type alloc_hdr, + void * object_data) + { + (void)object_data; + + // when gc is moving objects, to- and from- spaces have been + // reversed: forwarding pointers are located in from-space and + // refer to to-space. + + object_age age = d.header2age(alloc_hdr); + + generation g = d.config_.age2gen(age); + + assert(d.runstate_.is_running()); + + return (g < d.runstate_.gc_upto()); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ICollector_DX1Collector.cpp */ diff --git a/src/gc/IGCObject_Any.cpp b/src/gc/IGCObject_Any.cpp new file mode 100644 index 00000000..22e1cf48 --- /dev/null +++ b/src/gc/IGCObject_Any.cpp @@ -0,0 +1,34 @@ +/** @file IGCObject_Any.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "gcobject/IGCObject_Any.hpp" +#include + +namespace xo { + using xo::facet::DVariantPlaceholder; + using xo::facet::typeseq; + using xo::facet::valid_facet_implementation; + + namespace mm { + + void + IGCObject_Any::_fatal() { + std::cerr << "fatal" + << ": attempt to call uninitialized" + << " IGCObject_Any method" + << std::endl; + std::terminate(); + } + + int32_t + IGCObject_Any::s_typeseq = typeseq::id(); + + bool + IGCObject_Any::_valid = valid_facet_implementation(); + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IGCObject_Any.cpp */ diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt new file mode 100644 index 00000000..c48dbdb5 --- /dev/null +++ b/utest/CMakeLists.txt @@ -0,0 +1,21 @@ +# xo-gc/utest/CMakeLists.txt +# + +set(UTEST_EXE utest.gc) +set(UTEST_SRCS + gc_utest_main.cpp + Collector.test.cpp + DX1CollectorIterator.test.cpp + random_allocs.cpp +) + +if (ENABLE_TESTING) + xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS}) + xo_headeronly_dependency(${UTEST_EXE} randomgen) + xo_self_dependency(${UTEST_EXE} xo_gc) +# xo_headeronly_dependency(${UTEST_EXE} indentlog) + xo_headeronly_dependency(${UTEST_EXE} xo_facet) + xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2) +endif() + +# end CMakeLists.txt diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp new file mode 100644 index 00000000..f322c286 --- /dev/null +++ b/utest/Collector.test.cpp @@ -0,0 +1,243 @@ +/** @file Collector.test.cpp + * + * @author Roland Conybeare, Dec 2025 + * + * NOTE: properly unit testing gc behavior requires + * xo-object2 dependency; + * see xo-object2/utest + **/ + +#include +#include "Collector.hpp" +#include "random_allocs.hpp" +#include "detail/ICollector_DX1Collector.hpp" +#include "detail/IAllocator_DX1Collector.hpp" +//#include "gc/DX1Collector.hpp" +#include +#include +#include +#include +#include +#include + +namespace xo { + using xo::mm::AAllocator; + using xo::mm::ACollector; + using xo::mm::CollectorConfig; + using xo::mm::DX1Collector; + using xo::mm::ArenaConfig; + using xo::mm::AllocHeaderConfig; + using xo::mm::generation; + using xo::mm::c_max_generation; + using xo::facet::with_facet; + using xo::scope; + + namespace ut { + // checklist + // - obj constructible [ ] + // - obj truthy [ ] + // - obj constructible [ ] + // + // - obj constructible [ ] + // - obj allocation + + TEST_CASE("collector-any-null", "[alloc2][gc][ACollector]") + { + /* empty variant collector */ + obj gc1; + + REQUIRE(!gc1); + REQUIRE(gc1.iface() != nullptr); + REQUIRE(gc1.data() == nullptr); + } + + TEST_CASE("DX1Collector-1", "[alloc2][gc][DX1Collector]") + { + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard_byte*/, + 0 /*tseq_bits*/, + 0 /*age_bits*/, + 16 /*size_bits*/), }; + CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 2, + .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; + + DX1Collector gc = DX1Collector{cfg}; + + generation g0 = generation{0}; + REQUIRE(gc.to_space(g0)); + REQUIRE(gc.from_space(g0)); + REQUIRE(gc.to_space(g0)->is_mapped()); + REQUIRE(gc.from_space(g0)->is_mapped()); + + generation g1 = generation{1}; + REQUIRE(gc.to_space(g1)); + REQUIRE(gc.from_space(g1)); + REQUIRE(gc.to_space(g1)->is_mapped()); + REQUIRE(gc.from_space(g1)->is_mapped()); + + /* verify from/to x N/T are unique */ + REQUIRE(gc.to_space(g0) != gc.from_space(g0)); + REQUIRE(gc.to_space(g1) != gc.to_space(g0)); + REQUIRE(gc.from_space(g1) != gc.from_space(g0)); + REQUIRE(gc.to_space(g0) != gc.from_space(g1)); + + for (generation gi = generation(2); gi < c_max_generation; ++gi) { + INFO(xtag("gi", gi)); + + REQUIRE(!gc.to_space(gi)); + REQUIRE(!gc.from_space(gi)); + + REQUIRE(!gc.space_storage_[0][gi].is_mapped()); + REQUIRE(!gc.space_storage_[1][gi].is_mapped()); + } + } + + TEST_CASE("collector-x1-obj", "[alloc2][gc]") + { + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard_byte*/, + 0 /*tseq_bits*/, + 0 /*age_bits*/, + 16 /*size_bits*/), }; + CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 2, + .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; + + DX1Collector gc = DX1Collector{cfg}; + + /* typed collector -- repr known at compile time */ + obj x1(&gc); + + REQUIRE(x1.iface()); + REQUIRE(x1.data()); + } + + TEST_CASE("collector-x1-facet-mkobj", "[alloc2][gc]") + { + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard_byte*/, + 0 /*tseq-bits*/, + 0 /*age-bits*/, + 16 /*size-bits*/), }; + CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 2, + .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; + + DX1Collector gc = DX1Collector{cfg}; + + /* typed collector -- repr inferred at compile time */ + auto x1 = with_facet::mkobj(&gc); + + REQUIRE(x1.iface()); + REQUIRE(x1.data()); + } + + TEST_CASE("collector-x1-alloc", "[alloc2][gc]") + { + scope log(XO_DEBUG(false), "DX1Collector alloc test"); + + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard-byte*/, + 0 /*tseq-bits*/, + 0 /*age-bits*/, + 16 /*size-bits*/), }; + + /* collector with one generation collapses to a non-generational copying collector */ + CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 1, + .gc_trigger_v_ = {{64*1024, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; + + DX1Collector x1state = DX1Collector{cfg}; + + /* typed collector */ + auto x1gc = with_facet::mkobj(&x1state); + auto x1alloc = with_facet::mkobj(&x1state); + + REQUIRE(x1gc.iface()); + REQUIRE(x1gc.data()); + + REQUIRE(x1alloc.iface()); + REQUIRE(x1alloc.data()); + + rng::Seed seed; + log && log(xtag("seed", seed)); + + auto rng = rng::xoshiro256ss(seed); + + bool catch_flag = false; + REQUIRE(utest::AllocUtil::random_allocs(25, catch_flag, &rng, x1alloc)); + } + + TEST_CASE("collector-x1-alloc2", "[alloc2][gc]") + { + scope log(XO_DEBUG(false), + "DX1Collector alloc test2"); + + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(8 /*guard_z*/, + 0xfd /*guard-byte*/, + 0 /*tseq-bits*/, + 0 /*age-bits*/, + 16 /*size-bits*/), + }; + + /* collector with one generation collapses to a non-generational copying collector */ + CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 1, + .gc_trigger_v_ = {{64*1024, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; + + /* X1 allocator+collector */ + DX1Collector x1state = DX1Collector{cfg}; + + /* typed collector i/face */ + auto x1gc = with_facet::mkobj(&x1state); + /* typed allocator i/face */ + auto x1alloc = with_facet::mkobj(&x1state); + + REQUIRE(x1gc.iface()); + REQUIRE(x1gc.data()); + + REQUIRE(x1alloc.iface()); + REQUIRE(x1alloc.data()); + + rng::Seed seed; + log && log("ratio: seed=", seed); + + auto rng = rng::xoshiro256ss(seed); + + REQUIRE(utest::AllocUtil::random_allocs(25, false, &rng, x1alloc)); + } + } +} + +/* end Collector.test.cpp */ diff --git a/utest/DX1CollectorIterator.test.cpp b/utest/DX1CollectorIterator.test.cpp new file mode 100644 index 00000000..ba3ba09c --- /dev/null +++ b/utest/DX1CollectorIterator.test.cpp @@ -0,0 +1,221 @@ +/** @file DX1CollectorIterator.test.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include +#include "AllocIterator.hpp" +#include "DX1CollectorIterator.hpp" +#include "detail/IAllocator_DX1Collector.hpp" +#include "detail/IAllocIterator_DX1CollectorIterator.hpp" +#include "arena/ArenaConfig.hpp" +#include "padding.hpp" +#include +#include +#include + +namespace xo { + using xo::mm::AAllocator; + using xo::mm::AAllocIterator; +// using xo::mm::IAllocIterator_Any; + using xo::mm::IAllocIterator_Xfer; + using xo::mm::IAllocIterator_DX1CollectorIterator; + using xo::mm::DX1Collector; + using xo::mm::DX1CollectorIterator; + using xo::mm::DArena; + using xo::mm::DArenaIterator; + using xo::mm::CollectorConfig; + using xo::mm::ArenaConfig; + using xo::mm::AllocHeaderConfig; + using xo::mm::cmpresult; + using xo::mm::padding; + using xo::facet::with_facet; + using std::byte; + + namespace ut { + TEST_CASE("IAllocIterator_Xfer_DX1CollectorIterator", "[alloc2]") + { + /* verify IAllocIterator_Xfer is constructible + satisfies concept checks */ + IAllocIterator_Xfer xfer; + REQUIRE(IAllocIterator_Xfer::_valid); + } + + TEST_CASE("DX1CollectorIterator-1", "[alloc2][gc][DX1Collector]") + { + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard_byte*/, + 0 /*tseq_bits*/, + 0 /*age_bits*/, + 16 /*size_bits*/), }; + CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 2, + .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; + + DX1Collector gc = DX1Collector{cfg}; + + auto ix = gc.begin(); + auto end_ix = gc.end(); + + REQUIRE(ix.is_valid()); + REQUIRE(end_ix.is_valid()); + REQUIRE(ix == end_ix); + + /* verify obj 'fat pointer' packaging */ + obj ix_vt{&ix}; + obj end_ix_vt{&end_ix}; + + REQUIRE(ix_vt.iface()); + REQUIRE(ix_vt.data()); + REQUIRE(end_ix_vt.iface()); + REQUIRE(end_ix_vt.data()); + + cmpresult cmp = ix_vt.compare(end_ix_vt); + + REQUIRE(cmp.is_equal()); + REQUIRE(ix_vt == end_ix_vt); + } + + TEST_CASE("DX1CollectorIterator-2", "[alloc2][gc][DX1Collector]") + { + scope log(XO_DEBUG(false)); + + ArenaConfig arena_cfg = { .name_ = "_test_unused", + .size_ = 4*1024*1024, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(0 /*guard_z*/, + 0xfd /*guard_byte*/, + 0 /*tseq_bits*/, + 0 /*age_bits*/, + 16 /*size_bits*/), }; + CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 2, + .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; + + DX1Collector gc = DX1Collector{cfg}; + obj a1o{&gc}; + + REQUIRE(a1o.reserved() >= arena_cfg.size_); + REQUIRE(a1o.committed() == 0); + REQUIRE(a1o.available() == 0); + REQUIRE(a1o.allocated() == 0); + + size_t req_z = 13; + byte * mem = gc.alloc(req_z); + + REQUIRE(mem != nullptr); + + log && log("should have iterators separated by one alloc"); + + { + auto ix = gc.begin(); + auto end_ix = gc.end(); + + REQUIRE(ix.is_valid()); + REQUIRE(end_ix.is_valid()); + REQUIRE(ix != end_ix); + + /* verify obj 'fat pointer' packaging */ + auto ix_vt = with_facet::mkobj(&ix); + auto end_ix_vt = with_facet::mkobj(&end_ix); + + REQUIRE(ix_vt.iface()); + REQUIRE(ix_vt.data()); + REQUIRE(end_ix_vt.iface()); + REQUIRE(end_ix_vt.data()); + + cmpresult cmp = ix_vt.compare(end_ix_vt); + + REQUIRE(cmp.is_lesser()); + REQUIRE(ix_vt != end_ix_vt); + + /* we only did one alloc, should be able + * to visit it + */ + auto info = ix_vt.deref(); + + REQUIRE(info.is_valid()); + REQUIRE(info.payload().first == mem); + REQUIRE(info.size() == padding::with_padding(req_z)); + + ix_vt.next(); + + log && log(xtag("ix.gen", ix.gen_ix()), + xtag("ix.arena_ix.arena", ix.arena_ix().arena_)); + log && log(xtag("end_ix.gen", end_ix.gen_ix()), + xtag("end_ix.arena_ix.arena", end_ix.arena_ix().arena_)); + + REQUIRE(ix_vt == end_ix_vt); + } + + { + //auto range = gc.alloc_range + + DArena scratch_mm + = DArena::map( + ArenaConfig{ + .size_ = 4*1024, + .hugepage_z_ = 4*1024}); + + auto range = a1o.alloc_range(scratch_mm); + + obj ix = range.begin(); + obj end_ix = range.end(); + + REQUIRE(ix.iface()); + REQUIRE(ix.data()); + REQUIRE(end_ix.iface()); + REQUIRE(end_ix.data()); + + REQUIRE(scratch_mm.allocated() >= 2*sizeof(DArenaIterator)); + REQUIRE(scratch_mm.available() > 0); + + REQUIRE(ix.compare(ix).is_equal()); + + REQUIRE(ix != end_ix); + + { + REQUIRE(ix.deref().is_valid()); + REQUIRE(ix.deref().size() == padding::with_padding(req_z)); + + auto [payload_lo, payload_hi] = ix.deref().payload(); + + REQUIRE(payload_lo == mem); + REQUIRE(payload_hi == mem + ix.deref().size()); + } + + { + ++ix; + + REQUIRE(ix == end_ix); + } + } + + // repeat, this time using range iteration + { + DArena scratch_mm + = DArena::map( + ArenaConfig{ + .size_ = 4*1024, + .hugepage_z_ = 4*1024}); + + for (const auto & info : a1o.alloc_range(scratch_mm)) { + REQUIRE(info.is_valid()); + REQUIRE(info.size() == padding::with_padding(req_z)); + REQUIRE(info.payload().first == mem); + REQUIRE(info.payload().second == mem + info.size()); + } + } + } + } /*namespace ut*/ +} /*namespace xo*/ + +/* end DX1CollectorIterator.test.cpp */ diff --git a/utest/gc_utest_main.cpp b/utest/gc_utest_main.cpp new file mode 100644 index 00000000..8c426003 --- /dev/null +++ b/utest/gc_utest_main.cpp @@ -0,0 +1,6 @@ +/* file gc_utest_main.cpp */ + +#define CATCH_CONFIG_MAIN +#include "catch2/catch.hpp" + +/* end gc_utest_main.cpp */ diff --git a/utest/random_allocs.cpp b/utest/random_allocs.cpp new file mode 100644 index 00000000..56d0cb85 --- /dev/null +++ b/utest/random_allocs.cpp @@ -0,0 +1,217 @@ +/** @file random_allocs.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "random_allocs.hpp" +#include "arena/DArena.hpp" +#include "padding.hpp" +#include +#include +#include +#include + +namespace utest { + using xo::mm::AllocInfo; + using xo::mm::DArena; + using xo::mm::ArenaConfig; + using xo::mm::padding; + using xo::rng::xoshiro256ss; + using xo::facet::obj; + using xo::scope; + using xo::xtag; + using std::uint32_t; + using std::byte; + + /* remember an allocation result. + * application owns memory in [lo, lo+z) + */ + struct Alloc { + Alloc() = default; + Alloc(byte * lo, size_t z) : lo_{lo}, z_{z} {} + + byte * lo() const { return lo_; } + byte * hi() const { return lo_ + z_; } + + byte * lo_ = nullptr; + size_t z_ = 0; + }; + + bool + AllocUtil::random_allocs(uint32_t n_alloc, + bool catch_flag, + xoshiro256ss * p_rgen, + obj mm) + { + scope log(XO_DEBUG(catch_flag), xtag("n-alloc", n_alloc)); + + /* track allocs. verify: + * - allocs are non-overlapping + * - allocs have valid alloc header + * - allocs surrounded by guard bytes + * + * allocs sorted on Alloc::lo + */ + std::map allocs_by_lo_map; + /* allocs sorted on Alloc::hi */ + std::map allocs_by_hi_map; + + for (uint32_t i_alloc = 0; i_alloc < n_alloc; ++i_alloc) { + std::normal_distribution ngen{5.0, 1.5}; + + double si = ngen(*p_rgen); + double zi = ::pow(2.0, si); + std::size_t z = ::ceil(zi); + + bool ok_flag = true; + + std::byte * mem = mm.alloc(z); + + log && log(xtag("i_alloc", i_alloc), + xtag("si", si), + xtag("zi", zi), + xtag("mem", mem)); + log && log(xtag("used", mm.allocated()), + xtag("avail", mm.available()), + xtag("commit", mm.committed()), + xtag("resv", mm.reserved())); + + + REQUIRE_ORFAIL(ok_flag, catch_flag, mem != nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, mm.contains(mem)); + REQUIRE_ORFAIL(ok_flag, catch_flag, mm.last_error().error_seq_ == 0); + REQUIRE_ORFAIL(ok_flag, catch_flag, mm.last_error().error_ == xo::mm::error::none); + + { + auto ix = allocs_by_lo_map.lower_bound(mem); + if (ix != allocs_by_lo_map.end()) { + REQUIRE_ORFAIL(ok_flag, catch_flag, (ix->first > mem + z)); + } + } + + { + auto ix = allocs_by_hi_map.upper_bound(mem); + if (ix != allocs_by_hi_map.end()) { + --ix; + REQUIRE_ORFAIL(ok_flag, catch_flag, (ix->first < mem)); + } + } + + allocs_by_lo_map[mem] = Alloc(mem, z); + allocs_by_hi_map[mem + z] = &(allocs_by_lo_map[mem]); + + /* verify we can recover alloc info */ + AllocInfo info = mm.alloc_info(mem); + + REQUIRE_ORFAIL(ok_flag, catch_flag, info.is_valid()); + + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.size() == padding::with_padding(z)); + + /* age isn't configured -> 0 = sentinel */ + REQUIRE_ORFAIL(ok_flag, catch_flag, info.age() == 0); + /* tseq isn't configured -> 0 = sentinel */ + REQUIRE_ORFAIL(ok_flag, catch_flag, info.tseq() == 0); + + if ((info.p_config_->guard_z_ > 0) + || info.guard_lo().first + || info.guard_lo().second + || info.guard_hi().first + || info.guard_hi().second) + { + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_lo().first != nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_lo().second != nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_lo().first + info.guard_z() + == info.guard_lo().second); + + for (const byte * p = info.guard_lo().first; + p != info.guard_lo().second; ++p) + { + REQUIRE_ORFAIL(ok_flag, catch_flag, (char)*p == info.guard_byte()); + } + + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_hi().first != nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_hi().second != nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_hi().first + info.guard_z() + == info.guard_hi().second); + + for (const byte * p = info.guard_hi().first; + p != info.guard_hi().second; ++p) + { + REQUIRE_ORFAIL(ok_flag, catch_flag, (char)*p == info.guard_byte()); + } + + + } else { + /* control here only if all of: + * - guard_z is zero + * - guard_lo empty + * - guard_hi empty + */ + + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_lo().first == nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_lo().second == nullptr); + + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_hi().first == nullptr); + REQUIRE_ORFAIL(ok_flag, catch_flag, + info.guard_hi().second == nullptr); + + } + + /** scratch arena for iterators **/ + DArena scratch_mm = DArena::map(ArenaConfig{.name_ = "scratch", + .size_ = 4*1024, + .hugepage_z_ = 4*1024 }); + auto range = mm.alloc_range(scratch_mm); + + /* limit iteration test to a few cases: + * - 1st loop + * - median loop + * - last loop + */ + if (i_alloc == 0 || i_alloc == n_alloc || 2*i_alloc == n_alloc) + { + /* verify iteration visits all the allocs, exactly once */ + + /* temp copy; remove allocs from this map as we encounter + * them via range iteration below + */ + auto alloc_map = allocs_by_lo_map; + + if (log) { + log(xtag("allocs_by_lo_map.size", allocs_by_lo_map.size())); + + for (auto & kv : allocs_by_lo_map) { + log(xtag("key", kv.first), xtag("value", kv.second.lo()), xtag("hi", kv.second.hi())); + } + } + + for (AllocInfo info : range) { + INFO(tostr(xtag("alloc_map.size", alloc_map.size()), + xtag("i_alloc", i_alloc))); + INFO(tostr(xtag("payload.first", info.payload().first))); + + const std::byte * alloc_lo = info.payload().first; + + REQUIRE_ORFAIL(ok_flag, catch_flag, + alloc_map.find(alloc_lo) != alloc_map.end()); + + alloc_map.erase(alloc_lo); + } + } + } + + return true; + } +} + +/* end random_allocs.cpp */ diff --git a/utest/random_allocs.hpp b/utest/random_allocs.hpp new file mode 100644 index 00000000..ab0afc4e --- /dev/null +++ b/utest/random_allocs.hpp @@ -0,0 +1,45 @@ +/** @file random_allocs.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "Allocator.hpp" +#include +#include + +namespace utest { + +/* note: trivial REQUIRE() call in else branch bc we still want + * catch2 to count assertions when verification succeeds + */ +# define REQUIRE_ORCAPTURE(ok_flag, catch_flag, expr) \ + if (catch_flag) { \ + REQUIRE((expr)); \ + } else { \ + REQUIRE(true); \ + ok_flag &= (expr); \ + } + +# define REQUIRE_ORFAIL(ok_flag, catch_flag, expr) \ + REQUIRE_ORCAPTURE(ok_flag, catch_flag, expr); \ + if (!ok_flag) \ + return ok_flag + + + + struct AllocUtil { + using AAllocator = xo::mm::AAllocator; + + /** generate a random sequence of allocations. + * verify allocator behavior + **/ + static bool random_allocs(std::uint32_t n_alloc, + bool catch_flag, + xo::rng::xoshiro256ss * p_rgen, + xo::facet::obj alloc); + }; +} + +/* end random_allocs.hpp */ From eb061d139c3325f5b245f175239f9677ada328e8 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 22 Dec 2025 23:31:12 -0500 Subject: [PATCH 002/174] xo-gc xo-alloc: refactor for file organization --- include/xo/gc/{gcobject => }/AGCObject.hpp | 0 include/xo/gc/GCObject.hpp | 8 ++++---- include/xo/gc/detail/ACollector.hpp | 11 +++++++---- include/xo/gc/{gcobject => detail}/IGCObject_Any.hpp | 0 include/xo/gc/{gcobject => detail}/IGCObject_Xfer.hpp | 0 include/xo/gc/{gcobject => detail}/RGCObject.hpp | 0 src/gc/IGCObject_Any.cpp | 2 +- 7 files changed, 12 insertions(+), 9 deletions(-) rename include/xo/gc/{gcobject => }/AGCObject.hpp (100%) rename include/xo/gc/{gcobject => detail}/IGCObject_Any.hpp (100%) rename include/xo/gc/{gcobject => detail}/IGCObject_Xfer.hpp (100%) rename include/xo/gc/{gcobject => detail}/RGCObject.hpp (100%) diff --git a/include/xo/gc/gcobject/AGCObject.hpp b/include/xo/gc/AGCObject.hpp similarity index 100% rename from include/xo/gc/gcobject/AGCObject.hpp rename to include/xo/gc/AGCObject.hpp diff --git a/include/xo/gc/GCObject.hpp b/include/xo/gc/GCObject.hpp index edc89c32..524002a4 100644 --- a/include/xo/gc/GCObject.hpp +++ b/include/xo/gc/GCObject.hpp @@ -5,9 +5,9 @@ #pragma once -#include "gcobject/AGCObject.hpp" -#include "gcobject/IGCObject_Any.hpp" -#include "gcobject/IGCObject_Xfer.hpp" -#include "gcobject/RGCObject.hpp" +#include "AGCObject.hpp" +#include "detail/IGCObject_Any.hpp" +#include "detail/IGCObject_Xfer.hpp" +#include "detail/RGCObject.hpp" /* end GCObject.hpp */ diff --git a/include/xo/gc/detail/ACollector.hpp b/include/xo/gc/detail/ACollector.hpp index 9c32125d..d0d34bd7 100644 --- a/include/xo/gc/detail/ACollector.hpp +++ b/include/xo/gc/detail/ACollector.hpp @@ -5,7 +5,7 @@ #pragma once -#include "gcobject/IGCObject_Any.hpp" +#include "IGCObject_Any.hpp" #include #include @@ -34,9 +34,12 @@ namespace xo { virtual int32_t _typeseq() const noexcept = 0; - virtual size_type allocated(Copaque d, generation g, role r) const noexcept = 0; - virtual size_type reserved(Copaque d, generation g, role r) const noexcept = 0; - virtual size_type committed(Copaque d, generation g, role r) const noexcept = 0; + virtual size_type allocated(Copaque d, + generation g, role r) const noexcept = 0; + virtual size_type reserved(Copaque d, + generation g, role r) const noexcept = 0; + virtual size_type committed(Copaque d, + generation g, role r) const noexcept = 0; /** install interface @p iface for representation with typeseq @p tseq * in collector @p d. diff --git a/include/xo/gc/gcobject/IGCObject_Any.hpp b/include/xo/gc/detail/IGCObject_Any.hpp similarity index 100% rename from include/xo/gc/gcobject/IGCObject_Any.hpp rename to include/xo/gc/detail/IGCObject_Any.hpp diff --git a/include/xo/gc/gcobject/IGCObject_Xfer.hpp b/include/xo/gc/detail/IGCObject_Xfer.hpp similarity index 100% rename from include/xo/gc/gcobject/IGCObject_Xfer.hpp rename to include/xo/gc/detail/IGCObject_Xfer.hpp diff --git a/include/xo/gc/gcobject/RGCObject.hpp b/include/xo/gc/detail/RGCObject.hpp similarity index 100% rename from include/xo/gc/gcobject/RGCObject.hpp rename to include/xo/gc/detail/RGCObject.hpp diff --git a/src/gc/IGCObject_Any.cpp b/src/gc/IGCObject_Any.cpp index 22e1cf48..0de21224 100644 --- a/src/gc/IGCObject_Any.cpp +++ b/src/gc/IGCObject_Any.cpp @@ -3,7 +3,7 @@ * @author Roland Conybeare, Dec 2025 **/ -#include "gcobject/IGCObject_Any.hpp" +#include "detail/IGCObject_Any.hpp" #include namespace xo { From 1757786bbbc1aea564b5003e2a8d24fb0aa1d673 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 23 Dec 2025 01:20:11 -0500 Subject: [PATCH 003/174] xo-alloc2: ++ docs + scaffold xo-gc docs --- docs/ACollector-reference.rst | 41 ++++++++++++++++++++ docs/CMakeLists.txt | 17 +++++++++ docs/README | 70 +++++++++++++++++++++++++++++++++++ docs/implementation.rst | 42 +++++++++++++++++++++ docs/index.rst | 21 +++++++++++ 5 files changed, 191 insertions(+) create mode 100644 docs/ACollector-reference.rst create mode 100644 docs/CMakeLists.txt create mode 100644 docs/README create mode 100644 docs/implementation.rst create mode 100644 docs/index.rst diff --git a/docs/ACollector-reference.rst b/docs/ACollector-reference.rst new file mode 100644 index 00000000..bc858c18 --- /dev/null +++ b/docs/ACollector-reference.rst @@ -0,0 +1,41 @@ +.. _ACollector-reference: + +ACollector Reference +==================== + +Abstract interface facet for generational garbage collector. + +Context +------- + +.. ditaa:: + :--scale: 0.99 + + +--------------------------------------------------+-----------------+ + | IAllocIterator_DX1CollectorIterator | | + | IAllocator_DX1Collector | RGCObject | + | ICollector_DX1Collector | IGCObject_Xfer | + | ICollector_Xfer | IGCObject_Any | + | ICollector_Any | | + +--------------------------------------------------+-----------------+ + +----------------------+--------------+------------+-----------------+ + | DX1CollectorIterator | DX1Collector | ACollector | AGCObject | + | | | | | + +----------------------+--------------+------------+-----------------+ + +--------------------------------------------------------------------+ + | CollectorConfig generation object_age role | + +--------------------------------------------------------------------+ + +.. code-block:: cpp + + #include + +Class +----- + +.. doxygenclass:: xo::mm::ACollector + +Methods +------- + +.. doxygengroup:: mm-collector-methods diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt new file mode 100644 index 00000000..34052860 --- /dev/null +++ b/docs/CMakeLists.txt @@ -0,0 +1,17 @@ +# xo-gc/docs/CMakeLists.txt + +xo_doxygen_collect_deps() +xo_docdir_doxygen_config() +xo_docdir_sphinx_config( + index.rst +# glossary.rst +# implementation.rst +# ArenaConfig-reference.rst +# DArena-reference.rst + #install.rst + #introduction.rst + #implementation.rst +) + +# see xo-reader/doc or xo-unit/doc for working examples +# example.rst install.rst implementation.rst diff --git a/docs/README b/docs/README new file mode 100644 index 00000000..2fab6399 --- /dev/null +++ b/docs/README @@ -0,0 +1,70 @@ +build + + +-----------------------------------------------+ + | cmake | + | CMakeLists.txt | + | $PREFIX/share/cmake/xo_macros/xo_cxx.cmake | + +-----------------------------------------------+ + | + | +----------------------+ + +------------------------------------------------->| .build/docs/Doxyfile | + | +----------------------+ + | | + | /------------/ + | | + | v + | +---------------------------------------+ +-----------------+ + +---->| doxygen |--->| .build/docs/dox | + | | $PREFIX/share/xo-macros/Doxyfile.in | | +- html/ | + | +---------------------------------------+ | +- xml/ | + | +-----------------+ + | | + | /------------/ + | | + | v + | +---------------------------------------+ +--------------------+ + \---->| sphinx |--->| .build/docs/sphinx | + | +- conf.py | | +- html/ | + | +- _static/ | +--------------------+ + | +- *.rst | + +---------------------------------------+ + +files + + README this file + CMakeLists.txt build entry point + conf.py sphinx config + _static static files for sphinx + +map + + index.rst + +- install.rst + +- examples.rst + +- unit-quantities.rst + +- classes.rst + +- glossary.rst + ... + +examples + +.. doxygenclass:: ${c++ class name} + :project: + :path: + :members: + :protected-members: + :private-members: + :undoc-members: + :member-groups: + :members-only: + :outline: + :no-link: + :allow-dot-graphs: + +.. doxygendefine:: ${c preprocessor define} + +.. doxygenconcept:: ${c++ concept definition} + +.. doxygenenum:: ${c++ enum definition} + +.. doxygenfunction:: ${c++ function name} diff --git a/docs/implementation.rst b/docs/implementation.rst new file mode 100644 index 00000000..e3434c0c --- /dev/null +++ b/docs/implementation.rst @@ -0,0 +1,42 @@ +.. _implementation: + +.. toctree:: + :maxdepth: 2 + +Components +========== + +Library dependency tower for *xo-gc* + +.. ditaa:: + + + +----------------+ + | xo_gc | + +----------------+ + | xo_alloc2 | + +----------------+ + | xo_facet | + +----------------+ + | xo_cmake | + +----------------+ + +Abstraction tower for *xo-gc* components: + +.. ditaa:: + :--scale: 0.85 + + +--------------------------------------------------+-----------------+ + | IAllocIterator_DX1CollectorIterator | | + | IAllocator_DX1Collector | RGCObject | + | ICollector_DX1Collector | IGCObject_Xfer | + | ICollector_Xfer | IGCObject_Any | + | ICollector_Any | | + +--------------------------------------------------+-----------------+ + +----------------------+--------------+------------+-----------------+ + | DX1CollectorIterator | DX1Collector | ACollector | AGCObject | + | | | | | + +----------------------+--------------+------------+-----------------+ + +--------------------------------------------------------------------+ + | CollectorConfig generation object_age role | + +--------------------------------------------------------------------+ diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..c973002a --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,21 @@ +# xo-gc documentation mster file + +xo-gc documetation +================== + +xo-gc provides a garbage collcetor +with plugin architecture for collectable types. + +Features: + +* generational +* compacting and copying +* gc progress observable via callbacks + +.. toctree:: + :maxdepth: 2 + :caption: xo-gc contents + + ACollector-reference.rst + genindex + search From 3dbc08b049abe84e979839745103edc3d64c370e Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 23 Dec 2025 21:06:38 -0500 Subject: [PATCH 004/174] xo-alloc2: ++ documentation + threshold size for THP feature --- utest/random_allocs.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utest/random_allocs.cpp b/utest/random_allocs.cpp index 56d0cb85..40315342 100644 --- a/utest/random_allocs.cpp +++ b/utest/random_allocs.cpp @@ -80,7 +80,7 @@ namespace utest { REQUIRE_ORFAIL(ok_flag, catch_flag, mem != nullptr); REQUIRE_ORFAIL(ok_flag, catch_flag, mm.contains(mem)); REQUIRE_ORFAIL(ok_flag, catch_flag, mm.last_error().error_seq_ == 0); - REQUIRE_ORFAIL(ok_flag, catch_flag, mm.last_error().error_ == xo::mm::error::none); + REQUIRE_ORFAIL(ok_flag, catch_flag, mm.last_error().error_ == xo::mm::error::ok); { auto ix = allocs_by_lo_map.lower_bound(mem); From fc670668bacdb5f882dc887c0c7e37d74113b28c Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 24 Dec 2025 19:39:11 -0500 Subject: [PATCH 005/174] xo-gc xo-object2 xo-alloc2: templates for FOMO [WIP] --- include/xo/gc/AGCObject.hpp | 53 ----------------------------- include/xo/gc/GCObject.hpp | 2 +- include/xo/gc/detail/ACollector.hpp | 2 +- 3 files changed, 2 insertions(+), 55 deletions(-) delete mode 100644 include/xo/gc/AGCObject.hpp diff --git a/include/xo/gc/AGCObject.hpp b/include/xo/gc/AGCObject.hpp deleted file mode 100644 index 1e0a6d45..00000000 --- a/include/xo/gc/AGCObject.hpp +++ /dev/null @@ -1,53 +0,0 @@ -/** @file AGCObject.hpp - * - * @author Roland Conybeare, Dec 2025 - **/ - -#pragma once - -#include "Allocator.hpp" -#include "xo/facet/facet_implementation.hpp" -#include "xo/facet/typeseq.hpp" -#include "xo/facet/obj.hpp" // for obj in shallow_copy -#include -#include - -namespace xo { - namespace mm { - using Copaque = const void *; - using Opaque = void *; - - /** @class AObject - * @brief Abstract facet for collector-eligible data - * - * Data that supports AGCObject can have memory managed - * by ACollector - **/ - struct AGCObject { - using size_type = std::size_t; - - /** RTTI: unique id# for actual runtime data representation **/ - virtual int32_t _typeseq() const noexcept = 0; - - virtual size_type shallow_size(Copaque d) const noexcept = 0; - virtual Opaque * shallow_copy(Copaque d, - obj mm) const noexcept = 0; - virtual size_type forward_children(Opaque d) const noexcept = 0; - }; - - // implementation IGCObject_DRepr of AGCObject for state DRepr - // should provide a specialization: - // - // template <> - // struct xo::facet::FacetImplementation { - // using ImplType = IGCObject_DRepr; - // }; - // - // then IGCObject_ImplType --> IGCObject_DRepr - // - template - using IGCObject_ImplType = xo::facet::FacetImplType; - } /*namespace mm*/ -} /*namespace xo*/ - -/* end AGCObject.hpp */ diff --git a/include/xo/gc/GCObject.hpp b/include/xo/gc/GCObject.hpp index 524002a4..2674b2ae 100644 --- a/include/xo/gc/GCObject.hpp +++ b/include/xo/gc/GCObject.hpp @@ -5,7 +5,7 @@ #pragma once -#include "AGCObject.hpp" +#include "detail/AGCObject.hpp" #include "detail/IGCObject_Any.hpp" #include "detail/IGCObject_Xfer.hpp" #include "detail/RGCObject.hpp" diff --git a/include/xo/gc/detail/ACollector.hpp b/include/xo/gc/detail/ACollector.hpp index d0d34bd7..24bd219e 100644 --- a/include/xo/gc/detail/ACollector.hpp +++ b/include/xo/gc/detail/ACollector.hpp @@ -27,7 +27,7 @@ namespace xo { /** @class ACollector * @brief Abstract facet for the XO garbage collector * - * Collector also supports the @ref AAllocator facet, see also + * A collector implementation will also support the @ref AAllocator facet, see also **/ struct ACollector { using size_type = std::size_t; From f46d733f11c04b78b141ab151ced51673022c779 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 24 Dec 2025 19:40:48 -0500 Subject: [PATCH 006/174] xo-facet: facet template [WIP] --- include/xo/gc/detail/AGCObject.hpp | 53 ++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 include/xo/gc/detail/AGCObject.hpp diff --git a/include/xo/gc/detail/AGCObject.hpp b/include/xo/gc/detail/AGCObject.hpp new file mode 100644 index 00000000..1e0a6d45 --- /dev/null +++ b/include/xo/gc/detail/AGCObject.hpp @@ -0,0 +1,53 @@ +/** @file AGCObject.hpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#pragma once + +#include "Allocator.hpp" +#include "xo/facet/facet_implementation.hpp" +#include "xo/facet/typeseq.hpp" +#include "xo/facet/obj.hpp" // for obj in shallow_copy +#include +#include + +namespace xo { + namespace mm { + using Copaque = const void *; + using Opaque = void *; + + /** @class AObject + * @brief Abstract facet for collector-eligible data + * + * Data that supports AGCObject can have memory managed + * by ACollector + **/ + struct AGCObject { + using size_type = std::size_t; + + /** RTTI: unique id# for actual runtime data representation **/ + virtual int32_t _typeseq() const noexcept = 0; + + virtual size_type shallow_size(Copaque d) const noexcept = 0; + virtual Opaque * shallow_copy(Copaque d, + obj mm) const noexcept = 0; + virtual size_type forward_children(Opaque d) const noexcept = 0; + }; + + // implementation IGCObject_DRepr of AGCObject for state DRepr + // should provide a specialization: + // + // template <> + // struct xo::facet::FacetImplementation { + // using ImplType = IGCObject_DRepr; + // }; + // + // then IGCObject_ImplType --> IGCObject_DRepr + // + template + using IGCObject_ImplType = xo::facet::FacetImplType; + } /*namespace mm*/ +} /*namespace xo*/ + +/* end AGCObject.hpp */ From 1a8d5fcabeccc5cb265c4f5a6fd3a5fac602a226 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 29 Dec 2025 14:32:52 -0500 Subject: [PATCH 007/174] xo-gc xo-object2 xo-facet: builds w/ ISequence,Dlist --- include/xo/gc/detail/ACollector.hpp | 9 ++++++--- include/xo/gc/detail/AGCObject.hpp | 9 ++++++--- include/xo/gc/detail/ICollector_Any.hpp | 2 +- include/xo/gc/detail/ICollector_DX1Collector.hpp | 2 +- include/xo/gc/detail/ICollector_Xfer.hpp | 4 ++-- include/xo/gc/detail/IGCObject_Any.hpp | 8 +++++--- include/xo/gc/detail/IGCObject_Xfer.hpp | 12 +++++++----- include/xo/gc/detail/RCollector.hpp | 2 +- include/xo/gc/detail/RGCObject.hpp | 2 +- src/gc/ICollector_DX1Collector.cpp | 8 +++++--- 10 files changed, 35 insertions(+), 23 deletions(-) diff --git a/include/xo/gc/detail/ACollector.hpp b/include/xo/gc/detail/ACollector.hpp index 24bd219e..5a41f913 100644 --- a/include/xo/gc/detail/ACollector.hpp +++ b/include/xo/gc/detail/ACollector.hpp @@ -5,7 +5,7 @@ #pragma once -#include "IGCObject_Any.hpp" +//#include "IGCObject_Any.hpp" #include #include @@ -22,6 +22,7 @@ namespace xo { using Copaque = const void *; using Opaque = void *; + struct AGCObject; struct IGCObject_Any; // see IGCObject_Any.hpp /** @class ACollector @@ -53,10 +54,12 @@ namespace xo { virtual void install_type(Opaque d, int32_t tseq, IGCObject_Any & iface) = 0; virtual void add_gc_root(Opaque d, int32_t tseq, Opaque * root) = 0; - /** evacuate @p *lhs to to-space and replace with forwarding pointer + /** evacuate @p *lhs, that refers to state with interface @p lhs_iface, + * to collector @p d's to-space. Replace *lhs_data with forwarding pointer + * * Require: gc in progress **/ - virtual void forward_inplace(Opaque d, obj * lhs) = 0; + virtual void forward_inplace(Opaque d, AGCObject * lhs_iface, void ** lhs_data) = 0; }; } diff --git a/include/xo/gc/detail/AGCObject.hpp b/include/xo/gc/detail/AGCObject.hpp index 1e0a6d45..ae2b8a08 100644 --- a/include/xo/gc/detail/AGCObject.hpp +++ b/include/xo/gc/detail/AGCObject.hpp @@ -17,6 +17,8 @@ namespace xo { using Copaque = const void *; using Opaque = void *; + struct ACollector; + /** @class AObject * @brief Abstract facet for collector-eligible data * @@ -30,9 +32,10 @@ namespace xo { virtual int32_t _typeseq() const noexcept = 0; virtual size_type shallow_size(Copaque d) const noexcept = 0; - virtual Opaque * shallow_copy(Copaque d, - obj mm) const noexcept = 0; - virtual size_type forward_children(Opaque d) const noexcept = 0; + virtual Opaque shallow_copy(Copaque d, + obj mm) const noexcept = 0; + virtual size_type forward_children(Opaque d, + obj) const noexcept = 0; }; // implementation IGCObject_DRepr of AGCObject for state DRepr diff --git a/include/xo/gc/detail/ICollector_Any.hpp b/include/xo/gc/detail/ICollector_Any.hpp index c9779687..b4836f66 100644 --- a/include/xo/gc/detail/ICollector_Any.hpp +++ b/include/xo/gc/detail/ICollector_Any.hpp @@ -36,7 +36,7 @@ namespace xo { // non-const methods [[noreturn]] void install_type(Opaque, int32_t, IGCObject_Any &) noexcept override { _fatal(); } [[noreturn]] void add_gc_root(Opaque, int32_t, Opaque *) override { _fatal(); } - [[noreturn]] void forward_inplace(Opaque, obj *) override { _fatal(); } + [[noreturn]] void forward_inplace(Opaque, AGCObject *, void **) override { _fatal(); } private: [[noreturn]] static void _fatal(); diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index 9bc6e129..c531ae13 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -44,7 +44,7 @@ namespace xo { static void install_type(DX1Collector & d, int32_t seq, IGCObject_Any & iface); static void add_gc_root(DX1Collector & d, int32_t tseq, Opaque * root); - static void forward_inplace(DX1Collector & d, obj * lhs); + static void forward_inplace(DX1Collector & d, AGCObject * lhs_iface, void ** lhs_data); static int32_t s_typeseq; static bool _valid; diff --git a/include/xo/gc/detail/ICollector_Xfer.hpp b/include/xo/gc/detail/ICollector_Xfer.hpp index 2177b63a..c44bd3ca 100644 --- a/include/xo/gc/detail/ICollector_Xfer.hpp +++ b/include/xo/gc/detail/ICollector_Xfer.hpp @@ -49,8 +49,8 @@ namespace xo { void add_gc_root(Opaque d, int32_t tseq, Opaque * root) override { I::add_gc_root(_dcast(d), tseq, root); } - void forward_inplace(Opaque d, obj * lhs) override { - I::forward_inplace(_dcast(d), lhs); + void forward_inplace(Opaque d, AGCObject * lhs_iface, void ** lhs_data) override { + I::forward_inplace(_dcast(d), lhs_iface, lhs_data); } private: diff --git a/include/xo/gc/detail/IGCObject_Any.hpp b/include/xo/gc/detail/IGCObject_Any.hpp index 039a1259..b387b9b9 100644 --- a/include/xo/gc/detail/IGCObject_Any.hpp +++ b/include/xo/gc/detail/IGCObject_Any.hpp @@ -6,6 +6,7 @@ #pragma once #include "AGCObject.hpp" +#include "Collector.hpp" #include namespace xo { @@ -31,9 +32,10 @@ namespace xo { int32_t _typeseq() const noexcept override { return s_typeseq; } [[noreturn]] size_type shallow_size(Copaque) const noexcept override { _fatal(); } - [[noreturn]] Opaque * shallow_copy(Copaque, - obj) const noexcept override { _fatal(); } - [[noreturn]] size_type forward_children(Opaque) const noexcept override { _fatal(); } + [[noreturn]] Opaque shallow_copy(Copaque, + obj) const noexcept override { _fatal(); } + [[noreturn]] size_type forward_children(Opaque, + obj) const noexcept override { _fatal(); } private: [[noreturn]] static void _fatal(); diff --git a/include/xo/gc/detail/IGCObject_Xfer.hpp b/include/xo/gc/detail/IGCObject_Xfer.hpp index 75f78fd6..95e20ff1 100644 --- a/include/xo/gc/detail/IGCObject_Xfer.hpp +++ b/include/xo/gc/detail/IGCObject_Xfer.hpp @@ -6,6 +6,7 @@ #pragma once #include "AGCObject.hpp" +#include "ACollector.hpp" namespace xo { namespace mm { @@ -28,16 +29,17 @@ namespace xo { int32_t _typeseq() const noexcept override { return s_typeseq; } size_type shallow_size(Copaque d) const noexcept override { - return I::shallow_copy(_dcast(d)); + return I::shallow_size(_dcast(d)); } - Opaque * shallow_copy(Copaque d, obj mm) const noexcept override { - return I::shallow_size(_dcast(d), mm); + Opaque shallow_copy(Copaque d, obj mm) const noexcept override { + return I::shallow_copy(_dcast(d), mm); } // non-const methods - size_type forward_children(Opaque d) const noexcept override { - return I::forward_children(d); + size_type forward_children(Opaque d, + obj gc) const noexcept override { + return I::forward_children(_dcast(d), gc); } private: diff --git a/include/xo/gc/detail/RCollector.hpp b/include/xo/gc/detail/RCollector.hpp index 753af3c7..47e9c43b 100644 --- a/include/xo/gc/detail/RCollector.hpp +++ b/include/xo/gc/detail/RCollector.hpp @@ -30,7 +30,7 @@ namespace xo { void install_type(int32_t tseq, IGCObject_Any & iface) { return O::iface()->install_type(O::data(), tseq, iface); } void add_gc_root(int32_t tseq, Opaque * root) { O::iface()->add_gc_root(O::data(), tseq, root); } - void forward_inplace(obj * lhs) { O::iface()->forward_inplace(O::data(), lhs); } + void forward_inplace(AGCObject * lhs_iface, void ** lhs_data) { O::iface()->forward_inplace(O::data(), lhs_iface, lhs_data); } static bool _valid; }; diff --git a/include/xo/gc/detail/RGCObject.hpp b/include/xo/gc/detail/RGCObject.hpp index 054b995e..29bebf15 100644 --- a/include/xo/gc/detail/RGCObject.hpp +++ b/include/xo/gc/detail/RGCObject.hpp @@ -25,7 +25,7 @@ namespace xo { int32_t _typeseq() const noexcept { return O::iface()->_typeseq(); } size_type shallow_size() const noexcept { O::iface()->shallow_size(O::data()); } - Opaque * shallow_copy(obj mm) const noexcept { O::iface()->shallow_copy(O::data(), mm); } + Opaque shallow_copy(obj mm) const noexcept { O::iface()->shallow_copy(O::data(), mm); } size_type forward_children() noexcept { O::iface()->forward_children(O::data()); } static bool _valid; diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp index 41e05f9d..b4d8f24d 100644 --- a/src/gc/ICollector_DX1Collector.cpp +++ b/src/gc/ICollector_DX1Collector.cpp @@ -73,8 +73,10 @@ namespace xo { void ICollector_DX1Collector::forward_inplace(DX1Collector & d, - obj * lhs) + AGCObject * lhs_iface, + void ** lhs_data) { + (void)lhs_iface; assert(d.runstate_.is_running()); /* @@ -90,7 +92,7 @@ namespace xo { * +----------+ */ - void * object_data = (byte *)(*lhs).opaque_data(); + void * object_data = (byte *)lhs_data; if (!d.contains(role::from_space(), object_data)) { /* *lhs isn't in GC-allocated space. @@ -143,7 +145,7 @@ namespace xo { void * dest = *(void**)object_data; /* update *lhs in-place */ - (*lhs).reset_opaque(dest); + *lhs_data = dest; } else if (check_move_policy(d, alloc_hdr, object_data)) { /* copy object *lhs + replace with forwarding pointer */ From 9d3db8c4aa33cc089abf97593b0e4053faf53ef7 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 2 Jan 2026 09:52:16 -0500 Subject: [PATCH 008/174] xo-facet: typeseq strongly typed --- include/xo/gc/detail/ACollector.hpp | 7 ++-- include/xo/gc/detail/AGCObject.hpp | 3 +- include/xo/gc/detail/ICollector_Any.hpp | 5 +-- .../xo/gc/detail/ICollector_DX1Collector.hpp | 3 +- include/xo/gc/detail/ICollector_Xfer.hpp | 11 +++--- include/xo/gc/detail/IGCObject_Any.hpp | 5 +-- include/xo/gc/detail/IGCObject_Xfer.hpp | 6 ++-- include/xo/gc/detail/RCollector.hpp | 2 +- include/xo/gc/detail/RGCObject.hpp | 3 +- src/gc/DX1Collector.cpp | 35 ++++++++++++++++++- src/gc/ICollector_Any.cpp | 2 +- src/gc/ICollector_DX1Collector.cpp | 11 ++---- src/gc/IGCObject_Any.cpp | 2 +- 13 files changed, 65 insertions(+), 30 deletions(-) diff --git a/include/xo/gc/detail/ACollector.hpp b/include/xo/gc/detail/ACollector.hpp index 5a41f913..0298ad2e 100644 --- a/include/xo/gc/detail/ACollector.hpp +++ b/include/xo/gc/detail/ACollector.hpp @@ -31,9 +31,10 @@ namespace xo { * A collector implementation will also support the @ref AAllocator facet, see also **/ struct ACollector { + using typeseq = xo::facet::typeseq; using size_type = std::size_t; - virtual int32_t _typeseq() const noexcept = 0; + virtual typeseq _typeseq() const noexcept = 0; virtual size_type allocated(Copaque d, generation g, role r) const noexcept = 0; @@ -50,8 +51,10 @@ namespace xo { * @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) **/ - virtual void install_type(Opaque d, int32_t tseq, IGCObject_Any & iface) = 0; + virtual bool install_type(Opaque d, const AGCObject & iface) = 0; virtual void add_gc_root(Opaque d, int32_t tseq, Opaque * root) = 0; /** evacuate @p *lhs, that refers to state with interface @p lhs_iface, diff --git a/include/xo/gc/detail/AGCObject.hpp b/include/xo/gc/detail/AGCObject.hpp index ae2b8a08..7c2ac308 100644 --- a/include/xo/gc/detail/AGCObject.hpp +++ b/include/xo/gc/detail/AGCObject.hpp @@ -26,10 +26,11 @@ namespace xo { * by ACollector **/ struct AGCObject { + using typeseq = xo::facet::typeseq; using size_type = std::size_t; /** RTTI: unique id# for actual runtime data representation **/ - virtual int32_t _typeseq() const noexcept = 0; + virtual typeseq _typeseq() const noexcept = 0; virtual size_type shallow_size(Copaque d) const noexcept = 0; virtual Opaque shallow_copy(Copaque d, diff --git a/include/xo/gc/detail/ICollector_Any.hpp b/include/xo/gc/detail/ICollector_Any.hpp index b4836f66..ad55265d 100644 --- a/include/xo/gc/detail/ICollector_Any.hpp +++ b/include/xo/gc/detail/ICollector_Any.hpp @@ -6,6 +6,7 @@ #pragma once #include "ACollector.hpp" +#include "AGCObject.hpp" //#include namespace xo { @@ -34,7 +35,7 @@ namespace xo { [[noreturn]] size_type committed(Copaque, generation, role) const noexcept override { _fatal(); } // non-const methods - [[noreturn]] void install_type(Opaque, int32_t, IGCObject_Any &) noexcept override { _fatal(); } + [[noreturn]] bool install_type(Opaque, const AGCObject &) noexcept override { _fatal(); } [[noreturn]] void add_gc_root(Opaque, int32_t, Opaque *) override { _fatal(); } [[noreturn]] void forward_inplace(Opaque, AGCObject *, void **) override { _fatal(); } @@ -42,7 +43,7 @@ namespace xo { [[noreturn]] static void _fatal(); public: - static int32_t s_typeseq; + static typeseq s_typeseq; static bool _valid; }; } /*namespace mm*/ diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index c531ae13..19392896 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -41,8 +41,7 @@ namespace xo { static size_type reserved(const DX1Collector & d, generation g, role r); static size_type committed(const DX1Collector & d, generation g, role r); - static void install_type(DX1Collector & d, - int32_t seq, IGCObject_Any & iface); + static bool install_type(DX1Collector & d, const AGCObject & iface); static void add_gc_root(DX1Collector & d, int32_t tseq, Opaque * root); static void forward_inplace(DX1Collector & d, AGCObject * lhs_iface, void ** lhs_data); diff --git a/include/xo/gc/detail/ICollector_Xfer.hpp b/include/xo/gc/detail/ICollector_Xfer.hpp index c44bd3ca..d184cb9b 100644 --- a/include/xo/gc/detail/ICollector_Xfer.hpp +++ b/include/xo/gc/detail/ICollector_Xfer.hpp @@ -6,6 +6,7 @@ #pragma once #include "ACollector.hpp" +#include "AGCObject.hpp" namespace xo { namespace mm { @@ -30,7 +31,7 @@ namespace xo { // const methods - int32_t _typeseq() const noexcept override { return s_typeseq; } + typeseq _typeseq() const noexcept override { return s_typeseq; } size_type allocated(Copaque d, generation g, role r) const noexcept override { return I::allocated(_dcast(d), g, r); } @@ -43,8 +44,8 @@ namespace xo { // non-const methods - void install_type(Opaque d, int32_t tseq, IGCObject_Any & iface) override { - I::install_type(_dcast(d), tseq, iface); + bool install_type(Opaque d, const AGCObject & iface) override { + return I::install_type(_dcast(d), iface); } void add_gc_root(Opaque d, int32_t tseq, Opaque * root) override { I::add_gc_root(_dcast(d), tseq, root); @@ -57,12 +58,12 @@ namespace xo { using I = Impl; public: - static int32_t s_typeseq; + static typeseq s_typeseq; static bool _valid; }; template - int32_t + xo::facet::typeseq ICollector_Xfer::s_typeseq = facet::typeseq::id(); template diff --git a/include/xo/gc/detail/IGCObject_Any.hpp b/include/xo/gc/detail/IGCObject_Any.hpp index b387b9b9..88b19e8b 100644 --- a/include/xo/gc/detail/IGCObject_Any.hpp +++ b/include/xo/gc/detail/IGCObject_Any.hpp @@ -24,12 +24,13 @@ namespace xo { * @brief AGCObject implementation for empty variant instance **/ struct IGCObject_Any : public AGCObject { + using typeseq = xo::facet::typeseq; using size_type = std::size_t; const AGCObject * iface() const { return std::launder(this); } // from AGCObject - int32_t _typeseq() const noexcept override { return s_typeseq; } + typeseq _typeseq() const noexcept override { return s_typeseq; } [[noreturn]] size_type shallow_size(Copaque) const noexcept override { _fatal(); } [[noreturn]] Opaque shallow_copy(Copaque, @@ -41,7 +42,7 @@ namespace xo { [[noreturn]] static void _fatal(); public: - static int32_t s_typeseq; + static typeseq s_typeseq; static bool _valid; }; } /*namespace mm*/ diff --git a/include/xo/gc/detail/IGCObject_Xfer.hpp b/include/xo/gc/detail/IGCObject_Xfer.hpp index 95e20ff1..a9deb4ad 100644 --- a/include/xo/gc/detail/IGCObject_Xfer.hpp +++ b/include/xo/gc/detail/IGCObject_Xfer.hpp @@ -27,7 +27,7 @@ namespace xo { // const methods - int32_t _typeseq() const noexcept override { return s_typeseq; } + typeseq _typeseq() const noexcept override { return s_typeseq; } size_type shallow_size(Copaque d) const noexcept override { return I::shallow_size(_dcast(d)); } @@ -46,12 +46,12 @@ namespace xo { using I = Impl; public: - static int32_t s_typeseq; + static typeseq s_typeseq; static bool _valid; }; template - int32_t + xo::facet::typeseq IGCObject_Xfer::s_typeseq = facet::typeseq::id(); template diff --git a/include/xo/gc/detail/RCollector.hpp b/include/xo/gc/detail/RCollector.hpp index 47e9c43b..e6eeca6a 100644 --- a/include/xo/gc/detail/RCollector.hpp +++ b/include/xo/gc/detail/RCollector.hpp @@ -27,7 +27,7 @@ namespace xo { size_type reserved(generation g, role r) const noexcept { return O::iface()->reserved(O::data(), g, r); } size_type committed(generation g, role r) const noexcept { return O::iface()->committed(O::data(), g, r); } - void install_type(int32_t tseq, IGCObject_Any & iface) { return O::iface()->install_type(O::data(), tseq, iface); } + bool install_type(const AGCObject & iface) { return O::iface()->install_type(O::data(), iface); } void add_gc_root(int32_t tseq, Opaque * root) { O::iface()->add_gc_root(O::data(), tseq, root); } void forward_inplace(AGCObject * lhs_iface, void ** lhs_data) { O::iface()->forward_inplace(O::data(), lhs_iface, lhs_data); } diff --git a/include/xo/gc/detail/RGCObject.hpp b/include/xo/gc/detail/RGCObject.hpp index 29bebf15..4c3205b0 100644 --- a/include/xo/gc/detail/RGCObject.hpp +++ b/include/xo/gc/detail/RGCObject.hpp @@ -18,12 +18,13 @@ namespace xo { public: using ObjectType = Object; using DataPtr = Object::DataPtr; + using typeseq = xo::facet::typeseq; using size_type = std::size_t; RGCObject() = default; RGCObject(Object::DataPtr data) : Object{std::move(data)} {} - int32_t _typeseq() const noexcept { return O::iface()->_typeseq(); } + typeseq _typeseq() const noexcept { return O::iface()->_typeseq(); } size_type shallow_size() const noexcept { O::iface()->shallow_size(O::data()); } Opaque shallow_copy(obj mm) const noexcept { O::iface()->shallow_copy(O::data(), mm); } size_type forward_children() noexcept { O::iface()->forward_children(O::data()); } diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 7e91ddbd..de747764 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -13,9 +13,12 @@ #include #include #include +#include +#include // for ::getpagesize() namespace xo { using xo::mm::AAllocator; + using xo::facet::typeseq; using xo::facet::with_facet; namespace mm { @@ -61,6 +64,19 @@ namespace xo { config_.arena_config_.header_.age_bits_ + config_.arena_config_.header_.tseq_bits_ <= 64); + size_t page_z = getpagesize(); + + /* 1MB reserved address space enough for up to 128k distinct types. + * In this case don't want to use hugepages since actual #of types + * likely << .size/8 + */ + object_types_ = DArena::map( + ArenaConfig{ + .name_ = "x1-object-types", + .size_ = cfg.object_types_z_, + .hugepage_z_ = page_z, + .store_header_flag_ = false}); + for (uint32_t igen = 0, ngen = cfg.n_generation_; igen < ngen; ++igen) { space_storage_[0][igen] = DArena::map(cfg.arena_config_); space_storage_[1][igen] = DArena::map(cfg.arena_config_); @@ -103,7 +119,7 @@ namespace xo { accumulate_total_aux(const DX1Collector & d, size_t (DArena::* get_stat_fn)() const) noexcept { - size_t z = 0; + size_t z = (d.object_types_.*get_stat_fn)(); for (role ri : role::all()) { for (generation gj{0}; gj < d.config_.n_generation_; ++gj) { @@ -182,6 +198,23 @@ namespace xo { return config_.arena_config_.header_.is_forwarding_tseq(hdr); } + bool + DX1Collector::install_type(const AGCObject & meta) noexcept + { + typeseq tseq = meta._typeseq(); + + bool ok = object_types_.expand(sizeof(AGCObject) * (tseq.seqno() + 1)); + if (!ok) + return false; + + AGCObject * v = reinterpret_cast(object_types_.lo_); + + /* explicitly copying vtable pointer here */ + std::memcpy((void*)&(v[tseq.seqno()]), (void*)&meta, sizeof(AGCObject)); + + return true; + } + auto DX1Collector::alloc(size_type z) noexcept -> value_type { diff --git a/src/gc/ICollector_Any.cpp b/src/gc/ICollector_Any.cpp index d3ea1fb8..99563bf9 100644 --- a/src/gc/ICollector_Any.cpp +++ b/src/gc/ICollector_Any.cpp @@ -27,7 +27,7 @@ namespace xo { std::terminate(); } - int32_t + typeseq ICollector_Any::s_typeseq = typeseq::id(); bool diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp index b4d8f24d..5428e75b 100644 --- a/src/gc/ICollector_DX1Collector.cpp +++ b/src/gc/ICollector_DX1Collector.cpp @@ -47,16 +47,11 @@ namespace xo { return stat_helper(d, &DArena::committed, g, r); } - void + bool ICollector_DX1Collector::install_type(DX1Collector & d, - std::int32_t tseq, - IGCObject_Any & iface) + const AGCObject & iface) { - (void)d; - (void)tseq; - (void)iface; - - assert(false); + return d.install_type(iface); } void diff --git a/src/gc/IGCObject_Any.cpp b/src/gc/IGCObject_Any.cpp index 0de21224..f39445c8 100644 --- a/src/gc/IGCObject_Any.cpp +++ b/src/gc/IGCObject_Any.cpp @@ -22,7 +22,7 @@ namespace xo { std::terminate(); } - int32_t + typeseq IGCObject_Any::s_typeseq = typeseq::id(); bool From f6e515a5b4bcfb51e8f24959372e466de39bef1b Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 2 Jan 2026 09:53:23 -0500 Subject: [PATCH 009/174] xo-object2: type registration + gc fixes --- include/xo/gc/DX1Collector.hpp | 29 +++++++++++++++++++++++-- include/xo/gc/detail/ICollector_Any.hpp | 3 ++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 778f0518..c5b84839 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -5,11 +5,13 @@ #pragma once -#include "arena/ArenaConfig.hpp" -#include "arena/DArena.hpp" +#include "GCObject.hpp" #include "generation.hpp" #include "object_age.hpp" #include "role.hpp" +#include +#include +#include #include #include @@ -86,6 +88,9 @@ namespace xo { **/ ArenaConfig arena_config_; + /** storage for N object types requires 8*N bytes **/ + std::size_t object_types_z_ = 2*1024*1024; + /** number of bits to represent generation **/ std::uint64_t gen_bits_ = 8; @@ -154,8 +159,15 @@ namespace xo { using value_type = DArena::value_type; using header_type = DArena::header_type; + /** hard max typeseq for collector-registered types **/ + static constexpr size_t c_max_typeseq = 4096; + + /** Create X1 collector instance. **/ explicit DX1Collector(const CollectorConfig & cfg); + std::string_view name() const { return config_.name_; } + + const DArena * get_object_types() const noexcept { return &object_types_; } const DArena * get_space(role r, generation g) const noexcept { return space_[r][g]; } 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); } @@ -194,6 +206,14 @@ namespace xo { /** Retreive bookkeeping info for allocation at @p mem. **/ AllocInfo alloc_info(value_type mem) const noexcept; + // ----- type registration ----- + + /** Register object type with this collector. + * Provides shallow copy and pointer forwarding for instances of this + * type. + **/ + bool install_type(const AGCObject & meta) noexcept; + // ----- allocation ----- /** simple allocation. new allocs always in gen0 to-space **/ @@ -239,6 +259,11 @@ namespace xo { /** current gc state **/ GCRunState runstate_; + /** (ab)using arena to get an extensible array of object types. + * For each type need to store one (8-byte) IGCObject_Any instance, + **/ + DArena object_types_; + /** collector-managed memory here. * - space_[1] is from-space * - space_[0] is to-space diff --git a/include/xo/gc/detail/ICollector_Any.hpp b/include/xo/gc/detail/ICollector_Any.hpp index ad55265d..5b0ce66d 100644 --- a/include/xo/gc/detail/ICollector_Any.hpp +++ b/include/xo/gc/detail/ICollector_Any.hpp @@ -24,10 +24,11 @@ namespace xo { * @brief Stub Collector Implementation for empty variant instance **/ struct ICollector_Any : public ACollector { + using typeseq = xo::facet::typeseq; using size_type = std::size_t; // from ACollector - int32_t _typeseq() const noexcept override { return s_typeseq; } + typeseq _typeseq() const noexcept override { return s_typeseq; } // const methods [[noreturn]] size_type allocated(Copaque, generation, role) const noexcept override { _fatal(); } From f8df25537e71f267b89aa074539e634c214cce61 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 2 Jan 2026 10:20:19 -0500 Subject: [PATCH 010/174] xo-alloc: explicit typeseq arg to alloc --- include/xo/gc/DX1Collector.hpp | 14 ++++++++++---- include/xo/gc/detail/IAllocator_DX1Collector.hpp | 5 +++-- src/gc/DX1Collector.cpp | 8 ++++---- src/gc/IAllocator_DX1Collector.cpp | 13 +++++++++---- utest/DX1CollectorIterator.test.cpp | 3 ++- utest/random_allocs.cpp | 4 +++- 6 files changed, 31 insertions(+), 16 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index c5b84839..bd9d5a65 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -155,6 +155,7 @@ namespace xo { // ----- DX1Collector ----- struct DX1Collector { + using typeseq = xo::facet::typeseq; using size_type = DArena::size_type; using value_type = DArena::value_type; using header_type = DArena::header_type; @@ -216,9 +217,14 @@ namespace xo { // ----- allocation ----- - /** simple allocation. new allocs always in gen0 to-space **/ - value_type alloc(size_type z) noexcept; - /** compound allocation. To be followed immediately by: + /** simple allocation. allocate @p z bytes of memory + * for an object of type @p t. + * New allocs always in gen0 to-space + **/ + value_type alloc(typeseq t, size_type z) noexcept; + /** compound allocation. Allocate @p z bytes of memory + * for an object of type @p t. + * To be followed immediately by: * 1. zero or more calls to sub_alloc(zi, complete=false), then * 2. exactly one call to sub_alloc(zi, complete=true) * all the allocs in a compound allocation share the same @@ -226,7 +232,7 @@ namespace xo { * allocation with size z + sum(zi). * New allocs always in gen0 to-space **/ - value_type super_alloc(size_type z) noexcept; + value_type super_alloc(typeseq t, size_type z) noexcept; /** sub-allocation with preceding compound allocation. * New allocs always in gen0 to-space **/ diff --git a/include/xo/gc/detail/IAllocator_DX1Collector.hpp b/include/xo/gc/detail/IAllocator_DX1Collector.hpp index c525dadb..6ba79b59 100644 --- a/include/xo/gc/detail/IAllocator_DX1Collector.hpp +++ b/include/xo/gc/detail/IAllocator_DX1Collector.hpp @@ -29,6 +29,7 @@ namespace xo { * RAllocator RCollector.hpp */ struct IAllocator_DX1Collector { + using typeseq = xo::facet::typeseq; using size_type = std::size_t; using value_type = std::byte *; using range_type = AAllocator::range_type; @@ -58,8 +59,8 @@ namespace xo { static range_type alloc_range(const DX1Collector & d, DArena & ialloc) noexcept; /** always alloc in gen0 to-space **/ - static value_type alloc(DX1Collector & d, size_type z) noexcept; - static value_type super_alloc(DX1Collector & d, size_type z) noexcept; + static value_type alloc(DX1Collector & d, typeseq t, size_type z) noexcept; + static value_type super_alloc(DX1Collector & d, typeseq t, size_type z) noexcept; static value_type sub_alloc(DX1Collector & d, size_type z, bool complete) noexcept; /** expand gen0 spaces (both from-space and to-space) **/ static bool expand(DX1Collector & d, size_type z) noexcept; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index de747764..26223b92 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -216,14 +216,14 @@ namespace xo { } auto - DX1Collector::alloc(size_type z) noexcept -> value_type + DX1Collector::alloc(typeseq t, size_type z) noexcept -> value_type { - return with_facet::mkobj(new_space()).alloc(z); + return with_facet::mkobj(new_space()).alloc(t, z); } auto - DX1Collector::super_alloc(size_type z) noexcept -> value_type { - return with_facet::mkobj(new_space()).super_alloc(z); + DX1Collector::super_alloc(typeseq t, size_type z) noexcept -> value_type { + return with_facet::mkobj(new_space()).super_alloc(t, z); } auto diff --git a/src/gc/IAllocator_DX1Collector.cpp b/src/gc/IAllocator_DX1Collector.cpp index d3e172fc..b5b26b5e 100644 --- a/src/gc/IAllocator_DX1Collector.cpp +++ b/src/gc/IAllocator_DX1Collector.cpp @@ -12,6 +12,7 @@ namespace xo { using xo::facet::with_facet; + using xo::facet::typeseq; using std::size_t; using std::byte; @@ -80,15 +81,19 @@ namespace xo { } auto - IAllocator_DX1Collector::alloc(DX1Collector & d, size_type z) noexcept -> value_type + IAllocator_DX1Collector::alloc(DX1Collector & d, + typeseq t, + size_type z) noexcept -> value_type { - return d.alloc(z); + return d.alloc(t, z); } auto - IAllocator_DX1Collector::super_alloc(DX1Collector & d, size_type z) noexcept -> value_type + IAllocator_DX1Collector::super_alloc(DX1Collector & d, + typeseq t, + size_type z) noexcept -> value_type { - return d.super_alloc(z); + return d.super_alloc(t, z); } auto diff --git a/utest/DX1CollectorIterator.test.cpp b/utest/DX1CollectorIterator.test.cpp index ba3ba09c..58a498c5 100644 --- a/utest/DX1CollectorIterator.test.cpp +++ b/utest/DX1CollectorIterator.test.cpp @@ -30,6 +30,7 @@ namespace xo { using xo::mm::cmpresult; using xo::mm::padding; using xo::facet::with_facet; + using xo::facet::typeseq; using std::byte; namespace ut { @@ -109,7 +110,7 @@ namespace xo { REQUIRE(a1o.allocated() == 0); size_t req_z = 13; - byte * mem = gc.alloc(req_z); + byte * mem = gc.alloc(typeseq::anon(), req_z); REQUIRE(mem != nullptr); diff --git a/utest/random_allocs.cpp b/utest/random_allocs.cpp index 40315342..654c6c9a 100644 --- a/utest/random_allocs.cpp +++ b/utest/random_allocs.cpp @@ -43,6 +43,8 @@ namespace utest { xoshiro256ss * p_rgen, obj mm) { + using xo::facet::typeseq; + scope log(XO_DEBUG(catch_flag), xtag("n-alloc", n_alloc)); /* track allocs. verify: @@ -65,7 +67,7 @@ namespace utest { bool ok_flag = true; - std::byte * mem = mm.alloc(z); + std::byte * mem = mm.alloc(typeseq::anon(), z); log && log(xtag("i_alloc", i_alloc), xtag("si", si), From 25a85b71d3d3d171eee1c6400e5bad242578458c Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 2 Jan 2026 18:55:53 -0500 Subject: [PATCH 011/174] xo-object2: utest: ++ allocation in collector utest --- include/xo/gc/DX1Collector.hpp | 5 +++++ include/xo/gc/detail/IAllocator_DX1Collector.hpp | 1 + src/gc/DX1Collector.cpp | 5 +++++ src/gc/IAllocator_DX1Collector.cpp | 6 ++++++ 4 files changed, 17 insertions(+) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index bd9d5a65..429d2204 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -237,6 +237,11 @@ namespace xo { * New allocs always in gen0 to-space **/ value_type sub_alloc(size_type z, bool complete) noexcept; + /** Allocate copy of source object at @p src. + * Source must be owned by this collector instance. + * Copy will have incremented age. + **/ + value_type alloc_copy(value_type src) noexcept; /** expand gen0 committed size to at least @p z. **/ bool expand(size_type z) noexcept; diff --git a/include/xo/gc/detail/IAllocator_DX1Collector.hpp b/include/xo/gc/detail/IAllocator_DX1Collector.hpp index 6ba79b59..0ed1ab1f 100644 --- a/include/xo/gc/detail/IAllocator_DX1Collector.hpp +++ b/include/xo/gc/detail/IAllocator_DX1Collector.hpp @@ -62,6 +62,7 @@ namespace xo { static value_type alloc(DX1Collector & d, typeseq t, size_type z) noexcept; static value_type super_alloc(DX1Collector & d, typeseq t, size_type z) noexcept; static value_type sub_alloc(DX1Collector & d, size_type z, bool complete) noexcept; + static value_type alloc_copy(DX1Collector & d, value_type src) noexcept; /** expand gen0 spaces (both from-space and to-space) **/ static bool expand(DX1Collector & d, size_type z) noexcept; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 26223b92..edfd842c 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -231,6 +231,11 @@ namespace xo { return with_facet::mkobj(new_space()).sub_alloc(z, complete); } + auto + DX1Collector::alloc_copy(value_type src) noexcept -> value_type { + return with_facet::mkobj(new_space()).alloc_copy(src); + } + bool DX1Collector::expand(size_type z) noexcept { diff --git a/src/gc/IAllocator_DX1Collector.cpp b/src/gc/IAllocator_DX1Collector.cpp index b5b26b5e..6c4dc5bf 100644 --- a/src/gc/IAllocator_DX1Collector.cpp +++ b/src/gc/IAllocator_DX1Collector.cpp @@ -102,6 +102,12 @@ namespace xo { return d.sub_alloc(z, complete); } + auto + IAllocator_DX1Collector::alloc_copy(DX1Collector & d, value_type src) noexcept -> value_type + { + return d.alloc_copy(src); + } + bool IAllocator_DX1Collector::expand(DX1Collector & d, size_type z) noexcept { From 454d353c9497787fdcda1dbe408d51a973c75116 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 2 Jan 2026 22:33:54 -0500 Subject: [PATCH 012/174] xo-gc: + Collector.is_type_installed() --- include/xo/gc/DX1Collector.hpp | 6 ++++++ include/xo/gc/detail/ACollector.hpp | 2 ++ include/xo/gc/detail/ICollector_Any.hpp | 1 + include/xo/gc/detail/ICollector_DX1Collector.hpp | 2 ++ include/xo/gc/detail/ICollector_Xfer.hpp | 3 +++ include/xo/gc/detail/RCollector.hpp | 2 ++ src/gc/DX1Collector.cpp | 13 +++++++++++++ src/gc/ICollector_DX1Collector.cpp | 6 ++++++ 8 files changed, 35 insertions(+) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 429d2204..9c7c69da 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -204,6 +204,12 @@ namespace xo { /** true iff original alloc has been replaced by a forwarding pointer **/ bool is_forwarding_header(header_type hdr) const noexcept; + /** true iff type with id @p tseq has known metadata + * (i.e. has appeared in preceding call to install_type + * for this collector) + **/ + bool is_type_installed(typeseq tseq) const noexcept; + /** Retreive bookkeeping info for allocation at @p mem. **/ AllocInfo alloc_info(value_type mem) const noexcept; diff --git a/include/xo/gc/detail/ACollector.hpp b/include/xo/gc/detail/ACollector.hpp index 0298ad2e..69f10085 100644 --- a/include/xo/gc/detail/ACollector.hpp +++ b/include/xo/gc/detail/ACollector.hpp @@ -42,6 +42,8 @@ namespace xo { generation g, role r) const noexcept = 0; virtual size_type committed(Copaque d, generation g, role r) const noexcept = 0; + virtual bool is_type_installed(Copaque d, + typeseq tseq) const noexcept = 0; /** install interface @p iface for representation with typeseq @p tseq * in collector @p d. diff --git a/include/xo/gc/detail/ICollector_Any.hpp b/include/xo/gc/detail/ICollector_Any.hpp index 5b0ce66d..3b0b10a0 100644 --- a/include/xo/gc/detail/ICollector_Any.hpp +++ b/include/xo/gc/detail/ICollector_Any.hpp @@ -34,6 +34,7 @@ namespace xo { [[noreturn]] size_type allocated(Copaque, generation, role) const noexcept override { _fatal(); } [[noreturn]] size_type reserved(Copaque, generation, role) const noexcept override { _fatal(); } [[noreturn]] size_type committed(Copaque, generation, role) const noexcept override { _fatal(); } + [[noreturn]] bool is_type_installed(Copaque, typeseq) const noexcept override { _fatal(); } // non-const methods [[noreturn]] bool install_type(Opaque, const AGCObject &) noexcept override { _fatal(); } diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index 19392896..233eeba0 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -30,6 +30,7 @@ namespace xo { struct ICollector_DX1Collector { using size_type = std::size_t; using header_type = DArena::header_type; + using typeseq = xo::facet::typeseq; static bool check_move_policy(const DX1Collector & d, header_type alloc_hdr, @@ -40,6 +41,7 @@ namespace xo { static size_type allocated(const DX1Collector & d, generation g, role r); static size_type reserved(const DX1Collector & d, generation g, role r); static size_type committed(const DX1Collector & d, generation g, role r); + static bool is_type_installed(const DX1Collector & d, typeseq tseq); static bool install_type(DX1Collector & d, const AGCObject & iface); static void add_gc_root(DX1Collector & d, int32_t tseq, Opaque * root); diff --git a/include/xo/gc/detail/ICollector_Xfer.hpp b/include/xo/gc/detail/ICollector_Xfer.hpp index d184cb9b..352dc82f 100644 --- a/include/xo/gc/detail/ICollector_Xfer.hpp +++ b/include/xo/gc/detail/ICollector_Xfer.hpp @@ -41,6 +41,9 @@ namespace xo { size_type committed(Copaque d, generation g, role r) const noexcept override { return I::committed(_dcast(d), g, r); } + bool is_type_installed(Copaque d, typeseq tseq) const noexcept override { + return I::is_type_installed(_dcast(d), tseq); + } // non-const methods diff --git a/include/xo/gc/detail/RCollector.hpp b/include/xo/gc/detail/RCollector.hpp index e6eeca6a..bd28dc66 100644 --- a/include/xo/gc/detail/RCollector.hpp +++ b/include/xo/gc/detail/RCollector.hpp @@ -17,6 +17,7 @@ namespace xo { using ObjectType = Object; using DataPtr = Object::DataPtr; using size_type = std::size_t; + using typeseq = ACollector::typeseq; //using value_type = std::byte *; RCollector() = default; @@ -26,6 +27,7 @@ namespace xo { size_type allocated(generation g, role r) const noexcept { return O::iface()->allocated(O::data(), g, r); } size_type reserved(generation g, role r) const noexcept { return O::iface()->reserved(O::data(), g, r); } size_type committed(generation g, role r) const noexcept { return O::iface()->committed(O::data(), g, r); } + bool is_type_installed(typeseq tseq) const noexcept { return O::iface()->is_type_installed(O::data(), tseq); } bool install_type(const AGCObject & iface) { return O::iface()->install_type(O::data(), iface); } void add_gc_root(int32_t tseq, Opaque * root) { O::iface()->add_gc_root(O::data(), tseq, root); } diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index edfd842c..d9e7e283 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -198,6 +198,19 @@ namespace xo { return config_.arena_config_.header_.is_forwarding_tseq(hdr); } + bool + DX1Collector::is_type_installed(typeseq tseq) const noexcept + { + if (object_types_.committed() < sizeof(AGCObject) * (tseq.seqno() + 1)) + return false; + + AGCObject * v = reinterpret_cast(object_types_.lo_); + + void * vtable = *(void **)&(v[tseq.seqno()]); + + return (vtable != nullptr); + } + bool DX1Collector::install_type(const AGCObject & meta) noexcept { diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp index 5428e75b..82e5e937 100644 --- a/src/gc/ICollector_DX1Collector.cpp +++ b/src/gc/ICollector_DX1Collector.cpp @@ -47,6 +47,12 @@ namespace xo { return stat_helper(d, &DArena::committed, g, r); } + bool + ICollector_DX1Collector::is_type_installed(const DX1Collector & d, typeseq tseq) + { + return d.is_type_installed(tseq); + } + bool ICollector_DX1Collector::install_type(DX1Collector & d, const AGCObject & iface) From c77a86dd5caafca9238ab5150f93b105d976dcc5 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 3 Jan 2026 00:19:26 -0500 Subject: [PATCH 013/174] xo-gc: + execute_gc() scaffold --- include/xo/gc/DX1Collector.hpp | 27 +++++++++ include/xo/gc/detail/ACollector.hpp | 10 ++++ include/xo/gc/detail/ICollector_Any.hpp | 1 + .../xo/gc/detail/ICollector_DX1Collector.hpp | 1 + include/xo/gc/detail/ICollector_Xfer.hpp | 6 +- include/xo/gc/detail/RCollector.hpp | 2 +- src/gc/DX1Collector.cpp | 59 +++++++++++++++++++ src/gc/ICollector_DX1Collector.cpp | 11 ++-- 8 files changed, 111 insertions(+), 6 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 9c7c69da..c14d96e0 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -221,6 +221,22 @@ namespace xo { **/ bool install_type(const AGCObject & meta) noexcept; + /** add GC root at @p root_addr, with type @p typeseq **/ + void add_gc_root(typeseq tseq, Opaque * root_addr) noexcept; + + /** 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. + **/ + void request_gc(generation upto) noexcept; + + /** Execute gc immediately, for all generations < @p upto **/ + void execute_gc(generation upto) noexcept; + // ----- allocation ----- /** simple allocation. allocate @p z bytes of memory @@ -269,6 +285,12 @@ namespace xo { /** discard all allocated memory **/ void clear() noexcept; + private: + /** swap from- and to- roles for all generations < @p upto **/ + void swap_roles(generation upto) noexcept; + /** copy roots + everything reachable from them, to to-space **/ + void copy_roots(generation upto) noexcept; + public: /** garbage collector configuration **/ CollectorConfig config_; @@ -281,6 +303,11 @@ namespace xo { **/ DArena object_types_; + /** gc disabled whenever gc_blocked_ > 0 **/ + uint32_t gc_blocked_ = 0; + /** if > 0: need gc for all generations < gc_pending_upto_ **/ + generation gc_pending_upto_; + /** collector-managed memory here. * - space_[1] is from-space * - space_[0] is to-space diff --git a/include/xo/gc/detail/ACollector.hpp b/include/xo/gc/detail/ACollector.hpp index 69f10085..9c738a60 100644 --- a/include/xo/gc/detail/ACollector.hpp +++ b/include/xo/gc/detail/ACollector.hpp @@ -59,6 +59,16 @@ namespace xo { virtual bool install_type(Opaque d, const AGCObject & iface) = 0; virtual void add_gc_root(Opaque d, int32_t tseq, Opaque * root) = 0; + /** 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. + **/ + virtual void request_gc(Opaque d, generation upto) = 0; + /** evacuate @p *lhs, that refers to state with interface @p lhs_iface, * to collector @p d's to-space. Replace *lhs_data with forwarding pointer * diff --git a/include/xo/gc/detail/ICollector_Any.hpp b/include/xo/gc/detail/ICollector_Any.hpp index 3b0b10a0..76d46959 100644 --- a/include/xo/gc/detail/ICollector_Any.hpp +++ b/include/xo/gc/detail/ICollector_Any.hpp @@ -39,6 +39,7 @@ namespace xo { // non-const methods [[noreturn]] bool install_type(Opaque, const AGCObject &) noexcept override { _fatal(); } [[noreturn]] void add_gc_root(Opaque, int32_t, Opaque *) override { _fatal(); } + [[noreturn]] void request_gc(Opaque, generation) override { _fatal(); } [[noreturn]] void forward_inplace(Opaque, AGCObject *, void **) override { _fatal(); } private: diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index 233eeba0..82330435 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -45,6 +45,7 @@ namespace xo { static bool install_type(DX1Collector & d, const AGCObject & iface); static void add_gc_root(DX1Collector & d, int32_t tseq, Opaque * root); + static void request_gc(DX1Collector & d, generation upto); static void forward_inplace(DX1Collector & d, AGCObject * lhs_iface, void ** lhs_data); static int32_t s_typeseq; diff --git a/include/xo/gc/detail/ICollector_Xfer.hpp b/include/xo/gc/detail/ICollector_Xfer.hpp index 352dc82f..00599bd5 100644 --- a/include/xo/gc/detail/ICollector_Xfer.hpp +++ b/include/xo/gc/detail/ICollector_Xfer.hpp @@ -53,7 +53,11 @@ namespace xo { void add_gc_root(Opaque d, int32_t tseq, Opaque * root) override { I::add_gc_root(_dcast(d), tseq, root); } - void forward_inplace(Opaque d, AGCObject * lhs_iface, void ** lhs_data) override { + void request_gc(Opaque d, generation upto) override { + I::request_gc(_dcast(d), upto); + } + void forward_inplace(Opaque d, + AGCObject * lhs_iface, void ** lhs_data) override { I::forward_inplace(_dcast(d), lhs_iface, lhs_data); } diff --git a/include/xo/gc/detail/RCollector.hpp b/include/xo/gc/detail/RCollector.hpp index bd28dc66..9af508d8 100644 --- a/include/xo/gc/detail/RCollector.hpp +++ b/include/xo/gc/detail/RCollector.hpp @@ -31,7 +31,7 @@ namespace xo { bool install_type(const AGCObject & iface) { return O::iface()->install_type(O::data(), iface); } void add_gc_root(int32_t tseq, Opaque * root) { O::iface()->add_gc_root(O::data(), tseq, root); } - + void request_gc(generation g) { O::iface()->request_gc(O::data(), g); } void forward_inplace(AGCObject * lhs_iface, void ** lhs_data) { O::iface()->forward_inplace(O::data(), lhs_iface, lhs_data); } static bool _valid; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index d9e7e283..45da4f69 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -228,6 +228,65 @@ namespace xo { return true; } + void + DX1Collector::add_gc_root(typeseq tseq, + Opaque * root) noexcept + { + (void)tseq; + (void)root; + } + + void + DX1Collector::request_gc(generation upto) noexcept + { + if (gc_blocked_ > 0) { + if (gc_pending_upto_ < upto) { + this->gc_pending_upto_ = upto; + } + + /* intend collecting later */ + } else { + this->execute_gc(upto); + } + } + + void + DX1Collector::execute_gc(generation upto) noexcept + { + scope log(XO_DEBUG(true), xtag("upto", upto)); + + //auto t0 = std::chrono::steady_clock::now(); + + log && log("step 0a : [STUB] snapshot alloc state"); + + log && log("step 0b : [STUB] scan for object statistics"); + + log && log("step 1 : swap from/to roles"); + this->swap_roles(upto); + + log && log("step 2a : copy roots"); + this->copy_roots(upto); + + log && log("step 2b : [STUB] copy pinned"); + log && log("step 3a : [STUB] run destructors"); + log && log("step 3b : [STUB] keep reachable weak pointers"); + log && log("step 4 : [STUB] cleanup"); + } + + void + DX1Collector::swap_roles(generation upto) noexcept + { + for (generation g = generation{0}; g < upto; ++g) { + std::swap(space_[role::to_space()][g], space_[role::from_space()][g]); + } + } + + void + DX1Collector::copy_roots(generation upto) noexcept + { + scope log(XO_DEBUG(true), "STUB", xtag("upto", upto)); + } + auto DX1Collector::alloc(typeseq t, size_type z) noexcept -> value_type { diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp index 82e5e937..2f049046 100644 --- a/src/gc/ICollector_DX1Collector.cpp +++ b/src/gc/ICollector_DX1Collector.cpp @@ -65,11 +65,14 @@ namespace xo { int32_t tseq, Opaque * root) { - (void)d; - (void)tseq; - (void)root; + d.add_gc_root(typeseq(tseq), root); + } - assert(false); + void + ICollector_DX1Collector::request_gc(DX1Collector & d, + generation upto) + { + d.request_gc(upto); } void From e35a6ae587eb308bfc06563d40cbae12c2a04dea Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 3 Jan 2026 00:33:35 -0500 Subject: [PATCH 014/174] xo-gc: + poly gc root method --- include/xo/gc/DX1Collector.hpp | 2 +- include/xo/gc/detail/ACollector.hpp | 3 ++- include/xo/gc/detail/ICollector_Any.hpp | 2 +- include/xo/gc/detail/ICollector_DX1Collector.hpp | 2 +- include/xo/gc/detail/ICollector_Xfer.hpp | 4 ++-- include/xo/gc/detail/RCollector.hpp | 2 +- src/gc/DX1Collector.cpp | 6 ++---- src/gc/ICollector_DX1Collector.cpp | 7 +++---- 8 files changed, 13 insertions(+), 15 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index c14d96e0..10b6b7fa 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -222,7 +222,7 @@ namespace xo { bool install_type(const AGCObject & meta) noexcept; /** add GC root at @p root_addr, with type @p typeseq **/ - void add_gc_root(typeseq tseq, Opaque * root_addr) noexcept; + void add_gc_root_poly(obj * p_root) noexcept; /** Request immediate collection. * 1. if collection is enabled, immediately collect all generations diff --git a/include/xo/gc/detail/ACollector.hpp b/include/xo/gc/detail/ACollector.hpp index 9c738a60..36b4bffb 100644 --- a/include/xo/gc/detail/ACollector.hpp +++ b/include/xo/gc/detail/ACollector.hpp @@ -57,7 +57,8 @@ namespace xo { * Return false if installation fails (e.g. memory exhausted) **/ virtual bool install_type(Opaque d, const AGCObject & iface) = 0; - virtual void add_gc_root(Opaque d, int32_t tseq, Opaque * root) = 0; + virtual void add_gc_root_poly(Opaque d, obj * p_root) = 0; + //virtual void add_gc_root_typed(Opaque d, typeseq tseq, Opaque * root) = 0; /** Request immediate collection. * 1. if collection is enabled, immediately collect all generations diff --git a/include/xo/gc/detail/ICollector_Any.hpp b/include/xo/gc/detail/ICollector_Any.hpp index 76d46959..6ed758c2 100644 --- a/include/xo/gc/detail/ICollector_Any.hpp +++ b/include/xo/gc/detail/ICollector_Any.hpp @@ -38,7 +38,7 @@ namespace xo { // non-const methods [[noreturn]] bool install_type(Opaque, const AGCObject &) noexcept override { _fatal(); } - [[noreturn]] void add_gc_root(Opaque, int32_t, Opaque *) override { _fatal(); } + [[noreturn]] void add_gc_root_poly(Opaque, obj *) override { _fatal(); } [[noreturn]] void request_gc(Opaque, generation) override { _fatal(); } [[noreturn]] void forward_inplace(Opaque, AGCObject *, void **) override { _fatal(); } diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index 82330435..61010310 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -44,7 +44,7 @@ namespace xo { static bool is_type_installed(const DX1Collector & d, typeseq tseq); static bool install_type(DX1Collector & d, const AGCObject & iface); - static void add_gc_root(DX1Collector & d, int32_t tseq, Opaque * root); + static void add_gc_root_poly(DX1Collector & d, obj * p_root); static void request_gc(DX1Collector & d, generation upto); static void forward_inplace(DX1Collector & d, AGCObject * lhs_iface, void ** lhs_data); diff --git a/include/xo/gc/detail/ICollector_Xfer.hpp b/include/xo/gc/detail/ICollector_Xfer.hpp index 00599bd5..d7d43ef4 100644 --- a/include/xo/gc/detail/ICollector_Xfer.hpp +++ b/include/xo/gc/detail/ICollector_Xfer.hpp @@ -50,8 +50,8 @@ namespace xo { bool install_type(Opaque d, const AGCObject & iface) override { return I::install_type(_dcast(d), iface); } - void add_gc_root(Opaque d, int32_t tseq, Opaque * root) override { - I::add_gc_root(_dcast(d), tseq, root); + void add_gc_root_poly(Opaque d, obj * p_root) override { + I::add_gc_root_poly(_dcast(d), p_root); } void request_gc(Opaque d, generation upto) override { I::request_gc(_dcast(d), upto); diff --git a/include/xo/gc/detail/RCollector.hpp b/include/xo/gc/detail/RCollector.hpp index 9af508d8..da8fc44e 100644 --- a/include/xo/gc/detail/RCollector.hpp +++ b/include/xo/gc/detail/RCollector.hpp @@ -30,7 +30,7 @@ namespace xo { bool is_type_installed(typeseq tseq) const noexcept { return O::iface()->is_type_installed(O::data(), tseq); } bool install_type(const AGCObject & iface) { return O::iface()->install_type(O::data(), iface); } - void add_gc_root(int32_t tseq, Opaque * root) { O::iface()->add_gc_root(O::data(), tseq, root); } + void add_gc_root_poly(obj * p_root) { O::iface()->add_gc_root(O::data(), p_root); } void request_gc(generation g) { O::iface()->request_gc(O::data(), g); } void forward_inplace(AGCObject * lhs_iface, void ** lhs_data) { O::iface()->forward_inplace(O::data(), lhs_iface, lhs_data); } diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 45da4f69..46ea2ac8 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -229,11 +229,9 @@ namespace xo { } void - DX1Collector::add_gc_root(typeseq tseq, - Opaque * root) noexcept + DX1Collector::add_gc_root_poly(obj * p_root) noexcept { - (void)tseq; - (void)root; + (void)p_root; } void diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp index 2f049046..a138c9b6 100644 --- a/src/gc/ICollector_DX1Collector.cpp +++ b/src/gc/ICollector_DX1Collector.cpp @@ -61,11 +61,10 @@ namespace xo { } void - ICollector_DX1Collector::add_gc_root(DX1Collector & d, - int32_t tseq, - Opaque * root) + ICollector_DX1Collector::add_gc_root_poly(DX1Collector & d, + obj * p_root) { - d.add_gc_root(typeseq(tseq), root); + d.add_gc_root_poly(p_root); } void From 2e7f57465509e0c1990ead84ae01b239d79c24bc Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 3 Jan 2026 13:53:21 -0500 Subject: [PATCH 015/174] xo-gc: + arena for object roots --- include/xo/gc/DX1Collector.hpp | 9 +++++++++ include/xo/gc/detail/RCollector.hpp | 8 +++++++- src/gc/DX1Collector.cpp | 23 +++++++++++++++++++---- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 10b6b7fa..f6ec9b2d 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -91,6 +91,9 @@ namespace xo { /** storage for N object types requires 8*N bytes **/ std::size_t object_types_z_ = 2*1024*1024; + /** storage for N object roots requires 8*N bytes **/ + std::size_t object_roots_z_ = 16*1024; + /** number of bits to represent generation **/ std::uint64_t gen_bits_ = 8; @@ -169,6 +172,7 @@ namespace xo { std::string_view name() const { return config_.name_; } const DArena * get_object_types() const noexcept { return &object_types_; } + const DArena * get_roots() const noexcept { return &roots_; } const DArena * get_space(role r, generation g) const noexcept { return space_[r][g]; } 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); } @@ -308,6 +312,11 @@ namespace xo { /** if > 0: need gc for all generations < gc_pending_upto_ **/ generation gc_pending_upto_; + /** (ab)using arena to get extensible list of root objects. + * For each root store one address (type obj*) + **/ + DArena roots_; + /** collector-managed memory here. * - space_[1] is from-space * - space_[0] is to-space diff --git a/include/xo/gc/detail/RCollector.hpp b/include/xo/gc/detail/RCollector.hpp index da8fc44e..5e3eddb3 100644 --- a/include/xo/gc/detail/RCollector.hpp +++ b/include/xo/gc/detail/RCollector.hpp @@ -30,10 +30,16 @@ namespace xo { bool is_type_installed(typeseq tseq) const noexcept { return O::iface()->is_type_installed(O::data(), tseq); } bool install_type(const AGCObject & iface) { return O::iface()->install_type(O::data(), iface); } - void add_gc_root_poly(obj * p_root) { O::iface()->add_gc_root(O::data(), p_root); } + void add_gc_root_poly(obj * p_root) { O::iface()->add_gc_root_poly(O::data(), p_root); } void request_gc(generation g) { O::iface()->request_gc(O::data(), g); } void forward_inplace(AGCObject * lhs_iface, void ** lhs_data) { O::iface()->forward_inplace(O::data(), lhs_iface, lhs_data); } + /** add root @p p_root **/ + template + void add_gc_root(obj * p_root) { + O::iface()->add_gc_root_poly(O::data(), (obj *)p_root); + } + static bool _valid; }; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 46ea2ac8..2c86d5dd 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -77,6 +77,13 @@ namespace xo { .hugepage_z_ = page_z, .store_header_flag_ = false}); + roots_ = DArena::map( + ArenaConfig{ + .name_ = "x1-object-roots", + .size_ = cfg.object_roots_z_, + .hugepage_z_ = page_z, + .store_header_flag_ = false}); + for (uint32_t igen = 0, ngen = cfg.n_generation_; igen < ngen; ++igen) { space_storage_[0][igen] = DArena::map(cfg.arena_config_); space_storage_[1][igen] = DArena::map(cfg.arena_config_); @@ -119,7 +126,10 @@ namespace xo { accumulate_total_aux(const DX1Collector & d, size_t (DArena::* get_stat_fn)() const) noexcept { - size_t z = (d.object_types_.*get_stat_fn)(); + size_t z1 = (d.object_types_.*get_stat_fn)(); + size_t z2 = (d.roots_.*get_stat_fn)(); + + size_t z3 = 0; for (role ri : role::all()) { for (generation gj{0}; gj < d.config_.n_generation_; ++gj) { @@ -127,11 +137,11 @@ namespace xo { assert(arena); - z += (arena->*get_stat_fn)(); + z3 += (arena->*get_stat_fn)(); } } - return z; + return z1 + z2 + z3; } } @@ -231,7 +241,12 @@ namespace xo { void DX1Collector::add_gc_root_poly(obj * p_root) noexcept { - (void)p_root; + std::byte * mem + = roots_.alloc(typeseq::anon(), + sizeof(obj*)); + assert(mem); + + *(obj **)mem = p_root; } void From f27dd9f0a1d3681f014576dd35d2c74d07d2a514 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 4 Jan 2026 00:34:19 -0500 Subject: [PATCH 016/174] xo-gc: copy/move step for collection phase --- include/xo/gc/DX1Collector.hpp | 36 +++- src/gc/DX1Collector.cpp | 310 ++++++++++++++++++++++++++++- src/gc/ICollector_DX1Collector.cpp | 104 +--------- 3 files changed, 344 insertions(+), 106 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index f6ec9b2d..bde9f04d 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -144,10 +144,10 @@ namespace xo { static GCRunState gc_not_running(); static GCRunState gc_upto(generation g); - bool is_running() const { return gc_upto_ > 0; } - generation gc_upto() const { return gc_upto_; } + bool is_running() const { return gc_upto_ > 0; } + private: /** running gc collecting all generations gi < gc_upto **/ generation gc_upto_; @@ -217,7 +217,12 @@ namespace xo { /** Retreive bookkeeping info for allocation at @p mem. **/ AllocInfo alloc_info(value_type mem) const noexcept; - // ----- type registration ----- + // ----- app memory model ----- + + /** lookup interface from type sequence + * (can use tseq = typeseq::id() for type T) + **/ + const AGCObject * lookup_type(typeseq tseq) const noexcept; /** Register object type with this collector. * Provides shallow copy and pointer forwarding for instances of this @@ -228,6 +233,8 @@ namespace xo { /** add GC root at @p root_addr, with type @p typeseq **/ void add_gc_root_poly(obj * p_root) noexcept; + // ----- collection ----- + /** Request immediate collection. * 1. if collection is enabled, immediately collect all generations * up to (but not including) g @@ -241,6 +248,23 @@ namespace xo { /** Execute gc immediately, for all generations < @p upto **/ void execute_gc(generation upto) noexcept; + /** Evacuate object at @p *lhs_data to to-space. + * Replace original with forwarding pointer to new location + **/ + void forward_inplace(AGCObject * lhs_iface, void ** lhs_data); + + /** evacuate object with type @p iface at address @p from_src + * to to-space. Return new to-space location. + **/ + void * shallow_move(const AGCObject * iface, void * from_src); + + /** true iff {alloc_hdr, object_data} should move for + * currently-running collection. + * + * Require: runstate_.is_running() + **/ + bool check_move_policy(header_type alloc_hdr, void * object_data) const noexcept; + // ----- allocation ----- /** simple allocation. allocate @p z bytes of memory @@ -295,6 +319,12 @@ namespace xo { /** copy roots + everything reachable from them, to to-space **/ void copy_roots(generation upto) noexcept; + /** move subgraph at @p from_src to to-space. + * + * Require: runstate_.is_running() + **/ + void * deep_move(void * from_src, generation upto); + public: /** garbage collector configuration **/ CollectorConfig config_; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 2c86d5dd..cc7eb051 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -4,6 +4,8 @@ **/ #include "Allocator.hpp" +#include "detail/IAllocator_DX1Collector.hpp" +#include "detail/ICollector_DX1Collector.hpp" #include "arena/IAllocator_DArena.hpp" #include "xo/gc/DX1Collector.hpp" #include "xo/gc/DX1CollectorIterator.hpp" @@ -221,6 +223,15 @@ namespace xo { return (vtable != nullptr); } + const AGCObject * + DX1Collector::lookup_type(typeseq tseq) const noexcept + { + AGCObject * v = reinterpret_cast(object_types_.lo_); + + return &(v[tseq.seqno()]); + } + + /* editor bait: register_type */ bool DX1Collector::install_type(const AGCObject & meta) noexcept { @@ -268,8 +279,13 @@ namespace xo { { scope log(XO_DEBUG(true), xtag("upto", upto)); + assert(!runstate_.is_running()); + //auto t0 = std::chrono::steady_clock::now(); + log && log("step 0a : update run state"); + this->runstate_ = GCRunState::gc_upto(upto); + log && log("step 0a : [STUB] snapshot alloc state"); log && log("step 0b : [STUB] scan for object statistics"); @@ -294,10 +310,302 @@ namespace xo { } } + /* + * rules: + * - from_src must be in from-space + * - object type stored in alloc header + * - return value is new location in to-space + * + * - preserving i/face pointer + * - replace destination with forwarding pointer + * + * EDITOR: gc -> self + */ + void * + DX1Collector::deep_move(void * from_src, generation upto) + { + scope log(XO_DEBUG(config_.debug_flag_)); + + if (!from_src) + return nullptr; + + if (!this->contains(role::from_space(), from_src)) { + /* presumeably memory not owned by collector + * (e.g. Boolean {true, false}) + */ + return from_src; + } + + AllocInfo info = this->alloc_info((std::byte *)from_src); + AllocHeader hdr = info.header(); + typeseq tseq(info.tseq()); + + if (is_forwarding_header(hdr)) { + /* already forwarded - pickup destination + * + * Coordinates with forward_inplace() + */ + + return *(void **)from_src; + } + + /* here: object at from_src not already forwarded */ + + if (!this->check_move_policy(hdr, from_src)) { + /* object at from_src in generation that is not being collected */ + return from_src; + } + + /** + * To-space: + * + * to_lo = start of to-space + * w,W = white objects. An object x is white if x + * + all immediate children of x are in to-space + * (also implies this GC cycle put it there) + * g,G = grey objects. An object x is gray if it's in to-space, + * but possibly has >0 black children + * _ = free to-space memory + * N = nursery space (generation{0}) + * T = tenured space (generation{1}) + * + * wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________... + * ^ ^ ^ + * to_lo grey_lo(N) free_ptr(N) + * + * After moving children of one object, + * advancing {nursery_grey_lo, nursery_free_ptr} + * + * wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG______... + * ^ ^ ^ + * to_lo grey_lo(N) free_ptr(N) + * + * Invariant: + * + * objects in [to_lo, gray_lo) are white. + * all gray objects are in [gray_lo, free_ptr) + * memory starting at free_ptr is free. + * + * deep_move terminates when gray_lo catches up to free_ptr + * + * Above is simplified. Complication is that GC (including incremental) may + * promote objects from nursery (N) to tenured (T) + * + * So more accurate before/after picture + * + * N wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________... + * ^ ^ ^ + * to_lo(N) grey_lo(N) free_ptr(N) + * + * T wwwwwwwwwwwwwwgggggggggggg_______________________________... + * ^ ^ ^ + * to_lo(T) grey_lo(T) free_ptr(N) + * + * After moving children of one object, + * advancing {nursery_grey_lo, nursery_free_ptr} + * + * N wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG_____... + * ^ ^ ^ + * to_lo(N) grey_lo(N) free_ptr(N) + * + * T wwwwwwwwwwwwwwggggggggggggGGGGG_________________________... + * ^ ^ ^ + * to_lo(T) grey_lo(T) free_ptr(T) + * + * deep_move terminates when both: + * - gray_lo(N) catches up with free_ptr(N) + * - gray_lo(T) catches up with free_ptr(T) + * + **/ + + /* TODO: AllocIterator pointing to free pointer */ + std::array gray_lo_v; + { + for (uint32_t g = 0; g < upto; ++g) { + gray_lo_v[g] = this->to_space(generation{g})->free_; + } + } + + obj alloc(this); + const AGCObject * iface = lookup_type(tseq); + void * to_dest = this->shallow_move(iface, from_src); + + std::size_t fixup_work = 0; + + /* TODO: + * - loop here is bad for memory locality + * - replace with depth-first traversal + */ + do { + fixup_work = 0; + + for (generation g = generation{0}; g < upto; ++g) { + /* TODO: use AllocIterator here */ + while(gray_lo_v[g] < to_space(g)->free_) { + AllocHeader * hdr = (AllocHeader *)gray_lo_v[g]; + void * src = (hdr + 1); + + log && log("fwd children", xtag("src", src)); + + const auto & hdr_cfg = config_.arena_config_.header_; + typeseq tseq = typeseq(hdr_cfg.tseq(*hdr)); + size_t z = hdr_cfg.size_with_padding(*hdr); + + const AGCObject * iface = this->lookup_type(tseq); + obj gc(this); + + iface->forward_children(src, gc); + + gray_lo_v[g] = ((std::byte *)src) + z; + ++fixup_work; + } + } + } while (fixup_work > 0); + + return to_dest; + } + void DX1Collector::copy_roots(generation upto) noexcept { - scope log(XO_DEBUG(true), "STUB", xtag("upto", upto)); + for (obj ** p_root = (obj **)roots_.lo_; + p_root < (obj **)roots_.free_; ++p_root) + { + (*p_root)->reset_opaque(this->deep_move((*p_root)->data_, upto)); + } + } + + void + DX1Collector::forward_inplace(AGCObject * lhs_iface, + void ** lhs_data) + { + /* coordinates with DX1Collector::_deep_move() */ + + (void)lhs_iface; + assert(runstate_.is_running()); + + /* + * lhs obj + * | +---------+ +---+-+----+ + * \--->| .iface | | T |G|size| header + * +---------+ object_data +---+-+----+ + * | .data x----------------->| alloc | + * +---------+ | data | + * | for | + * | instance | + * | ... | + * +----------+ + */ + + void * object_data = (std::byte *)lhs_data; + + if (!this->contains(role::from_space(), object_data)) { + /* *lhs isn't in GC-allocated space. + * + * This happens for a modest number of global + * constants, for example DBoolean {true, false}. + * + * It's important we recognize these up front. + * Since not allocated from GC, they don't have + * an alloc-header. + */ + return; + } + + /** NOTE: for form's sake: + * better to lookup actual arena that + * allocated object data. + * Only using this to get alloc header + * + **/ + DArena * some_arena = this->to_space(generation(0)); + + DArena::header_type * p_header + = some_arena->obj2hdr(object_data); + + DArena::header_type alloc_hdr = *p_header; + + /* recover allocation size */ + std::size_t alloc_z = some_arena->config_.header_.size_with_padding(alloc_hdr); + + /* need to be able to fit forwarding pointer + * in place of forwarded object. + * + * This is guaranteed anyway, by alignment rules + */ + assert(alloc_z > sizeof(uintptr_t)); + + if (this->is_forwarding_header(alloc_hdr)) { + /* *lhs already refers to a forwarding pointer */ + + /* + * lhs obj + * | +---------+ +---+-+----+ + * \--->| .iface | |FWD|G|size| alloc_hdr + * +---------+ object_data +---+-+----+ + * | .data x----------------->| x--------> + * +---------+ | | dest + * | | + * +----------+ + */ + void * dest = *(void**)object_data; + + /* update *lhs in-place */ + *lhs_data = dest; + } else if (this->check_move_policy(alloc_hdr, object_data)) { + /* copy object *lhs + replace with forwarding pointer */ + + /* which arena are we writing to? need allocator interface */ + + *lhs_data = this->shallow_move(lhs_iface, *lhs_data); + + } else { + /* object doesn't need to move. + * e.g. incremental collection + object is tenured + */ + } + } /*forward_inplace*/ + + void * + DX1Collector::shallow_move(const AGCObject * iface, void * from_src) + { + AllocInfo info = this->alloc_info((std::byte *)from_src); + obj alloc(this); + + void * to_dest = iface->shallow_copy(from_src, alloc); + + if(to_dest == from_src) { + assert(false); + } else { + *(const_cast(info.p_header_)) + = AllocHeader(config_ + .arena_config_ + .header_ + .mark_forwarding_tseq(*info.p_header_)); + + *(void **)from_src = to_dest; + } + + return to_dest; + } + + bool + DX1Collector::check_move_policy(header_type alloc_hdr, + void * object_data) const noexcept + { + (void)object_data; + + // when gc is moving objects, to- and from- spaces have been + // reversed: forwarding pointers are located in from-space and + // refer to to-space. + + object_age age = this->header2age(alloc_hdr); + + generation g = config_.age2gen(age); + + assert(runstate_.is_running()); + + return (g < runstate_.gc_upto()); } auto diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp index a138c9b6..e30182de 100644 --- a/src/gc/ICollector_DX1Collector.cpp +++ b/src/gc/ICollector_DX1Collector.cpp @@ -79,95 +79,7 @@ namespace xo { AGCObject * lhs_iface, void ** lhs_data) { - (void)lhs_iface; - assert(d.runstate_.is_running()); - - /* - * lhs obj - * | +---------+ +---+-+----+ - * \--->| .iface | | T |G|size| header - * +---------+ object_data +---+-+----+ - * | .data x----------------->| alloc | - * +---------+ | data | - * | for | - * | instance | - * | ... | - * +----------+ - */ - - void * object_data = (byte *)lhs_data; - - if (!d.contains(role::from_space(), object_data)) { - /* *lhs isn't in GC-allocated space. - * - * This happens for a modest number of global - * constant, for example DBoolean {true, false}. - * - * It's important we recognize these up front. - * Since not allocated from GC, they don't have - * an alloc-header. - */ - return; - } - - /** NOTE: for form's sake: - * better to lookup actual arena that - * allocated object data. - * - **/ - DArena * some_arena = d.to_space(generation(0)); - - DArena::header_type * p_header - = some_arena->obj2hdr(object_data); - - DArena::header_type alloc_hdr = *p_header; - - /* recover allocation size */ - std::size_t alloc_z = some_arena->config_.header_.size(alloc_hdr); - - /* need to be able to fit forwarding pointer - * in place of forwarded object. - * - * This is guaranteed anyway, by alignment rules - */ - assert(alloc_z > sizeof(uintptr_t)); - - if (d.is_forwarding_header(alloc_hdr)) { - /* *lhs already refers to a forwarding pointer */ - - /* - * lhs obj - * | +---------+ +---+-+----+ - * \--->| .iface | |FWD|G|size| alloc_hdr - * +---------+ object_data +---+-+----+ - * | .data x----------------->| x--------> - * +---------+ | | dest - * | | - * +----------+ - */ - void * dest = *(void**)object_data; - - /* update *lhs in-place */ - *lhs_data = dest; - } else if (check_move_policy(d, alloc_hdr, object_data)) { - /* copy object *lhs + replace with forwarding pointer */ - - /* which arena are we writing to? need allocator interface */ - - assert(false); - -#ifdef NOT_YET - // to do this need IAllocator_DX1Collector fully implemented - - void * copy = (*lhs).shallow_copy(xxx); - - xxx mm xxx; -#endif - } else { - /* object doesn't need to move. - * e.g. incremental collection + object is tenured - */ - } + d.forward_inplace(lhs_iface, lhs_data); } bool @@ -175,19 +87,7 @@ namespace xo { header_type alloc_hdr, void * object_data) { - (void)object_data; - - // when gc is moving objects, to- and from- spaces have been - // reversed: forwarding pointers are located in from-space and - // refer to to-space. - - object_age age = d.header2age(alloc_hdr); - - generation g = d.config_.age2gen(age); - - assert(d.runstate_.is_running()); - - return (g < d.runstate_.gc_upto()); + return d.check_move_policy(alloc_hdr, object_data); } } /*namespace mm*/ From 109b23db0ecd4a48ffe724def3c97a85e8f05429 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 4 Jan 2026 15:33:08 -0500 Subject: [PATCH 017/174] xo-gc xo-object2: gc bugfixes and logging [WORKING] --- src/gc/DX1Collector.cpp | 124 ++++++++++++++++++++++++++++++++++------ 1 file changed, 107 insertions(+), 17 deletions(-) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index cc7eb051..e5f951f2 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -293,6 +293,9 @@ namespace xo { log && log("step 1 : swap from/to roles"); this->swap_roles(upto); + log && log(xtag("from_0", get_space(role::from_space(), generation{0})->lo_), + xtag("to_0", get_space(role::to_space(), generation{0})->lo_)); + log && log("step 2a : copy roots"); this->copy_roots(upto); @@ -305,7 +308,11 @@ namespace xo { void DX1Collector::swap_roles(generation upto) noexcept { + scope log(XO_DEBUG(true), xtag("upto", upto)); + for (generation g = generation{0}; g < upto; ++g) { + log && log("swap roles", xtag("g", g)); + std::swap(space_[role::to_space()][g], space_[role::from_space()][g]); } } @@ -345,6 +352,7 @@ namespace xo { * * Coordinates with forward_inplace() */ + log && log("disposition: already forwarded"); return *(void **)from_src; } @@ -353,6 +361,8 @@ namespace xo { if (!this->check_move_policy(hdr, from_src)) { /* object at from_src in generation that is not being collected */ + log && log("disposition: not moving from_src"); + return from_src; } @@ -418,6 +428,8 @@ namespace xo { * **/ + log && log("disposition: move subtree"); + /* TODO: AllocIterator pointing to free pointer */ std::array gray_lo_v; { @@ -462,16 +474,24 @@ namespace xo { } } while (fixup_work > 0); + log && log(xtag("to_dest", to_dest)); + return to_dest; } void DX1Collector::copy_roots(generation upto) noexcept { + scope log(XO_DEBUG(true)); + for (obj ** p_root = (obj **)roots_.lo_; p_root < (obj **)roots_.free_; ++p_root) { + log && log("copy root", xtag("**p_root.data.pre", (**p_root).data_)); + (*p_root)->reset_opaque(this->deep_move((*p_root)->data_, upto)); + + log && log(xtag("**p_root.data.post", (**p_root).data_)); } } @@ -479,6 +499,10 @@ namespace xo { DX1Collector::forward_inplace(AGCObject * lhs_iface, void ** lhs_data) { + scope log(XO_DEBUG(config_.debug_flag_), + xtag("lhs_data", lhs_data), + xtag("*lhs_data", *lhs_data)); + /* coordinates with DX1Collector::_deep_move() */ (void)lhs_iface; @@ -497,28 +521,31 @@ namespace xo { * +----------+ */ - void * object_data = (std::byte *)lhs_data; + void * object_data = (std::byte *)*lhs_data; if (!this->contains(role::from_space(), object_data)) { - /* *lhs isn't in GC-allocated space. + /* *lhs_data either: + * 1. already in to-space + * 2. not in GC-allocated space at all + * (small number of niche examples of this) * - * This happens for a modest number of global - * constants, for example DBoolean {true, false}. - * - * It's important we recognize these up front. + * It's important we recognize case (2) up front. * Since not allocated from GC, they don't have * an alloc-header. */ + log && log("disposition: not in from-space"); + return; } + log && log("disposition: in from-space"); + /** NOTE: for form's sake: - * better to lookup actual arena that + * lookup actual arena that * allocated object data. * Only using this to get alloc header - * **/ - DArena * some_arena = this->to_space(generation(0)); + DArena * some_arena = this->from_space(generation(0)); DArena::header_type * p_header = some_arena->obj2hdr(object_data); @@ -528,37 +555,96 @@ namespace xo { /* recover allocation size */ std::size_t alloc_z = some_arena->config_.header_.size_with_padding(alloc_hdr); + log && log(xtag("some_arena.lo", some_arena->lo_), + xtag("p_header", p_header), + xtag("alloc_z", alloc_z)); + /* need to be able to fit forwarding pointer * in place of forwarded object. * * This is guaranteed anyway, by alignment rules */ - assert(alloc_z > sizeof(uintptr_t)); + assert(alloc_z >= sizeof(uintptr_t)); if (this->is_forwarding_header(alloc_hdr)) { - /* *lhs already refers to a forwarding pointer */ + /* *lhs_data already refers to a forwarding pointer */ /* - * lhs obj + * lhs obj (from-space) * | +---------+ +---+-+----+ * \--->| .iface | |FWD|G|size| alloc_hdr * +---------+ object_data +---+-+----+ - * | .data x----------------->| x--------> - * +---------+ | | dest + * | .data x----------------->| x--------\ + * +---------+ | | | dest + * | | | + * +----------+ | + * | + * (to-space) | + * +---+-+----+ | + * |FWD|G|size|<--/ + * +---+-+----+ + * | | + * | | * | | * +----------+ */ void * dest = *(void**)object_data; - /* update *lhs in-place */ *lhs_data = dest; + /* + * lhs obj + * | +---------+ + * \--->| .iface | + * +---------+ + * | .data x------------\ + * +---------+ | + * | dest + * | + * | + * | (to-space) + * | +---+-+----+ + * \---->|FWD|G|size| + * +---+-+----+ + * | | + * | | + * | | + * +----------+ + */ } else if (this->check_move_policy(alloc_hdr, object_data)) { /* copy object *lhs + replace with forwarding pointer */ - /* which arena are we writing to? need allocator interface */ + /* + * lhs obj (from-space) + * | +---------+ +---+-+----+ + * \--->| .iface | |FWD|G|size| alloc_hdr + * +---------+ object_data +---+-+----+ + * | .data x----------------->| | + * +---------+ | | + * | | + * +----------+ + */ *lhs_data = this->shallow_move(lhs_iface, *lhs_data); + /* + * lhs obj (from-space) + * | +---------+ +---+-+----+ + * \--->| .iface | |FWD|G|SIZE| + * +---------+ +---+-+----+ + * | .data x------------\ | x--------\ + * +---------+ | | | | + * | | | | + * dest | +----------+ | + * | | + * | (to-space) | + * | +---+-+----+ | + * \---->|FWD|G|size|<--/ + * +---+-+----+ + * | | + * | | + * | | + * +----------+ + */ } else { /* object doesn't need to move. * e.g. incremental collection + object is tenured @@ -569,11 +655,15 @@ namespace xo { void * DX1Collector::shallow_move(const AGCObject * iface, void * from_src) { + scope log(XO_DEBUG(config_.debug_flag_)); + AllocInfo info = this->alloc_info((std::byte *)from_src); obj alloc(this); void * to_dest = iface->shallow_copy(from_src, alloc); + log && log(xtag("from_src", from_src), xtag("to_dest", to_dest)); + if(to_dest == from_src) { assert(false); } else { @@ -699,7 +789,7 @@ namespace xo { DX1Collector::reverse_roles(generation g) noexcept { assert(g < config_.n_generation_); - std::swap(space_[0][g], space_[1][g]); + std::swap(space_[role::from_space()][g], space_[role::to_space()][g]); } void From e2e621dc526726f3cb18176bfd7b61d955454dd4 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 4 Jan 2026 23:03:18 -0500 Subject: [PATCH 018/174] + xo-printable2 + build fixes for cmake config --- CMakeLists.txt | 3 ++- cmake/{xo-gcConfig.cmake.in => xo_gcConfig.cmake.in} | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename cmake/{xo-gcConfig.cmake.in => xo_gcConfig.cmake.in} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4480a1ad..fbe3bab2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,7 +21,8 @@ add_definitions(${PROJECT_CXX_FLAGS}) # must complete definition of expression lib before configuring examples add_subdirectory(src/gc) add_subdirectory(utest) -#xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) + +xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) # ---------------------------------------------------------------- # docs targets depend on other library/utest/exec targets above, diff --git a/cmake/xo-gcConfig.cmake.in b/cmake/xo_gcConfig.cmake.in similarity index 100% rename from cmake/xo-gcConfig.cmake.in rename to cmake/xo_gcConfig.cmake.in From a34c3d96f902ba0f546528c39df573c91d46e3b7 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 00:41:37 -0500 Subject: [PATCH 019/174] xo-arena: annex ArenaConfig.hpp from xo-alloc2/ --- include/xo/gc/DX1Collector.hpp | 2 +- utest/DX1CollectorIterator.test.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index bde9f04d..40295b5e 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -10,8 +10,8 @@ #include "object_age.hpp" #include "role.hpp" #include -#include #include +#include #include #include diff --git a/utest/DX1CollectorIterator.test.cpp b/utest/DX1CollectorIterator.test.cpp index 58a498c5..99347fad 100644 --- a/utest/DX1CollectorIterator.test.cpp +++ b/utest/DX1CollectorIterator.test.cpp @@ -8,7 +8,7 @@ #include "DX1CollectorIterator.hpp" #include "detail/IAllocator_DX1Collector.hpp" #include "detail/IAllocIterator_DX1CollectorIterator.hpp" -#include "arena/ArenaConfig.hpp" +#include #include "padding.hpp" #include #include From a8cf04d9f6abb87e5865e1e1c0bb2783cae9a393 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 6 Jan 2026 00:49:41 -0500 Subject: [PATCH 020/174] xo-arena: annex DArena.* DArenaIterator.* from xo-alloc2 --- include/xo/gc/DX1Collector.hpp | 2 +- include/xo/gc/DX1CollectorIterator.hpp | 4 ++-- utest/random_allocs.cpp | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 40295b5e..33c9f56d 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -10,7 +10,7 @@ #include "object_age.hpp" #include "role.hpp" #include -#include +#include #include #include #include diff --git a/include/xo/gc/DX1CollectorIterator.hpp b/include/xo/gc/DX1CollectorIterator.hpp index af1e867a..98a1f1d2 100644 --- a/include/xo/gc/DX1CollectorIterator.hpp +++ b/include/xo/gc/DX1CollectorIterator.hpp @@ -7,8 +7,8 @@ #include "AllocInfo.hpp" #include "generation.hpp" -#include "arena/DArenaIterator.hpp" -#include "cmpresult.hpp" +#include +#include namespace xo { namespace mm { diff --git a/utest/random_allocs.cpp b/utest/random_allocs.cpp index 654c6c9a..1f7f833f 100644 --- a/utest/random_allocs.cpp +++ b/utest/random_allocs.cpp @@ -4,8 +4,8 @@ **/ #include "random_allocs.hpp" -#include "arena/DArena.hpp" -#include "padding.hpp" +#include +#include #include #include #include From 03b663f291d61ab830682aa9ca4814e6add8380d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 14 Jan 2026 11:36:16 -0500 Subject: [PATCH 021/174] xo-facet xo-gc xo-alloc2: facet cleanup + use genfacet for GCObject --- CMakeLists.txt | 12 ++ idl/GCObject.json5 | 67 +++++++++++ include/xo/gc/GCObject.hpp | 12 +- include/xo/gc/detail/ACollector.hpp | 4 +- include/xo/gc/detail/AGCObject.hpp | 102 +++++++++------- .../xo/gc/detail/IAllocator_DX1Collector.hpp | 1 + include/xo/gc/detail/IGCObject_Any.hpp | 105 ++++++++++------ include/xo/gc/detail/IGCObject_Xfer.hpp | 112 +++++++++++------- include/xo/gc/detail/RGCObject.hpp | 97 ++++++++++----- src/gc/DX1CollectorIterator.cpp | 1 + src/gc/IAllocator_DX1Collector.cpp | 1 + src/gc/IGCObject_Any.cpp | 51 +++++--- 12 files changed, 394 insertions(+), 171 deletions(-) create mode 100644 idl/GCObject.json5 diff --git a/CMakeLists.txt b/CMakeLists.txt index fbe3bab2..2ad70af5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,18 @@ add_definitions(${PROJECT_CXX_FLAGS}) # ---------------------------------------------------------------- +# note: manual target; generated code committed to git +xo_add_genfacet( + TARGET xo-gc-facet-gcobject + FACET GCObject + INPUT idl/GCObject.json5 + OUTPUT_HPP_DIR include/xo/gc + OUTPUT_IMPL_SUBDIR detail + OUTPUT_CPP_DIR src/gc +) + +# ---------------------------------------------------------------- + # must complete definition of expression lib before configuring examples add_subdirectory(src/gc) add_subdirectory(utest) diff --git a/idl/GCObject.json5 b/idl/GCObject.json5 new file mode 100644 index 00000000..d35916b8 --- /dev/null +++ b/idl/GCObject.json5 @@ -0,0 +1,67 @@ +{ + mode: "facet", + includes: [ + "", + "", + "", + "", + ], + pretext: [ + "namespace xo { namespace mm { struct ACollector; }}", + ], + namespace1: "xo", + namespace2: "mm", + facet: "GCObject", + detail_subdir: "detail", + brief: "xxx", + using_doxygen: true, + doc: [ + "GC hooks for collector-aware data" + ], + types: [ + // using size_type = std::size_t + { + name: "size_type", + doc: ["type for an amount of memory"], + definition: "std::size_t", + }, + ], + const_methods: [ + // size_type shallow_size() const noexcept + { + name: "shallow_size", + doc: ["memory consumption for this instance"], + return_type: "size_type", + args: [], + const: true, + noexcept: true, + attributes: [], + }, + // Opaque shallow_copy(obj>) const noexcept + { + name: "shallow_copy", + doc: ["copy instance using allocator"], + return_type: "Opaque", + args:[ + {type: "obj", name: "mm"}, + ], + const: true, + noexcept: true, + attributes: [], + }, + ], + nonconst_methods: [ + // size_type forward_children(obj) const noexcept + { + name: "forward_children", + doc: ["during GC: forward immdiate children"], + return_type: "size_type", + args: [ + {type: "obj", name: "gc"}, + ], + const: true, + noexcept: true, + attributes: [], + }, + ], +} diff --git a/include/xo/gc/GCObject.hpp b/include/xo/gc/GCObject.hpp index 2674b2ae..ebb6d26f 100644 --- a/include/xo/gc/GCObject.hpp +++ b/include/xo/gc/GCObject.hpp @@ -1,6 +1,14 @@ /** @file GCObject.hpp * - * @author Roland Conybeare, Dec 2025 + * Generated automagically from ingredients: + * 1. code generator: + * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * arguments: + * --input [idl/GCObject.json5] + * 2. jinja2 template for facet .hpp file: + * [facet.hpp.j2] + * 3. idl for facet methods + * [idl/GCObject.json5] **/ #pragma once @@ -10,4 +18,4 @@ #include "detail/IGCObject_Xfer.hpp" #include "detail/RGCObject.hpp" -/* end GCObject.hpp */ +/* end GCObject.hpp */ \ No newline at end of file diff --git a/include/xo/gc/detail/ACollector.hpp b/include/xo/gc/detail/ACollector.hpp index 36b4bffb..0fd031fb 100644 --- a/include/xo/gc/detail/ACollector.hpp +++ b/include/xo/gc/detail/ACollector.hpp @@ -22,8 +22,8 @@ namespace xo { using Copaque = const void *; using Opaque = void *; - struct AGCObject; - struct IGCObject_Any; // see IGCObject_Any.hpp + class AGCObject; + class IGCObject_Any; // see IGCObject_Any.hpp /** @class ACollector * @brief Abstract facet for the XO garbage collector diff --git a/include/xo/gc/detail/AGCObject.hpp b/include/xo/gc/detail/AGCObject.hpp index 7c2ac308..5210dc97 100644 --- a/include/xo/gc/detail/AGCObject.hpp +++ b/include/xo/gc/detail/AGCObject.hpp @@ -1,57 +1,79 @@ /** @file AGCObject.hpp * - * @author Roland Conybeare, Dec 2025 + * Generated automagically from ingredients: + * 1. code generator: + * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * arguments: + * --input [idl/GCObject.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [abstract_facet.hpp.j2] + * 3. idl for facet methods + * [idl/GCObject.json5] **/ #pragma once -#include "Allocator.hpp" -#include "xo/facet/facet_implementation.hpp" -#include "xo/facet/typeseq.hpp" -#include "xo/facet/obj.hpp" // for obj in shallow_copy +// includes (via {facet_includes}) +#include +#include #include #include +#include +#include +#include + +namespace xo { namespace mm { struct ACollector; }} namespace xo { - namespace mm { - using Copaque = const void *; - using Opaque = void *; +namespace mm { - struct ACollector; +using Copaque = const void *; +using Opaque = void *; - /** @class AObject - * @brief Abstract facet for collector-eligible data - * - * Data that supports AGCObject can have memory managed - * by ACollector - **/ - struct AGCObject { - using typeseq = xo::facet::typeseq; - using size_type = std::size_t; +/** +GC hooks for collector-aware data +**/ +class AGCObject { +public: + /** @defgroup mm-gcobject-type-traits **/ + ///@{ + // types + /** integer identifying a type **/ + using typeseq = xo::facet::typeseq; + /** type for an amount of memory **/ + using size_type = std::size_t; + ///@} - /** RTTI: unique id# for actual runtime data representation **/ - virtual typeseq _typeseq() const noexcept = 0; + /** @defgroup mm-gcobject-methods **/ + ///@{ + // const methods + /** RTTI: unique id# for actual runtime data representation **/ + virtual typeseq _typeseq() const noexcept = 0; + /** memory consumption for this instance **/ + virtual size_type shallow_size(Copaque data) const noexcept = 0; + /** copy instance using allocator **/ + virtual Opaque shallow_copy(Copaque data, obj mm) const noexcept = 0; - virtual size_type shallow_size(Copaque d) const noexcept = 0; - virtual Opaque shallow_copy(Copaque d, - obj mm) const noexcept = 0; - virtual size_type forward_children(Opaque d, - obj) const noexcept = 0; - }; + // nonconst methods + /** during GC: forward immdiate children **/ + virtual size_type forward_children(Opaque data, obj gc) const noexcept = 0; + ///@} +}; /*AGCObject*/ - // implementation IGCObject_DRepr of AGCObject for state DRepr - // should provide a specialization: - // - // template <> - // struct xo::facet::FacetImplementation { - // using ImplType = IGCObject_DRepr; - // }; - // - // then IGCObject_ImplType --> IGCObject_DRepr - // - template - using IGCObject_ImplType = xo::facet::FacetImplType; - } /*namespace mm*/ +/** Implementation IGCObject_DRepr of AGCObject for state DRepr + * should provide a specialization: + * + * template <> + * struct xo::facet::FacetImplementation { + * using Impltype = IGCObject_DRepr; + * }; + * + * then IGCObject_ImplType --> IGCObject_DRepr + **/ +template +using IGCObject_ImplType = xo::facet::FacetImplType; + +} /*namespace mm*/ } /*namespace xo*/ -/* end AGCObject.hpp */ +/* AGCObject.hpp */ \ No newline at end of file diff --git a/include/xo/gc/detail/IAllocator_DX1Collector.hpp b/include/xo/gc/detail/IAllocator_DX1Collector.hpp index 0ed1ab1f..424f24ff 100644 --- a/include/xo/gc/detail/IAllocator_DX1Collector.hpp +++ b/include/xo/gc/detail/IAllocator_DX1Collector.hpp @@ -6,6 +6,7 @@ #pragma once #include "Allocator.hpp" +#include "Collector.hpp" #include "DX1Collector.hpp" namespace xo { diff --git a/include/xo/gc/detail/IGCObject_Any.hpp b/include/xo/gc/detail/IGCObject_Any.hpp index 88b19e8b..9f086669 100644 --- a/include/xo/gc/detail/IGCObject_Any.hpp +++ b/include/xo/gc/detail/IGCObject_Any.hpp @@ -1,51 +1,88 @@ /** @file IGCObject_Any.hpp * - * @author Roland Conybeare, Dec 2025 + * Generated automagically from ingredients: + * 1. code generator: + * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * arguments: + * --input [idl/GCObject.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [iface_facet_any.hpp.j2] + * 3. idl for facet methods + * [idl/GCObject.json5] **/ #pragma once #include "AGCObject.hpp" -#include "Collector.hpp" -#include +#include + +namespace xo { namespace mm { class IGCObject_Any; } } namespace xo { - namespace mm { struct IGCObject_Any; } +namespace facet { - namespace facet { - template <> - struct FacetImplementation { - using ImplType = xo::mm::IGCObject_Any; - }; - } +template <> +struct FacetImplementation +{ + using ImplType = xo::mm::IGCObject_Any; +}; - namespace mm { - /** @class IGCObject_Any - * @brief AGCObject implementation for empty variant instance - **/ - struct IGCObject_Any : public AGCObject { - using typeseq = xo::facet::typeseq; - using size_type = std::size_t; +} +} - const AGCObject * iface() const { return std::launder(this); } +namespace xo { +namespace mm { - // from AGCObject - typeseq _typeseq() const noexcept override { return s_typeseq; } + /** @class IGCObject_Any + * @brief AGCObject implementation for empty variant instance + **/ + class IGCObject_Any : public AGCObject { + public: + /** @defgroup mm-gcobject-any-type-traits **/ + ///@{ - [[noreturn]] size_type shallow_size(Copaque) const noexcept override { _fatal(); } - [[noreturn]] Opaque shallow_copy(Copaque, - obj) const noexcept override { _fatal(); } - [[noreturn]] size_type forward_children(Opaque, - obj) const noexcept override { _fatal(); } + /** integer identifying a type **/ + using typeseq = xo::facet::typeseq; + using size_type = AGCObject::size_type; - private: - [[noreturn]] static void _fatal(); + ///@} + /** @defgroup mm-gcobject-any-methods **/ + ///@{ - public: - static typeseq s_typeseq; - static bool _valid; - }; - } /*namespace mm*/ -} /*namespace xo*/ + const AGCObject * iface() const { return std::launder(this); } -/* end IGCObject_Any.hpp */ + // from AGCObject + + // const methods + typeseq _typeseq() const noexcept override { return s_typeseq; } + [[noreturn]] size_type shallow_size(Copaque) const noexcept override { _fatal(); } + [[noreturn]] Opaque shallow_copy(Copaque, obj) const noexcept override { _fatal(); } + + // nonconst methods + [[noreturn]] size_type forward_children(Opaque, obj) const noexcept override; + + ///@} + + private: + /** @defgraoup mm-gcobject-any-private-methods **/ + ///@{ + + [[noreturn]] static void _fatal(); + + ///@} + + public: + /** @defgroup mm-gcobject-any-member-vars **/ + ///@{ + + static typeseq s_typeseq; + static bool _valid; + + ///@} + }; + +} /*namespace mm */ +} /*namespace xo */ + +/* IGCObject_Any.hpp */ \ No newline at end of file diff --git a/include/xo/gc/detail/IGCObject_Xfer.hpp b/include/xo/gc/detail/IGCObject_Xfer.hpp index a9deb4ad..2ece6b01 100644 --- a/include/xo/gc/detail/IGCObject_Xfer.hpp +++ b/include/xo/gc/detail/IGCObject_Xfer.hpp @@ -1,64 +1,90 @@ /** @file IGCObject_Xfer.hpp * - * @author Roland Conybeare, Dec 2025 + * Generated automagically from ingredients: + * 1. code generator: + * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * arguments: + * --input [idl/GCObject.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [iface_facet_any.hpp.j2] + * 3. idl for facet methods + * [idl/GCObject.json5] **/ #pragma once -#include "AGCObject.hpp" -#include "ACollector.hpp" +#include +#include +#include +#include namespace xo { - namespace mm { - /** @class IGCObject_Xfer - * - * Adapts typed GC object implementation @tparam IGCObject_DRepr - * to type-erased @ref AGCObject interface - **/ - template - struct IGCObject_Xfer : public AGCObject { - using Impl = IGCObject_DRepr; - using size_type = AGCObject::size_type; +namespace mm { + /** @class IGCObject_Xfer + **/ + template + class IGCObject_Xfer : public AGCObject { + public: + /** @defgroup mm-gcobject-xfer-type-traits **/ + ///@{ + /** actual implementation (not generated; often delegates to DRepr) **/ + using Impl = IGCObject_DRepr; + /** integer identifying a type **/ + using typeseq = AGCObject::typeseq; + using size_type = AGCObject::size_type; + ///@} - static const DRepr & _dcast(Copaque d) { return *(const DRepr *)d; } - static DRepr & _dcast(Opaque d) { return *(DRepr *)d; } + /** @defgroup mm-gcobject-xfer-methods **/ + ///@{ - // from AGCObject + static const DRepr & _dcast(Copaque d) { return *(const DRepr *)d; } + static DRepr & _dcast(Opaque d) { return *(DRepr *)d; } - // const methods + // from AGCObject - typeseq _typeseq() const noexcept override { return s_typeseq; } - size_type shallow_size(Copaque d) const noexcept override { - return I::shallow_size(_dcast(d)); - } - Opaque shallow_copy(Copaque d, obj mm) const noexcept override { - return I::shallow_copy(_dcast(d), mm); - } + // const methods + typeseq _typeseq() const noexcept override { return s_typeseq; } + size_type shallow_size(Copaque data) const noexcept override { + return I::shallow_size(_dcast(data)); + } + Opaque shallow_copy(Copaque data, obj mm) const noexcept override { + return I::shallow_copy(_dcast(data), mm); + } - // non-const methods + // non-const methods + size_type forward_children(Opaque data, obj gc) const noexcept override { + return I::forward_children(_dcast(data), gc); + } - size_type forward_children(Opaque d, - obj gc) const noexcept override { - return I::forward_children(_dcast(d), gc); - } + ///@} - private: - using I = Impl; + private: + using I = Impl; - public: - static typeseq s_typeseq; - static bool _valid; - }; + public: + /** @defgroup mm-gcobject-xfer-member-vars **/ + ///@{ - template - xo::facet::typeseq - IGCObject_Xfer::s_typeseq = facet::typeseq::id(); + /** typeseq for template parameter DRepr **/ + static typeseq s_typeseq; + /** true iff satisfies facet implementation **/ + static bool _valid; - template - bool - IGCObject_Xfer::_valid = facet::valid_facet_implementation(); + ///@} + }; - } /*namespace mm*/ + template + xo::facet::typeseq + IGCObject_Xfer::s_typeseq + = xo::facet::typeseq::id(); + + template + bool + IGCObject_Xfer::_valid + = xo::facet::valid_facet_implementation(); + +} /*namespace mm */ } /*namespace xo*/ -/* end IGCObject_Xfer.hpp */ +/* end IGCObject_Xfer.hpp */ \ No newline at end of file diff --git a/include/xo/gc/detail/RGCObject.hpp b/include/xo/gc/detail/RGCObject.hpp index 4c3205b0..ace699c5 100644 --- a/include/xo/gc/detail/RGCObject.hpp +++ b/include/xo/gc/detail/RGCObject.hpp @@ -1,48 +1,83 @@ /** @file RGCObject.hpp * - * @author Roland Conybeare, Dec 2025 + * Generated automagically from ingredients: + * 1. code generator: + * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * arguments: + * --input [idl/GCObject.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [iface_facet_any.hpp.j2] + * 3. idl for facet methods + * [idl/GCObject.json5] **/ #pragma once #include "AGCObject.hpp" -#include namespace xo { - namespace mm { - /** @class RGCObject **/ - template - struct RGCObject : public Object { - private: - using O = Object; - public: - using ObjectType = Object; - using DataPtr = Object::DataPtr; - using typeseq = xo::facet::typeseq; - using size_type = std::size_t; +namespace mm { - RGCObject() = default; - RGCObject(Object::DataPtr data) : Object{std::move(data)} {} +/** @class RGCObject + **/ +template +class RGCObject : public Object { +private: + using O = Object; - typeseq _typeseq() const noexcept { return O::iface()->_typeseq(); } - size_type shallow_size() const noexcept { O::iface()->shallow_size(O::data()); } - Opaque shallow_copy(obj mm) const noexcept { O::iface()->shallow_copy(O::data(), mm); } - size_type forward_children() noexcept { O::iface()->forward_children(O::data()); } +public: + /** @defgroup mm-gcobject-router-type-traits **/ + ///@{ + using ObjectType = Object; + using DataPtr = Object::DataPtr; + using typeseq = xo::reflect::typeseq; + using size_type = AGCObject::size_type; + ///@} - static bool _valid; - }; + /** @defgroup mm-gcobject-router-ctors **/ + ///@{ + RGCObject() {} + RGCObject(Object::DataPtr data) : Object{std::move(data)} {} - template - bool - RGCObject::_valid = facet::valid_object_router(); - } /*namespace mm*/ + ///@} + /** @defgroup mm-gcobject-router-methods **/ + ///@{ - namespace facet { - template - struct RoutingFor { - using RoutingType = xo::mm::RGCObject; - }; + // const methods + typeseq _typeseq() const noexcept { return O::iface()->_typeseq(); } + size_type shallow_size() const noexcept { + return O::iface()->shallow_size(O::data()); } + Opaque shallow_copy(obj mm) const noexcept { + return O::iface()->shallow_copy(O::data(), mm); + } + + // non-const methods + size_type forward_children(obj gc) noexcept { + return O::iface()->forward_children(O::data(), gc); + } + + ///@} + /** @defgroup mm-gcobject-member-vars **/ + ///@{ + + static bool _valid; + + ///@} +}; + +template +bool +RGCObject::_valid = xo::facet::valid_object_router(); + +} /*namespace mm*/ } /*namespace xo*/ -/* end RGCObject.hpp */ +namespace xo { namespace facet { + template + struct RoutingFor { + using RoutingType = xo::mm::RGCObject; + }; +} } + +/* end RGCObject.hpp */ \ No newline at end of file diff --git a/src/gc/DX1CollectorIterator.cpp b/src/gc/DX1CollectorIterator.cpp index 3942468b..7980d319 100644 --- a/src/gc/DX1CollectorIterator.cpp +++ b/src/gc/DX1CollectorIterator.cpp @@ -3,6 +3,7 @@ * @author Roland Conybeare, Dec 2025 **/ +#include "Collector.hpp" #include "xo/gc/DX1CollectorIterator.hpp" #include "xo/gc/DX1Collector.hpp" #include diff --git a/src/gc/IAllocator_DX1Collector.cpp b/src/gc/IAllocator_DX1Collector.cpp index 6c4dc5bf..58e89941 100644 --- a/src/gc/IAllocator_DX1Collector.cpp +++ b/src/gc/IAllocator_DX1Collector.cpp @@ -5,6 +5,7 @@ * See also ICollector_DX1Collector.cpp for collector facet **/ +#include "Collector.hpp" // for obj argument to GCObject.forward_children() #include "detail/IAllocator_DX1Collector.hpp" #include "detail/IAllocIterator_DX1CollectorIterator.hpp" #include "DX1CollectorIterator.hpp" diff --git a/src/gc/IGCObject_Any.cpp b/src/gc/IGCObject_Any.cpp index f39445c8..ff6d395d 100644 --- a/src/gc/IGCObject_Any.cpp +++ b/src/gc/IGCObject_Any.cpp @@ -1,34 +1,47 @@ /** @file IGCObject_Any.cpp * - * @author Roland Conybeare, Dec 2025 **/ #include "detail/IGCObject_Any.hpp" #include namespace xo { - using xo::facet::DVariantPlaceholder; - using xo::facet::typeseq; - using xo::facet::valid_facet_implementation; +namespace mm { - namespace mm { +using xo::facet::DVariantPlaceholder; +using xo::facet::typeseq; +using xo::facet::valid_facet_implementation; - void - IGCObject_Any::_fatal() { - std::cerr << "fatal" - << ": attempt to call uninitialized" - << " IGCObject_Any method" - << std::endl; - std::terminate(); - } +void +IGCObject_Any::_fatal() +{ + /* control here on uninitialized IAllocator_Any. + * Initialized instance will have specific implementation type + */ + std::cerr << "fatal" + << ": attempt to call uninitialized" + << " IGCObject_Any method" + << std::endl; + std::terminate(); +} - typeseq - IGCObject_Any::s_typeseq = typeseq::id(); +typeseq +IGCObject_Any::s_typeseq = typeseq::id(); - bool - IGCObject_Any::_valid = valid_facet_implementation(); +bool +IGCObject_Any::_valid + = valid_facet_implementation(); - } /*namespace mm*/ +// nonconst methods + +auto +IGCObject_Any::forward_children(Opaque, obj) const noexcept -> size_type +{ + _fatal(); +} + + +} /*namespace mm*/ } /*namespace xo*/ -/* end IGCObject_Any.cpp */ +/* end IGCObject_Any.cpp */ \ No newline at end of file From a455ca068bab0699752e3ac00c630c0ec2f86bdc Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 14 Jan 2026 14:37:44 -0500 Subject: [PATCH 022/174] xo-object2 xo-gc xo-facet: codegen updates + IGCObject_DString --- idl/GCObject.json5 | 10 ++++++++++ include/xo/gc/GCObject.hpp | 2 +- include/xo/gc/detail/AGCObject.hpp | 8 +++++++- include/xo/gc/detail/IGCObject_Any.hpp | 4 +++- include/xo/gc/detail/IGCObject_Xfer.hpp | 4 +++- include/xo/gc/detail/RGCObject.hpp | 4 +++- 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/idl/GCObject.json5 b/idl/GCObject.json5 index d35916b8..ef00b5d9 100644 --- a/idl/GCObject.json5 +++ b/idl/GCObject.json5 @@ -25,6 +25,16 @@ doc: ["type for an amount of memory"], definition: "std::size_t", }, + { + name: "AAllocator", + doc: ["fomo allocator type"], + definition: "xo::mm::AAllocator", + }, + { + name: "ACollector", + doc: ["fomo collector type"], + definition: "xo::mm::ACollector", + }, ], const_methods: [ // size_type shallow_size() const noexcept diff --git a/include/xo/gc/GCObject.hpp b/include/xo/gc/GCObject.hpp index ebb6d26f..25834817 100644 --- a/include/xo/gc/GCObject.hpp +++ b/include/xo/gc/GCObject.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * [/home/roland/proj/xo-umbrella2-claude1/xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for facet .hpp file: diff --git a/include/xo/gc/detail/AGCObject.hpp b/include/xo/gc/detail/AGCObject.hpp index 5210dc97..b7a5ffa3 100644 --- a/include/xo/gc/detail/AGCObject.hpp +++ b/include/xo/gc/detail/AGCObject.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * [/home/roland/proj/xo-umbrella2-claude1/xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for abstract facet .hpp file: @@ -40,8 +40,14 @@ public: // types /** integer identifying a type **/ using typeseq = xo::facet::typeseq; + using Copaque = const void *; + using Opaque = void *; /** type for an amount of memory **/ using size_type = std::size_t; + /** fomo allocator type **/ + using AAllocator = xo::mm::AAllocator; + /** fomo collector type **/ + using ACollector = xo::mm::ACollector; ///@} /** @defgroup mm-gcobject-methods **/ diff --git a/include/xo/gc/detail/IGCObject_Any.hpp b/include/xo/gc/detail/IGCObject_Any.hpp index 9f086669..c930693f 100644 --- a/include/xo/gc/detail/IGCObject_Any.hpp +++ b/include/xo/gc/detail/IGCObject_Any.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * [/home/roland/proj/xo-umbrella2-claude1/xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for abstract facet .hpp file: @@ -45,6 +45,8 @@ namespace mm { /** integer identifying a type **/ using typeseq = xo::facet::typeseq; using size_type = AGCObject::size_type; + using AAllocator = AGCObject::AAllocator; + using ACollector = AGCObject::ACollector; ///@} /** @defgroup mm-gcobject-any-methods **/ diff --git a/include/xo/gc/detail/IGCObject_Xfer.hpp b/include/xo/gc/detail/IGCObject_Xfer.hpp index 2ece6b01..655263bf 100644 --- a/include/xo/gc/detail/IGCObject_Xfer.hpp +++ b/include/xo/gc/detail/IGCObject_Xfer.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * [/home/roland/proj/xo-umbrella2-claude1/xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for abstract facet .hpp file: @@ -32,6 +32,8 @@ namespace mm { /** integer identifying a type **/ using typeseq = AGCObject::typeseq; using size_type = AGCObject::size_type; + using AAllocator = AGCObject::AAllocator; + using ACollector = AGCObject::ACollector; ///@} /** @defgroup mm-gcobject-xfer-methods **/ diff --git a/include/xo/gc/detail/RGCObject.hpp b/include/xo/gc/detail/RGCObject.hpp index ace699c5..e54a2539 100644 --- a/include/xo/gc/detail/RGCObject.hpp +++ b/include/xo/gc/detail/RGCObject.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * [/home/roland/proj/xo-umbrella2-claude1/xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for abstract facet .hpp file: @@ -32,6 +32,8 @@ public: using DataPtr = Object::DataPtr; using typeseq = xo::reflect::typeseq; using size_type = AGCObject::size_type; + using AAllocator = AGCObject::AAllocator; + using ACollector = AGCObject::ACollector; ///@} /** @defgroup mm-gcobject-router-ctors **/ From c817f0198783dee18da8d7f73fbe5ff523baadf4 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 16 Jan 2026 13:05:58 -0500 Subject: [PATCH 023/174] xo-expression2: + DUniqueString, use in StringTable --- src/gc/DX1Collector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index e5f951f2..263e5b8e 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -338,7 +338,7 @@ namespace xo { if (!this->contains(role::from_space(), from_src)) { /* presumeably memory not owned by collector - * (e.g. Boolean {true, false}) + * (e.g. DBoolean {true, false}, DUniqueString {owned by StringTable} etc.) */ return from_src; } From 0e8fb5c96501500a5284de6879159b7e7ac5e5d9 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 16 Jan 2026 16:10:00 -0500 Subject: [PATCH 024/174] xo-gc: + CollectorTypeRegistry for streamlined init --- include/xo/gc/CollectorTypeRegistry.hpp | 74 +++++++++++++++++++++++++ src/gc/CMakeLists.txt | 3 + src/gc/CollectorTypeRegistry.cpp | 45 +++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 include/xo/gc/CollectorTypeRegistry.hpp create mode 100644 src/gc/CollectorTypeRegistry.cpp diff --git a/include/xo/gc/CollectorTypeRegistry.hpp b/include/xo/gc/CollectorTypeRegistry.hpp new file mode 100644 index 00000000..4474e20c --- /dev/null +++ b/include/xo/gc/CollectorTypeRegistry.hpp @@ -0,0 +1,74 @@ +/** @file CollectorTypeRegistry.hpp + * + * @brief Runtime type registration for gc-aware types + * + * @author Roland Conybeare, Jan 2026 + **/ + +#pragma once + +#include "Collector.hpp" +#include + +namespace xo { + namespace mm { + /** @class CollectorTypeRegistry + * + * @brief Runtime registry for gc-aware types + * + * Singleton to remember known gc-aware types; + * use to simplify registering such types + * with a collector instance. + * + * Remark: splitting work here between + * 1. static initializer work: tracking gc-aware types, + * 2. runtime post-configuration work: report + * gc-aware types to GC instances + * + * Use: + * 1. subsystem foo provides function foo_register_types(obj gc) + * Function calls + * gc.install_type(impl_for()) + * for each gc-aware type provided by subsystem foo + * + * Example: in file xo-object2/src/object2/object2_register_types.cpp, see + * object2_register_types() + * + * 2. during subsystem init, call + * CollectorTypeRegistry::instance().register_types(&foo_register_types); + * + * Example: in file xo-object2/src/object2/init_object2.cpp, see + * InitSubsys::init() + * + * 3. during Collector setup, call + * obj gc = ...; + * CollectorTypeRegistry::instance().install_types(gc); + * + * Example: in file xo-object2/utest/X1Collector.test.cpp + * TEST_CASE("x1") + **/ + class CollectorTypeRegistry { + public: + using init_function_type = std::function)>; + + public: + /** singleton instance **/ + static CollectorTypeRegistry & instance(); + + /** remember a gc-aware type-registration function **/ + void register_types(init_function_type init_fn); + + /** register known GC-aware types with @p gc. + * Calls @c gc.isntall_type() for each + * such type. + **/ + bool install_types(obj gc); + + private: + /** initialization steps for a new Collector instance **/ + std::vector init_seq_v_; + }; + } +} + +/* end CollectorTypeRegistry.hpp */ diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index 2d42c4c9..44cef925 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -3,8 +3,11 @@ set(SELF_LIB xo_gc) set(SELF_SRCS + CollectorTypeRegistry.cpp + ICollector_Any.cpp IGCObject_Any.cpp + IAllocator_DX1Collector.cpp ICollector_DX1Collector.cpp IAllocIterator_DX1CollectorIterator.cpp diff --git a/src/gc/CollectorTypeRegistry.cpp b/src/gc/CollectorTypeRegistry.cpp new file mode 100644 index 00000000..cb039dd8 --- /dev/null +++ b/src/gc/CollectorTypeRegistry.cpp @@ -0,0 +1,45 @@ +/** @file CollectorTypeRegistry.cpp + **/ + +#include "CollectorTypeRegistry.hpp" +#include + +namespace xo { + namespace mm { + CollectorTypeRegistry & + CollectorTypeRegistry::instance() { + static CollectorTypeRegistry s_instance; + + return s_instance; + } + + void + CollectorTypeRegistry::register_types(init_function_type fn) { + scope log(XO_DEBUG(true)); + + init_seq_v_.push_back(fn); + } + + bool + CollectorTypeRegistry::install_types(obj gc) { + scope log(XO_DEBUG(true)); + + bool ok = true; + + size_t i = 0; + size_t n = init_seq_v_.size(); + log && log("run n init steps", xtag("n", n)); + + for (const auto & fn : init_seq_v_) { + log && log("do install fn (", i+1, "/", n, ")"); + + ok = ok & fn(gc); + } + + return ok; + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end CollectorTypeRegistry.cpp */ From 8247972d9698f928965f882318f3e85496967582 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 23 Jan 2026 11:54:32 -0500 Subject: [PATCH 025/174] xo-reader2: + example app 'readerreplxx' --- include/xo/gc/DX1Collector.hpp | 11 +++++++++++ src/gc/DX1Collector.cpp | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 33c9f56d..78b55d20 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -69,6 +69,12 @@ namespace xo { constexpr std::uint64_t tseq_mask_shifted() const; #endif + + /** copy of this config, + * with @c arena_config_.size_ set to @p gen_z + **/ + CollectorConfig with_size(std::size_t gen_z); + generation age2gen(object_age age) const noexcept { return generation(age % n_survive_threshold_); } @@ -169,6 +175,11 @@ namespace xo { /** Create X1 collector instance. **/ explicit DX1Collector(const CollectorConfig & cfg); + /** create instance with default configuration, + * generation size @p gen_z + **/ + DX1Collector make_std(std::size_t gen_z); + std::string_view name() const { return config_.name_; } const DArena * get_object_types() const noexcept { return &object_types_; } diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 263e5b8e..3a77e15f 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -24,6 +24,15 @@ namespace xo { using xo::facet::with_facet; namespace mm { + + CollectorConfig + CollectorConfig::with_size(std::size_t gen_z) + { + CollectorConfig copy = *this; + copy.arena_config_ = arena_config_.with_size(gen_z); + return copy; + } + #ifdef NOT_USING constexpr std::uint64_t CollectorConfig::gen_mult() const { From 9f426680361beb57a12064d26d5ef3cbe5f5dc7d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 25 Jan 2026 10:09:53 -0500 Subject: [PATCH 026/174] xo-gc: + GCObjectConversion template --- include/xo/gc/GCObjectConversion.hpp | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 include/xo/gc/GCObjectConversion.hpp diff --git a/include/xo/gc/GCObjectConversion.hpp b/include/xo/gc/GCObjectConversion.hpp new file mode 100644 index 00000000..6e3e84df --- /dev/null +++ b/include/xo/gc/GCObjectConversion.hpp @@ -0,0 +1,30 @@ +/** @file GCObjectConversion.hpp + * + * @author Roland Conybeare, Jan 2026 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace scm { + /** @brief compile-time conversion obj <-> T + * + * Specialize for each T that participates in conversion. + * Methods here aren't implemented + **/ + template + struct GCObjectConversion { + using AGCObject = xo::mm::AGCObject; + using AAllocator = xo::mm::AAllocator; + + static obj to_gco(obj mm, const T & x); + static T from_gco(obj mm, obj gco); + }; + + } /*namespace scm */ +} /*namespace xo*/ + +/* end GCObjectConversion.hpp */ From c98a5c77a59f64483ada9c126c7ff3370c89d8f0 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 25 Jan 2026 10:14:39 -0500 Subject: [PATCH 027/174] xo-gc: needs explicit xo_facet dep --- cmake/xo_gcConfig.cmake.in | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmake/xo_gcConfig.cmake.in b/cmake/xo_gcConfig.cmake.in index c32a8368..aebd133c 100644 --- a/cmake/xo_gcConfig.cmake.in +++ b/cmake/xo_gcConfig.cmake.in @@ -1,7 +1,9 @@ @PACKAGE_INIT@ include(CMakeFindDependencyMacro) -#find_dependency(indentlog) find_dependency(xo_alloc2) +find_dependency(xo_facet) +find_dependency(indentlog) + include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") check_required_components("@PROJECT_NAME@") From d5e22163afc6dd238d4ecfe795ba9eb1af68a0bb Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 26 Jan 2026 12:38:17 -0500 Subject: [PATCH 028/174] xo-procedure2 xo-object2: + polymorphic primitive support --- include/xo/gc/GCObjectConversion.hpp | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/include/xo/gc/GCObjectConversion.hpp b/include/xo/gc/GCObjectConversion.hpp index 6e3e84df..8a950c82 100644 --- a/include/xo/gc/GCObjectConversion.hpp +++ b/include/xo/gc/GCObjectConversion.hpp @@ -7,6 +7,8 @@ #include #include +#include +#include namespace xo { namespace scm { @@ -20,10 +22,70 @@ namespace xo { using AGCObject = xo::mm::AGCObject; using AAllocator = xo::mm::AAllocator; + /** find gc-aware representation for @p x. + * If necessary allocate from @p mm, but may + * refer to @p x in-place + **/ static obj to_gco(obj mm, const T & x); + /** convert to native representation @tparam T from gc-aware + * @p gco. If necessary allocate from @p mm, but + * may instead refer to @p x in-place + **/ static T from_gco(obj mm, obj gco); }; + /** Motivating use-case for GCObjectConversion is to transform + * primitive function arguments and results to/from gc-aware + * representation. + * + * However: Schematika also supports runtime polymorphism + * which leads to primitives that expect obj arguments. + * + * Also, Schematika expression parser needs representation for + * expressions, before type unification. + * + * Consider a function like: + * def fact = lambda (n : i64) { if (n <= 0) then 1 else (n * fact(n - 1)); } + * During expression parsing the rhs argument to multiply has unknown type. + * To construct an expression for input to unification will use polymorphic + * binding for multiply primitive, relying on specialization here for + * its implementation. + **/ + template + struct GCObjectConversion> { + using AGCObject = xo::mm::AGCObject; + using AAllocator = xo::mm::AAllocator; + using FacetRegistry = xo::facet::FacetRegistry; + using DVariantPlaceholder = xo::facet::DVariantPlaceholder; + + static obj to_gco(obj, + obj gco) { + if constexpr (std::is_same_v) { + // trivial conversion! + return gco; + } else if constexpr (std::is_same_v) { + // runtime polymorphism + return FacetRegistry::instance().variant(gco); + } else /* DRepr != DVariantPlaceholder */ { + // known content w/ fat object pointer + return obj(gco.data()); + } + } + + static obj from_gco(obj, + obj gco) { + if constexpr (std::is_same_v) { + // trivial conversion + return gco; + } else { + // both runtime and comptime polymorphism + // use same path here, since representation of @p gco + // is type-erased here + + return FacetRegistry::instance().variant(gco); + } + } + }; } /*namespace scm */ } /*namespace xo*/ From b056518e11947e94a75e0cb01736c7c5e7860360 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 31 Jan 2026 01:44:53 -0500 Subject: [PATCH 029/174] xo-expression2 xo-gc: DSequenceExpr builds [WIP] --- CMakeLists.txt | 4 ++++ idl/GCObject.json5 | 6 ++++-- include/xo/gc/GCObject.hpp | 3 ++- include/xo/gc/detail/AGCObject.hpp | 2 +- include/xo/gc/detail/IGCObject_Any.hpp | 2 +- include/xo/gc/detail/IGCObject_Xfer.hpp | 2 +- include/xo/gc/detail/RGCObject.hpp | 7 +++++-- 7 files changed, 18 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ad70af5..c0bf8418 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,10 @@ xo_add_genfacet( # ---------------------------------------------------------------- +xo_add_genfacet_all(xo-gc-genfacet-all) + +# ---------------------------------------------------------------- + # must complete definition of expression lib before configuring examples add_subdirectory(src/gc) add_subdirectory(utest) diff --git a/idl/GCObject.json5 b/idl/GCObject.json5 index ef00b5d9..f2336678 100644 --- a/idl/GCObject.json5 +++ b/idl/GCObject.json5 @@ -6,11 +6,13 @@ "", "", ], + // extra includes in GCObject.hpp, if any + user_hpp_includes: [], + namespace1: "xo", + namespace2: "mm", pretext: [ "namespace xo { namespace mm { struct ACollector; }}", ], - namespace1: "xo", - namespace2: "mm", facet: "GCObject", detail_subdir: "detail", brief: "xxx", diff --git a/include/xo/gc/GCObject.hpp b/include/xo/gc/GCObject.hpp index 25834817..2eede6b3 100644 --- a/include/xo/gc/GCObject.hpp +++ b/include/xo/gc/GCObject.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/home/roland/proj/xo-umbrella2-claude1/xo-facet/codegen/genfacet] + * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for facet .hpp file: @@ -18,4 +18,5 @@ #include "detail/IGCObject_Xfer.hpp" #include "detail/RGCObject.hpp" + /* end GCObject.hpp */ \ No newline at end of file diff --git a/include/xo/gc/detail/AGCObject.hpp b/include/xo/gc/detail/AGCObject.hpp index b7a5ffa3..ab6b4096 100644 --- a/include/xo/gc/detail/AGCObject.hpp +++ b/include/xo/gc/detail/AGCObject.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/home/roland/proj/xo-umbrella2-claude1/xo-facet/codegen/genfacet] + * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for abstract facet .hpp file: diff --git a/include/xo/gc/detail/IGCObject_Any.hpp b/include/xo/gc/detail/IGCObject_Any.hpp index c930693f..e1b274a7 100644 --- a/include/xo/gc/detail/IGCObject_Any.hpp +++ b/include/xo/gc/detail/IGCObject_Any.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/home/roland/proj/xo-umbrella2-claude1/xo-facet/codegen/genfacet] + * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for abstract facet .hpp file: diff --git a/include/xo/gc/detail/IGCObject_Xfer.hpp b/include/xo/gc/detail/IGCObject_Xfer.hpp index 655263bf..d25eb81e 100644 --- a/include/xo/gc/detail/IGCObject_Xfer.hpp +++ b/include/xo/gc/detail/IGCObject_Xfer.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/home/roland/proj/xo-umbrella2-claude1/xo-facet/codegen/genfacet] + * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for abstract facet .hpp file: diff --git a/include/xo/gc/detail/RGCObject.hpp b/include/xo/gc/detail/RGCObject.hpp index e54a2539..435d5a64 100644 --- a/include/xo/gc/detail/RGCObject.hpp +++ b/include/xo/gc/detail/RGCObject.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/home/roland/proj/xo-umbrella2-claude1/xo-facet/codegen/genfacet] + * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for abstract facet .hpp file: @@ -40,6 +40,9 @@ public: ///@{ RGCObject() {} RGCObject(Object::DataPtr data) : Object{std::move(data)} {} + RGCObject(const AGCObject * iface, void * data) + requires std::is_same_v + : Object(iface, data) {} ///@} /** @defgroup mm-gcobject-router-methods **/ @@ -54,7 +57,7 @@ public: return O::iface()->shallow_copy(O::data(), mm); } - // non-const methods + // non-const methods (still const in router!) size_type forward_children(obj gc) noexcept { return O::iface()->forward_children(O::data(), gc); } From 8f09a6045ee6e15030bbfdc235608c8e0fbf4a9f Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 1 Feb 2026 17:26:24 -0500 Subject: [PATCH 030/174] xo-reader2 xo-gc: streamline example + DX1Collector.ref() method --- include/xo/gc/DX1Collector.hpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 78b55d20..40c7f4f2 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -175,10 +175,16 @@ namespace xo { /** Create X1 collector instance. **/ explicit DX1Collector(const CollectorConfig & cfg); + /** faceted object pointer to this instance */ + template + obj ref() { return obj(this); } + +#ifdef NOT_YET /** create instance with default configuration, * generation size @p gen_z **/ - DX1Collector make_std(std::size_t gen_z); + static DX1Collector make_std(std::size_t gen_z); +#endif std::string_view name() const { return config_.name_; } From 5683623f044a524f60a55c0291fe4b4722a811e1 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 1 Feb 2026 22:12:28 -0500 Subject: [PATCH 031/174] xo-interpreter2: work towards utest w/ vsm+reader [WIP] --- include/xo/gc/DX1Collector.hpp | 103 +--------------------------- include/xo/gc/X1CollectorConfig.hpp | 92 +++++++++++++++++++++++++ src/gc/DX1Collector.cpp | 12 ++-- utest/Collector.test.cpp | 62 ++++++++--------- utest/DX1CollectorIterator.test.cpp | 26 +++---- 5 files changed, 145 insertions(+), 150 deletions(-) create mode 100644 include/xo/gc/X1CollectorConfig.hpp diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 40c7f4f2..af41ae0d 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -5,6 +5,7 @@ #pragma once +#include "X1CollectorConfig.hpp" #include "GCObject.hpp" #include "generation.hpp" #include "object_age.hpp" @@ -40,104 +41,6 @@ namespace xo { }; #endif - struct CollectorConfig { - using size_type = std::size_t; - -#ifdef OBSOLETE // get from arena_config_.header_ - /* - * alloc header - * TTTTTTTTTTTTGGGGGZZZZZZZZZZZZ - * < tseq >< size > - * - * masking - * - * ..432107654321076543210 bit - * - * > < .gen_bits - * 0..............01111111 gen_mask_unshifted - * 0..011111110..........0 gen_mask_shifted - * > < gen_shift - */ - //constexpr std::uint64_t gen_mult() const; - constexpr std::uint64_t gen_shift() const; - constexpr std::uint64_t gen_mask_unshifted() const; - constexpr std::uint64_t gen_mask_shifted() const; - - //constexpr std::uint64_t tseq_mult() const; - constexpr std::uint64_t tseq_shift() const; - constexpr std::uint64_t tseq_mask_unshifted() const; - constexpr std::uint64_t tseq_mask_shifted() const; -#endif - - - /** copy of this config, - * with @c arena_config_.size_ set to @p gen_z - **/ - CollectorConfig with_size(std::size_t gen_z); - - generation age2gen(object_age age) const noexcept { - return generation(age % n_survive_threshold_); - } - - public: - // ----- Instance Variables ----- - - /** optional name, for diagnostics **/ - std::string name_; - - /** Configuration for collector spaces. - * Will have at least {nursery,tenured} x {from,to} spaces. - * Not using name_ member. - * - * REQUIRE: - * - arena_config_.store_header_flag_ must be true - **/ - ArenaConfig arena_config_; - - /** storage for N object types requires 8*N bytes **/ - std::size_t object_types_z_ = 2*1024*1024; - - /** storage for N object roots requires 8*N bytes **/ - std::size_t object_roots_z_ = 16*1024; - - /** number of bits to represent generation **/ - std::uint64_t gen_bits_ = 8; - - /** number of bits to represent tseq **/ - std::uint64_t tseq_bits_ = 24; - - /** Number of generations. - * Must be at least 2. - **/ - uint32_t n_generation_ = 2; - - /** Number of promotion steps. - * An object that survives this number of collections - * advances to the next generation. - **/ - uint32_t n_survive_threshold_ = 2; - - /** Trigger garbage collection when to-space allocation for - * generation g reaches gc_trigger_v_[g] - **/ - std::array gc_trigger_v_; - - /** true -> enable incremental collection. - * false -> only do full collection. - * - * Incremental collection requires mutation logs. - **/ - bool allow_incremental_gc_ = true; - - /** If non-zero remember statistics for - * the last @p stats_history_z_ collections. - **/ - uint32_t stats_history_z_ = false; - - /** true to enable debug logging **/ - bool debug_flag_ = false; - }; - // ----- GCRunState ----- /** @class GCRunState @@ -173,7 +76,7 @@ namespace xo { static constexpr size_t c_max_typeseq = 4096; /** Create X1 collector instance. **/ - explicit DX1Collector(const CollectorConfig & cfg); + explicit DX1Collector(const X1CollectorConfig & cfg); /** faceted object pointer to this instance */ template @@ -344,7 +247,7 @@ namespace xo { public: /** garbage collector configuration **/ - CollectorConfig config_; + X1CollectorConfig config_; /** current gc state **/ GCRunState runstate_; diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp new file mode 100644 index 00000000..54296e2c --- /dev/null +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -0,0 +1,92 @@ +/** @file X1CollectorConfig.hpp + * + * @author Roland Conybeare, Jan 2026 + **/ + +#pragma once + +#include "object_age.hpp" +#include "generation.hpp" +#include +#include +#include + +namespace xo { + namespace mm { + struct X1CollectorConfig { + using size_type = std::size_t; + + /** copy of this config, + * with @c arena_config_.size_ set to @p gen_z + **/ + X1CollectorConfig with_size(std::size_t gen_z); + + generation age2gen(object_age age) const noexcept { + return generation(age % n_survive_threshold_); + } + + public: + // ----- Instance Variables ----- + + /** optional name, for diagnostics **/ + std::string name_; + + /** Configuration for collector spaces. + * Will have at least {nursery,tenured} x {from,to} spaces. + * Not using name_ member. + * + * REQUIRE: + * - arena_config_.store_header_flag_ must be true + **/ + ArenaConfig arena_config_; + + /** storage for N object types requires 8*N bytes **/ + std::size_t object_types_z_ = 2*1024*1024; + + /** storage for N object roots requires 8*N bytes **/ + std::size_t object_roots_z_ = 16*1024; + + /** number of bits to represent generation **/ + std::uint64_t gen_bits_ = 8; + + /** number of bits to represent tseq **/ + std::uint64_t tseq_bits_ = 24; + + /** Number of generations. + * Must be at least 2. + **/ + uint32_t n_generation_ = 2; + + /** Number of promotion steps. + * An object that survives this number of collections + * advances to the next generation. + **/ + uint32_t n_survive_threshold_ = 2; + + /** Trigger garbage collection when to-space allocation for + * generation g reaches gc_trigger_v_[g] + **/ + std::array gc_trigger_v_; + + /** true -> enable incremental collection. + * false -> only do full collection. + * + * Incremental collection requires mutation logs. + **/ + bool allow_incremental_gc_ = true; + + /** If non-zero remember statistics for + * the last @p stats_history_z_ collections. + **/ + uint32_t stats_history_z_ = false; + + /** true to enable debug logging **/ + bool debug_flag_ = false; + }; + + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end X1CollectorConfig.hpp */ + diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 3a77e15f..16bdedc3 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -25,24 +25,24 @@ namespace xo { namespace mm { - CollectorConfig - CollectorConfig::with_size(std::size_t gen_z) + X1CollectorConfig + X1CollectorConfig::with_size(std::size_t gen_z) { - CollectorConfig copy = *this; + X1CollectorConfig copy = *this; copy.arena_config_ = arena_config_.with_size(gen_z); return copy; } #ifdef NOT_USING constexpr std::uint64_t - CollectorConfig::gen_mult() const { + X1CollectorConfig::gen_mult() const { return 1ul << arena_config_.header_size_bits_; } #endif #ifdef NOT_USING constexpr std::uint64_t - CollectorConfig::tseq_mult() const { + X1CollectorConfig::tseq_mult() const { return 1ul << (gen_bits_ + arena_config_.header_size_bits_); } #endif @@ -69,7 +69,7 @@ namespace xo { using size_type = xo::mm::DX1Collector::size_type; - DX1Collector::DX1Collector(const CollectorConfig & cfg) : config_{cfg} + DX1Collector::DX1Collector(const X1CollectorConfig & cfg) : config_{cfg} { assert(config_.arena_config_.header_.size_bits_ + config_.arena_config_.header_.age_bits_ + diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp index f322c286..318c9c21 100644 --- a/utest/Collector.test.cpp +++ b/utest/Collector.test.cpp @@ -23,7 +23,7 @@ namespace xo { using xo::mm::AAllocator; using xo::mm::ACollector; - using xo::mm::CollectorConfig; + using xo::mm::X1CollectorConfig; using xo::mm::DX1Collector; using xo::mm::ArenaConfig; using xo::mm::AllocHeaderConfig; @@ -61,12 +61,12 @@ namespace xo { 0 /*tseq_bits*/, 0 /*age_bits*/, 16 /*size_bits*/), }; - CollectorConfig cfg = { .arena_config_ = arena_cfg, - .n_generation_ = 2, - .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0}} }; + X1CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 2, + .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; DX1Collector gc = DX1Collector{cfg}; @@ -109,12 +109,12 @@ namespace xo { 0 /*tseq_bits*/, 0 /*age_bits*/, 16 /*size_bits*/), }; - CollectorConfig cfg = { .arena_config_ = arena_cfg, - .n_generation_ = 2, - .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0}} }; + X1CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 2, + .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; DX1Collector gc = DX1Collector{cfg}; @@ -135,12 +135,12 @@ namespace xo { 0 /*tseq-bits*/, 0 /*age-bits*/, 16 /*size-bits*/), }; - CollectorConfig cfg = { .arena_config_ = arena_cfg, - .n_generation_ = 2, - .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0}} }; + X1CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 2, + .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; DX1Collector gc = DX1Collector{cfg}; @@ -165,12 +165,12 @@ namespace xo { 16 /*size-bits*/), }; /* collector with one generation collapses to a non-generational copying collector */ - CollectorConfig cfg = { .arena_config_ = arena_cfg, - .n_generation_ = 1, - .gc_trigger_v_ = {{64*1024, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0}} }; + X1CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 1, + .gc_trigger_v_ = {{64*1024, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; DX1Collector x1state = DX1Collector{cfg}; @@ -209,12 +209,12 @@ namespace xo { }; /* collector with one generation collapses to a non-generational copying collector */ - CollectorConfig cfg = { .arena_config_ = arena_cfg, - .n_generation_ = 1, - .gc_trigger_v_ = {{64*1024, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0}} }; + X1CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 1, + .gc_trigger_v_ = {{64*1024, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; /* X1 allocator+collector */ DX1Collector x1state = DX1Collector{cfg}; diff --git a/utest/DX1CollectorIterator.test.cpp b/utest/DX1CollectorIterator.test.cpp index 99347fad..314eb4a8 100644 --- a/utest/DX1CollectorIterator.test.cpp +++ b/utest/DX1CollectorIterator.test.cpp @@ -24,7 +24,7 @@ namespace xo { using xo::mm::DX1CollectorIterator; using xo::mm::DArena; using xo::mm::DArenaIterator; - using xo::mm::CollectorConfig; + using xo::mm::X1CollectorConfig; using xo::mm::ArenaConfig; using xo::mm::AllocHeaderConfig; using xo::mm::cmpresult; @@ -51,12 +51,12 @@ namespace xo { 0 /*tseq_bits*/, 0 /*age_bits*/, 16 /*size_bits*/), }; - CollectorConfig cfg = { .arena_config_ = arena_cfg, - .n_generation_ = 2, - .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0}} }; + X1CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 2, + .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; DX1Collector gc = DX1Collector{cfg}; @@ -94,12 +94,12 @@ namespace xo { 0 /*tseq_bits*/, 0 /*age_bits*/, 16 /*size_bits*/), }; - CollectorConfig cfg = { .arena_config_ = arena_cfg, - .n_generation_ = 2, - .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0}} }; + X1CollectorConfig cfg = { .arena_config_ = arena_cfg, + .n_generation_ = 2, + .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}} }; DX1Collector gc = DX1Collector{cfg}; obj a1o{&gc}; From 10bc9289c4d542c6e8489ebc7ab39a221c76ae2b Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 3 Feb 2026 01:05:36 -0500 Subject: [PATCH 032/174] xo-interpreter2 .. xo-arena. memory pool introspection --- include/xo/gc/DX1Collector.hpp | 7 ++++- .../xo/gc/detail/IAllocator_DX1Collector.hpp | 2 ++ src/gc/DX1Collector.cpp | 27 +++++++++++++++++-- src/gc/IAllocator_DX1Collector.cpp | 6 +++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index af41ae0d..2ef7148a 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -81,7 +81,7 @@ namespace xo { /** faceted object pointer to this instance */ template obj ref() { return obj(this); } - + #ifdef NOT_YET /** create instance with default configuration, * generation size @p gen_z @@ -110,6 +110,11 @@ namespace xo { /** total allocated memory in bytes, across all {role, generation} **/ size_type allocated_total() const noexcept; + /** introspection for memory use. + * Call @p visitor(info) for each pool owned by this allocator + **/ + void visit_pools(const MemorySizeVisitor & visitor) const; + /** true iff address @p addr allocated from this collector * in role @p r (according to current GC state) **/ diff --git a/include/xo/gc/detail/IAllocator_DX1Collector.hpp b/include/xo/gc/detail/IAllocator_DX1Collector.hpp index 424f24ff..ca529990 100644 --- a/include/xo/gc/detail/IAllocator_DX1Collector.hpp +++ b/include/xo/gc/detail/IAllocator_DX1Collector.hpp @@ -48,6 +48,8 @@ namespace xo { static size_type available(const DX1Collector &) noexcept; /** space used by @p d across all {roles, generations}. **/ static size_type allocated(const DX1Collector &) noexcept; + /** visit memory pools owned by this allocator; call fn(info) for each pool **/ + static void visit_pools(const DX1Collector & d, const MemorySizeVisitor & fn); /** true iff address @p p comes from collector @p d **/ static bool contains(const DX1Collector & d, const void * p) noexcept; /** report last error, if any, for collector @p d **/ diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 16bdedc3..a4375da5 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -96,8 +96,18 @@ namespace xo { .store_header_flag_ = false}); for (uint32_t igen = 0, ngen = cfg.n_generation_; igen < ngen; ++igen) { - space_storage_[0][igen] = DArena::map(cfg.arena_config_); - space_storage_[1][igen] = DArena::map(cfg.arena_config_); + { + char buf[40]; + snprintf(buf, sizeof(buf), "x1-space-G%u-a", igen); + + space_storage_[0][igen] = DArena::map(cfg.arena_config_.with_name(std::string(buf))); + } + { + char buf[40]; + snprintf(buf, sizeof(buf), "x1-space-G%u-b", igen); + + space_storage_[1][igen] = DArena::map(cfg.arena_config_.with_name(std::string(buf))); + } space_[role::to_space()][igen] = &space_storage_[0][igen]; space_[role::from_space()][igen] = &space_storage_[1][igen]; @@ -109,6 +119,19 @@ namespace xo { } } + void + DX1Collector::visit_pools(const MemorySizeVisitor & visitor) const + { + object_types_.visit_pools(visitor); + roots_.visit_pools(visitor); + + for (uint32_t i = 0; i < c_n_role; ++i) { + for (uint32_t j = 0; j < config_.n_generation_; ++j) { + space_storage_[i][j].visit_pools(visitor); + } + } + } + bool DX1Collector::contains(role r, const void * addr) const noexcept { diff --git a/src/gc/IAllocator_DX1Collector.cpp b/src/gc/IAllocator_DX1Collector.cpp index 58e89941..039e84df 100644 --- a/src/gc/IAllocator_DX1Collector.cpp +++ b/src/gc/IAllocator_DX1Collector.cpp @@ -56,6 +56,12 @@ namespace xo { return d.allocated_total(); } + void + IAllocator_DX1Collector::visit_pools(const DX1Collector & d, const MemorySizeVisitor & visitor) + { + d.visit_pools(visitor); + } + bool IAllocator_DX1Collector::contains(const DX1Collector & d, const void * addr) noexcept { From 06a35fa095419fab5d2d06d54dd09296356fdedc Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 5 Feb 2026 15:45:40 -0500 Subject: [PATCH 033/174] xo-reader2 stack: top-level lambda w/ apply parses --- idl/GCObject.json5 | 1 + 1 file changed, 1 insertion(+) diff --git a/idl/GCObject.json5 b/idl/GCObject.json5 index f2336678..bf0d78cd 100644 --- a/idl/GCObject.json5 +++ b/idl/GCObject.json5 @@ -76,4 +76,5 @@ attributes: [], }, ], + router_facet_explicit_content: [ ], } From d0a0cf0ad71782bd826dd599261f1edacc596605 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 8 Feb 2026 01:01:03 -0500 Subject: [PATCH 034/174] xo-interpreter2 stack: work on VSM for apply -> closure action [WIP] --- include/xo/gc/detail/RGCObject.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xo/gc/detail/RGCObject.hpp b/include/xo/gc/detail/RGCObject.hpp index 435d5a64..ab4bcc54 100644 --- a/include/xo/gc/detail/RGCObject.hpp +++ b/include/xo/gc/detail/RGCObject.hpp @@ -85,4 +85,4 @@ namespace xo { namespace facet { }; } } -/* end RGCObject.hpp */ \ No newline at end of file +/* end RGCObject.hpp */ From fed2d03930891836cdaef231d2a131a5cd1fe0b7 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 13 Feb 2026 15:15:08 -0500 Subject: [PATCH 035/174] xo-gc stack: streamline object pointer forwarding --- idl/GCObject.json5 | 2 +- include/xo/gc/GCObject.hpp | 28 +++++++++++++++++++++++++++- include/xo/gc/detail/RCollector.hpp | 10 ++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/idl/GCObject.json5 b/idl/GCObject.json5 index bf0d78cd..30fc0d3a 100644 --- a/idl/GCObject.json5 +++ b/idl/GCObject.json5 @@ -76,5 +76,5 @@ attributes: [], }, ], - router_facet_explicit_content: [ ], + router_facet_explicit_content: [] } diff --git a/include/xo/gc/GCObject.hpp b/include/xo/gc/GCObject.hpp index 2eede6b3..970d2c31 100644 --- a/include/xo/gc/GCObject.hpp +++ b/include/xo/gc/GCObject.hpp @@ -18,5 +18,31 @@ #include "detail/IGCObject_Xfer.hpp" #include "detail/RGCObject.hpp" +namespace xo { + namespace mm { + /** defined here to avoid #include cycle, since + * template obj awkward to make available there + **/ + template + template + void + RCollector::forward_inplace(xo::facet::obj * p_obj) + { + this->forward_inplace(p_obj->iface(), (void **)&(p_obj->data_)); + } + + template + template + void + RCollector::forward_inplace(DRepr ** p_repr) + { + // fetch static interface for DRepr + auto iface = xo::facet::impl_for(); + + this->forward_inplace(&iface, (void **)p_repr); + } + } +} + +/* end GCObject.hpp */ -/* end GCObject.hpp */ \ No newline at end of file diff --git a/include/xo/gc/detail/RCollector.hpp b/include/xo/gc/detail/RCollector.hpp index 5e3eddb3..9cce4844 100644 --- a/include/xo/gc/detail/RCollector.hpp +++ b/include/xo/gc/detail/RCollector.hpp @@ -23,6 +23,16 @@ namespace xo { RCollector() = default; RCollector(DataPtr data) : Object{std::move(data)} {} + /** forward op in place. Defined in GCObject.hpp to avoid #include cycle **/ + template + void forward_inplace(obj * p_obj); + + /** another convenience template for forwarding. + * Defined in RGCObject.hpp to avoid #include cycle. + **/ + template + void forward_inplace(DRepr ** pp_repr); + int32_t _typeseq() const noexcept { return O::iface()->_typeseq(); } size_type allocated(generation g, role r) const noexcept { return O::iface()->allocated(O::data(), g, r); } size_type reserved(generation g, role r) const noexcept { return O::iface()->reserved(O::data(), g, r); } From a4f655d621a86bdf44b75e29d22ceb6fe665c87d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 15 Feb 2026 13:06:33 -0500 Subject: [PATCH 036/174] xo-gc: + X1Collector.hpp convenience header --- include/xo/gc/X1Collector.hpp | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 include/xo/gc/X1Collector.hpp diff --git a/include/xo/gc/X1Collector.hpp b/include/xo/gc/X1Collector.hpp new file mode 100644 index 00000000..c9d00da3 --- /dev/null +++ b/include/xo/gc/X1Collector.hpp @@ -0,0 +1,11 @@ +/** @file X1Collector.hpp + * + * @author Roland Conybeare, Feb 2026 + **/ + +#pragma once + +#include "DX1Collector.hpp" +#include "detail/ICollector_DX1Collector.hpp" + +/* end X1Collector.hpp */ From 01c63418fdabca49eed1da79bbbeeaf6c971601e Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 15 Feb 2026 13:15:11 -0500 Subject: [PATCH 037/174] xo-gc: + X1CollectorConfig.with_name() --- include/xo/gc/X1CollectorConfig.hpp | 7 ++++++- src/gc/DX1Collector.cpp | 22 ++++++++-------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp index 54296e2c..dec9545d 100644 --- a/include/xo/gc/X1CollectorConfig.hpp +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -17,7 +17,12 @@ namespace xo { using size_type = std::size_t; /** copy of this config, - * with @c arena_config_.size_ set to @p gen_z + * but with @ref name_ set to @p name + **/ + X1CollectorConfig with_name(std::string name); + + /** copy of this config, + * but with @c arena_config_.size_ set to @p gen_z **/ X1CollectorConfig with_size(std::size_t gen_z); diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index a4375da5..ede4a734 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -25,6 +25,14 @@ namespace xo { namespace mm { + X1CollectorConfig + X1CollectorConfig::with_name(std::string name) + { + X1CollectorConfig copy = *this; + copy.name_ = std::move(name); + return copy; + } + X1CollectorConfig X1CollectorConfig::with_size(std::size_t gen_z) { @@ -33,20 +41,6 @@ namespace xo { return copy; } -#ifdef NOT_USING - constexpr std::uint64_t - X1CollectorConfig::gen_mult() const { - return 1ul << arena_config_.header_size_bits_; - } -#endif - -#ifdef NOT_USING - constexpr std::uint64_t - X1CollectorConfig::tseq_mult() const { - return 1ul << (gen_bits_ + arena_config_.header_size_bits_); - } -#endif - // ----- GCRunState ----- GCRunState::GCRunState(generation gc_upto) From a750e85b9a59739177f7606e456f2a3a085f4c57 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 15 Feb 2026 22:57:15 -0500 Subject: [PATCH 038/174] xo-reader2 stack: + TypeRegistry --- src/gc/DX1Collector.cpp | 2 +- utest/DX1CollectorIterator.test.cpp | 2 +- utest/random_allocs.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index ede4a734..8cbd0bbd 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -279,7 +279,7 @@ namespace xo { DX1Collector::add_gc_root_poly(obj * p_root) noexcept { std::byte * mem - = roots_.alloc(typeseq::anon(), + = roots_.alloc(typeseq::sentinel(), sizeof(obj*)); assert(mem); diff --git a/utest/DX1CollectorIterator.test.cpp b/utest/DX1CollectorIterator.test.cpp index 314eb4a8..137c07f3 100644 --- a/utest/DX1CollectorIterator.test.cpp +++ b/utest/DX1CollectorIterator.test.cpp @@ -110,7 +110,7 @@ namespace xo { REQUIRE(a1o.allocated() == 0); size_t req_z = 13; - byte * mem = gc.alloc(typeseq::anon(), req_z); + byte * mem = gc.alloc(typeseq::sentinel(), req_z); REQUIRE(mem != nullptr); diff --git a/utest/random_allocs.cpp b/utest/random_allocs.cpp index 1f7f833f..2325140d 100644 --- a/utest/random_allocs.cpp +++ b/utest/random_allocs.cpp @@ -67,7 +67,7 @@ namespace utest { bool ok_flag = true; - std::byte * mem = mm.alloc(typeseq::anon(), z); + std::byte * mem = mm.alloc(typeseq::sentinel(), z); log && log(xtag("i_alloc", i_alloc), xtag("si", si), From cd143dce297979abe6f524647422540d989ec8bf Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 16 Feb 2026 17:46:51 -0500 Subject: [PATCH 039/174] xo-interpreter2 stack: OUTPUT_CPP_DIR cmake->idl/ --- CMakeLists.txt | 1 - idl/GCObject.json5 | 1 + include/xo/gc/GCObject.hpp | 3 +-- include/xo/gc/detail/AGCObject.hpp | 6 ++++-- include/xo/gc/detail/IGCObject_Any.hpp | 9 ++++++--- include/xo/gc/detail/IGCObject_Xfer.hpp | 9 ++++++--- include/xo/gc/detail/RGCObject.hpp | 9 +++++++-- src/gc/IGCObject_Any.cpp | 2 +- 8 files changed, 26 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c0bf8418..59f13d7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,6 @@ xo_add_genfacet( INPUT idl/GCObject.json5 OUTPUT_HPP_DIR include/xo/gc OUTPUT_IMPL_SUBDIR detail - OUTPUT_CPP_DIR src/gc ) # ---------------------------------------------------------------- diff --git a/idl/GCObject.json5 b/idl/GCObject.json5 index 30fc0d3a..242bea1e 100644 --- a/idl/GCObject.json5 +++ b/idl/GCObject.json5 @@ -1,5 +1,6 @@ { mode: "facet", + output_cpp_dir: "src/gc", includes: [ "", "", diff --git a/include/xo/gc/GCObject.hpp b/include/xo/gc/GCObject.hpp index 970d2c31..c40ffb3a 100644 --- a/include/xo/gc/GCObject.hpp +++ b/include/xo/gc/GCObject.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * [xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for facet .hpp file: @@ -45,4 +45,3 @@ namespace xo { } /* end GCObject.hpp */ - diff --git a/include/xo/gc/detail/AGCObject.hpp b/include/xo/gc/detail/AGCObject.hpp index ab6b4096..98f7e258 100644 --- a/include/xo/gc/detail/AGCObject.hpp +++ b/include/xo/gc/detail/AGCObject.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * [xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for abstract facet .hpp file: @@ -55,6 +55,8 @@ public: // const methods /** RTTI: unique id# for actual runtime data representation **/ virtual typeseq _typeseq() const noexcept = 0; + /** destroy instance @p d; calls c++ dtor only for actual runtime type; does not recover memory **/ + virtual void _drop(Opaque d) const noexcept = 0; /** memory consumption for this instance **/ virtual size_type shallow_size(Copaque data) const noexcept = 0; /** copy instance using allocator **/ @@ -82,4 +84,4 @@ using IGCObject_ImplType = xo::facet::FacetImplType; } /*namespace mm*/ } /*namespace xo*/ -/* AGCObject.hpp */ \ No newline at end of file +/* AGCObject.hpp */ diff --git a/include/xo/gc/detail/IGCObject_Any.hpp b/include/xo/gc/detail/IGCObject_Any.hpp index e1b274a7..9ff7bb2e 100644 --- a/include/xo/gc/detail/IGCObject_Any.hpp +++ b/include/xo/gc/detail/IGCObject_Any.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * [xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for abstract facet .hpp file: @@ -56,8 +56,11 @@ namespace mm { // from AGCObject - // const methods + // builtin methods typeseq _typeseq() const noexcept override { return s_typeseq; } + [[noreturn]] void _drop(Opaque) const noexcept override { _fatal(); } + + // const methods [[noreturn]] size_type shallow_size(Copaque) const noexcept override { _fatal(); } [[noreturn]] Opaque shallow_copy(Copaque, obj) const noexcept override { _fatal(); } @@ -87,4 +90,4 @@ namespace mm { } /*namespace mm */ } /*namespace xo */ -/* IGCObject_Any.hpp */ \ No newline at end of file +/* IGCObject_Any.hpp */ diff --git a/include/xo/gc/detail/IGCObject_Xfer.hpp b/include/xo/gc/detail/IGCObject_Xfer.hpp index d25eb81e..7f094ab7 100644 --- a/include/xo/gc/detail/IGCObject_Xfer.hpp +++ b/include/xo/gc/detail/IGCObject_Xfer.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * [xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for abstract facet .hpp file: @@ -44,8 +44,11 @@ namespace mm { // from AGCObject - // const methods + // builtin methods typeseq _typeseq() const noexcept override { return s_typeseq; } + void _drop(Opaque d) const noexcept override { _dcast(d).~DRepr(); } + + // const methods size_type shallow_size(Copaque data) const noexcept override { return I::shallow_size(_dcast(data)); } @@ -89,4 +92,4 @@ namespace mm { } /*namespace mm */ } /*namespace xo*/ -/* end IGCObject_Xfer.hpp */ \ No newline at end of file +/* end IGCObject_Xfer.hpp */ diff --git a/include/xo/gc/detail/RGCObject.hpp b/include/xo/gc/detail/RGCObject.hpp index ab4bcc54..d24994a8 100644 --- a/include/xo/gc/detail/RGCObject.hpp +++ b/include/xo/gc/detail/RGCObject.hpp @@ -2,7 +2,7 @@ * * Generated automagically from ingredients: * 1. code generator: - * [/Users/roland/proj/xo-umbrella2/xo-facet/codegen/genfacet] + * [xo-facet/codegen/genfacet] * arguments: * --input [idl/GCObject.json5] * 2. jinja2 template for abstract facet .hpp file: @@ -48,8 +48,13 @@ public: /** @defgroup mm-gcobject-router-methods **/ ///@{ - // const methods + // explicit injected content + + // builtin methods typeseq _typeseq() const noexcept { return O::iface()->_typeseq(); } + void _drop() const noexcept { O::iface()->_drop(O::data()); } + + // const methods size_type shallow_size() const noexcept { return O::iface()->shallow_size(O::data()); } diff --git a/src/gc/IGCObject_Any.cpp b/src/gc/IGCObject_Any.cpp index ff6d395d..778760ad 100644 --- a/src/gc/IGCObject_Any.cpp +++ b/src/gc/IGCObject_Any.cpp @@ -44,4 +44,4 @@ IGCObject_Any::forward_children(Opaque, obj) const noexcept -> siz } /*namespace mm*/ } /*namespace xo*/ -/* end IGCObject_Any.cpp */ \ No newline at end of file +/* end IGCObject_Any.cpp */ From 9df80109df82d0302a6ad8a070824661530e9328 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 16 Feb 2026 22:33:32 -0500 Subject: [PATCH 040/174] xo-facet: move output-hpp-dir + subdir to idl/*.json5 --- idl/GCObject.json5 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/idl/GCObject.json5 b/idl/GCObject.json5 index 242bea1e..7056f0b8 100644 --- a/idl/GCObject.json5 +++ b/idl/GCObject.json5 @@ -1,6 +1,8 @@ { mode: "facet", output_cpp_dir: "src/gc", + output_hpp_dir: "include/xo/gc", + output_impl_subdir: "detail", includes: [ "", "", From bbc0db9b560dd48670dda8c757b350eb93dab668 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 16 Feb 2026 22:43:24 -0500 Subject: [PATCH 041/174] xo-gc xo-cmake: simplify idl -> *.*pp codegen --- CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 59f13d7e..df57b1e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,8 +23,6 @@ xo_add_genfacet( TARGET xo-gc-facet-gcobject FACET GCObject INPUT idl/GCObject.json5 - OUTPUT_HPP_DIR include/xo/gc - OUTPUT_IMPL_SUBDIR detail ) # ---------------------------------------------------------------- From a27b49d93dd20925dd176fc0222ff18db4919c2f Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 17 Feb 2026 14:42:17 -0500 Subject: [PATCH 042/174] xo-interpreter2 stack: define-expr's work at top-level --- include/xo/gc/PolyForwarderUtil.hpp | 66 +++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 include/xo/gc/PolyForwarderUtil.hpp diff --git a/include/xo/gc/PolyForwarderUtil.hpp b/include/xo/gc/PolyForwarderUtil.hpp new file mode 100644 index 00000000..19f99c9b --- /dev/null +++ b/include/xo/gc/PolyForwarderUtil.hpp @@ -0,0 +1,66 @@ +/** @file PolyForwarderUtil.hpp + * + * @author Roland Conybeare, Feb 2026 + **/ + +#pragma once + +#include "Collector.hpp" +#include + +namespace xo { + namespace mm { + + /** Utility class for forwarding support on + * faceted object pointers that have some primary + * facet _other_ than AGCObject. + * + * For fop with AGCObject facet, with collector gc: + * + * obj gc = ..; + * obj ptr = ..; + * + * gc.forward_inplace(&ptr); + * + * for fop with some other facet: + * + * obj ptr = ..; + * PolyForwarderUtil::forward_inplace(gc, &ptr); + * + * or + * poly_forward_inplace(gc, &ptr); + **/ + class PolyForwarderUtil { + public: + template + static void forward_inplace(obj gc, obj * p_ptr) { + using xo::facet::FacetRegistry; + + /** + * p_ptr + * v FacetRegistry + * +--------+---------+ .variant() +-----------+---------+ + * | AFacet | DRepr x | -----------------> | AGCobject | DRepr x | + * +--------+-------|-+ +-----------+-------|-+ + * | | + * | /-------------------------------------------/ + * | | + * v v + * +-------+ + * | DRepr | + * +-------+ + **/ + + auto gco = FacetRegistry::instance().variant(*p_ptr); + gc.forward_inplace(gco.iface(), (void **)&(p_ptr->data_)); + } + }; + + template + void poly_forward_inplace(obj gc, obj * p_ptr) { + PolyForwarderUtil::forward_inplace(gc, p_ptr); + } + } /*namespace mm*/ +} /*namespace xo*/ + +/* end PolyForwarderUtil.hpp */ From 8ee82675b7c095e34c6ee2b31bcd952399b81395 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 26 Feb 2026 14:27:02 +1100 Subject: [PATCH 043/174] xo-gc: docs build [WIP] + misc fixes + works w/ nix --- CMakeLists.txt | 2 +- cmake/xo_gcConfig.cmake.in | 1 + docs/_static/README | 1 + docs/_static/img/favicon.ico | Bin 0 -> 309936 bytes docs/conf.py | 39 +++++++++++++++++++++++++++++++++++ src/gc/CMakeLists.txt | 1 + 6 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 docs/_static/README create mode 100644 docs/_static/img/favicon.ico create mode 100644 docs/conf.py diff --git a/CMakeLists.txt b/CMakeLists.txt index df57b1e3..4a05fc4a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,6 @@ xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets # docs targets depend on other library/utest/exec targets above, # --> must come after them. # -#add_subdirectory(docs) +add_subdirectory(docs) # end CMakeLists.txt diff --git a/cmake/xo_gcConfig.cmake.in b/cmake/xo_gcConfig.cmake.in index aebd133c..a7eea6c0 100644 --- a/cmake/xo_gcConfig.cmake.in +++ b/cmake/xo_gcConfig.cmake.in @@ -3,6 +3,7 @@ include(CMakeFindDependencyMacro) find_dependency(xo_alloc2) find_dependency(xo_facet) +find_dependency(subsys) find_dependency(indentlog) include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") diff --git a/docs/_static/README b/docs/_static/README new file mode 100644 index 00000000..7297d046 --- /dev/null +++ b/docs/_static/README @@ -0,0 +1 @@ +add any static {.html, .js, ..} files for sphinx to pickup here diff --git a/docs/_static/img/favicon.ico b/docs/_static/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4163dd69c734f8186cf1ce5e726213cebd231c31 GIT binary patch literal 309936 zcmZQzU}WH800Bk@1%|zv3=GQ{7#I#50EsIwXaq4aBx^A+G&Df@9E=RzHB1Z%2@w8@ zDGUsoTbLOf93XrRCkBRSNfrhJ0|#lWE5$ikqY0O79?U|^U$ zn}tC_0>bZ5U|_Ib!@?jS0O4n_Ffbh6%EHhY;OEXI1#%~^r-w@rND_oO*cccXVv1Iz zF)(Phc)B=-RNQ)dx4b6i>dX%x@9!*6Gnu4wa+A(!HtFQYZ7*je9SRb%TBzP4cZ$tP zVYbt&Pn&i>;q_KHJ!zBSLY3J9UJ4VK77ABwWnMNbtss9=YN_w;@ALjX?m2M7c-yHn zb2QI?|2$Jb#qjf;nbq?w|27CXaVWNsOmI2hTLyV&ero8F#WKIIuVn&NhzE8B9^U#LMNYX4rLd$CPc&nsSS&rGvkI^&_$nF1L# z_d{D)7<)DQJFe|mC~$Iu_x|S_4_;rx#UxfO_dfqc+Q(0(Ypg%a_slVu3-bv2A>zrf zXvLrJsUOz94$zigSokL68AJOnzTeh-m!~H22n0p%%r5;^zh#*{N5pIPJqsUpDm+*z z`nsb2nVibGJsf%U7rtG+rLLghAksRuzu}sNm*LiZ(rVxN`0MS2Ch9vir>{Bw79 zl(7G@ik0@~dUIuM7MR)^Z(o1LMF9jHA_`AEEcku(!@Zza+ojJ-Uex+`v~=dw@6th^ zKgP+jG@c{8zvI-Y3y=8?(UF<-aX6vKr6HmCfFzyMOK4XUi8l ziPZhL5^k@0v z8PmGW{&{5fPXF{H`!$Z~G4Fjg`)c62LfLJaZHM~V zmKMmg-&~wEBRcwQ^uxUoH!raF?@6%U@kgs|dwfX4xpc*ui?(f?C%=Plx~A)uGM}3j zcJf(sY1wz)C(IVYD+*;{?Pa%ZYo z_Pq#>_j+4pBkR_McI>J6<7V-M=|%O#;MJ>tOnrPI!Ft)kF7=A4TFNz{JBqxYF?Q^k zVZ%6W&7y;aKgzEsN(B_P$~$Osv|Ci)k?_l%#jOA5T$#&GRRslyCwKPRH%@b2uydmE zAw|1<>vgZB0!ELhsdKP?}rQHfY z{ytq8Zl7j7OD1mmTLzcP$e$c0G7VCpXD!#A*17%hNmr0Sk!9{Jm2VQU>{a{Tb`&`a zHJn&kn7rV3<+9y+%cmwxuJ4|bR8uPY{LkauJF089Ek4QX$)J>8_g|UgJR7SzV{q>A z$=NS6Pai)&vt-lxHOQL8WZpT^hY_1DTwl-0qB_{ov@f6{ks7h0WK z=oOf3J+G!nwk$6v;o6Oh+mt0OJsFhL)7~@4-*I<7@znTf)L;1pBE9R9^1hnEWTTEb z1T#;TZ8w^&wb^p%fzq4bf6aevSGoCZhTF0xGkc+*A@TgRwLaw{z1xyCS)Oiqypi27 zD*IOMaU-Eg3@&*t3X@XQM2q}eV~_dIb0|Kc`<6w)m%%WGYx8^;4vv+w*>@_MPCmW& z@4iR#d*gd$0?MI+H=}rFa$jNi>L6^O_cw#>7{lVmKnIaxe#@_>D^oT5Wpmi$G@^p9 z-aWqK*`j;5GhVAOc4+x5oX2s7VcBx|$%d`_1cPrV=PW!XQMBiJ!+~!{MY@7^EPgE! z`^r?#lVPfB;=ZlwOxBFMnX41H{#BeY|F>aQtJg%G=~L^QcPDVPuF(AI`&ET;ioJMn z>3!B8>Iw2b59RrHYHxVBL7KaLkB~lJ^Ynj@%0J|~8yQVvI5oNZ=d}C$7A!NmBj-%s zE?K1+U+VlZ|Hn*iDGn2b@Lvor6AKJ-ztuDKvGB1hn=t=;MDnfQb-(8486DmAD&wgN zgUK#?xBA%5{>7e58nvb~w*4tQaPfeT#K(QV?JjVaFuZQOdLSwx_Sb#uM!5y{f45sb zVOuSGRCL?@9;Q!!8ou5t6H!)CP*7N4qPc+8LdJsi2Txi?o<)sMV;oxzgAUU@=4nY> zwdNPtml)jtGyCNZX*c;7i^DfYeg3%XXVLX=+0yxfBb% zAHfn&)NXTKSYh!(^}y)^!3VTwHM=ZGxZFrRl$^Uq`QJq!_yHjIB*XS{#8KvaL`cgd=5yQXz<*WT0E|}5Tm2s4_gZ^{M_a&De1{@bh)5~Q62Vl_%2PFc zvZKC|==*1xYkxXfmb3h_(>5#l>{UMht%sYRki$m~W_HH=jCQ9Vq`lml$n_`pK=%wk zo9v+41*ZN%w_Rl$EGe13l@BF95X}9}Qo}9AqpFTzN!1YG`!wbx=M6llJ*8H)a z+4HtI!-seA3eyxeedhaP_M~Xm+lo()*^fT5ED!B}q`!)1o845YhS^V{vc<#ADqElFt+jO2Je3|`P`hd4^Uq#3M~zSV-DZ5+rovEN z#;~vb<=y+Y429nwxO>3&z-*DogYpHBwkKR&{NT5Z>AKG6s}IR!SudLtD3I~zpTz$B zI}>@A&&jl2a7W`~CXcFS&&+Mr`~`N=dVe;4xj(ldQ)!ReM6rL2=l5|jkz2bfPViR*O#hYg(`0SuQky!%+VZIT@mbch?o9MMaOkFj!VmukpEn%MRf%ep zZj4X7^+$E(f7chi8}pNGzqiF~lg)6RuC!M1W#RKlQ|H8sF3f*rxJ~(#>`KNb^O5S>#uF3_L1%@fIe3xe^C@9#3Gx!IyvHlMh-!8DL%29LK zwXovZuQ$wpJA+N!BJG6!f&W+U>ioS{+Vj;my?3>R{;E6T)lHLL^ht_%GW;|Y+`+cF zNI!b2ecMZ(JzNo))(>hT{~4I{G)*~v@9Wk-dNN7ww+(YIIy}7oV)nlkzxGw{<@Gnw%$L*4r$(;;uI#bN$^kh1TysuKYZ9ptphPVc7?Lg>dP@DIMX^Fo|=Pf$0TCdpGjj z?re?u`+@t*mm@!#+v<|IbC%$us|xzI^Y!76=Hn?-Eu@+$Udjb8W)b#$Gj5)>#n(1^MCn;pVbO7S-S3tFAXXZ_g9M_J@7u zd7F&GXRg#-coc1^G>O42&Hq9D$$gX7OaD;(VDE6KY{SvCpUwuE@e!YxCLiy8*z`&x zUGwM?r#n*`kA1d@^IP(x@;2K)v5a5KeVMi%Ppgk&j^T~*diLoPTiTZ8D%oX|7~1mH z&#=tT$^FOYBl=0qYtP&n`K#qx`||Fr6ux<})Z%!d#l_Maa;=k3ONY$<-}#e~yXyDO zt!tZqFOzq?oxi@d_c2F1=LHH&P4_w; z_~o!!<-(qMKA)1}=Ul%RcV-py^9P}Cjo)nGJFN2cY@5O1Q;|=F=PY_qxxeJpew7AA zDb|?wmyG9@2!B5NUwX-h=RUuKKU|t^VZ43$I;}G|p1DN{dCpp;WYQM1sIPgk>B@C+ zRfiR={xn?c%Zr-6Hu13If1jq-jIGZ*4l@MD?c9qg0 zb01!8eyK4jcG{`i8fW5qX1-(O5B@Ej`r`DAxW7K~HB3M1d;SFY1xlqQeoHx1qOYU; zyG9^z5#N!T4KwWu2JwgEa$we`jMH&V%|XQJ0cHMjp{E*?Pxxj`&9Ro zx5(w|lV8qz!MZQO+QG1>bjH0uIj#Ra+sfkRYv@}uzMuT_UWHi3HrWLdcC9nnX05&a zJ~pB6twL+*^rUN5DX#?n%zFFa$cI|K&3X305A)T3uQ3RWoYk;@@=v=;*_DdZmgE{n z7pDIT2tL3+Vd<4e!p9Ta8qN2HGdAT|Z zv8mnW3*fouIU~5txOT?+#Ahws8&@(L`X2V#+K_*f&yVL^$^-cny+5)CK5ku1{SFnb_yo3U$BAX)$;h#Ean zXIsy=S;yk?-V)^^ebKpd8~acEY23?@lWgq}Saj~-mfz-k=TBp}oXf6mCwOemqsh6o z|2?-ci>PKQ&7987KBHLCWuk@1=GO3KEFZGZbm=R$J&rQEAI`7feZ2dS#smG7e-oxn zs$HG0$oAObj9B69d(mqT$Q`bany9Uwe6H+ti0|$@Wd)0#T%A~(^3v$`!O&fXGq(Pj zX*Y*;#!&~So-5x^)b!i7m@u!gntkT=pUEeBbSGL@Grqq0$1a-rO_KEvud6=amhS6U zzFpmU=j1gWj)u>hNmnB2s*2Gp|O z93H(a|Ns6(qiq5^zgV6zdEoP#M%goxXjGxaRVY%@7%=UjT zQ{<{^`7Zy~ciE?NB**yrhLh*Z51sMM+$tJzQLUk5;rdrx8|OaXTl-q3_v~W2H;37O%S^=sJ7>HSZ&?1ojP-8Ol=UYTs~+Go z^Jfq?{JwcBOF+n_y~=%UZ)+^BH&@q2$T@h;R({<6`Ge`2YX$dAMu8 ze4!%!Wo-eGK@L2N80QEc$d+ni68+OJ&zQk)=W)QbJNDJIkV`W`ua^^Cp) zrxqBt?iW@2`C;{eFL%VZJV^Io=@j{Muvt>7SZ0HOr5R&I`&r=wT3SyxNcO&5^gS`3 zf1X}KY5j)IgoW(wHhfvlZ_T}@%+5Mf@EMdd6&Ca>%@CYX-dL%@>(2HeZlmbG-bn|# z82qk2IB{XY2}RaBPS2(`rr+XPrLZ<`?!GGC;~6^w5AvkG?%M8j(1rE?;)iSa55C+c zJ3&KSDq`Bh=Uqj1PBsr7o0LZ|cz1uDZSU;6;HApRDgj(*t3 zj|TVWG3soJUC};~Wg7d3NcqVpS|k=KH(JbAZ?F$Mvbm`J`SN8KR1%yR`fuxpl{yEt{dD*t<{Jcj*CaT25YhC@9!!KXApUZ9S*1yZTg3M(jPCipm zc))wcYU(G03-7)^Ji@RmNBn`z6(*s=9SV1+D;_E6Wf$96$rRDFwC}*49;PpC3r!EP zJg{D6)92FX@?iF^Ahn#1noi4^-{-D)Hd%BP>(tLI9~1(XFH}4^^OVkB-O5uoX^i3u zpP7$*Sjx0n^v1GP8q--e*k5>SsLJwOE4b{`$(1K2CMNx`F;Z{fSSA)wVCl=im==B4 z%Hm|BY;H_|on~>;g_oz!WQZiKusM_S!?#tVokhU7f8&9ZPrqH=Kk>0?j=q8ek8^{j z+Uz2Yc9sBUmqhynb7}El4z6X1->SruEFMYLJxln-$fcLAps+xEM*gio?u>!b;=u}i zO^ZcbmWU~|DNcQu&gDJ3>4^@bOQYq5H3|QX*8O8%keDyOZSqA1L!P*4^P?@*bkY?R z6b_j7&*WNmW5)!h*$)n^dphrhOI*q-3oV_w7bjj8zq-L%v|!&nqpMpjU#gyS>bY{D z=RnhGPo^6hpXTPow5V=2ZBb?HP&^fO^?NSMHB8#H{%yyc(r#nZPh z^xT%M$YQX(*{U+vbK{b>ZaV$cAmPk4*N90MW|o%=D$SNM;+H*cpw1#1b8ef` zwF}G7^evydNPXRlvdiN7M-;A1+Nq#$p?9hN`8lc#sYW-XK>oh8@NKVQ2|HKDX4zg1 z!O|2zhS-!iwK?UMo(!G}OrE^*4nN;APxDlHaCl|xm$1{591JUxPwWZVC+z*#)AA*= zgv6izgR?{YT7UFT6ZyBx>(w_A<*QE5ST;<1bGqfTR{GoqI{`y>nJIq*t8SIt+Ouk6 z?V+O+denCM?cZiiw4b{|=W*jQ9{c>GJj_uOU)wPVJ^R?vCY~D`vM-}z@#Tm2 z;&e|l9G#GQ+cC>hZ26SuHR_l27D$;feBQED)bHnlgZ>9}g>AyNgq`wXpXhBpq4Lq) z_J~=`43!=Xj}7NGa>+5g)1SU+(ZiQ`P+k8YC!c|JM1G$-zd zX@T(5eHCG!KW=+?nYrRt*~5Fc7Bhdj?$GXFy}|9Ef`UTof5pz4paW-9{1~R+;#;TC z>9w2msyDYYuf1BJXL-lXH>BmVp>VC)-sN%%pPwl_)&I-qVmfDe;>NR0@-vQW z@!r29`|Yq#;58G`5a6`wlHTuy=h z+p>5%SLU+lnlHW{J7eb3#RmCt&Leah3P^R^DXwLju#f2<}R4d=J+Y}e*gX_ zzwaIT_wQ=<|6j|Q{|cN;PG0#$zP9A5n*Lv_0*ht&2X;;JWN=*|wL@Yuht%djz3hJk zgOC2cP<9}x`2Nph?N?f+?~qPn_hIh)#{4}Z!F=f)w}AOt{4Zxc4O=Sg?3BG%WRE)Y z{_pdaJjgcq%Y1WBlcz|3aHRV( z@wMIiK5?+TRaA0+^KRF~V$-e1877Z#A zgy5s+Yag-)Xvt(|-LjPF?NgljMs4B?m)8tF@fWW~++isRp2X0WZn;c2ohfbga-Z66 z(*uqN{AOI9bnPbJu1hgRoyR`^TD8~ZkIt19@lIC_y)3tSt+c6mFCEqVc*m9N{%Mz% z9G+qI)n(S~=K@@$iUWVm38> z{Kxv;4AV{GCmB5%l*QTnj+}WrZ(r+v?}Ro`>R5lwgu82z@QOPzMV)F-oRntXxLxXG z&z!!0Pwid)nQqy86*(q_v)4V>|EnPM`|JAIZvVC@EBH1{ZK|^NkTCQsS z_nW`-3w;*<5lzhx5gK#{4=kKf`AFOF`61TkO2&zIf5u1p<{ox& zTmH!1`H5RWnZ~+9{}mLLhH%WOHF?P9t~&32_Ihn0Zm~bhr$(qSJ_+O5{OfgT0^3b@ z1qFpWU*7j0RB1UX5_rB~rRCEp*ClS-^zOU&Bi(f3>?uWR;+Z%9&$xCf(Ch8WQjJUU zudDLb@74PrSNnBV({V*m=qP+QS>|Bh^fGxSZ%FFT+ffPiKD`Hymx!I3@K8Xu@<(aK z%6YL1eyJ6ti&Z~tZ{%}&nlZ<6VZf5rYMWC2+5P?#u;1ptI%}`9ki)+cX8)vL?8_3{ zc#QAP;*w~eX>K4MEF96dMpI%+e#|bm^RMNlUl{&x=8|XlIpfx3`pv-17UsgAc~V3pv#NoMFkppUCw^XVZ7y9W$Jj`i_24+>-c6>C zLbZB%Q7iw~HNJ08>Msr9U2(H+)#HzC$7-#c)@esxj+=3P8cTr0-2=e~YR-J@oP6@% zJ$Hk%ZI2DLPu?<+?U}@3R`j!cy0HJ&2RnG%9ezlyopUQl=a0dLXAZsLU8_2xe!Td0 zx$eP!lmE|ID|0;=T7?4A7lt>xUVnEeH`iMJ zuc~*q_e!xkgItGRC(ZI@;wJmVEOg(??>s--#`yo-J^Qz*G#u(?{?8zHOypVSjB^Ka zvKsChvQK%_G}Cj3*yjI^uD|{*wO2g$FhkZZc;$(uUMu&=6h(V@HMVKF?@Sgwct5E| zb6>(i{?Gq*O=5U+li`o7apMZH8}oGEo5l1-Hk2@J$p4l8+Ti!HkSVs`c8a&}D?GLG zuY!Vt#qFt|w*NmiC1}AFmy!=<3!L(0ZQMT|vY&VAb@sHHXF7#)Y0{rQH(WN@+O(FJ zxsR!j>7RIozz?|%%A!Sajd6UQ>-p?gSbAlr`|T9@*T3F>wbqo+sp0F_c3V$>^7rc! zwg4HnWo-BRBh@doLhyNm|7QX|YZIP-`Mf3gSh0eFg4G_DMDx(aoiPf2o;^SL zTZ0yu>K@oUYnLjw*q{G<_RYJ((B*AjV4ik);%5WjmbId3tvBo6$(+e^;ZHJCzS;f8 zxli>pze&{cvZFGOf|iBMy>{{Ov$Nc*z4dCW_peX+9duO8G$Bo4UiZ!D1daBITaw#k zE@kz9R%D-FpKfjqS zv)%6J{{6ShpZL#cyu#oW+z@F{B3xi?z;2xUf#b}Yg9pX>q|R)4eDlC`L*KsshSnLq z{`dX&Tivd`{J%A)5D zuN#;Re)Gt5-0V--;JlYhqW7ip4dEZ@XVx4xJy1O(Zz z%Z8q+2UJ?a&g8kBZs0fA+H!S<<|$c+^+k7{Jud3iYn(JNi5NZ-bf7^JMW0&7MjMXAEC!o?%hrzkFFV;hi_@j)fDX%ck(D20Nyl znK5@(zxBI~v$L|*-(>yv)n!?`m32R~*f$ankzW@vT0~UwFPhSB=nQ1~We#Bgh$;#HeV>aS}4tl7I)TX%11>r%tRjfvNblRZ5dl)jrRWB&N|%VR&E z+4a_E9xaMB>`YX97F{diYEY;nr{CpV`Z;gGA^D7^nzBj{CIM!XWwmw_1l}Fg@t5oN zP%ifGkiPk|85O8rOuak<9YW? z`4`C&ES+(zN5|)wO2^5U_4#ba7)ocns%w!^>$ta%@9A$(r-fZRN+&PaZFiUF)Y`5J zaaTW8L)%}{2E|Nl7Bk+S&o)8%KaI$RbRp3$*BVY2|UIkKLV%iFE*=c`LX1|?wq*b z4~+L2^_xEZ+A{rC`F;7=)l;fxzU|g!>AmlGVWZvr;~rj20|3{Q)kOP5u}KUrCj+XO`Sx}zMx%22=Z{qc7@ejKvP z+xz&;p8c~_7y^rwt`@J9t?hfi>z{Ab>(r;494B0p^7#Ah+3e$cCcoIZUtztbf`Wp> zlsWPLf-c_tDQ5rQ^=)O{c+n;JZb3y z`{U=j-Ll2DY}0#m&bnNyf@czg%T0^-d%~FP?*+u({o#Ck?Ou+quYVkT1z6T+zm0uj z@lc%c+1bsL*DnDz&rTi7uQTr3^~*Ch(Y$Quq1{p0ceAaR`DU)Y7-j$X_s8OGH3 z_jujzZyTg0e%Eu3agA*G_HfD%)twiQSIA#?pCD|3AAPwRi#l)4T6~ zcg|;-tG-8U)n0`e-(Lsk>^GI4%%X7W-p=Ri7A@cIn`>Qn>dO-GUtK{HgQpg$KU=J~ zz;uC0#;=3*ZtN$evlhqp|Gd>3`+u|8?fj!@DvTXnh370+cc|~WyX+#baes6k9TNt9Nlv5_116U8cE%<>R*f#CoH149ENXsxFLNQ!~Mfr)`3 zg#m&gBBSJJ2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kin zXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeD zjE2By2#kinXb6mkz-S1JhQMeDjE2C73;{+41}1(6A#QmFE*5qM4rb;N849DG9^N7F zKRcV@za0a^^NcL6Pg&V&U$e5ker09l{LRYh_>-M8{aaSn+#lIFv;Jjg_x;JrD*Kj| z75X(h$M8i?zTowua>lDg6~jA#MtwIdL*Qdd3d7ypLbi{YSw_FJva0@OXYc--o&Drr zcJ`0|+1dYb;eXlLfBt1>zx|t?efn2+&ZKYIIi6p#vIPJC|IctFICxlw)u^Y2PY8U> z&SiL+SIF@xGt=W&cJ_+D+1amgrFCM&|7B-?`@Fgc#;&*n=?EeJX_PEkCIpY7avycDE%(VR9(ZO&pHg-sc->9R9 zZU{U}Ok{YMk;(8TEnWV1cJ`+K+1dZdNyn7v`InV-`&VYB`~UiShL5SKLpLx-T|T5j zz*t0t;cI3V!{@9l>EBsdTPaDyl?p@NTj2o=t!TyXommUSq$H^vfTbMYuaEHLh zyaI*~c?BY%IoN?tqh9mz6cc6~CZb*bcn}r1fs7>)RJ16{K zcJ@zv>2EN|{m#yweXppT=~I5ukO<#VM-J`~c#)dMa4#u^E(2)3Vlap8sM`j22wabfV)$QJ$nY~WvjLX2h64RJJNxj< ztZbeaSvi9{fJa?NuMl8jU|?WpU|`^5U|*VU|`T?U|=v|U|_IhU|_Ie zU|_I?VoL@FMq@?>HeCUB1qHRI2?-m9Lc)i+@o#qagAZBR3Li!%gz1&Q27^7U3=9lH z3=9ma(8L_hz`y_+1?gd6U|7Pyz_6Wxf#Em<1H%;t28MeK3=Gd07#Q9#FfhDlU|{&f zz`*dCfq~%*lm_wNGcqu}W?*1^T4`+j4VJcs0{t&L`~9!%Y?I&FqZ46+Az{+M4II!y z98_R~W_x-W7#KD%Ffg2BU|@I(O~Zc}7#KjK;1uIZQ`3L{b8@IQSxbjJ^!;CUwhL(B zmIh%zY9xI_fDzho(P3a0l5K0|SFGw9USS zfq~%>0|Ub^TxBvI@xRb^{Bs5dhAWJW%m>+6*;g?#F--y8ch11T0Gb(!XJB9mVqjqK zU|?WyU|?XdVqjn}XJBA3Wnf@11M!)eSwP|*^1QqW_v7O354HBs->fX>(ezJ;R7;r^ zT+lRE&A`C0pMinlEdv7ssB9*-&HkN%f#C_XU%MEZzGD~|80;7r7}P<7Jxq+uT&iMH z;1kwnmXwg>+PlfA4EIw~x&CBj4a!BIxcU+Qva{cP%gi+TIeJ(MN%2i5#h|nZ8hiu| zPJr6+pfpcRdIy#3?-&>u&N47CfYN#~0|SFAbi4;Nun8LEq^cJ#M@BNN@$g{!ote1= zS6Um4;{UR=QRTZ!(Ijk22h*Go`HcujDdlHF6kXxuzg7&;gj81573 z%Yy2HV+;%oRSXOaMhpxL3ePS-@uKm5(kzWF&bOYU3d z=s}^>NdZKe0&3@g%KLU`+a6aN2UONShPLNHeE|WYOdnuHf3mX~erM-^7J+^r?0MjK zcJ`FSumr}7siSRwIwc!W`ySL*zk{c|2aV&OXJBBcWME*>U|?VXjRDcgd6Zl8F(ZTF zLq>++-|Xx|gE{?!4p0A)lWX&5blivXNGEIpXgmkh!2+dgTw^()`PG9A3=AN3;)IPH zoGLC=)HD3gNN4z&l@&LbHv;_2&R*~@JBJO_XCIs)IV@ZSD!W1BGFurK7(nS8TU!St zzMFx80kj5lNVIVX#mu{`9ER7K+5CU9v$qV!IskOR#@DPYQ&3xNZC*Jc?~KZm7Xq>j z3=CZi3=E(#N^I#K)Xo8w_Yu%FYvkE72z9yay~F@oPw*o<+xA~}_VYoO2mXA|%Bp1W zPi8!yKHBf4LGoc^U|;~|#EbAf?@0W?PinxCS969(SMS6Mj>GqTGV ze`ea*5B*JFapcw`J}|6F8XV2A|GSt%$C%4sX8H)uv3Y3gP8osq%tH78f% zZ&ns)c^uu(gZ`JDeep+5uJ!+%Y=&34pqX=;d3U543>uUj?->~kKQl4}e`ja+{L9Y%Li6Guv<)*S=kK4atX}2931}{85u!G zSV#`HFdRVt2tv1&f5Xz=dBMQIP{6>z0NS27fKD75R^QIZW;m6c&iWxM+wxa-&h&rT z*$@6_Xa6O;PRPl5+iz=om5GrNv|Sli9{pfsVDK9nAwn%zg1yej!0?}$f#E+Z1H*q7 z28RDkh!Z}@wg)t9xeUh=WzcaVpeHL1_}sbNTyrA{SyLuPR?x97;6wl_qZk<&`iF10 zf=*<5#>l|%ho6Drzaay||0o8A|Md(E|7SBW{9nz$@P9J{!~cy84F8ugF#Maq!1%L} zf$^s&1H&&R28KUu3=FVhoL~rnw#e=Rg(n&Yl{M?3XQUA{WK;p}5cpqO%J3mQgW*YD zA?wTBe5r3)*;c=^vZB6c=M?|V&Tjsno!#{>JG<>mW@hD&%&g=u>FKVY)6!IL$Hj34 z7+W&%vU4!7FtczlFff9ao50gQ0|UcJ1_lODy^Y)QAteLy8ff*vUoHlQzt#*4f7=-t z{vT&x`2U81;r|~7hX4Nw;2#VO{~s_g{NKR9@IRe_;fE3f!*6K)fHfdMOYTpgrF)Pg zKwHZDphu};)i^3RARz!+(*ueh7zP!R76alEs#*&3&VMcjhW~yH4F5MVF#P{OAgvS1 z3x63H{@rC@_}k6E@JE}0;TI$1BoVm7LFZk9hVbE*L-PS>dwC%P0|V%IHMrtY#-IoR z(D|gGv1jBs%N-OhBij*RuYlV0X3%!cR|?ZTwtVq`f$?`01Jfrl2BtKSOF>72T!W@} zumhm%rwj~?p!svqv@b~AC>~rP06Mo9w8RQI&Op=eLo~j?-r#0n_@B?f@c#)7(mirs z0JWd?Ffb%$FfcG+N&8H985kJ6m>5ArprD;xgDc`kT>}mQ&=|x5XjuhLJPZsBpm`e5 z@c>{2LxTW09n{4BA{K;$d~c1m z3>UN%D`n_7AC&a|Npugd&?5#$VM_)^&?&P+Cj>{hOF?tK z*U{QQpyPpuK(K(a>K`5khJSPETjpcSSC9WQFkF7i!1yl!Jbxwz+QB;nf^gJ{pmkRp z(b7NYs4&nms)ILln4d8)@T_HIVE9+U!0`Xqz^8v$p7?v6f$@hf10$$EGI)b;)MY3k z(1w=&L1P!7ZK)`V1`p>y0|O&yUhB6F1H->(gD367^1%O93=H3S7#KiH+y+mmjk*M9 z2!M{a25mP5HKsurG$#N$z8$BAft3KYd;ao5*DDNAn;)LmvDyEFf#F9yxE%!E)jqJH zHEKJ-5CDz$fUfBPrF{?vB`{E)AgGc43P884ft>g!oPpv0_d!_pL-WJmlMKvn#Tl4D zw+YcdltyhNDFi^baxW;) z(k*~SEh0SxK7OeD!~f5NJN<+6z#ayMAH0wgt!Nr3 zqlQx!0-$sbTCWZ(`$075>YPC~1_W{zDE3Z!IMKgN77n9*3hAkRJl*>mWc^{ejLb zAm2o)>-@{W!1xnXo_-oS>Hq&$28KWEkXuHn9w4IzQZEDo(az=o4P1cMMo`ZNicJDl zO@A_mP}&EL1>I&~{4K)(-fBv50E`+yyAS}Sbx@iIB{C2Ot!oURopq#|3~~|}_YJA^ z&-nKP1M3HK1{ToCZlwBoR6QL-0CeaH`Z*oRbhMvGF#Ov;^wR(Tw9)iW$FxG8RghDM_drX2ST0z|z`!s_wy%OZ z{#prwD%&KOX>AkRdq>H?>Kkn>uGSo#N@wxvIs{;8T6Xki#=70_}} z0z<-}6XHNS#Asn38D@g>0H`hTcL?QyOAHME#YWRV8PQE&EueNjXw@e;kwNpo&cStZ z8Q5u{vh>vuO8?6l82&OdFo0I>(KkXzZ6hfJhCuoU_knpC82%j`Lg_yfQuc!m8Y3w_ zMitXH1VHT{(CKgBLKa&7Z)ad&03BgQUzgyt4Rmlm$a(*2hDiE<%E0hn8wRghAs$gQ)!v3Mf!J;t%Kq^A|%P4=jb=EC6!&C?4D)z{0@502=QB zrF$3#-Pi`o7lX*jyeTf2!KxLM!&gX$>5p) zf;qXAfq@Zp0?Ieg=_juTaUNL5zyLmh9p>;+dQgV|BQFC3<59Hs&x}FoWCeu>3=F*I znHd=WP8g&a;57roKYPfa&uHO4C=(Hiqd;RnjKT~IjObT544x%$DDDNNDNsY`k0JxZ zzjK2u5By_b_}#+5aD|D1;SGwbM>&H!1h6fE23-*|I33M$A6i%a_JLk$1Y2@E(DZf& zhX0^#%7Zi9MqPy~1hg3#7+ygW6{vs(;X$+o2Ai`%M+|-jUCC9z!0`XuAgBi}GBEtp z9c}kui>pB|2D-B03sU+Aow4aV=v~bK%BKH8TVom-82*EIv4=5N1 zf5?rx5G4e{85kIlj{yZ8iD-_ZV$gCxdEk!%1H*sNaU)*_pdJ7%^06H)_XlkPA;w{) zNM%1LK|Nw%U{EGT#;#;8`hqO(V%t{=*svT3=IF*GBDh0WMGJCWnkdi#=yYvj)8&k zDFXwGJ_ExLtRAt1(5MhY0s~_30+e7t=e2^4gd1i-1M=E`&?aIY28REt3=IDq85sUY zF);kkVqo}R#K7?$V!0;b*jh!h2!#_y|hX0_2;cg5J4Cv)UJ0l}#&EPN#qLJwx z(3Owi!~tys-DF^3kRF*qfz{Wd3=9k>pVVq}3=E@n0G%R$ zM%E;uwf(;_Ffh2&$gZJoEa*s7(85zt*$=}lL)}v&*!`f*U7$nWU~vheLF1sKZGY^E z0<=cE|ZM~`qZFfgDW91JQ~mN76e@X*m6qgD|P0Z& zz`Jf}y8twH25S3*(mo7>))jgTZEp=5*MsU}&>RgcZa_4s><=0?J{(w&fYytn@1p>P zF=$i1=D<2_)OH32hByWWh97ACG*Es3<%`h(qD}~a_EDpcD}(aD76t|eaq3txY6=-4 zAkV!@$6xgG%$4H%Ap4P-d|D~I9quX=`a|2i1X|LL3sewHcB56d0^Tu28POMj0}g~i!mJk8o+S+*EEK6f6p+S`}c<7+`r!p=l}o5 ziqHM~#c=NLYlgFbPBWbPIhEn~SAT~6uSFORzF}ZE11iGtMh56Ymjd+h9%e?C2h2?D zCd`bWX)wI{M#&HhW)P@81}vpVmSV_fZ^QV z8w}_F|06ryBj*7y``=%NGk>lyocxi=aQHnx!=?WW4Cg^rD%_(cSkCeUDF9t&rvO(q z%Aj`$fcDiL#8MA{*3N;>S{?dr(gSZ87@FoYFdX}0&T!_>4!rFfa9YOE_90I07sHu9 zn;4FNHDvhzpMil5T!etu;~vKn#-Ori^h^hOr+7^JK=HE|OZZ--pyI?+N6Qvw!a}ocs~QC?>1Oz{rGthB>I*2c7Q#suM6{V^m}yLI5-- z2bvEC&542H2N{Fv2h)LY6h(G|+B`G&vN0V0R>W}r-v_#-eONtl;r}bfl-e^4Oe~=D z8<4`2@dX0|gYW1$-4w+xEi{A9kN~a21zl8t6kp(eG3Z>;Xa)ub(0U?T*iBtCw>@BB zSbBzq;na^uzxpx+nA3)_8=v-o#K{-#*xr%|| z>VF1?<6mtU&i#8lplKhJ{y`X&2M)Y`%@C4N%Fe*R%Ekzq2B2BojT%gy5CGjT;K{(i zfPS_vC=Y<}YX$~}Rt5$J(ArY!*hmdi_Pt_Y*!e`1;qt6pe zFr4~X!EpZH-$9lBL3!Z(zuycef8>D2MSIub@yMvuAPWJ|`Eex-3=Ge)jFE!!18Dsm zXs@>IfUey=@{xgI{~Jw)vw!XmzO)Y=FFo_;D#N~43JeE9M>`C%SRQo_z7PPlhin)a z7|fd339zE?23{LjF! z`x(CQ9+exMAt1!SzyP|F9CQykdisa)LF3z?d3{T|?JGF`71ZaIV>tKk`ruCg|Nk?b z`+JVz$S3j9e!$>N;6%6zG=O2iz`y{yqXBd$AuQda)1XQobf%LZP46sEse_F9ocmGXSVI6bP6|4+Z#VjyUbKD!=7Y;h;5N|z{|u*oP6wYsvlO%qX0S%@sJn;^0ni$`7zPFg(7XWX zylM1$0LBLuc%Zh?MrJ0KOlD?g(D_7^uYCl~cb)kyH6+?Tu=IcS?>UBp?}Ql+z9TY- zN0kkM5CHWJK<5qZ!!dsh%Lo4%85luhr=am9&>jTPo=j)xTmtF)4nTe1Q$K7N&j0^B zWXgZgKAV%@4H-^>ii;r-v!hNVB?Lg@rJy!HsO=A$CxfSZw004)IA{zNbXFZ`V7!ij zfguojf0-Dxy#(rWV0Xg>(3tbnFZZzoWa1r0J=y?1$y0Q$GqAZvAIq@B>}jGxQ^S)CV|2091B^?y3jv{{!v42c6B1e&!jz zynrqTTAvEi4?5TJ3o`@5ImWeD*D`?D7|^`j#=b8YmZnLePyK9Vc>JG%!7UtTz>P`_ zyAS})qk+~8fyRwM`^!M%LZEUU)E_`k=S1)%V zKR|;gzZn^rUNFwue{9I5|I@!(8SejQVCV!56O4rD9rYubApkm)3v?DfsNDowj|$q0 z3A!%@v_9LHnSsHLantRIL$3XQ;(HOp-Tw>>p`b23nISW(Z8QX~{AXY|`8|1vjsN^% zIQuu00W@|y8iS*Nk`Ork3$*slf#KZ0FGD5|ocsHp;mAi*hGU>1EJ{2-s&~YP0B8-t zfw!^@XaC+FGU@;9pNkCpUyCw;_N$Hf2pkOnib7z=69$HL*I5|O{9ZL=(*K#?a~Y~; zFfoA6-lfR%qnby22;{aiFx>plz;NPQ>fqe>h&Cq&I$!t1x5&};&m%tNQZE2Nd&&=g zRAK;~yEfqST4?DV-92aiTxQt&LW*G@Xxfi@K`?642o8b8Cm9$d6&V@M{O%p}ZJ;y1 z8^L?vmw}c&N^*VJ!0JIO_)DQhZbM6m%xewx>`*(-o_*XTC6QHGl)b;+TX(K!Y zc0Xre2+w6?IQe6+t_A(eaO!6nxa{9Gy7zsAr&ii{A9M!F!MD;3Xa5`@bnT$CfA%pP zd?(6q6trcUHr^jKb%cjN+cL;GC?|ioF`WDNYS83?vwt5m9RF$yz89l)(Fl*EQSZ|< z1hzk7V3@KEbjMLH!?}Oo20Ra(`}c|A*q3CmpGU|0!ND*}&^QD@^QznKvooCi+sJVK zKl%52f%>eJ;B$Y!F&zI|!La8!%jkJOG>-F8<55H4_!kC-!ygnB6ciZF{AnDZs^Bxj ziEkAQt1hxLY`Tr=yiwk02+$)0wm)QG*mRqn;p7icbLb`AnnP#*K4UofBZcAkSJu(t zPI^T2s2w;%0MsQ|f1Qcp_*YkkGrvJYL$o{|8gv!pL58EB?HFoiF)}PahtpZ35~CqN zw-9Jv$iVRSKLf+wmr@KTe>5?i`}c(Eb-~#`4;W7UtYH9Ms|`BZq+>bVyfb|0Jq&06+@bK;&_4|4{@r9a^|PH}|7$IV>AM*jR)KB}A<8wQ zibg|VU_xNwF$RW-o0%96y;o#7{VSQ_%O0c-)Dw1e{M3I`Mr$c z^sjh^V_)PM_P=HvJ<9nXE^aaiQ&wjdWN%q zCNmuWI+x+Zw>b>w{!L;y^|OxQ+_to%QQ_yb66V~96EQVlr1kj;S@^B*jZ6pw!xAc+8+ z9+1R8K#chh7KbPSC!z;XH-p0zNxT877c7pHVIZ#j{~sKvh;;i0?AibS|D&bz9}Hky z{{IJsDvbUA|Njqgub}3m15j)Kg8~Sy=Kuc&xDyVb59(WpgP`X8 zVBr4)^)1Lz4UACE0|v$)P$eMo|Nk2yR09J;0o*wtA&5B0CkjX*_5XhZND|_6xHCaQ z5OHuwp%g_2K$0K_Ga!Y?|Ns9X;-DCUx(uuiCJu^eG;y%0;1mN@385Y!#KA0x=>Pu^ zac~O8C60*E|4{WH??RF?%$WZVK&l|(DEjuM_o>WLQz2QxJB zL#+J|HisJGAYWoL2cI~^zo_m9sV7eyq!a3^|NlRLcv!-p2yswMpqc|I=}^VNJj`?g z4o^_VM5|gM;-K`2nw}BjsOc6W4oc@J*$xt4;Cz9S?V#eI2tWx=s5mG-5y79@TC z2YC$a5x4{-{ewJ!lKi3h0#r0&6~|~vK+X9Nvj}Fv0ho4(2Vug{d<`n~P~8d1@Bjb* z|AFc+NV$PW{10mQK*}XhS%)eP5A}bj;fzZhp$e`74Kf3SkvJca0}e_22Xa7wizSf8|Ns9Wy!ikB0fZy|A$u6f(f^P= zjOT$A%`jc0~|u& zG6co^KVWBIivI_-zOj^P{~H*fjR&wsuv@^y0|t9Ym5UM%poR%MR`Guf4E3PkWI$8@ zzkz`ft2j7pKn?|m2-r?A0SM^}73^6EfnOXYejysb?!+aIl71lSG1Cu39GngzOcWBAI4I7*2@*v$Bt((3 z7fc!ym&n-*CJssvn8E)aOPvDM(STMZLWTc7K<~RiB|zl{l!;1xM&*w31}FqH7#JA9 zsgjX_fssK0oS;E0h7<+{FoueZQllX-8UmwWGz3ONU^E0qLtr!nMnhmU1V%$(Gz3ON zU^E0qLtr!nMnhmU1V%$(Gz3ONU^E1%7Xr6pViXvzwGS0|FW~c{?E?-_dh%PKNSD_m!19nUv~DBzuDP4e`jYG{LIW!evqEQ z_$D)JK-@lRDfuDrKRcV@XLb(5m+V}g-`Uwof3vgC{L9Y%4UKD9oa3WG>EZ65?ChE^ znOPG5^Ya)UCnl2bpi!N43xNxTWeoqbvlxD4=cxV7&R+RHJNqXI@s6G*{{GF*KKeU5 z+wMVlD8sk(bh`Ot)FRSD;B86@!~en}hVPkK=6|!Z&r%TYDCyu|cJ|{xSy^E(l9Cv| zXJnA>uu&Z}4}l%ktqlLOG8uklWgGp?&b~~oxQChZFFX6?uk4)Q|0%Hyud}jg?w?T$ zNDF~)+1U(Vva+TBW@jIS#Tm8ff7#jheq?8x{m;#1m>fOY9w04-Xs7yhdIrP2)HLQl zS=qhRj&pSL{$yuw`;wg_@GU!sb{-lvnWPZ-o1Me(D?8ivUv~Bz^!TCy|M#z~tn~i{ z1q=`3;z)AmsAB4e!28@hhNpRjEPt}I=hGmrQQCnp^Z#aNpZJuOCGsh2bZmh7F-n?= zzp}C!er9E9{magN3X2~)&_90X_Cm3{#X|8sH}erIK;p^fKZ z&9~G{7k{#|r~Yq`Ww@U)+83Z&gpz2`tE?=B%%E_FKUrCGs2R7!nDs9^`}pUqY`)K< zeL)gq4X<(*1_lN`1_lNh1_lOQ1_lOO1_lOS1_p*`1_p*S1_p+F1_lPu#7r3j14A(b z17i+98%O%3h=|Mob8={&=aJI|XiVsPR<`nw(fG$3p*SQN85kJ285kIp85kIx85kHc z85kJ)7#JAVGcYimWME*p%fP_!nt_4g3j+hgF9rq%(5^Djra27E$jJDAzO&1}|GBwz zjsO4I+3){mXB+>^9xeZI#4)-sF9QRE9s>gdsM9osfq~&50|Ucj1_p+23=9ky@k*>1 z8xzz2Ej~W~=^g+7va>(`%+0g(iz^TrStyH$~ul*H`9atFFX7B z&#WvBQ2!IdVIxm~63 z<$qQdL)9Em5YW%D$wXGi?c&i+A7gZAHEA(iu>c)!NL zz);G-zyOj5jpqzl7<|pnX84?y&GR=qdp)h=AGEgUTUM6R2pIQcXJB9mgSOd0c^z&dmIOI4J0Up@G4FLou-{qFh{(GCZK{L8;3IrGC&n`(C7Y2c-c}S^%{H z2Bm|Eb<~TDEQW{anQY&(bL{?R=ga{0+yCL&5A-iP`_KRE?58i2lGZEo3EW|T%u9Y_ zWMFU`&<+lUhe2nKeq&}}_$SH0@ZXk!;eR{>!~aqShX1t;4FAg*82%y|Tr#@%rFuc#rXa16vE%zfQ$LCLWcIBV! zY*2bw@h>}jVHhJQ^@SO4GK!OFu z*$fQ-uQM?G|H8lkx-tQD;4B>fV_^9Im4V^^O$LVl3m6#w`Z6&5=7TJy;DALas67W- zX8|@8y5<5jrVDBV!qf~)dUs$L!wSzphM$?4Og}O*nC7^6FuqAnLiei=0|Nu-&|gsa zfDREJzzP851W-Qy@4&$De-kv$|AVfCAQFFLVEDg-f$@(!1LI@Rd|My`1H%)f^nDI8 zzBjl^IrIpjGaodr1*-!C;P~QDt!@%(W4UzGUD{Z`GV3@QFy6y&? zs2CU+4>2$>>VQ1P$cT5k9i(I=VNgB+txW)h6R5%sr@!YxR{s}aVE8wef#Lse3gaG{ zCXW4QV9)>?%)r2~n}LBz1>`R#Mo|zoicv$L2C0k$tzklyr6~`Tbr14H<82F5TG28ML-QROuC%Fs0&)CK_chd}uSH0KE#6Q{jz7{4+wFueh_ z+v^z^KsQv78_&4({rk_r@aqo)dl48Ljbr=e+sIs*g4GiVublYxOjiuT?B zB{k6W+CNtYhW~G=Q|9AJ2me1ZF#PcccXQc5%dTnfyCG(S00RR9XdDa_DWLfe(3&+G zdjXUefAKOf{NGOFxQEsme-AJ)eG+0|0xi9zvDb#4@u0EK70~<-s*poz>jhX<^AD6K zL47u89O2Br)R6zh!1yPMff1C%LBl_^_2CdS9W?d>3LFpytv{!s7eH40V`gCZzlc70 zAD*t(F);jQV_*O!aTGEt>dn zs2VEBz`y{S9|OhzOXxax>idNm)b|2e@xN(6;-B$fA2|NmZd2cHL)%2qyzfzH`44Ib z7}3r<;P}rSkoaf#R|$@Pc0byAZ)lnf+Viv@8vmfe(Sml~0LQ;K1H=EHbWQ_*7#RMC zf#aWco*SwrgBHx}hQ>c=ucI~XJOb+f|5s*U`2Ud3@&AH>;lK81{9}z-tpRR2di;Xg>;!WFdqJ%98)Y85sU=rcpB3!@%%Q1~T&pvI}DCD2WjQ<_ruBAE8wM zs3`y%6~a(NeF0FMfSQE=K&|sFR9*=RihDIkvk$bi=MkWR@K$b~@B4iDHPdWiv z30j%)M}mRj?{o%+|CFu_`oX~PZ$1OVUs*`L2c@rJ6jQYE3h2a1P(*+*=rl^&*n-zo zP(1zRU|{$k$-wacG#NVrelaloJI}!QJAr}m3l{@ul`&rXMkSF#0CXA#DDFXc0Jnkv zWnf_a!oa}zTbzOMZwdp$zwHbR|6bx*B@XJ^zh+=~cani&+7<=|-DwOA%z}_Zwvl`{ z%ET7}pcCw1GXS8|ia{q<<5N!uIZ!eLl?OjSgHw#Zbr=}_M?m-gPGMm9KZk+gKWIJ| zw0|Iqf#I431A`!F-xp~7@;d_qWANyv50WAYbWS;_;|B_M(D_HS?*NkIJfakX&hY@b zAB49+&tE4>cV1_n^t1;sz892lMGB|Zk*pgliO*n!x@ z>l{2PK_gjRNd5pFTuQ^WjDyGH6uJZyMu(u`2I?k*_5@OBKD*(U(|rP zWawB7TJP}=8fG`3chd|V*JHZ05$YFES$2Yffk6UO$*2fY2-G2k*IWh$hM|5w3g}#? zOHe<7x)9Y!ejR0^h5+cidf5I?Q2P&bJ`Spip~(a7X#)8NR0e>~fuX~`)*%^Cp!f#W z`JiwD-CY5?&t^z^1kKT)JJvQq%K(tyK>M2Lc4j-8TLvo^wC)RZ{}3pAK)7MBI+RRz zfzES>ooxVGcm_Jl8`MW2)3{-$%>`-34^&2i&Pg0jo&?1;Xg@P(Y6+CSL1$bIf|1QZ z5yX%M5e%!5ib&9PRztn%hb?SDM_j;;xdJ5vP;*BKo5B$$209`SG)x8x6HqZp<28Q+ z8oHos89`lJkY7P~B?AKkXwBPzI%)td2W{~L9eEB43lIiP_zuUsj~v9o3=9mgVGhtf z^j-!A22c|SS7-~L;%Y$?-IEv?7+`51 zRR4h%z6{QDK^GonVEFT&f#JX#ZibUTj2KS;PGUIoyNluM--Qh4|F2>=`*$hBnLm>l zPXEehIQheo;mAi(hE@=*U;l5f7ln*Z&zAm_Y46(C|N2#lut(RPQeV zZ9svxeL?$OKv(V#o~u7*AAqcdJ@H+F;qY7Fr54Mmf`&We+=jU|Hq2Y{rk#r z_V0OyGryY|j(yf<*nF3XVK?ZeVR$Tp;&}no&;J=27;mvKvFfpaj>P2xjh4XGjRXd0 zmpUDj8|9vGn-a&DXz<(Le{oT#5=LM)=m&d@s06PB=7Z_TWN+fzt`a{=MnLXy&gc8zwrM* z!-2Pt84PVfd-_0e|Br!z5j4dIsta(rW~7OO#)m-tU(j*^P#A&mRR#tI(9{Th`UaPXNSfeey5>N9seV_;Zyk(uGl?>2_>|Nl}w{z2uz#sB{qHs8L^m{6w6kk>(dKMW5O zLE{6UtDRx{U_ogB)HVQJ6$rW-f%(sy`78V!5_TKWuXG{f3UpmYFYgZc}0RJ|hd$VUc-L+^PQ&i>g-)3^u4 zKL~^R1xG*YGMoUd@uxw^4Id*x^#N$tbPP07Qev3_xW>7ZW41Gb1A-XzYNvv~cjltwI%o_Lw1f_{kusKnfx&=*fkB9YfdTtC%h^Am_%~-b z|NjGB;~!K8ocUeEaOFP(L-S(tLSkg=0+k1#^Kq&e7#Kiz%77YEpt1lNgT|9UWd$fb zfW{a>bqHuvEohTHsO@0P$iSe*xc){8!-fC9=^p=Q|4aq90cPwO*)d7E??L$+bcJ+3 z^ax$h@rlUk0G$mgbU}Ry&{)iKrlQ{64423l-v{MkO7PjgD;c)jWn$P1ng*sk5JpX4 z0M+@RrSzcdhD#V27*;VbFkEI}U;v$Y42pB~bO7T2X3TE6G$8SR{{MQ0v%gsw&Vrma z8hca=0Y*?V0GkG?^FjAb8bZgBiy0UgCNeNEfYzPuWME(b9k6qRF~4gU!^MHf|9_S; zOx?l6uxa!tY;bJRj{uD^g05KPW@2DaP*7mp{Vasx{QqC{Zvp)6Wq9%*)B~lT-$!ji z4FT}1&v!kBbN^n`J^oMqOk=qCpMl{Ts3Jmj)+mooApjbIIsAd2;q2dIbd7(|Ov8!q zW(=Sa2s(Ln)EX=yu=+9s!{`4D3}=3~&^7+g{yD&K;4Lr1LC|0}7I%#b(Jur*qi`pG zm@u6C_mWO&;4j1JUpWkS|1&U*_WtP?&$w&?t@=6io`vD;ANtG!ocni<;p7i#hEt%$ z@3XyG93OO%W(2L14B1xJ_^a&QRd(Z z0nnnbJe>G+} z`=^=V+}~r+;bQbDgFg)C{(WRP`}YdN*}t;m@-+;zvU_h||w3k>ONj*9r zw4an8NuCfNw7d`DV327HNFF!u*=d|-fx|NjAT#t#Vp0rLm2asU58_zesPpz-|h0n`~FLqHhB|HA-L2IK!= zKt%h8|NsAgKnk@F_5c4rK$geCKY(N&NIf$DM?KhA3?REe7=v$*CO-)HDDJlh#{x9i z|NsACj~X7>_{iZ0lK;WL0Ew>u5dD7`K#2zw8vp-;!s8!26+rm^8z89xqyiM4{~w_7 z!L9(w{{Ih_N9Kcte?XGM|Ns9%@%{h*A2j|yxO*V-|G=per1c<(`2QcB4@rLi|Nn1> z$TvbLOg>cM0hm10pa;-m17^_&X!`sEbqFN={*#9}6q3FlFv2|Y2W(CQLjhFZKaf5D z7#I|w=Kcpo5GcFC_zj@21|16vbqFX!d_YR459Cqf@dG=U4>AU<;}0Why#HgU2jxb% z{Qm}iP;P~%>;DHBQTZPjysJw!?7i>ENa&ZbOJdpXIY>8SN zqnQ66H6MYj1Qm!N8itY6HAoDdkDSjz@*j}$J(LfQ1`r#Bk<&GZkDRVSeB^Wu;{R_z z1Q(e902GU0)=(le7#J857#J8p!OO@1y;61p0|Nu-ymAl*#p@^@4S~@R7!85Z5Eu=C z(GVC7fzc2c4S~@R7!85Z5MX$nmCbM=C5_>GRu<3CtZc2nSy}GCva&;eXJ-ff$;z_( zk)0#|E+?1me|`bOudM9R5TjBEyvxjDxRQ~>@I5PA@ppE1>A&pk1OKwKU;NL`{_#IM z`|tnk>_7jrv%mby&c6RQJA3t?tnBdbS=s#mGcy^krjDfhw5Sx|NXGok$z}MKldGWc zCp$a!Uv~D5|Jm99{%2?ZCxCzc&CcHOD=XXPYeoj+&rHx^i%6!9GKmU-@0nQ)pEEKA z|7K@Th9-7G;f^l<^lw&H?3eU(reC9RKvbB+6#dN3VfdDv!~Hiqdm6NmK@VGE`EUPZ z=S2Najc53n1G>}#X2U3rD+FGpXEOZnX=V76on7!hJNp-bvW8$h{L9XM@H0ExIMEb)KY*_Wsh?#O2R&CZ_q zKP#T$MOF^Ts!@z31VHKYcUG4DzwGQU$YDnf_P^}x6W_A3`M-_4?iuL3e9)Q5>I@7F zpmUK!7#J8p&2rGW>Fo>*44{cX(0z-O85kJ4iVO_a{mRTFrln7?_C=1jf7#hjerIQ^ z{LUWM-DA+f!~6^k4EhWV44`{!IvE%kHZd?TfbOPv!N9-(I-?#mO%6Kl6D#&nRQ&%d zGn1sSMGj{??00{1at#0G47>6NbPoXNK4s9o@}N7VPe9N6Mh<7teUhMi zF;F}4S9T8H-|Xz3|Jm8!hzM&)9OdNv|DTidA2jm(Cp$a*O_v}I4=K{6GK@J36 zLIq0mw-^{0qCxvk=n>BN?D(9O&G0EJoBelocHCdcm>g)z7ZT1m`e@gpqW-s8Sp1LI z)T{%U{W3QfM9~3*f(}${{9|EY_|L<@@SmT7;V%~h%Nr&JhK~#kNuaJKhBJ`wya)AB zw=gg;=whfEK!HaYSq%UGA7=QHl`ZopJ0}}7y9??|{m;(+3Z0?(`7b;B82t z>>SX^v_G;oY`<9*|1#vbbsEG#h=Y0kS2GBLy5Mip5py2-x zO75-<4F5MVF#P`r+U|h^|6^eI|B`{>-#iA!{{{>U>@o}t4C|mt1#|~4=xQ!dQABlD z4uFBm3=9mQyWK%eH$U3g589pcmydzre-i`4|Mxh;{6BW_ANLs;Y)(PL{xt&wVn7@mD)VEh`xzyR863vw#e zTsasF0(Bxmp1RJ!z#u^#2Z6>IK}P=bB)!xDg*jGylY!y41_J}AM5B%?2Zoz0!C?d!bjv9y&E_>J*82)djLHPe?VEo$#4uAf= zlsk3sn9v6ef6&Anh0SYF+3=4a+OY+tSt{dyix^m*GBPlL`oe=J04a0{XpsvjQ*WiX z4a)S3fr06lFayKCi!@394F6X#F#cjsKMAkbk0R|1I{$yuh__u<};SZWPfI5`|=M6rspoPAm^naLv zfq|QH=iPvY(O)YDhX2o~69@m!Ffja;h0McH?!rN40<8VHjpFtv%wvq-85mgZFfuUw z%VS{p|DD1(c)`H%A2gQ7z`*bZX2WozL6dr*^uL(8W9%R!|FbhN{I6$V`2Us6IC#mx z@IQuu;RoX|P5Yq5kgK2t252EXmD3fdF8&Lu3(^=EKqo$e+mOVxOaC%3{5!|M02;&o zJ6yva6xN{j{&8sdgX#g07?m+-Oz0P=EYf3O_&1e-;r~NCb22{}7(P8@VA#8cf$_8o z1H)tn24>K_3zeNTc#H$h0zYP8VEDejVEVz#!0=aP`~b{PTP|zegKV!fCkt=gFF^gF>!Dj z0$Kq9YIA}TW7i;ZB4{Hbs4oI?*Fvhbi3gDfp-$0;ZWIBP{XS5!!9qncFff2NQG8-x zU>Iy}qgEJ!Hiv-H*cAo_2GBe(walf4IiN-S8=z$)Xkou7H4Gg@W`NqA`=I^<^}hy@ z)8I}4ZG?s`c&?>x-w)DIPDP`$ev z>aUFq;N^YL^R@@255yQ47(nywprR7AZ+bAhmDm6TRcW80d+q&+)ii+WKwUA=Vqs7m zfQG3+#peJzha#&%%{y3nT0e-I{e(P~1l{um>KcH~!~pHVBcx|A%9la+GrR`f+cFp( zh{H{weIhLkkTsy7^(W;FG~YXd#U=aSFfi;ALQ70PTGQjjMt-GJ%du zqu#P@d>%jYk%8gBTV{sSzl<5q{^?{m_wO{rxqq)2&j0_xaQ^=?B6wHhdnGWfZ|}|t%HmOJ)(@Ype^I%J8KZ?1hpYRU1Lz+ z3{)q8*1v$xt^!SMkk>}t^^}3(`hNz7)4x-pbt@u0!@`tQ`ojMo3}=4FG2Hmiz_9u5 zAPh!|odnvf3M!9aXV8Gk+GPw3436Y<@z4BbU^x9tgyGyj@{1m9QFQL_VTR*h1sP6& zTF?}Gc`zCPnlW=?U|;}klLvVcR4jp_0(8V4Xr-kz3llpN!6NwFKL&;~zrDfDN^Ids zw%EU~45xm&GMojq90p_XQO!-D5p2*Nb5KzPYNmqX0aP^IU}Rtfbs<1I4?#zFV6ONE zHG4qC&$)kX6c_#2;_2+4dT_I9;W4Utb`To`>au}GdqG_W&`JZ)atu&hfD+(S1_lOD zvuP^ysH^}61_o=;(Ugor;+hO6e{7;j_@Dc`oMHV9CLCSlK^&A6ISo`UfzmtZ=si$Z z2vi4xX8u5N0mGoU0p*qVj0y@oHyAhHy~A*kh`tuKM2=7F+`nB6C%&;WoB$oyMUkh5 zs%B8fA9Nn52Ll5GXdDT2#s_Ho31}-Q=Es<-u?kRM}9)rNh$@P!H+EH&O6F zGd%35P#K^5cZ}iK7a@jYpz-tJ8FW+u$GW4PNxqoLV$^&Qro?B>(30=0ucqd|rY=l&ffD-O>6J;-qKyB@gBv+}}74^h(n56%aF zL6eYj3}^mKW;p-vEm84s?%x}RvwtQqocJcgaN$402pe%AJ)8;YIQgA{;n){ehSR^S z8P5Kl%y91CC5H3=K0qf4{(|Xq|K2m4`*)Gy+~0`|r+!*8?0?P5a0IlBn2_P4@ovk#!Knc?CM z42)RW3`pXjy;%@N;Ik(nOwiIg&>=1$9;90ZV*UT$zyMl^3TFRf;0Mp-gE&7JKxd6W z?0UcsQU+lkut%^#UOK=4cH2J?33c-ikT0Nq`5^!Q|NjpRAd>w-{r~^}|A74agW&+k zj{l7O|NlRLIh_Chf2d*q|MP<^0U7iE|NnN7L!f~KX2Zq*F+eN_slmnm16OnS{~vfD z;bnuo^8f$;!~YRxVz8lZI1F+dG8^j7!yxxV+3g^IfdT;Hmj?`>V1TfHAhG{*Kodnf zD3qXVkP&~NdO_@e$ZU|n|Njsnh#QdE5N*xiaQ*`}?jV@`1I&H^W`6)X1r#rT7$NKr z_MqrW0L%W6N5sw_q}Tz)bptHq8IeMuq5eMuEZIJgM}*!7W(4~WgB?6ZK#2_&`~N|^ zgds`n|9{ZVZ5aE113cFLBgF`AHo^`6k=*tlDMtPy#mIkU^E0qLtr!n25Sg>$;@K-kdevs zD=S;;9xH1{t%JNaiu2FvfkeSsxWVGK&||8ufIm#g~z%g%m@&+kZbAOB`&C;ZRJVEB*) zx~GvSs|KYa(50Hcv$JLYWoMs5@*my{9YN~;WoO_0ot3Q(x*T{=dW;B1y~)gC_@A8( zx?DORcye4pRRwJNx9X z>}-KSdWkb=V=Cz24p5f`wD25s-X`e&GtkB5pdA6A9ZOqe80PVay!@$4*x}*m*g7T7qff00(Ea=_=(19SJ`!_aA zaB&^D5gq-FO#i?7lbxgWcVOEDpm+w|vI+7t=ztT@B6H9w1Q!_?7#1)vFn~79x-u{@ zXfiP|i!(7Xfp$uPHfn;>KlA>;K(W7BStONxur%^7JNw@6>})wu89D&*5AwGvv?~re z1RS*M4s_r`4FdxM=l~ng!3Cgvj?-yh)6*GVrKB=}E{TQti4^)@cJ{XKS=k&v=-(Cs zZTQf$p>erDd`#1eM)Cv$MJWWoIu#@*BZ$_?Ml%^H)~3 z@GttsJ?J8MP}vPy$h`x4m=4+gh6UFBxHyJ?+2Hn{^1tlt19-y#bdh*&F6bih^MA9m z4M1`KB0U3U6fJ4c*?6F`9JKMjh=Bn-22M5mK$m)h>aaiA*@}O&v*-QK&i;fg9G)g5 z{9o?o_F{KHfMv6p8N=t?d@|htO1hu{e$ZLr|5X?m{yQ)*{C8tu_-o3*@K=O^0d#&f zXdgHDz@7*O1_sd1Ns#|R2l{}@Ju>aZukBk_HpADfY>q$K*^Zz~=0R!ve|Glqf7#i4 z{^#aSNY~Ms%*4nDay$iR_<=@||Fc5R_+P`o@c%Ib!~bs#44_fu{~s6_{$F5V_}|FD z@I;A$fgu%|&q2GXL45okppxwKoIHj{Ir)s=v$EO$WM%Vy$;beeJ0R0Fq3sFK9SR^m zi5TSff4mF~|62*)Onvzd1A_!;bN@mH21e0Aavd*d&m8EiT+lg!piPY=_#d?Y=Qk+c z+Zh=C{{~HM;m5!KGcX+f$iT4Km4N|vWIqYk(N5_U=pCM*%i4*w1>|>d-1`uScPw`W z{M*dH_>-4`5p?bkk=D^x8K}SqrAq_itpSyF|3G(BfX;r$;z!WL4^I4zf#IJo3?_i@VPXNDAxXTgG*YR@z`y{Ce^5Fg z(h5-8|L;Qn-7x>QGBEyP8zgxE)D8okYyfJ1li>e<-V6-?e~}sgdl?x2a14@r7(oZD zf%?%T+}{e0e@hDPC|=3H_=}l=5p=2t5tIi0b&%ozCkzb#bs_#I-bNa#1f_q_!405$bV;xV zlm`BS+WMDB2m{c)Ky?fZKSB4|kYE>0m6B2Zflf1r9gg(hm4V^^eIml(4+F#h`3wyI z1qM;vg8~-R?g7<*X&?@17?cM+UG%U5B+0c`2B)`;ScB@F9Arq2c(ZQ zn`o#SbeJ`${R0|PCc~Pa3=E8*_V8bj-vSvJ{)6sl+R4BGT2b_W6$8W8RSXQ)Cm0w6 zH5nL4XkwG$5;C=b4sZdDDTBt<$ut6{4OAw8?&1O8+s4Jf@E_E-H-}z)2Re%$rf{&( zpz)@43=9mQaaPbtPqc6ss9y;h&jgKaQX!0~>O|0jFOa)PX#0?5zZnAq18A&k@*umD z33PWVDDQ&~dnC(aL}~)%1JI%8pm_{WBK6Q(8R+D=?FLtPC>XVQ0_^-{*`Bj7u3AnYadMEEY7kHywH@L@NUW z19|l?=t2R|Fw>bo77XY9?PNIr|0njd4gS4lxbVM+!94;r5dli~p!>=}^Q{bO0s~ep zgTerG=q>2fLeOEip!+PbMln`F(76Dh!{yKXc4RpB?+$iOP0B%SSY?$3XQt@jo&wk>00 zSPn`8n8pr(2&h~H%?W}|69uIO(A{OAaar8q06JR;bQtuxzx4#;9sT^R3;*6QocV1z z2+uGBjm3cKW6;1js9grCqmmgJ89{f8g4V(yq6T!r^O@hm4CnuyA=&?+@Bo#GH~%v* zfXWYqse^?DS`Pu5j0N>eK;ZzIF9o&LKz*n%=z0)MhK>~;4Cnt7ca{st&&U{5FC72M z&Hy?mY_Rwm(;c983aGpW)eoQ@I-mpcL2Wh=e#2<%eS_ib-|u9_|M`C>zzqaYI>NMf zFp7ZNGz{Q9I-sHgG>x9Y7?IP$aPIGCvV!2;zXJ>>e{eB?j-?rl9wy`_(9u$-f5{P5 z^&uyH82jAc*$f>k7>DdJFQ8*=4!mJzIQMS>8S(#<;q0F<@ZB^!K(jxD0(%h3&pXV( zaPBYYcriPM^WeLTh(G4@+`runr+*1DfSM44&}&3H33SXR=%}Kzf6ItI>h#>dy9{T3 zTQPu+NZki&brEgb;8X}Y;_u9F4hC=-5J(5-{+(wy^Vva|h;=QF=54MnhmU1O{CQkaP$E=%5CW2GCg^ zAR2bK0gQeCJ%KK}ox{{hna|4;o7(D5Jt|I|Z{Jwc{HCs=^^=rrh* z3xqzH{y$Lj{(-&lr~W_q+=f5({||tUwD?i~AAID+59sk2F#5xP@X;dwKSGY*`2X<# z|NqVZ{~vt(|NsAk|Nk>MgHD5hy8S~v_^6WqKjfiyvP0VhT1ZZ#ubW{Wg z54sdP>Y&jOpezLb7Z)>3h)ZYKoSMzBE-jZ~Ve-(}Dh}ET^dmcm8MMXyUv_rIzwGR} z|FW}Z{mstK`R+L{qKKv_Pu}E*(txW zvRMCQWzonL+1bCbZM}hteg2!Boe0{l{xy5>q$$w$slVCT zmjAP}UqjuAbNkr8?Ck4*va=Ni!M1VG)=1D40chrzfr0HuMrJ!9*F)w1{sV3Pr*8~^ zTnbuv=*Gan02=6@$iTp`f`Nenw22pV<=?IiUS3a#-NgGZJG=dVb{50kLePcc8Ja$DrhceADE-8N zHV-13Lj^V{MS~X4Er%}D=w@JGFd=5K05k_%{LjvQ1$84nKV1Eroh=XAFh~VA!;An; zTZEKnJYoz49(JG=5^q-r7;Xi1>Z7Ku9|6B%!e|`)M|6z-GKnpuS8!JJJ`6*s)`aLU~;d53t zs0E?_H#@uJUv~D)f7#hnzoe(%E0iKhk=3SF#xI|K{=Q}vjOb>AmX;^g4UA# zU%|leml=`^@P`bAvY?_9RJ)hqHwT=9Wf>U$UnL?AKwFU$=#v8&LE#T-{ehNr;`Ra~ zXebX92me61;5z|7Y+_*eCrF2|2e}uN{y|4iB!M{iG06SkZ4DI+4FBI^@dKz9+swcK zKK=ky)8RLcIF6&D1H(UC28REW85sT_WMKGzoPpu(4hDwWV+;(j zx(p1#^m02W1VAk

USYs{rwd#Qzx>7=JS|F#HE?G2vlgZ~&FWpmK~rJ|fb1;*~`) zFfbfsU|;|(<0M`ec9ozD-9hUSLA4@w71R|4l|6GA7#OIv^#gR2!deCf26}Dr03C}3 zN;^(e3>H%c1_n@%1r!%lG#0B-prw4985kIFFY&~xji4ZC8~bGj1_n^?mY`xX6@ZQ? z0@>kBrha_dKqDhv(5)i+_!MHufz~v9_z${jP>SK)-*|>||0Xh=`!}26+}|38vwy4@ zeu9o^VE~nxn;94wr!g=vg0^O17()dC(AJJg3=9mQo)~dQ%YfE$t-Z>~aQ3ekbUE`M zv^8Dt7*7A5$RMQvItC7O+#6^+4HcY<(+E(02Av8HT8ax=o{f8XKIob>(Di_4|2Q+8 z|MwKl-H0_==l=a?=vniGK~>j`!InUy8K<*Ik^qfZg)lHMoQ969f$B-j{03SAdgeDj z!?}NZaJn6~X7J+wZwwdz2ZOJ`1uefP$vI>z25pxDErA6c69#IpfZ_v`H$cl{nHWy} z1g(R!XE^_#xOJfC{w-xV{E?Z?*T{nE8_-N4sI3U{1L!!vBMb}-D;XFVCNfk^UBz(m zKj;cXM0$aRIVOFI0Tf@Lcq2PVDbfhqz5_Zs3Dj5uwK=*NN+)b!xbXiU!Sn<6%fEqk zKdR?J>j2OGabP(A{|iy>Klg7r!;w$)PCuw_M&^Oi!RcQD4CnssBf|aP8P5I<17Bx4 zSk^usdd~oM&)L6j4Cntp$Loi4|K>5A`pL}zTIYrw1_R9ox#!qtQ2pfxzP|K4Mqd8N zaPHp}hSR^qz*i`5dpOW8#GcYtSg787>q#>+Pax?_!0w61jKqDm}7ApM{Jl6UD{{ef5c!NBI zW`NQBVEzZt!I+a2;r67KH!*0gQ&I z`w!Lk{{u6~od5s-utVto_D~vBY=J@*>w!T1dQk=j23-aQ2GA^M zC<6mSjJup%-k0=rLRWr#`V3|b)p_Z)Q?pt%IlOeSdiGU#{@&~*f$V`UdJFff3Q zmg}>Xl3M&eHT4_5@cW;g{rg{bb|5G%P{&!|v1w5FEQPLW0L`-dGcYiKjy~XHW@2IC z;t^o@os|W;Hbd%vcJ_4w?tA|?JKKy3;RhOf1C7Uk#*9I8U_lHF44~CGSiSu_JBQ(2 zb`Ime?Cc(V?)#UWy$Q7Hg2MDC!N9-(I+g(xhk*<7jpZ`E_7%9 z%g+A)H!JJPzwB%iQ1s$IWgawj`Uf<{A;`e+Uy*^~ALz7276t~;s5b-X#8S`+0ia=0 zkW-PJfR_y_olbtR7FFU*CUv~ES|Jm7(|L5dfUhLv}!%tDEQk9E~;dwRa(gAEn zfv3ShQ$%764F9Vc82+DTVEF%tf#LrR28RE$85qi97#J8pQ!${Eu0c_Z%@BfOplaer zb`In3>};XG+1ZN!8ybWa6ckukKzrXo%ee3?$pN|WAINo}vw8o4nvtmZv|4bB zi$R2-OGs1zTIK}uJ&vU}pb0pT`~HLM2RGeN-3DpCF);k!!@%$lbeE4v72UnyAHqRz#p93vKY5 z2?`pJQ^DC8bVO+q1H;=$1_s0R3=C}c3=E*@9sDMcDXYl9zyKQGmnBm_X#NsZ#Dm&X zWEz228)(cPRK$>5If2?Jpdu7h72&mr3`tPC7Bs(yBYO~SB4|}PXpRt69TBaG2!)_) zh(Jg3P&)SrnhOHWn}F61;ixLHxCXq({Sza@nco5oXa6cPocSZkaPkK$gQhV^F)srH z1E?KIt-E_bEASw#Kv2sWzmMmGPNMh+nt2vsIQOrD;rzc-4CntpVmSZ*Cd2uE3mFc+ zvt(dktYu(eSj5P{#LEc22L2{|D_nBm;Nl?><6W~nax z|Icvv!y5+AC{T(~WMBf-!PpETTMRU#585yYT2Bp{Q3kDW0j(T^7zmoZy!@Ym;oLva z2^P3#(9Zrj#&GesFvDd~$p+C%WfGKIK&}2g3=9mQ*?UmSAJpC!U}#w+#&GW6F}yP~ z=l_3aIQQ2Re5wYO9fo8aDBM790?hz}N-~ z23pkuo}vA#4=GQO4)g=hCWHAu8P5GJW!U+caX@CMPX1tExb~lc;p|@<2FNTd=+qHV zS_G|PxzBL!Z!yEEpP&*IG*1W#clwi{`QA(5^+AFR=l(h~oco)^aPDsi!`VN|49C7O z(Rn=t{rx{`Bf$^=&!K>ZH$V(j`Uhyc9%zF;XweN4eE`%@{r~?1_zc(o{~I9mKL!V| z_z!-F{D=AnAbmd+|9=3P^MU#Q4-o$W|NsA>?NJZdA@l(x8niV6#7CxK>OVmB{g8*~ z``?eCLFWHI{{KIiKK#E4bjRBNhyNMC^oKgoloZ4NA94`-4+Dh$52GJI`QWK52GHDF zJtQ1JQ&OOrvPMv^0u(&hXwZBUXr>9Y4GDfe0BChCXub)A3C6&v0&+tDG^hv~_xzQW z%?cVc`r8?t@%&iP85ZCl_}0rsBWs?AiabvmZkD51$3i0sYC!W&;g8 z5@QxQYCvZH$<7x1mz}*Cec<(fcJ`Nl+1c@+U7bIuw@VP@Oi(`t)Vc)?tMNUKPl*1X zo&6Jw{h+y#qkpoq`9bjm(ntXYb=*PgZxb097@DCiRnYvxl!>;sNAc{qef%#wTZw{k zd(c2L$WD-5prflnx!IP1fl--(ff3Zo<9n8vnD;+B8?-kcuY=4uL4H{BL(MNy-T6qcT z6MzmY2aRmtR1Vs+3fhDHFFV`oe|Gkj|Jm7pLGJjJn)>%fbo93Wxp`v$azM>?bR)p! zDCjad4+e(+Jq!&0r!X-5k7Z!+uz+6r3|fs&)M&-`>}-ZR8Q{xL<^N@8NB+;vO`l|E zm(9z{3aVtVbnHNVsefz?4F8)L82*1j8gTo?z_8&V1A}b{1H(3S2b0PNT`n+-fq?;( zf)IMyKwIa)e)nQv`2QKnZsY;oTF5RkkUE4`5?P==6DY-^4tj%k*MZFa-;2X;kUI`D zP-iC_sP7Lt{2$?Tg#FX-*ngUV;U7O0c0++y-GXM=5cWgrb5Px$$iVO)bU6{ozvy@t z1H*qNDhy_V><5j!A=wXNg43Tc1H=CfSi%6b+sy!CHz=M!>dD5Sz38A(F*E}}_Jhm@ z?Vkqcfny8||Iaco96Zawu+^D?0Tg5)c{FXLaY1F(LkSxptQgUp6&x(rpm{_ zzz!2Q-)tO8=y41`)k5*{{It`hyBR=(n25m;(7*!> zA7nW5n}-3kM4U{E(X@doQP2QfGy?+zXfU&nVfyY93>U$JFfcoj=}Qb}{|eJ=&jfUr11+tliiK!~ z9RJ1uHvimT9fouNRzTC?D~9v`jxwD48_aOx8)SP7nl`F(k9`3Re1Qi4K}Qg)Fr59X z#c<}20K@XL3=AMSs#-RD3mjD0!01VszfBvBU|M3s?|BwH(|9}3!{Qv&{{15p5w;z!IfAE0){|5~6 z|H0=zI>>_-8vp}>Ym+1c!$a`G6y=Hybv^`H*h-|TFzf7#g?|Fg3}tD3g|%g&Dbn@xF#9@1sX1|7AO zgVu2db=X0jX@(y}9gTzHRZ#5&+E5Ov9YV#qxZVujrU=M*7U4jO6 z{$*!pAuUz~b*w=PL8Cx&Ki~^$LCsXqeT)C)7#RNBF);jBV_=X5jm3eExdH9A#M*>` zcAFLcWoM`T&(7ZdF*S8>x3%@WZd+UK&3+&+f}8?xCWk;*dVOPH`2U!JVd`cE24PbM z2K<@j8|Xm1>}-brd3o$zR#u?q9O#@fQ04;}4(ft{Flb5rb<}Ra-**fQ|E$UA{(zd! zpbe79=7W~%e?>JPRDFVvW`U|BngVsnkv8^%!(Ic+x+G9If$qsB+F+LZW!p^H-(A_v7eW2zwx*R$m)Jg*#(g4c8 z=yD9;R?+ESObloLC@@_7&&;3>stXuEQ`N-Y1q*H(gI4^4A`H}DfLH>$oC(x4KKmDR zJ4rXg`TzSFF8-g#klzVf*#Qn5uuh0-!Xzlmfbtlq(W}P5z{tX|^fc(+3m%5^|JJ}8 z-k?VC?q@Fc13+%Wya!*KrpS~T;o zFr53VLh&p;EQZhh1zkVZ$8i4tL5B1H7BigtYtC@}Kj>OA3@4E#0P47alIWS=pr*;` zUo7C=lc#@?Wf&!z{{R2a@c(~1!~g%x5PX1<;XeZ-!#{Rrh6DD@35@GyqK$fl32-a}1PL zKP^I-RJKO4ic6QUh?Cj8g+1c!%%94Olpn?XRwS`uBc$odm&b|q4 z6@XeaL7*BNhXEk3f(Euhw~~Suo_KUxSrz@w%K8q~532PmA^JfjHMquMWncgwX#pD8 z0aev5&<$QJ{|gF0tt#RF+1YFVXJ>zVpPG92OM1G=w+v9j1*}((f#Lsh28KTy85pXn z7#J8pi*n#j`<)GLm4VC<`k#|y>7$^K%*@Cr%EJmOu$$`d1Kx^_La^RxpFAKxD zf4U4;{~IzWX@UlxJs254X#=7JjRY0%pe-37Hwu8NPlogV5*W_^f5CA6|9ghHhn6r% z$_p{*g60FzbRxK*X(-UNP!NNFa4^H(7boCjA{YK&X1MfUgvcVBnSqOkkKyF^exzb^ zHN)Az3JT!6mJk+VXKlR?TFC{%q73K%U x_zy+~h69ca42=&N7?^)TaQjaNhW3vP43C=`7(OsFF#G_W uml diagrams + ] + +# note: breathe requires doxygen xml output -> must have GENERATE_XML = YES in Doxyfile.in +# match project name in Doxyfile.in +breathe_default_project = "xodoxxml" + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +pygments_style = 'sphinx' + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +#html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] +html_favicon = '_static/img/favicon.ico' diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index 44cef925..985592b0 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -21,4 +21,5 @@ xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 $ # note: deps here must also appear in cmake/xo_alloc2Config.cmake.in xo_dependency(${SELF_LIB} xo_alloc2) xo_dependency(${SELF_LIB} xo_facet) +xo_dependency(${SELF_LIB} subsys) xo_dependency(${SELF_LIB} indentlog) From 4699ddc6daddd30f3c9d31967a1d2de798340211 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 27 Feb 2026 11:31:54 +1100 Subject: [PATCH 044/174] xo: _Any.cpp files need for clang 18 build --- src/gc/ICollector_Any.cpp | 1 + src/gc/IGCObject_Any.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/gc/ICollector_Any.cpp b/src/gc/ICollector_Any.cpp index 99563bf9..301524fe 100644 --- a/src/gc/ICollector_Any.cpp +++ b/src/gc/ICollector_Any.cpp @@ -5,6 +5,7 @@ #include "detail/ICollector_Any.hpp" #include +#include namespace xo { using xo::facet::DVariantPlaceholder; diff --git a/src/gc/IGCObject_Any.cpp b/src/gc/IGCObject_Any.cpp index 778760ad..95c8cc84 100644 --- a/src/gc/IGCObject_Any.cpp +++ b/src/gc/IGCObject_Any.cpp @@ -4,6 +4,7 @@ #include "detail/IGCObject_Any.hpp" #include +#include namespace xo { namespace mm { From aee40de3e62ffee6153b9eb9001786cf3aac5fae Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 27 Feb 2026 19:38:53 +1100 Subject: [PATCH 045/174] xo-cmake: setup to make share target available via cmake install --- cmake/xo_gcConfig.cmake.in | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/xo_gcConfig.cmake.in b/cmake/xo_gcConfig.cmake.in index a7eea6c0..548eb737 100644 --- a/cmake/xo_gcConfig.cmake.in +++ b/cmake/xo_gcConfig.cmake.in @@ -7,4 +7,5 @@ find_dependency(subsys) find_dependency(indentlog) include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Share.cmake") check_required_components("@PROJECT_NAME@") From 62c3fcab01b0b57217b6e89789abdbd4e16c3805 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 27 Feb 2026 19:42:06 +1100 Subject: [PATCH 046/174] build: + install target for idl .json5 files --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a05fc4a..573e5acc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,10 @@ xo_add_genfacet_all(xo-gc-genfacet-all) add_subdirectory(src/gc) add_subdirectory(utest) +install(DIRECTORY idl/ + DESTINATION share/${PROJECT_NAME}/idl + FILES_MATCHING PATTERN "*.json5") + xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets) # ---------------------------------------------------------------- From 9515b15fd9becd53b92bf2c58762879821287e45 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 28 Feb 2026 13:22:21 +1100 Subject: [PATCH 047/174] xo-gc: build nit: missing #include in X1Collector.hpp --- include/xo/gc/X1Collector.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/xo/gc/X1Collector.hpp b/include/xo/gc/X1Collector.hpp index c9d00da3..38999844 100644 --- a/include/xo/gc/X1Collector.hpp +++ b/include/xo/gc/X1Collector.hpp @@ -7,5 +7,6 @@ #include "DX1Collector.hpp" #include "detail/ICollector_DX1Collector.hpp" +#include "detail/IAllocator_DX1Collector.hpp" /* end X1Collector.hpp */ From b8647c8141be1ebea1ee62e340e15752bdd2c3ab Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 4 Mar 2026 14:07:08 +1100 Subject: [PATCH 048/174] xo-gc: + debug in GCObjectConversion --- include/xo/gc/GCObjectConversion.hpp | 29 ++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/include/xo/gc/GCObjectConversion.hpp b/include/xo/gc/GCObjectConversion.hpp index 8a950c82..53148bfe 100644 --- a/include/xo/gc/GCObjectConversion.hpp +++ b/include/xo/gc/GCObjectConversion.hpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace xo { namespace scm { @@ -74,9 +75,33 @@ namespace xo { static obj from_gco(obj, obj gco) { + scope log(XO_DEBUG(false)); + if constexpr (std::is_same_v) { - // trivial conversion - return gco; + // Need accurate handling of DVariantPlaceholder. + // runtime type must be some concrete type. + // Only use obj::from when DRepr is a concrete type + + if constexpr (std::is_same_v) { + // At comptime gco has unknown repr. At runtime + // will have some known repr, which assignment here will transfer + return gco; + } else { + // Runtime conversion to concrete type DRepr + + auto retval = obj::from(gco); + + if (!retval) { + log.retroactively_enable(); + + log && log(xtag("gco.tseq", gco._typeseq())); + log && log(xtag("DRepr.tseq", reflect::typeseq::id())); + } + + assert(retval); + + return retval; + } } else { // both runtime and comptime polymorphism // use same path here, since representation of @p gco From c3b6ec88fcc689d1052686bc15ad2750664bea61 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 4 Mar 2026 15:35:04 +1100 Subject: [PATCH 049/174] xo-gc: nit: prefer class ACollector to struct. --- idl/GCObject.json5 | 2 +- include/xo/gc/detail/ACollector.hpp | 3 ++- include/xo/gc/detail/AGCObject.hpp | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/idl/GCObject.json5 b/idl/GCObject.json5 index 7056f0b8..43747582 100644 --- a/idl/GCObject.json5 +++ b/idl/GCObject.json5 @@ -14,7 +14,7 @@ namespace1: "xo", namespace2: "mm", pretext: [ - "namespace xo { namespace mm { struct ACollector; }}", + "namespace xo { namespace mm { class ACollector; }}", ], facet: "GCObject", detail_subdir: "detail", diff --git a/include/xo/gc/detail/ACollector.hpp b/include/xo/gc/detail/ACollector.hpp index 0fd031fb..bd19fe51 100644 --- a/include/xo/gc/detail/ACollector.hpp +++ b/include/xo/gc/detail/ACollector.hpp @@ -30,7 +30,8 @@ namespace xo { * * A collector implementation will also support the @ref AAllocator facet, see also **/ - struct ACollector { + class ACollector { + public: using typeseq = xo::facet::typeseq; using size_type = std::size_t; diff --git a/include/xo/gc/detail/AGCObject.hpp b/include/xo/gc/detail/AGCObject.hpp index 98f7e258..6f6e7b92 100644 --- a/include/xo/gc/detail/AGCObject.hpp +++ b/include/xo/gc/detail/AGCObject.hpp @@ -22,7 +22,7 @@ #include #include -namespace xo { namespace mm { struct ACollector; }} +namespace xo { namespace mm { class ACollector; }} namespace xo { namespace mm { From 2a302f292b32f6dec4a88d314a38c985622006ab Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 4 Mar 2026 22:26:31 +1100 Subject: [PATCH 050/174] xo-gc xo-alloc2: move Collector faceet gc/ -> alloc2/ for levelling --- include/xo/gc/Collector.hpp | 13 --- include/xo/gc/GCObject.hpp | 47 --------- include/xo/gc/GCObjectConversion.hpp | 2 +- include/xo/gc/detail/ACollector.hpp | 83 ---------------- include/xo/gc/detail/AGCObject.hpp | 87 ----------------- include/xo/gc/detail/ICollector_Any.hpp | 55 ----------- .../xo/gc/detail/ICollector_DX1Collector.hpp | 4 +- include/xo/gc/detail/ICollector_Xfer.hpp | 83 ---------------- include/xo/gc/detail/IGCObject_Any.hpp | 93 ------------------ include/xo/gc/detail/IGCObject_Xfer.hpp | 95 ------------------- include/xo/gc/detail/RCollector.hpp | 69 -------------- include/xo/gc/detail/RGCObject.hpp | 93 ------------------ include/xo/gc/generation.hpp | 36 ------- include/xo/gc/role.hpp | 35 ------- src/gc/DX1Collector.cpp | 9 +- src/gc/ICollector_Any.cpp | 2 +- src/gc/IGCObject_Any.cpp | 2 +- 17 files changed, 10 insertions(+), 798 deletions(-) delete mode 100644 include/xo/gc/Collector.hpp delete mode 100644 include/xo/gc/GCObject.hpp delete mode 100644 include/xo/gc/detail/ACollector.hpp delete mode 100644 include/xo/gc/detail/AGCObject.hpp delete mode 100644 include/xo/gc/detail/ICollector_Any.hpp delete mode 100644 include/xo/gc/detail/ICollector_Xfer.hpp delete mode 100644 include/xo/gc/detail/IGCObject_Any.hpp delete mode 100644 include/xo/gc/detail/IGCObject_Xfer.hpp delete mode 100644 include/xo/gc/detail/RCollector.hpp delete mode 100644 include/xo/gc/detail/RGCObject.hpp delete mode 100644 include/xo/gc/generation.hpp delete mode 100644 include/xo/gc/role.hpp diff --git a/include/xo/gc/Collector.hpp b/include/xo/gc/Collector.hpp deleted file mode 100644 index b4214036..00000000 --- a/include/xo/gc/Collector.hpp +++ /dev/null @@ -1,13 +0,0 @@ -/** @file Collector.hpp - * - * @author Roland Conybeare, Dec 2025 - **/ - -#pragma once - -#include "detail/ACollector.hpp" -#include "detail/ICollector_Any.hpp" -#include "detail/ICollector_Xfer.hpp" -#include "detail/RCollector.hpp" - -/* end Collector.hpp */ diff --git a/include/xo/gc/GCObject.hpp b/include/xo/gc/GCObject.hpp deleted file mode 100644 index c40ffb3a..00000000 --- a/include/xo/gc/GCObject.hpp +++ /dev/null @@ -1,47 +0,0 @@ -/** @file GCObject.hpp - * - * Generated automagically from ingredients: - * 1. code generator: - * [xo-facet/codegen/genfacet] - * arguments: - * --input [idl/GCObject.json5] - * 2. jinja2 template for facet .hpp file: - * [facet.hpp.j2] - * 3. idl for facet methods - * [idl/GCObject.json5] - **/ - -#pragma once - -#include "detail/AGCObject.hpp" -#include "detail/IGCObject_Any.hpp" -#include "detail/IGCObject_Xfer.hpp" -#include "detail/RGCObject.hpp" - -namespace xo { - namespace mm { - /** defined here to avoid #include cycle, since - * template obj awkward to make available there - **/ - template - template - void - RCollector::forward_inplace(xo::facet::obj * p_obj) - { - this->forward_inplace(p_obj->iface(), (void **)&(p_obj->data_)); - } - - template - template - void - RCollector::forward_inplace(DRepr ** p_repr) - { - // fetch static interface for DRepr - auto iface = xo::facet::impl_for(); - - this->forward_inplace(&iface, (void **)p_repr); - } - } -} - -/* end GCObject.hpp */ diff --git a/include/xo/gc/GCObjectConversion.hpp b/include/xo/gc/GCObjectConversion.hpp index 53148bfe..59ec12fa 100644 --- a/include/xo/gc/GCObjectConversion.hpp +++ b/include/xo/gc/GCObjectConversion.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include #include #include #include diff --git a/include/xo/gc/detail/ACollector.hpp b/include/xo/gc/detail/ACollector.hpp deleted file mode 100644 index bd19fe51..00000000 --- a/include/xo/gc/detail/ACollector.hpp +++ /dev/null @@ -1,83 +0,0 @@ -/** @file ACollector.hpp - * - * @author Roland Conybeare, Dec 2025 - **/ - -#pragma once - -//#include "IGCObject_Any.hpp" - -#include -#include -#include - -#include "generation.hpp" -#include "role.hpp" - -#include -#include - -namespace xo { - namespace mm { - using Copaque = const void *; - using Opaque = void *; - - class AGCObject; - class IGCObject_Any; // see IGCObject_Any.hpp - - /** @class ACollector - * @brief Abstract facet for the XO garbage collector - * - * A collector implementation will also support the @ref AAllocator facet, see also - **/ - class ACollector { - public: - using typeseq = xo::facet::typeseq; - using size_type = std::size_t; - - virtual typeseq _typeseq() const noexcept = 0; - - virtual size_type allocated(Copaque d, - generation g, role r) const noexcept = 0; - virtual size_type reserved(Copaque d, - generation g, role r) const noexcept = 0; - virtual size_type committed(Copaque d, - generation g, role r) const noexcept = 0; - virtual bool is_type_installed(Copaque d, - typeseq tseq) const noexcept = 0; - - /** 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) - **/ - virtual bool install_type(Opaque d, const AGCObject & iface) = 0; - virtual void add_gc_root_poly(Opaque d, obj * p_root) = 0; - //virtual void add_gc_root_typed(Opaque d, typeseq tseq, Opaque * root) = 0; - - /** 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. - **/ - virtual void request_gc(Opaque d, generation upto) = 0; - - /** evacuate @p *lhs, that refers to state with interface @p lhs_iface, - * to collector @p d's to-space. Replace *lhs_data with forwarding pointer - * - * Require: gc in progress - **/ - virtual void forward_inplace(Opaque d, AGCObject * lhs_iface, void ** lhs_data) = 0; - }; - } - -} /*namespace xo*/ diff --git a/include/xo/gc/detail/AGCObject.hpp b/include/xo/gc/detail/AGCObject.hpp deleted file mode 100644 index 6f6e7b92..00000000 --- a/include/xo/gc/detail/AGCObject.hpp +++ /dev/null @@ -1,87 +0,0 @@ -/** @file AGCObject.hpp - * - * Generated automagically from ingredients: - * 1. code generator: - * [xo-facet/codegen/genfacet] - * arguments: - * --input [idl/GCObject.json5] - * 2. jinja2 template for abstract facet .hpp file: - * [abstract_facet.hpp.j2] - * 3. idl for facet methods - * [idl/GCObject.json5] - **/ - -#pragma once - -// includes (via {facet_includes}) -#include -#include -#include -#include -#include -#include -#include - -namespace xo { namespace mm { class ACollector; }} - -namespace xo { -namespace mm { - -using Copaque = const void *; -using Opaque = void *; - -/** -GC hooks for collector-aware data -**/ -class AGCObject { -public: - /** @defgroup mm-gcobject-type-traits **/ - ///@{ - // types - /** integer identifying a type **/ - using typeseq = xo::facet::typeseq; - using Copaque = const void *; - using Opaque = void *; - /** type for an amount of memory **/ - using size_type = std::size_t; - /** fomo allocator type **/ - using AAllocator = xo::mm::AAllocator; - /** fomo collector type **/ - using ACollector = xo::mm::ACollector; - ///@} - - /** @defgroup mm-gcobject-methods **/ - ///@{ - // const methods - /** RTTI: unique id# for actual runtime data representation **/ - virtual typeseq _typeseq() const noexcept = 0; - /** destroy instance @p d; calls c++ dtor only for actual runtime type; does not recover memory **/ - virtual void _drop(Opaque d) const noexcept = 0; - /** memory consumption for this instance **/ - virtual size_type shallow_size(Copaque data) const noexcept = 0; - /** copy instance using allocator **/ - virtual Opaque shallow_copy(Copaque data, obj mm) const noexcept = 0; - - // nonconst methods - /** during GC: forward immdiate children **/ - virtual size_type forward_children(Opaque data, obj gc) const noexcept = 0; - ///@} -}; /*AGCObject*/ - -/** Implementation IGCObject_DRepr of AGCObject for state DRepr - * should provide a specialization: - * - * template <> - * struct xo::facet::FacetImplementation { - * using Impltype = IGCObject_DRepr; - * }; - * - * then IGCObject_ImplType --> IGCObject_DRepr - **/ -template -using IGCObject_ImplType = xo::facet::FacetImplType; - -} /*namespace mm*/ -} /*namespace xo*/ - -/* AGCObject.hpp */ diff --git a/include/xo/gc/detail/ICollector_Any.hpp b/include/xo/gc/detail/ICollector_Any.hpp deleted file mode 100644 index 6ed758c2..00000000 --- a/include/xo/gc/detail/ICollector_Any.hpp +++ /dev/null @@ -1,55 +0,0 @@ -/** @file ICollector_Any.hpp - * - * @author Roland Conybeare, Dec 2025 - **/ - -#pragma once - -#include "ACollector.hpp" -#include "AGCObject.hpp" -//#include - -namespace xo { - namespace mm { struct ICollector_Any; } - - namespace facet { - template <> - struct FacetImplementation { - using ImplType = xo::mm::ICollector_Any; - }; - } - - namespace mm { - /** @class ICollector_Any - * @brief Stub Collector Implementation for empty variant instance - **/ - struct ICollector_Any : public ACollector { - using typeseq = xo::facet::typeseq; - using size_type = std::size_t; - - // from ACollector - typeseq _typeseq() const noexcept override { return s_typeseq; } - - // const methods - [[noreturn]] size_type allocated(Copaque, generation, role) const noexcept override { _fatal(); } - [[noreturn]] size_type reserved(Copaque, generation, role) const noexcept override { _fatal(); } - [[noreturn]] size_type committed(Copaque, generation, role) const noexcept override { _fatal(); } - [[noreturn]] bool is_type_installed(Copaque, typeseq) const noexcept override { _fatal(); } - - // non-const methods - [[noreturn]] bool install_type(Opaque, const AGCObject &) noexcept override { _fatal(); } - [[noreturn]] void add_gc_root_poly(Opaque, obj *) override { _fatal(); } - [[noreturn]] void request_gc(Opaque, generation) override { _fatal(); } - [[noreturn]] void forward_inplace(Opaque, AGCObject *, void **) override { _fatal(); } - - private: - [[noreturn]] static void _fatal(); - - public: - static typeseq s_typeseq; - static bool _valid; - }; - } /*namespace mm*/ -} /*namespace xo*/ - -/* end ICollector_Any.hpp */ diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index 61010310..0475a535 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -3,8 +3,8 @@ * @author Roland Conybeare, Dec 2025 **/ -#include "ACollector.hpp" -#include "ICollector_Xfer.hpp" +#include +#include #include "DX1Collector.hpp" namespace xo { diff --git a/include/xo/gc/detail/ICollector_Xfer.hpp b/include/xo/gc/detail/ICollector_Xfer.hpp deleted file mode 100644 index d7d43ef4..00000000 --- a/include/xo/gc/detail/ICollector_Xfer.hpp +++ /dev/null @@ -1,83 +0,0 @@ -/** @file ICollector_Xfer.hpp - * - * @author Roland Conybeare, 2025 - **/ - -#pragma once - -#include "ACollector.hpp" -#include "AGCObject.hpp" - -namespace xo { - namespace mm { - /** @class ICollector_Xfer - * - * Adapts typed ACollector implementation @tparam ICollector_DRepr - * to type-erased @ref ACollector interface - * - * See for example - * @ref ICollector_DX1Collector - **/ - template - struct ICollector_Xfer : public ACollector { - public: - using Impl = ICollector_DRepr; - using size_type = ACollector::size_type; - - static const DRepr & _dcast(Copaque d) { return *(const DRepr *)d; } - static DRepr & _dcast(Opaque d) { return *(DRepr *)d; } - - // from ACollector - - // const methods - - typeseq _typeseq() const noexcept override { return s_typeseq; } - size_type allocated(Copaque d, generation g, role r) const noexcept override { - return I::allocated(_dcast(d), g, r); - } - size_type reserved(Copaque d, generation g, role r) const noexcept override { - return I::reserved(_dcast(d), g, r); - } - size_type committed(Copaque d, generation g, role r) const noexcept override { - return I::committed(_dcast(d), g, r); - } - bool is_type_installed(Copaque d, typeseq tseq) const noexcept override { - return I::is_type_installed(_dcast(d), tseq); - } - - // non-const methods - - bool install_type(Opaque d, const AGCObject & iface) override { - return I::install_type(_dcast(d), iface); - } - void add_gc_root_poly(Opaque d, obj * p_root) override { - I::add_gc_root_poly(_dcast(d), p_root); - } - void request_gc(Opaque d, generation upto) override { - I::request_gc(_dcast(d), upto); - } - void forward_inplace(Opaque d, - AGCObject * lhs_iface, void ** lhs_data) override { - I::forward_inplace(_dcast(d), lhs_iface, lhs_data); - } - - private: - using I = Impl; - - public: - static typeseq s_typeseq; - static bool _valid; - }; - - template - xo::facet::typeseq - ICollector_Xfer::s_typeseq = facet::typeseq::id(); - - template - bool - ICollector_Xfer::_valid = facet::valid_facet_implementation(); - - } /*namespace mm*/ -} /*namespace xo*/ - -/* end ICollector_Xfer.hpp */ diff --git a/include/xo/gc/detail/IGCObject_Any.hpp b/include/xo/gc/detail/IGCObject_Any.hpp deleted file mode 100644 index 9ff7bb2e..00000000 --- a/include/xo/gc/detail/IGCObject_Any.hpp +++ /dev/null @@ -1,93 +0,0 @@ -/** @file IGCObject_Any.hpp - * - * Generated automagically from ingredients: - * 1. code generator: - * [xo-facet/codegen/genfacet] - * arguments: - * --input [idl/GCObject.json5] - * 2. jinja2 template for abstract facet .hpp file: - * [iface_facet_any.hpp.j2] - * 3. idl for facet methods - * [idl/GCObject.json5] - **/ - -#pragma once - -#include "AGCObject.hpp" -#include - -namespace xo { namespace mm { class IGCObject_Any; } } - -namespace xo { -namespace facet { - -template <> -struct FacetImplementation -{ - using ImplType = xo::mm::IGCObject_Any; -}; - -} -} - -namespace xo { -namespace mm { - - /** @class IGCObject_Any - * @brief AGCObject implementation for empty variant instance - **/ - class IGCObject_Any : public AGCObject { - public: - /** @defgroup mm-gcobject-any-type-traits **/ - ///@{ - - /** integer identifying a type **/ - using typeseq = xo::facet::typeseq; - using size_type = AGCObject::size_type; - using AAllocator = AGCObject::AAllocator; - using ACollector = AGCObject::ACollector; - - ///@} - /** @defgroup mm-gcobject-any-methods **/ - ///@{ - - const AGCObject * iface() const { return std::launder(this); } - - // from AGCObject - - // builtin methods - typeseq _typeseq() const noexcept override { return s_typeseq; } - [[noreturn]] void _drop(Opaque) const noexcept override { _fatal(); } - - // const methods - [[noreturn]] size_type shallow_size(Copaque) const noexcept override { _fatal(); } - [[noreturn]] Opaque shallow_copy(Copaque, obj) const noexcept override { _fatal(); } - - // nonconst methods - [[noreturn]] size_type forward_children(Opaque, obj) const noexcept override; - - ///@} - - private: - /** @defgraoup mm-gcobject-any-private-methods **/ - ///@{ - - [[noreturn]] static void _fatal(); - - ///@} - - public: - /** @defgroup mm-gcobject-any-member-vars **/ - ///@{ - - static typeseq s_typeseq; - static bool _valid; - - ///@} - }; - -} /*namespace mm */ -} /*namespace xo */ - -/* IGCObject_Any.hpp */ diff --git a/include/xo/gc/detail/IGCObject_Xfer.hpp b/include/xo/gc/detail/IGCObject_Xfer.hpp deleted file mode 100644 index 7f094ab7..00000000 --- a/include/xo/gc/detail/IGCObject_Xfer.hpp +++ /dev/null @@ -1,95 +0,0 @@ -/** @file IGCObject_Xfer.hpp - * - * Generated automagically from ingredients: - * 1. code generator: - * [xo-facet/codegen/genfacet] - * arguments: - * --input [idl/GCObject.json5] - * 2. jinja2 template for abstract facet .hpp file: - * [iface_facet_any.hpp.j2] - * 3. idl for facet methods - * [idl/GCObject.json5] - **/ - -#pragma once - -#include -#include -#include -#include - -namespace xo { -namespace mm { - /** @class IGCObject_Xfer - **/ - template - class IGCObject_Xfer : public AGCObject { - public: - /** @defgroup mm-gcobject-xfer-type-traits **/ - ///@{ - /** actual implementation (not generated; often delegates to DRepr) **/ - using Impl = IGCObject_DRepr; - /** integer identifying a type **/ - using typeseq = AGCObject::typeseq; - using size_type = AGCObject::size_type; - using AAllocator = AGCObject::AAllocator; - using ACollector = AGCObject::ACollector; - ///@} - - /** @defgroup mm-gcobject-xfer-methods **/ - ///@{ - - static const DRepr & _dcast(Copaque d) { return *(const DRepr *)d; } - static DRepr & _dcast(Opaque d) { return *(DRepr *)d; } - - // from AGCObject - - // builtin methods - typeseq _typeseq() const noexcept override { return s_typeseq; } - void _drop(Opaque d) const noexcept override { _dcast(d).~DRepr(); } - - // const methods - size_type shallow_size(Copaque data) const noexcept override { - return I::shallow_size(_dcast(data)); - } - Opaque shallow_copy(Copaque data, obj mm) const noexcept override { - return I::shallow_copy(_dcast(data), mm); - } - - // non-const methods - size_type forward_children(Opaque data, obj gc) const noexcept override { - return I::forward_children(_dcast(data), gc); - } - - ///@} - - private: - using I = Impl; - - public: - /** @defgroup mm-gcobject-xfer-member-vars **/ - ///@{ - - /** typeseq for template parameter DRepr **/ - static typeseq s_typeseq; - /** true iff satisfies facet implementation **/ - static bool _valid; - - ///@} - }; - - template - xo::facet::typeseq - IGCObject_Xfer::s_typeseq - = xo::facet::typeseq::id(); - - template - bool - IGCObject_Xfer::_valid - = xo::facet::valid_facet_implementation(); - -} /*namespace mm */ -} /*namespace xo*/ - -/* end IGCObject_Xfer.hpp */ diff --git a/include/xo/gc/detail/RCollector.hpp b/include/xo/gc/detail/RCollector.hpp deleted file mode 100644 index 9cce4844..00000000 --- a/include/xo/gc/detail/RCollector.hpp +++ /dev/null @@ -1,69 +0,0 @@ -/** @file RCollector.hpp - * - * @author Roland Conybeare, Dec 2025 - **/ - -#include "ACollector.hpp" -#include - -namespace xo { - namespace mm { - /** @class RCollector **/ - template - struct RCollector : public Object { - private: - using O = Object; - public: - using ObjectType = Object; - using DataPtr = Object::DataPtr; - using size_type = std::size_t; - using typeseq = ACollector::typeseq; - //using value_type = std::byte *; - - RCollector() = default; - RCollector(DataPtr data) : Object{std::move(data)} {} - - /** forward op in place. Defined in GCObject.hpp to avoid #include cycle **/ - template - void forward_inplace(obj * p_obj); - - /** another convenience template for forwarding. - * Defined in RGCObject.hpp to avoid #include cycle. - **/ - template - void forward_inplace(DRepr ** pp_repr); - - int32_t _typeseq() const noexcept { return O::iface()->_typeseq(); } - size_type allocated(generation g, role r) const noexcept { return O::iface()->allocated(O::data(), g, r); } - size_type reserved(generation g, role r) const noexcept { return O::iface()->reserved(O::data(), g, r); } - size_type committed(generation g, role r) const noexcept { return O::iface()->committed(O::data(), g, r); } - bool is_type_installed(typeseq tseq) const noexcept { return O::iface()->is_type_installed(O::data(), tseq); } - - bool install_type(const AGCObject & iface) { return O::iface()->install_type(O::data(), iface); } - void add_gc_root_poly(obj * p_root) { O::iface()->add_gc_root_poly(O::data(), p_root); } - void request_gc(generation g) { O::iface()->request_gc(O::data(), g); } - void forward_inplace(AGCObject * lhs_iface, void ** lhs_data) { O::iface()->forward_inplace(O::data(), lhs_iface, lhs_data); } - - /** add root @p p_root **/ - template - void add_gc_root(obj * p_root) { - O::iface()->add_gc_root_poly(O::data(), (obj *)p_root); - } - - static bool _valid; - }; - - template - bool - RCollector::_valid = facet::valid_object_router(); - } /*namespace mm*/ - - namespace facet { - template - struct RoutingFor { - using RoutingType = xo::mm::RCollector; - }; - } -} /*namespace xo*/ - -/* end RCollector.hpp */ diff --git a/include/xo/gc/detail/RGCObject.hpp b/include/xo/gc/detail/RGCObject.hpp deleted file mode 100644 index d24994a8..00000000 --- a/include/xo/gc/detail/RGCObject.hpp +++ /dev/null @@ -1,93 +0,0 @@ -/** @file RGCObject.hpp - * - * Generated automagically from ingredients: - * 1. code generator: - * [xo-facet/codegen/genfacet] - * arguments: - * --input [idl/GCObject.json5] - * 2. jinja2 template for abstract facet .hpp file: - * [iface_facet_any.hpp.j2] - * 3. idl for facet methods - * [idl/GCObject.json5] - **/ - -#pragma once - -#include "AGCObject.hpp" - -namespace xo { -namespace mm { - -/** @class RGCObject - **/ -template -class RGCObject : public Object { -private: - using O = Object; - -public: - /** @defgroup mm-gcobject-router-type-traits **/ - ///@{ - using ObjectType = Object; - using DataPtr = Object::DataPtr; - using typeseq = xo::reflect::typeseq; - using size_type = AGCObject::size_type; - using AAllocator = AGCObject::AAllocator; - using ACollector = AGCObject::ACollector; - ///@} - - /** @defgroup mm-gcobject-router-ctors **/ - ///@{ - RGCObject() {} - RGCObject(Object::DataPtr data) : Object{std::move(data)} {} - RGCObject(const AGCObject * iface, void * data) - requires std::is_same_v - : Object(iface, data) {} - - ///@} - /** @defgroup mm-gcobject-router-methods **/ - ///@{ - - // explicit injected content - - // builtin methods - typeseq _typeseq() const noexcept { return O::iface()->_typeseq(); } - void _drop() const noexcept { O::iface()->_drop(O::data()); } - - // const methods - size_type shallow_size() const noexcept { - return O::iface()->shallow_size(O::data()); - } - Opaque shallow_copy(obj mm) const noexcept { - return O::iface()->shallow_copy(O::data(), mm); - } - - // non-const methods (still const in router!) - size_type forward_children(obj gc) noexcept { - return O::iface()->forward_children(O::data(), gc); - } - - ///@} - /** @defgroup mm-gcobject-member-vars **/ - ///@{ - - static bool _valid; - - ///@} -}; - -template -bool -RGCObject::_valid = xo::facet::valid_object_router(); - -} /*namespace mm*/ -} /*namespace xo*/ - -namespace xo { namespace facet { - template - struct RoutingFor { - using RoutingType = xo::mm::RGCObject; - }; -} } - -/* end RGCObject.hpp */ diff --git a/include/xo/gc/generation.hpp b/include/xo/gc/generation.hpp deleted file mode 100644 index 12b36d06..00000000 --- a/include/xo/gc/generation.hpp +++ /dev/null @@ -1,36 +0,0 @@ -/** @file generation.hpp - * - * @author Roland Conybeare, Dec 2025 - **/ - -#pragma once - -#include -#include - -namespace xo { - namespace mm { - /** hard maximum number of generations **/ - static constexpr uint32_t c_max_generation = 16; - - /** @class generation - * @brief type-safe generation number - **/ - struct generation { - using value_type = std::uint32_t; - - constexpr generation() = default; - explicit constexpr generation(value_type x) : value_{x} {} - - static generation nursery() { return generation{0}; } - - constexpr operator value_type() const { return value_; } - - generation & operator++() { ++value_; return *this; } - - std::uint32_t value_ = 0; - }; - } -} - -/* end generation.hpp */ diff --git a/include/xo/gc/role.hpp b/include/xo/gc/role.hpp deleted file mode 100644 index 58fc72a2..00000000 --- a/include/xo/gc/role.hpp +++ /dev/null @@ -1,35 +0,0 @@ -/** @file role.hpp - * - * @author Roland Conybeare, Dec 2025 - **/ - -#pragma once - -#include -#include - -namespace xo { - namespace mm { - static constexpr uint32_t c_n_role = 2; - - struct role { - using value_type = std::uint32_t; - - explicit constexpr role(value_type x) : role_{x} {} - - static constexpr role to_space() { return role{0}; } - static constexpr role from_space() { return role{1}; } - - static constexpr std::array all() { return {{to_space(), from_space()}}; } - - static constexpr role begin() { return role{0}; } - static constexpr role end() { return role{2}; } - - operator value_type() const { return role_; } - - value_type role_ = 0; - }; - } /*namespace mm*/ -} /*namespace xo*/ - -/* end role.hpp */ diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 8cbd0bbd..78d27340 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -3,13 +3,14 @@ * @author Roland Conybeare, Dec 2025 **/ -#include "Allocator.hpp" +#include +#include #include "detail/IAllocator_DX1Collector.hpp" #include "detail/ICollector_DX1Collector.hpp" #include "arena/IAllocator_DArena.hpp" -#include "xo/gc/DX1Collector.hpp" -#include "xo/gc/DX1CollectorIterator.hpp" -#include "generation.hpp" +#include +#include +#include #include "object_age.hpp" #include #include diff --git a/src/gc/ICollector_Any.cpp b/src/gc/ICollector_Any.cpp index 301524fe..b39aa99e 100644 --- a/src/gc/ICollector_Any.cpp +++ b/src/gc/ICollector_Any.cpp @@ -3,7 +3,7 @@ * @author Roland Conybeare, Dec 2025 **/ -#include "detail/ICollector_Any.hpp" +#include "gc/ICollector_Any.hpp" #include #include diff --git a/src/gc/IGCObject_Any.cpp b/src/gc/IGCObject_Any.cpp index 95c8cc84..10ff8f6a 100644 --- a/src/gc/IGCObject_Any.cpp +++ b/src/gc/IGCObject_Any.cpp @@ -2,7 +2,7 @@ * **/ -#include "detail/IGCObject_Any.hpp" +#include "gc/IGCObject_Any.hpp" #include #include From 801b19dbcf852a674fa5a36139fcf65f8c7ad074 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 5 Mar 2026 00:50:58 +1100 Subject: [PATCH 051/174] refactor: + xo-stringtable2 w/ DString impl --- include/xo/gc/CollectorTypeRegistry.hpp | 74 ------------------------- src/gc/CMakeLists.txt | 5 -- src/gc/CollectorTypeRegistry.cpp | 45 --------------- src/gc/ICollector_Any.cpp | 40 ------------- src/gc/IGCObject_Any.cpp | 48 ---------------- 5 files changed, 212 deletions(-) delete mode 100644 include/xo/gc/CollectorTypeRegistry.hpp delete mode 100644 src/gc/CollectorTypeRegistry.cpp delete mode 100644 src/gc/ICollector_Any.cpp delete mode 100644 src/gc/IGCObject_Any.cpp diff --git a/include/xo/gc/CollectorTypeRegistry.hpp b/include/xo/gc/CollectorTypeRegistry.hpp deleted file mode 100644 index 4474e20c..00000000 --- a/include/xo/gc/CollectorTypeRegistry.hpp +++ /dev/null @@ -1,74 +0,0 @@ -/** @file CollectorTypeRegistry.hpp - * - * @brief Runtime type registration for gc-aware types - * - * @author Roland Conybeare, Jan 2026 - **/ - -#pragma once - -#include "Collector.hpp" -#include - -namespace xo { - namespace mm { - /** @class CollectorTypeRegistry - * - * @brief Runtime registry for gc-aware types - * - * Singleton to remember known gc-aware types; - * use to simplify registering such types - * with a collector instance. - * - * Remark: splitting work here between - * 1. static initializer work: tracking gc-aware types, - * 2. runtime post-configuration work: report - * gc-aware types to GC instances - * - * Use: - * 1. subsystem foo provides function foo_register_types(obj gc) - * Function calls - * gc.install_type(impl_for()) - * for each gc-aware type provided by subsystem foo - * - * Example: in file xo-object2/src/object2/object2_register_types.cpp, see - * object2_register_types() - * - * 2. during subsystem init, call - * CollectorTypeRegistry::instance().register_types(&foo_register_types); - * - * Example: in file xo-object2/src/object2/init_object2.cpp, see - * InitSubsys::init() - * - * 3. during Collector setup, call - * obj gc = ...; - * CollectorTypeRegistry::instance().install_types(gc); - * - * Example: in file xo-object2/utest/X1Collector.test.cpp - * TEST_CASE("x1") - **/ - class CollectorTypeRegistry { - public: - using init_function_type = std::function)>; - - public: - /** singleton instance **/ - static CollectorTypeRegistry & instance(); - - /** remember a gc-aware type-registration function **/ - void register_types(init_function_type init_fn); - - /** register known GC-aware types with @p gc. - * Calls @c gc.isntall_type() for each - * such type. - **/ - bool install_types(obj gc); - - private: - /** initialization steps for a new Collector instance **/ - std::vector init_seq_v_; - }; - } -} - -/* end CollectorTypeRegistry.hpp */ diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index 985592b0..6690932f 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -3,11 +3,6 @@ set(SELF_LIB xo_gc) set(SELF_SRCS - CollectorTypeRegistry.cpp - - ICollector_Any.cpp - IGCObject_Any.cpp - IAllocator_DX1Collector.cpp ICollector_DX1Collector.cpp IAllocIterator_DX1CollectorIterator.cpp diff --git a/src/gc/CollectorTypeRegistry.cpp b/src/gc/CollectorTypeRegistry.cpp deleted file mode 100644 index cb039dd8..00000000 --- a/src/gc/CollectorTypeRegistry.cpp +++ /dev/null @@ -1,45 +0,0 @@ -/** @file CollectorTypeRegistry.cpp - **/ - -#include "CollectorTypeRegistry.hpp" -#include - -namespace xo { - namespace mm { - CollectorTypeRegistry & - CollectorTypeRegistry::instance() { - static CollectorTypeRegistry s_instance; - - return s_instance; - } - - void - CollectorTypeRegistry::register_types(init_function_type fn) { - scope log(XO_DEBUG(true)); - - init_seq_v_.push_back(fn); - } - - bool - CollectorTypeRegistry::install_types(obj gc) { - scope log(XO_DEBUG(true)); - - bool ok = true; - - size_t i = 0; - size_t n = init_seq_v_.size(); - log && log("run n init steps", xtag("n", n)); - - for (const auto & fn : init_seq_v_) { - log && log("do install fn (", i+1, "/", n, ")"); - - ok = ok & fn(gc); - } - - return ok; - } - - } /*namespace mm*/ -} /*namespace xo*/ - -/* end CollectorTypeRegistry.cpp */ diff --git a/src/gc/ICollector_Any.cpp b/src/gc/ICollector_Any.cpp deleted file mode 100644 index b39aa99e..00000000 --- a/src/gc/ICollector_Any.cpp +++ /dev/null @@ -1,40 +0,0 @@ -/** @file ICollector_Any.cpp - * - * @author Roland Conybeare, Dec 2025 - **/ - -#include "gc/ICollector_Any.hpp" -#include -#include - -namespace xo { - using xo::facet::DVariantPlaceholder; - using xo::facet::typeseq; - using xo::facet::valid_facet_implementation; - - namespace mm { - - void - ICollector_Any::_fatal() { - /* control here on uninitialized ICollector_Any. - * Initialized instance will have specific implementation type - * e.g. ICollector_Xfer - */ - - std::cerr << "fatal" - << ": attempt to call uninitialized" - << " ICollector_Any method" - << std::endl; - std::terminate(); - } - - typeseq - ICollector_Any::s_typeseq = typeseq::id(); - - bool - ICollector_Any::_valid = valid_facet_implementation(); - - } /*namespace mm*/ -} /*namespace xo*/ - -/** end ICollector_Any.cpp */ diff --git a/src/gc/IGCObject_Any.cpp b/src/gc/IGCObject_Any.cpp deleted file mode 100644 index 10ff8f6a..00000000 --- a/src/gc/IGCObject_Any.cpp +++ /dev/null @@ -1,48 +0,0 @@ -/** @file IGCObject_Any.cpp - * - **/ - -#include "gc/IGCObject_Any.hpp" -#include -#include - -namespace xo { -namespace mm { - -using xo::facet::DVariantPlaceholder; -using xo::facet::typeseq; -using xo::facet::valid_facet_implementation; - -void -IGCObject_Any::_fatal() -{ - /* control here on uninitialized IAllocator_Any. - * Initialized instance will have specific implementation type - */ - std::cerr << "fatal" - << ": attempt to call uninitialized" - << " IGCObject_Any method" - << std::endl; - std::terminate(); -} - -typeseq -IGCObject_Any::s_typeseq = typeseq::id(); - -bool -IGCObject_Any::_valid - = valid_facet_implementation(); - -// nonconst methods - -auto -IGCObject_Any::forward_children(Opaque, obj) const noexcept -> size_type -{ - _fatal(); -} - - -} /*namespace mm*/ -} /*namespace xo*/ - -/* end IGCObject_Any.cpp */ From 413616c0984603b76df764c9d98a600a518de0d3 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 5 Mar 2026 13:02:12 +1100 Subject: [PATCH 052/174] xo-interpreter2 stack: refactor: string clases -> xo-stringtable2/ --- CMakeLists.txt | 13 -------- idl/GCObject.json5 | 83 ---------------------------------------------- 2 files changed, 96 deletions(-) delete mode 100644 idl/GCObject.json5 diff --git a/CMakeLists.txt b/CMakeLists.txt index 573e5acc..2edb0ac3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,19 +18,6 @@ add_definitions(${PROJECT_CXX_FLAGS}) # ---------------------------------------------------------------- -# note: manual target; generated code committed to git -xo_add_genfacet( - TARGET xo-gc-facet-gcobject - FACET GCObject - INPUT idl/GCObject.json5 -) - -# ---------------------------------------------------------------- - -xo_add_genfacet_all(xo-gc-genfacet-all) - -# ---------------------------------------------------------------- - # must complete definition of expression lib before configuring examples add_subdirectory(src/gc) add_subdirectory(utest) diff --git a/idl/GCObject.json5 b/idl/GCObject.json5 deleted file mode 100644 index 43747582..00000000 --- a/idl/GCObject.json5 +++ /dev/null @@ -1,83 +0,0 @@ -{ - mode: "facet", - output_cpp_dir: "src/gc", - output_hpp_dir: "include/xo/gc", - output_impl_subdir: "detail", - includes: [ - "", - "", - "", - "", - ], - // extra includes in GCObject.hpp, if any - user_hpp_includes: [], - namespace1: "xo", - namespace2: "mm", - pretext: [ - "namespace xo { namespace mm { class ACollector; }}", - ], - facet: "GCObject", - detail_subdir: "detail", - brief: "xxx", - using_doxygen: true, - doc: [ - "GC hooks for collector-aware data" - ], - types: [ - // using size_type = std::size_t - { - name: "size_type", - doc: ["type for an amount of memory"], - definition: "std::size_t", - }, - { - name: "AAllocator", - doc: ["fomo allocator type"], - definition: "xo::mm::AAllocator", - }, - { - name: "ACollector", - doc: ["fomo collector type"], - definition: "xo::mm::ACollector", - }, - ], - const_methods: [ - // size_type shallow_size() const noexcept - { - name: "shallow_size", - doc: ["memory consumption for this instance"], - return_type: "size_type", - args: [], - const: true, - noexcept: true, - attributes: [], - }, - // Opaque shallow_copy(obj>) const noexcept - { - name: "shallow_copy", - doc: ["copy instance using allocator"], - return_type: "Opaque", - args:[ - {type: "obj", name: "mm"}, - ], - const: true, - noexcept: true, - attributes: [], - }, - ], - nonconst_methods: [ - // size_type forward_children(obj) const noexcept - { - name: "forward_children", - doc: ["during GC: forward immdiate children"], - return_type: "size_type", - args: [ - {type: "obj", name: "gc"}, - ], - const: true, - noexcept: true, - attributes: [], - }, - ], - router_facet_explicit_content: [] -} From 79586af2faaa56d92ca006b0623d9346963c4c5a Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 5 Mar 2026 20:11:05 +1100 Subject: [PATCH 053/174] xo-object2 stack: refactor/tidy after GCObject -> alloc2 --- include/xo/gc/GCObjectConversion.hpp | 117 ------------------ .../xo/gc/detail/ICollector_DX1Collector.hpp | 2 + 2 files changed, 2 insertions(+), 117 deletions(-) delete mode 100644 include/xo/gc/GCObjectConversion.hpp diff --git a/include/xo/gc/GCObjectConversion.hpp b/include/xo/gc/GCObjectConversion.hpp deleted file mode 100644 index 59ec12fa..00000000 --- a/include/xo/gc/GCObjectConversion.hpp +++ /dev/null @@ -1,117 +0,0 @@ -/** @file GCObjectConversion.hpp - * - * @author Roland Conybeare, Jan 2026 - **/ - -#pragma once - -#include -#include -#include -#include -#include - -namespace xo { - namespace scm { - /** @brief compile-time conversion obj <-> T - * - * Specialize for each T that participates in conversion. - * Methods here aren't implemented - **/ - template - struct GCObjectConversion { - using AGCObject = xo::mm::AGCObject; - using AAllocator = xo::mm::AAllocator; - - /** find gc-aware representation for @p x. - * If necessary allocate from @p mm, but may - * refer to @p x in-place - **/ - static obj to_gco(obj mm, const T & x); - /** convert to native representation @tparam T from gc-aware - * @p gco. If necessary allocate from @p mm, but - * may instead refer to @p x in-place - **/ - static T from_gco(obj mm, obj gco); - }; - - /** Motivating use-case for GCObjectConversion is to transform - * primitive function arguments and results to/from gc-aware - * representation. - * - * However: Schematika also supports runtime polymorphism - * which leads to primitives that expect obj arguments. - * - * Also, Schematika expression parser needs representation for - * expressions, before type unification. - * - * Consider a function like: - * def fact = lambda (n : i64) { if (n <= 0) then 1 else (n * fact(n - 1)); } - * During expression parsing the rhs argument to multiply has unknown type. - * To construct an expression for input to unification will use polymorphic - * binding for multiply primitive, relying on specialization here for - * its implementation. - **/ - template - struct GCObjectConversion> { - using AGCObject = xo::mm::AGCObject; - using AAllocator = xo::mm::AAllocator; - using FacetRegistry = xo::facet::FacetRegistry; - using DVariantPlaceholder = xo::facet::DVariantPlaceholder; - - static obj to_gco(obj, - obj gco) { - if constexpr (std::is_same_v) { - // trivial conversion! - return gco; - } else if constexpr (std::is_same_v) { - // runtime polymorphism - return FacetRegistry::instance().variant(gco); - } else /* DRepr != DVariantPlaceholder */ { - // known content w/ fat object pointer - return obj(gco.data()); - } - } - - static obj from_gco(obj, - obj gco) { - scope log(XO_DEBUG(false)); - - if constexpr (std::is_same_v) { - // Need accurate handling of DVariantPlaceholder. - // runtime type must be some concrete type. - // Only use obj::from when DRepr is a concrete type - - if constexpr (std::is_same_v) { - // At comptime gco has unknown repr. At runtime - // will have some known repr, which assignment here will transfer - return gco; - } else { - // Runtime conversion to concrete type DRepr - - auto retval = obj::from(gco); - - if (!retval) { - log.retroactively_enable(); - - log && log(xtag("gco.tseq", gco._typeseq())); - log && log(xtag("DRepr.tseq", reflect::typeseq::id())); - } - - assert(retval); - - return retval; - } - } else { - // both runtime and comptime polymorphism - // use same path here, since representation of @p gco - // is type-erased here - - return FacetRegistry::instance().variant(gco); - } - } - }; - } /*namespace scm */ -} /*namespace xo*/ - -/* end GCObjectConversion.hpp */ diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index 0475a535..1db50e23 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -3,6 +3,8 @@ * @author Roland Conybeare, Dec 2025 **/ +#pragma once + #include #include #include "DX1Collector.hpp" From 87af7348141f7b9066d57db420eff45f5dc487e6 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 12 Mar 2026 20:26:08 -0500 Subject: [PATCH 054/174] xo-interpreter2 stack: refactor + bugfix operator expr --- include/xo/gc/detail/ICollector_DX1Collector.hpp | 1 + src/gc/DX1Collector.cpp | 8 ++++++++ src/gc/ICollector_DX1Collector.cpp | 7 +++++++ 3 files changed, 16 insertions(+) diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index 1db50e23..9e4c56a7 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -47,6 +47,7 @@ namespace xo { static bool install_type(DX1Collector & d, const AGCObject & iface); static void add_gc_root_poly(DX1Collector & d, obj * p_root); + static void remove_gc_root_poly(DX1Collector & d, obj * p_root); static void request_gc(DX1Collector & d, generation upto); static void forward_inplace(DX1Collector & d, AGCObject * lhs_iface, void ** lhs_data); diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 78d27340..25665d73 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -287,6 +287,14 @@ namespace xo { *(obj **)mem = p_root; } + void + DX1Collector::remove_gc_root_poly(obj * p_root) noexcept + { + // iterate over roots_, find p_root and drop it + + (void)p_root; + } + void DX1Collector::request_gc(generation upto) noexcept { diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp index e30182de..781fcfea 100644 --- a/src/gc/ICollector_DX1Collector.cpp +++ b/src/gc/ICollector_DX1Collector.cpp @@ -67,6 +67,13 @@ namespace xo { d.add_gc_root_poly(p_root); } + void + ICollector_DX1Collector::remove_gc_root_poly(DX1Collector & d, + obj * p_root) + { + d.remove_gc_root_poly(p_root); + } + void ICollector_DX1Collector::request_gc(DX1Collector & d, generation upto) From 67bed95cc2fdb2fee8107cd97036f6cd61c8bb94 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 12 Mar 2026 20:30:45 -0500 Subject: [PATCH 055/174] xo-gc: + remove_gc_root_poly() --- include/xo/gc/DX1Collector.hpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 2ef7148a..539c77eb 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -155,9 +155,12 @@ namespace xo { **/ bool install_type(const AGCObject & meta) noexcept; - /** add GC root at @p root_addr, with type @p typeseq **/ + /** add GC root at @p *p_root **/ void add_gc_root_poly(obj * p_root) noexcept; + /** remove GC root at @p *p_root **/ + void remove_gc_root_poly(obj * p_root) noexcept; + // ----- collection ----- /** Request immediate collection. From 2c2332c0a9514897ea4ca4e30e47442bcab02bc5 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 18 Mar 2026 09:50:01 -0400 Subject: [PATCH 056/174] xo-gc: + MutationLogEntry.hpp [WIP] --- include/xo/gc/MutationLogEntry.hpp | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 include/xo/gc/MutationLogEntry.hpp diff --git a/include/xo/gc/MutationLogEntry.hpp b/include/xo/gc/MutationLogEntry.hpp new file mode 100644 index 00000000..01c36660 --- /dev/null +++ b/include/xo/gc/MutationLogEntry.hpp @@ -0,0 +1,47 @@ +/** @file MutationLogEntry.hpp + * + * @author Roland Conybeare, Mar 2026 + **/ + +#pragma once + +#include "GCObject.hpp" + +namespace xo { + namespace mm { + + /** @brief Track a cross-generational pointer + * + * GC must update pointer when collecting space that target occupies + * + * Design notes: + * - parent must be located at the beginning of an allocation, + * (so that it's immediately preceded by allocation header) + * - destination can be something like + * obj + * but also something else such as + * {obj, obj, ..} + * - for collector need to traverse data pointer *data + **/ + class MutationLogEntry { + public: + MutationLogEntry(void * parent, void ** p_data, obj snap); + + private: + /** address of object containing logged mutation **/ + void * parent_ = nullptr; + /** address of target member of object at address @ref parent_, + * driving this log entry. + **/ + void ** p_data_ = nullptr; + /** AGCObject i/face pointer, asof assignment responsible for this log entry. + * If *p_data_ matches snap_.data(), then AGCObject interface is snap_.iface(). + * Otherwise log entry has been superseded by another assignment. + **/ + obj snap_; + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end MutationLogEntry.hpp */ From 6dc2bf1e939e99dedb341bc330371041a2e5d9eb Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 24 Mar 2026 22:06:20 -0400 Subject: [PATCH 057/174] xo-gc: X1Collector.assign_member() + GCRoots + use ArenaVector Using ArenaVector for mlog. --- include/xo/gc/DX1Collector.hpp | 115 ++++- include/xo/gc/MutationLogEntry.hpp | 2 +- include/xo/gc/X1CollectorConfig.hpp | 28 +- .../xo/gc/detail/ICollector_DX1Collector.hpp | 2 + include/xo/gc/object_age.hpp | 12 + src/gc/CMakeLists.txt | 6 + src/gc/DX1Collector.cpp | 445 ++++++++++++++---- src/gc/ICollector_DX1Collector.cpp | 9 + utest/Collector.test.cpp | 40 +- utest/DX1CollectorIterator.test.cpp | 16 +- 10 files changed, 559 insertions(+), 116 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 539c77eb..cf21cf83 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -7,11 +7,13 @@ #include "X1CollectorConfig.hpp" #include "GCObject.hpp" +#include "MutationLogEntry.hpp" #include "generation.hpp" #include "object_age.hpp" #include "role.hpp" #include #include +#include #include #include #include @@ -64,9 +66,35 @@ namespace xo { struct DX1CollectorIterator; + /** @brief GC root struct + * + * A root is traversed much like other gc-owned value: + * a. if root is in GC from-space, move it to to-space. + * b. if root is in GC to-space, skip. + * e.g. root belongs to generation not subject to collection this cycle. + * c. if root it not allocated by GC, still do in-place forward on its + * children. This is load-bearing for ParserStateMachine, for example. + * Allows non-GC object to refer to a dynamic set of gc-owned objects + **/ + struct GCRoot { + public: + GCRoot() = default; + explicit GCRoot(obj * x) : root_{x} {} + + obj * root() { return root_; } + + private: + obj * root_ = nullptr; + }; + // ----- DX1Collector ----- + /** @brief garbage collector 'X1' + **/ struct DX1Collector { + public: + using RootSet = DArenaVector; + using MutationLog = DArenaVector; using typeseq = xo::facet::typeseq; using size_type = DArena::size_type; using value_type = DArena::value_type; @@ -75,6 +103,7 @@ namespace xo { /** hard max typeseq for collector-registered types **/ static constexpr size_t c_max_typeseq = 4096; + public: /** Create X1 collector instance. **/ explicit DX1Collector(const X1CollectorConfig & cfg); @@ -92,7 +121,7 @@ namespace xo { std::string_view name() const { return config_.name_; } const DArena * get_object_types() const noexcept { return &object_types_; } - const DArena * get_roots() const noexcept { return &roots_; } + const RootSet * get_root_set() const noexcept { return &root_set_; } const DArena * get_space(role r, generation g) const noexcept { return space_[r][g]; } 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); } @@ -120,6 +149,16 @@ namespace xo { **/ bool contains(role r, const void * addr) const noexcept; + /** true iff address @p addr allocated from this collector and currently live + * in role @p r (according to current GC state) + **/ + bool contains_allocated(role r, const void * addr) const noexcept; + + /** generation to which pointer @p addr belongs, given role @p r; + * sentinel if not found in this collector + **/ + generation generation_of(role r, const void * addr) const noexcept; + /** return details from last error (will be in gen0 to-space) **/ AllocError last_error() const noexcept; @@ -224,6 +263,21 @@ namespace xo { **/ bool expand(size_type z) noexcept; + // ----- mutation ----- + + /** Modify a gc-owned pointer @p *p_lhs, within allocation @p parent, + * to point to @p rhs. + * + * Motivation: need special handling for cross-generational pointers with + * incremental gc. + * + * Require: + * - if parent is owned by this collector, it has it's own allocation + * (alloc header immediately precedes object address @c parent.data_) + * - address @p p_lhs falls within extent of allocation for @c parent.data_ + **/ + void assign_member(void * parent, obj * p_lhs, obj rhs); + // ----- iteration ----- /** alloc iterator at begin position **/ @@ -242,16 +296,36 @@ namespace xo { void clear() noexcept; private: + /** aux init function: initialize @ref object_types_ arena **/ + void _init_object_types(const X1CollectorConfig & cfg, std::size_t page_z); + /** aux init function: initialize @ref roots_ arena **/ + void _init_gc_roots(const X1CollectorConfig & cfg, std::size_t page_z); + /** aux init function: initialize @ref mlog_storage_[][] arenas **/ + void _init_mlogs(const X1CollectorConfig & cfg, std::size_t page_z); + /** aux init function: create mutation log **/ + MutationLog _make_mlog(uint32_t igen, char tag_char, size_t mlog_z, std::size_t page_z); + /** aux init function: initialize @ref space_storage_[][] arenas **/ + void _init_space(const X1CollectorConfig & cfg); + /** swap from- and to- roles for all generations < @p upto **/ void swap_roles(generation upto) noexcept; /** copy roots + everything reachable from them, to to-space **/ void copy_roots(generation upto) noexcept; - /** move subgraph at @p from_src to to-space. - * + /** cleanup after gc **/ + void cleanup_phase(generation upto); + + /** move root subgraph at @p from_src to to-space. + * If not in gc-space, visit immediate children and move them. * Require: runstate_.is_running() **/ - void * deep_move(void * from_src, generation upto); + void * _deep_move_root(obj from_src, generation upto); + /** move interior subgraph at @p from_src to to-space. + * no-op if not in gc-space. + **/ + void * _deep_move_interior(void * from_src, generation upto); + /** common driver for _deep_move_root(), _deep_move_interior() **/ + void * _deep_move_gc_owned(void * from_src, generation upto); public: /** garbage collector configuration **/ @@ -270,15 +344,42 @@ namespace xo { /** if > 0: need gc for all generations < gc_pending_upto_ **/ generation gc_pending_upto_; - /** (ab)using arena to get extensible list of root objects. + /** using arena to get extensible list of root objects. * For each root store one address (type obj*) + * + * An Object x that supports AGCObject, but doesn't live in gc-space, + * will get special treatment if it appears in root_set_: + * collector will traverse x to forward pointers to gc-owned + * targets. In other contexts collector doesn't look inside + * non-gc-owned objects + * + * editor bait: root_v **/ - DArena roots_; + RootSet root_set_; + + /** Cross-generational mutations tracked in MutationLogs. + * We need three logs per generation: + * A. one to observe and remember mutations in to-space + * during normal operation (between GC cycles) + * B. during GC: 2nd mlog to hold entries from from-mlog + * that will still be needed post-GC (because ptr direction + * from higher gen to lower gen after cycle). + * C. during GC: 3rd mlog to triage entries for which + * liveness of pointer source isn't yet established. + * + * NOTE: indexed on generation of pointer *destination* + **/ + std::array mlog_storage_[c_n_role + 1]; + + /** mlog pointers. The roles of mlog_storage_[*][g] get permuted + * as each collection cycle proceeds + **/ + std::array mlog_[c_n_role + 1]; /** collector-managed memory here. * - space_[1] is from-space * - space_[0] is to-space - * coordinates with role ingc/role.hpp, see also. + * coordinates with role in gc/role.hpp, see also. **/ /** arena objects for collector managed memory diff --git a/include/xo/gc/MutationLogEntry.hpp b/include/xo/gc/MutationLogEntry.hpp index 01c36660..74598272 100644 --- a/include/xo/gc/MutationLogEntry.hpp +++ b/include/xo/gc/MutationLogEntry.hpp @@ -5,7 +5,7 @@ #pragma once -#include "GCObject.hpp" +#include namespace xo { namespace mm { diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp index dec9545d..db96a43f 100644 --- a/include/xo/gc/X1CollectorConfig.hpp +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -26,6 +26,16 @@ namespace xo { **/ X1CollectorConfig with_size(std::size_t gen_z); + /** copy of this config, + * but with @c debug_flag_ set to @p x + **/ + X1CollectorConfig with_debug_flag(bool x); + + /** copy of this config, + * but with @c sanitize_flag_ set to @p x + **/ + X1CollectorConfig with_sanitize_flag(bool x); + generation age2gen(object_age age) const noexcept { return generation(age % n_survive_threshold_); } @@ -37,13 +47,19 @@ namespace xo { std::string name_; /** Configuration for collector spaces. - * Will have at least {nursery,tenured} x {from,to} spaces. + * Will have (2 x G) of these, + * where G is @ref n_generation_. * Not using name_ member. * * REQUIRE: * - arena_config_.store_header_flag_ must be true **/ - ArenaConfig arena_config_; + ArenaConfig arena_config_ = ArenaConfig().with_store_header_flag(true); + + /** storage for xgen pointer bookkeeping (aka remembered sets). + * Use 3x this value per generation + **/ + std::size_t mutation_log_z_ = 1024; /** storage for N object types requires 8*N bytes **/ std::size_t object_types_z_ = 2*1024*1024; @@ -85,13 +101,17 @@ namespace xo { **/ uint32_t stats_history_z_ = false; + /** true to enable sanitize features: + * 1. zero out from-space at end of GC cycle + **/ + bool sanitize_flag_ = false; + /** true to enable debug logging **/ bool debug_flag_ = false; }; - + } /*namespace mm*/ } /*namespace xo*/ /* end X1CollectorConfig.hpp */ - diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index 9e4c56a7..6984cd2e 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -49,6 +49,8 @@ namespace xo { static void add_gc_root_poly(DX1Collector & d, obj * p_root); static void remove_gc_root_poly(DX1Collector & d, obj * p_root); static void request_gc(DX1Collector & d, generation upto); + static void assign_member(DX1Collector & d, void * parent, + obj * p_lhs, obj & rhs); static void forward_inplace(DX1Collector & d, AGCObject * lhs_iface, void ** lhs_data); static int32_t s_typeseq; diff --git a/include/xo/gc/object_age.hpp b/include/xo/gc/object_age.hpp index ae876bda..fe0a48a8 100644 --- a/include/xo/gc/object_age.hpp +++ b/include/xo/gc/object_age.hpp @@ -26,6 +26,18 @@ namespace xo { std::uint32_t value_; }; + + inline bool operator==(object_age lhs, object_age rhs) { + return lhs.value_ == rhs.value_; + } + + inline bool operator<(object_age lhs, object_age rhs) { + return lhs.value_ < rhs.value_; + } + + inline bool operator>(object_age lhs, object_age rhs) { + return lhs.value_ > rhs.value_; + } } } diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index 6690932f..eee6864b 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -3,6 +3,9 @@ set(SELF_LIB xo_gc) set(SELF_SRCS + init_gc.cpp + SetupGc.cpp + IAllocator_DX1Collector.cpp ICollector_DX1Collector.cpp IAllocIterator_DX1CollectorIterator.cpp @@ -10,6 +13,9 @@ set(SELF_SRCS DX1Collector.cpp DX1CollectorIterator.cpp + X1CollectorConfig.cpp + MutationLogEntry.cpp + ) xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 25665d73..96f65a0e 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -21,27 +21,12 @@ namespace xo { using xo::mm::AAllocator; + using xo::facet::TypeRegistry; using xo::facet::typeseq; using xo::facet::with_facet; namespace mm { - X1CollectorConfig - X1CollectorConfig::with_name(std::string name) - { - X1CollectorConfig copy = *this; - copy.name_ = std::move(name); - return copy; - } - - X1CollectorConfig - X1CollectorConfig::with_size(std::size_t gen_z) - { - X1CollectorConfig copy = *this; - copy.arena_config_ = arena_config_.with_size(gen_z); - return copy; - } - // ----- GCRunState ----- GCRunState::GCRunState(generation gc_upto) @@ -72,45 +57,109 @@ namespace xo { size_t page_z = getpagesize(); + this->_init_object_types(cfg, page_z); + this->_init_gc_roots(cfg, page_z); + this->_init_mlogs(cfg, page_z); + this->_init_space(cfg); + } + + void + DX1Collector::_init_object_types(const X1CollectorConfig & cfg, std::size_t page_z) + { /* 1MB reserved address space enough for up to 128k distinct types. * In this case don't want to use hugepages since actual #of types * likely << .size/8 */ - object_types_ = DArena::map( - ArenaConfig{ - .name_ = "x1-object-types", - .size_ = cfg.object_types_z_, - .hugepage_z_ = page_z, - .store_header_flag_ = false}); + this->object_types_ + = DArena::map(ArenaConfig{.name_ = "x1-object-types", + .size_ = cfg.object_types_z_, + .hugepage_z_ = page_z, + .store_header_flag_ = false}); + } - roots_ = DArena::map( - ArenaConfig{ - .name_ = "x1-object-roots", - .size_ = cfg.object_roots_z_, - .hugepage_z_ = page_z, - .store_header_flag_ = false}); + void + DX1Collector::_init_gc_roots(const X1CollectorConfig & cfg, std::size_t page_z) + { + this->root_set_ + = RootSet::map(ArenaConfig{.name_ = "x1-object-roots", + .size_ = cfg.object_roots_z_, + .hugepage_z_ = page_z, + .store_header_flag_ = false}); + } + + void + DX1Collector::_init_mlogs(const X1CollectorConfig & cfg, std::size_t page_z) + { + for (uint32_t igen = 0, ngen = cfg.n_generation_; igen + 1 < ngen; ++igen) { + // special case: no use for mutation log for youngest generation, + // so don't trouble to allocate one + + if (igen + 1 < c_max_generation) { + this->mlog_storage_[0][igen] = _make_mlog(igen, 'a', cfg.mutation_log_z_, page_z); + this->mlog_storage_[1][igen] = _make_mlog(igen, 'b', cfg.mutation_log_z_, page_z); + this->mlog_storage_[2][igen] = _make_mlog(igen, 'c', cfg.mutation_log_z_, page_z); + + this->mlog_[0][igen] = &mlog_storage_[0][igen]; + this->mlog_[1][igen] = &mlog_storage_[1][igen]; + this->mlog_[2][igen] = &mlog_storage_[2][igen]; + } else { + assert(false); + } + } + + if (cfg.n_generation_ > 0) { + for (uint32_t igen = cfg.n_generation_ - 1; igen + 1 < c_max_generation; ++igen) { + this->mlog_[0][igen] = nullptr; + this->mlog_[1][igen] = nullptr; + this->mlog_[2][igen] = nullptr; + } + } else { + assert(false); + } + } + + auto + DX1Collector::_make_mlog(uint32_t igen, char tag_char, size_t mlog_z, size_t page_z) -> MutationLog + { + char buf[40]; + snprintf(buf, sizeof(buf), "x1-mlog-G%u-%c", igen, tag_char); + + return MutationLog::map(ArenaConfig{.name_ = std::string(buf), + .size_ = mlog_z, + .hugepage_z_ = page_z, + .store_header_flag_ = false}); + } + + void + DX1Collector::_init_space(const X1CollectorConfig & cfg) + { + assert(c_n_role == 2); for (uint32_t igen = 0, ngen = cfg.n_generation_; igen < ngen; ++igen) { - { - char buf[40]; - snprintf(buf, sizeof(buf), "x1-space-G%u-a", igen); + if (igen < c_max_generation) { + { + char buf[40]; + snprintf(buf, sizeof(buf), "x1-space-G%u-a", igen); - space_storage_[0][igen] = DArena::map(cfg.arena_config_.with_name(std::string(buf))); + this->space_storage_[0][igen] = DArena::map(cfg.arena_config_.with_name(std::string(buf))); + } + { + char buf[40]; + snprintf(buf, sizeof(buf), "x1-space-G%u-b", igen); + + this->space_storage_[1][igen] = DArena::map(cfg.arena_config_.with_name(std::string(buf))); + } + + this->space_[role::to_space()][igen] = &space_storage_[0][igen]; + this->space_[role::from_space()][igen] = &space_storage_[1][igen]; + } else { + assert(false); } - { - char buf[40]; - snprintf(buf, sizeof(buf), "x1-space-G%u-b", igen); - - space_storage_[1][igen] = DArena::map(cfg.arena_config_.with_name(std::string(buf))); - } - - space_[role::to_space()][igen] = &space_storage_[0][igen]; - space_[role::from_space()][igen] = &space_storage_[1][igen]; } for (uint32_t igen = cfg.n_generation_; igen < c_max_generation; ++igen) { - space_[role::to_space()][igen] = nullptr; - space_[role::from_space()][igen] = nullptr; + this->space_[role::to_space()][igen] = nullptr; + this->space_[role::from_space()][igen] = nullptr; } } @@ -118,26 +167,49 @@ namespace xo { DX1Collector::visit_pools(const MemorySizeVisitor & visitor) const { object_types_.visit_pools(visitor); - roots_.visit_pools(visitor); + root_set_.visit_pools(visitor); - for (uint32_t i = 0; i < c_n_role; ++i) { - for (uint32_t j = 0; j < config_.n_generation_; ++j) { + for (uint32_t j = 0; j < config_.n_generation_; ++j) { + for (uint32_t i = 0; i < c_n_role; ++i) { space_storage_[i][j].visit_pools(visitor); } } + + for (uint32_t j = 1; j < config_.n_generation_; ++j) { + for (uint32_t i = 0; i < c_n_role + 1; ++i) { + mlog_storage_[i][j].visit_pools(visitor); + } + } } bool DX1Collector::contains(role r, const void * addr) const noexcept + { + return !(this->generation_of(r, addr).is_sentinel()); + } + + bool + DX1Collector::contains_allocated(role r, const void * addr) const noexcept + { + generation g = this->generation_of(r, addr); + + if (g.is_sentinel()) + return false; + + return this->get_space(r, g)->contains_allocated(addr); + } + + generation + DX1Collector::generation_of(role r, const void * addr) const noexcept { for (generation gi{0}; gi < config_.n_generation_; ++gi) { const DArena * arena = get_space(r, gi); if (arena->contains(addr)) - return true; + return gi; } - return false; + return generation::sentinel(); } AllocError @@ -156,7 +228,7 @@ namespace xo { size_t (DArena::* get_stat_fn)() const) noexcept { size_t z1 = (d.object_types_.*get_stat_fn)(); - size_t z2 = (d.roots_.*get_stat_fn)(); + size_t z2 = (d.root_set_.store()->*get_stat_fn)(); size_t z3 = 0; @@ -255,7 +327,11 @@ namespace xo { { AGCObject * v = reinterpret_cast(object_types_.lo_); - return &(v[tseq.seqno()]); + const AGCObject * target = &(v[tseq.seqno()]); + + assert(reinterpret_cast(target) < object_types_.limit_); + + return target; } /* editor bait: register_type */ @@ -279,12 +355,7 @@ namespace xo { void DX1Collector::add_gc_root_poly(obj * p_root) noexcept { - std::byte * mem - = roots_.alloc(typeseq::sentinel(), - sizeof(obj*)); - assert(mem); - - *(obj **)mem = p_root; + root_set_.push_back(GCRoot(p_root)); } void @@ -337,7 +408,10 @@ namespace xo { log && log("step 2b : [STUB] copy pinned"); log && log("step 3a : [STUB] run destructors"); log && log("step 3b : [STUB] keep reachable weak pointers"); + log && log("step 4 : [STUB] cleanup"); + this->cleanup_phase(upto); + } void @@ -352,6 +426,69 @@ namespace xo { } } + void + DX1Collector::cleanup_phase(generation upto) + { + scope log(XO_DEBUG(true), xtag("upto", upto)); + + // everything live has been copied out of from-space + // -> now set to empty + // + for (generation g = generation{0}; g < upto; ++g) { + if (config_.sanitize_flag_) { + space_[role::from_space()][g]->scrub(); + } + + space_[role::from_space()][g]->clear(); + } + + this->runstate_ = GCRunState::gc_not_running(); + } + + void * + DX1Collector::_deep_move_root(obj from_src, + generation upto) + { + // NOTE: + // Some roots are non-gc-owned nodes. + // GC must still visit immediate children of these nodes + // to move gc-owned children. + // This implements virtual root node feature, + // intended to mitigate mutation log churn. + + scope log(XO_DEBUG(config_.debug_flag_)); + + if (!from_src) + return nullptr; + + bool src_in_from_space = this->contains(role::from_space(), from_src.data()); + + if (src_in_from_space) { + return _deep_move_gc_owned(from_src.data(), upto); + } else { + auto self = this->ref(); + from_src.forward_children(self); + return from_src.data(); + } + } + + void * + DX1Collector::_deep_move_interior(void * from_src, + generation upto) + { + scope log(XO_DEBUG(config_.debug_flag_)); + + if (!from_src) + return nullptr; + + bool src_in_from_space = this->contains(role::from_space(), from_src); + + if (!src_in_from_space) + return from_src; + + return _deep_move_gc_owned(from_src, upto); + } + /* * rules: * - from_src must be in from-space @@ -364,24 +501,17 @@ namespace xo { * EDITOR: gc -> self */ void * - DX1Collector::deep_move(void * from_src, generation upto) + DX1Collector::_deep_move_gc_owned(void * from_src, + generation upto) { scope log(XO_DEBUG(config_.debug_flag_)); - if (!from_src) - return nullptr; - - if (!this->contains(role::from_space(), from_src)) { - /* presumeably memory not owned by collector - * (e.g. DBoolean {true, false}, DUniqueString {owned by StringTable} etc.) - */ - return from_src; - } - AllocInfo info = this->alloc_info((std::byte *)from_src); AllocHeader hdr = info.header(); typeseq tseq(info.tseq()); + assert(this->contains_allocated(role::from_space(), from_src)); + if (is_forwarding_header(hdr)) { /* already forwarded - pickup destination * @@ -475,6 +605,9 @@ namespace xo { obj alloc(this); const AGCObject * iface = lookup_type(tseq); + + assert(iface->_has_null_vptr() == false); + void * to_dest = this->shallow_move(iface, from_src); std::size_t fixup_work = 0; @@ -487,23 +620,37 @@ namespace xo { fixup_work = 0; for (generation g = generation{0}; g < upto; ++g) { + /** object index for this pass **/ + size_t i_obj = 0; + /* TODO: use AllocIterator here */ while(gray_lo_v[g] < to_space(g)->free_) { AllocHeader * hdr = (AllocHeader *)gray_lo_v[g]; void * src = (hdr + 1); - log && log("fwd children", xtag("src", src)); - const auto & hdr_cfg = config_.arena_config_.header_; typeseq tseq = typeseq(hdr_cfg.tseq(*hdr)); size_t z = hdr_cfg.size_with_padding(*hdr); + log && log("deep_move_gc_owned: fwd to-space children", + xtag("g", g), + xtag("i_obj", i_obj), + xtag("src", src), + xtag("tseq", tseq), + xtag("tname", TypeRegistry::id2name(tseq)), + xtag("z", z)); + const AGCObject * iface = this->lookup_type(tseq); + + assert(iface->_has_null_vptr() == false); + obj gc(this); iface->forward_children(src, gc); gray_lo_v[g] = ((std::byte *)src) + z; + + ++i_obj; ++fixup_work; } } @@ -512,21 +659,25 @@ namespace xo { log && log(xtag("to_dest", to_dest)); return to_dest; - } + } /*_deep_move_gc_owned*/ void DX1Collector::copy_roots(generation upto) noexcept { scope log(XO_DEBUG(true)); - for (obj ** p_root = (obj **)roots_.lo_; - p_root < (obj **)roots_.free_; ++p_root) - { - log && log("copy root", xtag("**p_root.data.pre", (**p_root).data_)); + for (RootSet::size_type i = 0, n = root_set_.size(); i < n; ++i) { + GCRoot & slot = root_set_[i]; - (*p_root)->reset_opaque(this->deep_move((*p_root)->data_, upto)); + log && log("copy root", + xtag("slot.root()", slot.root()), + xtag("slot.root()->data_", slot.root()->data_)); - log && log(xtag("**p_root.data.post", (**p_root).data_)); + void * root_to = this->_deep_move_root(*slot.root(), upto); + + slot.root()->reset_opaque(root_to); + + log && log(xtag("slot.root()->data_", slot.root()->data_)); } } @@ -590,9 +741,18 @@ namespace xo { /* recover allocation size */ std::size_t alloc_z = some_arena->config_.header_.size_with_padding(alloc_hdr); - log && log(xtag("some_arena.lo", some_arena->lo_), - xtag("p_header", p_header), - xtag("alloc_z", alloc_z)); + if (log) { + log(xtag("some_arena.lo", some_arena->lo_), + xtag("p_header", p_header), + xtag("alloc_z", alloc_z)); + + AllocInfo info = this->alloc_info((std::byte *)object_data); + log(xtag("tseq", info.tseq()), + xtag("tname", TypeRegistry::id2name(typeseq(info.tseq()))), + xtag("is_forwarding_tseq", info.is_forwarding_tseq()), + xtag("age", info.age()), + xtag("size", info.size())); + } /* need to be able to fit forwarding pointer * in place of forwarded object. @@ -616,9 +776,9 @@ namespace xo { * | * (to-space) | * +---+-+----+ | - * |FWD|G|size|<--/ - * +---+-+----+ - * | | + * |TSQ|G|size| | + * +---+-+----+ | + * | | <-/ * | | * | | * +----------+ @@ -638,22 +798,33 @@ namespace xo { * | * | (to-space) * | +---+-+----+ - * \---->|FWD|G|size| - * +---+-+----+ - * | | + * | |TSQ|G|size| + * | +---+-+----+ + * \---> | | * | | * | | * +----------+ */ + + if (log) { + log("lhs_data already forwarded", xtag("dest", dest)); + + AllocInfo info = this->alloc_info((std::byte *)dest); + log(xtag("tseq", info.tseq()), + xtag("tname", TypeRegistry::id2name(typeseq(info.tseq()))), + xtag("age", info.age()), xtag("size", info.size())); + } } else if (this->check_move_policy(alloc_hdr, object_data)) { /* copy object *lhs + replace with forwarding pointer */ + log && log("forward object now"); + /* * lhs obj (from-space) * | +---------+ +---+-+----+ - * \--->| .iface | |FWD|G|size| alloc_hdr + * \--->| .iface | |TSQ|G|size| alloc_hdr * +---------+ object_data +---+-+----+ - * | .data x----------------->| | + * | .data x----------------> | | * +---------+ | | * | | * +----------+ @@ -673,14 +844,16 @@ namespace xo { * | | * | (to-space) | * | +---+-+----+ | - * \---->|FWD|G|size|<--/ - * +---+-+----+ - * | | + * | |TSQ|G|size| | + * | +---+-+----+ | + * \---> | | <-/ * | | * | | * +----------+ */ } else { + log && log("object not eligible/required to forward"); + /* object doesn't need to move. * e.g. incremental collection + object is tenured */ @@ -698,6 +871,9 @@ namespace xo { void * to_dest = iface->shallow_copy(from_src, alloc); log && log(xtag("from_src", from_src), xtag("to_dest", to_dest)); + log && log(xtag("tseq", info.tseq()), + xtag("tname", TypeRegistry::id2name(typeseq(info.tseq()))), + xtag("age", info.age()), xtag("size", info.size())); if(to_dest == from_src) { assert(false); @@ -781,6 +957,95 @@ namespace xo { return this->get_space(role::to_space(), generation{0})->alloc_info(mem); } + // editor bait: write barrier + void + DX1Collector::assign_member(void * parent, obj * p_lhs, obj rhs) + { + scope log(XO_DEBUG(config_.debug_flag_), + xtag("parent", parent), xtag("lhs", p_lhs), xtag("rhs", rhs.data())); + + // ++ stats.n_mutation_; + + *p_lhs = rhs; + + if (runstate_.is_running()) { + // for removal of all doubt: + // don't log mutations during GC cycle + return; + } + + if (!config_.allow_incremental_gc_) { + // only need to log mutations when incremental gc is enabled + return; + } + + // logging policy depends on: + // 1. generation of lhs + // 2. generation of rhs + + generation src_g = this->generation_of(role::to_space(), p_lhs); + + if (src_g.is_sentinel()) { + // only need mlog entries for gc-owned pointers. + // In this case pointer does not originate in gc-owned space + return; + } + + generation dest_g = this->generation_of(role::to_space(), rhs.data()); + + if (dest_g.is_sentinel()) { + // similarly, don't need mlog entry to non-gc-owned destination + return; + } + + if (src_g < dest_g) { + // young-to-old pointers don't need to be remembered, + // since a GC cycle that collects the old generation is guarnatted + // to also collect the young generation. + return; + } + + if (src_g == dest_g) { + // for pointers within the same generation, need to log + // if source is older than destination. + + const DArena * arena = this->get_space(role::to_space(), src_g); + + const DArena::header_type * src_hdr = arena->obj2hdr(parent); + const DArena::header_type * dest_hdr = arena->obj2hdr(rhs.data()); + + assert(src_hdr && dest_hdr); + + if (header2age(*src_hdr) <= header2age(*dest_hdr)) { + // source and destination have the same age; + // therefore are always collected on the same set of GC cycles + // -> no need to remember separately. + return; + } else { + // even though {src,dest} belong to the same generation: + // source will be eligible for promotion before destination. + // At that point pointer would become a cross-generational pointer, + // so need to track it now. + + log && log("xage ptr -> must log"); + } + } else { + log && log("xgen ptr -> must log"); + } + + // control here: we have an older->younger pointer, need to log it + + // mlog keyed by generation in which pointer _destination_ resides: + // collection that moves destination generation around needs to also + // update pointers such as this one + // + MutationLog * mlog = this->mlog_[role::to_space()][dest_g]; + + mlog->push_back(MutationLogEntry(parent, + reinterpret_cast(&(p_lhs->data_)), + rhs)); + } + DX1CollectorIterator DX1Collector::begin() const noexcept { diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp index 781fcfea..fd0c811e 100644 --- a/src/gc/ICollector_DX1Collector.cpp +++ b/src/gc/ICollector_DX1Collector.cpp @@ -81,6 +81,15 @@ namespace xo { d.request_gc(upto); } + void + ICollector_DX1Collector::assign_member(DX1Collector & d, + void * parent, + obj * p_lhs, + obj & rhs) + { + d.assign_member(parent, p_lhs, rhs); + } + void ICollector_DX1Collector::forward_inplace(DX1Collector & d, AGCObject * lhs_iface, diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp index 318c9c21..cf82fa93 100644 --- a/utest/Collector.test.cpp +++ b/utest/Collector.test.cpp @@ -63,10 +63,14 @@ namespace xo { 16 /*size_bits*/), }; X1CollectorConfig cfg = { .arena_config_ = arena_cfg, .n_generation_ = 2, - .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + .gc_trigger_v_ = {{64*1024, 1024*1024, +#ifdef NOPE + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0}} }; + 0, 0, 0, 0 +#endif + }} }; DX1Collector gc = DX1Collector{cfg}; @@ -111,10 +115,14 @@ namespace xo { 16 /*size_bits*/), }; X1CollectorConfig cfg = { .arena_config_ = arena_cfg, .n_generation_ = 2, - .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + .gc_trigger_v_ = {{64*1024, 1024*1024, +#ifdef NOPE + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0}} }; + 0, 0, 0, 0 +#endif + }} }; DX1Collector gc = DX1Collector{cfg}; @@ -137,10 +145,14 @@ namespace xo { 16 /*size-bits*/), }; X1CollectorConfig cfg = { .arena_config_ = arena_cfg, .n_generation_ = 2, - .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + .gc_trigger_v_ = {{64*1024, 1024*1024, +#ifdef NOPE + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0}} }; + 0, 0, 0, 0 +#endif + }} }; DX1Collector gc = DX1Collector{cfg}; @@ -167,10 +179,14 @@ namespace xo { /* collector with one generation collapses to a non-generational copying collector */ X1CollectorConfig cfg = { .arena_config_ = arena_cfg, .n_generation_ = 1, - .gc_trigger_v_ = {{64*1024, 0, 0, 0, + .gc_trigger_v_ = {{64*1024, 0, +#ifdef NOPE + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0}} }; + 0, 0, 0, 0 +#endif + }} }; DX1Collector x1state = DX1Collector{cfg}; @@ -211,10 +227,14 @@ namespace xo { /* collector with one generation collapses to a non-generational copying collector */ X1CollectorConfig cfg = { .arena_config_ = arena_cfg, .n_generation_ = 1, - .gc_trigger_v_ = {{64*1024, 0, 0, 0, + .gc_trigger_v_ = {{64*1024, 0, +#ifdef NOPE + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0}} }; + 0, 0, 0, 0 +#endif + }} }; /* X1 allocator+collector */ DX1Collector x1state = DX1Collector{cfg}; diff --git a/utest/DX1CollectorIterator.test.cpp b/utest/DX1CollectorIterator.test.cpp index 137c07f3..66a9e417 100644 --- a/utest/DX1CollectorIterator.test.cpp +++ b/utest/DX1CollectorIterator.test.cpp @@ -53,10 +53,14 @@ namespace xo { 16 /*size_bits*/), }; X1CollectorConfig cfg = { .arena_config_ = arena_cfg, .n_generation_ = 2, - .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + .gc_trigger_v_ = {{64*1024, 1024*1024, +#ifdef NOPE + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0}} }; + 0, 0, 0, 0 +#endif + }} }; DX1Collector gc = DX1Collector{cfg}; @@ -96,10 +100,14 @@ namespace xo { 16 /*size_bits*/), }; X1CollectorConfig cfg = { .arena_config_ = arena_cfg, .n_generation_ = 2, - .gc_trigger_v_ = {{64*1024, 1024*1024, 0, 0, + .gc_trigger_v_ = {{64*1024, 1024*1024, +#ifdef NOPE + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0}} }; + 0, 0, 0, 0 +#endif + }} }; DX1Collector gc = DX1Collector{cfg}; obj a1o{&gc}; From 57d895a41c856e6a1d4f8173179cc1475c73adf5 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 24 Mar 2026 22:20:23 -0400 Subject: [PATCH 058/174] xo-gc: + scaffold for gc primitives --- include/xo/gc/SetupGc.hpp | 20 ++++++++++++++++ include/xo/gc/init_gc.hpp | 21 ++++++++++++++++ src/gc/MutationLogEntry.cpp | 22 +++++++++++++++++ src/gc/SetupGc.cpp | 36 ++++++++++++++++++++++++++++ src/gc/X1CollectorConfig.cpp | 46 ++++++++++++++++++++++++++++++++++++ src/gc/init_gc.cpp | 34 ++++++++++++++++++++++++++ 6 files changed, 179 insertions(+) create mode 100644 include/xo/gc/SetupGc.hpp create mode 100644 include/xo/gc/init_gc.hpp create mode 100644 src/gc/MutationLogEntry.cpp create mode 100644 src/gc/SetupGc.cpp create mode 100644 src/gc/X1CollectorConfig.cpp create mode 100644 src/gc/init_gc.cpp diff --git a/include/xo/gc/SetupGc.hpp b/include/xo/gc/SetupGc.hpp new file mode 100644 index 00000000..bf5c5c6d --- /dev/null +++ b/include/xo/gc/SetupGc.hpp @@ -0,0 +1,20 @@ +/** @file SetupGc.hpp + * + * @author Roland Conybeare, Mar 2026 + **/ + +#pragma once + +namespace xo { + namespace mm { + + class SetupGc { + public: + /** Register gc (facet,impl) combinations with FacetRegistry **/ + static bool register_facets(); + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end SetupGc.hpp */ diff --git a/include/xo/gc/init_gc.hpp b/include/xo/gc/init_gc.hpp new file mode 100644 index 00000000..7270eea1 --- /dev/null +++ b/include/xo/gc/init_gc.hpp @@ -0,0 +1,21 @@ +/** @file init_gc.hpp + * + * @author Roland Conybeare, Mar 2026 + **/ + +#pragma once + +#include + +namespace xo { + /* tag to represent the xo-gc/ subsystem within ordered initialization */ + enum S_gc_tag {}; + + template <> + struct InitSubsys { + static void init(); + static InitEvidence require(); + }; +} /*namespace xo*/ + +/* end init_gc.hpp */ diff --git a/src/gc/MutationLogEntry.cpp b/src/gc/MutationLogEntry.cpp new file mode 100644 index 00000000..6e3a0edc --- /dev/null +++ b/src/gc/MutationLogEntry.cpp @@ -0,0 +1,22 @@ +/** @file MutationLogEntry.cpp + * + * @author Roland Conybeare, Mar 2026 + **/ + +#include "MutationLogEntry.hpp" + +namespace xo { + namespace mm { + + MutationLogEntry::MutationLogEntry(void * parent, + void ** p_data, + obj snap) + : parent_{parent}, + p_data_{p_data}, + snap_{snap} + {} + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end MutationLogEntry.cpp */ diff --git a/src/gc/SetupGc.cpp b/src/gc/SetupGc.cpp new file mode 100644 index 00000000..f99ea4e6 --- /dev/null +++ b/src/gc/SetupGc.cpp @@ -0,0 +1,36 @@ +/** @file SetupGc.cpp + * + * @author Roland Conybeare, Mar 2026 + **/ + +#include "SetupGc.hpp" +#include "X1Collector.hpp" +#include +#include + +namespace xo { + using xo::mm::AAllocator; + using xo::mm::ACollector; + using xo::mm::DX1Collector; + using xo::facet::FacetRegistry; + using xo::reflect::typeseq; + + namespace mm { + + bool + SetupGc::register_facets() + { + scope log(XO_DEBUG(true)); + + FacetRegistry::register_impl(); + FacetRegistry::register_impl(); + + log && log(xtag("DX1Collector.tseq", typeseq::id())); + log && log(xtag("ACollector.tseq", typeseq::id())); + + return true; + } + } +} /*namespace xo*/ + +/* end SetupGc.cpp */ diff --git a/src/gc/X1CollectorConfig.cpp b/src/gc/X1CollectorConfig.cpp new file mode 100644 index 00000000..812e276f --- /dev/null +++ b/src/gc/X1CollectorConfig.cpp @@ -0,0 +1,46 @@ +/** @file X1CollectorConfig.cpp + * + * @author Roland Conybeare, Mar 2026 + **/ + +#include "X1CollectorConfig.hpp" + +namespace xo { + namespace mm { + + X1CollectorConfig + X1CollectorConfig::with_name(std::string name) + { + X1CollectorConfig copy = *this; + copy.name_ = std::move(name); + return copy; + } + + X1CollectorConfig + X1CollectorConfig::with_size(std::size_t gen_z) + { + X1CollectorConfig copy = *this; + copy.arena_config_ = arena_config_.with_size(gen_z); + return copy; + } + + X1CollectorConfig + X1CollectorConfig::with_debug_flag(bool x) + { + X1CollectorConfig copy = *this; + copy.debug_flag_ = x; + return copy; + } + + X1CollectorConfig + X1CollectorConfig::with_sanitize_flag(bool x) + { + X1CollectorConfig copy = *this; + copy.sanitize_flag_ = x; + return copy; + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end X1CollectorConfig.cpp */ diff --git a/src/gc/init_gc.cpp b/src/gc/init_gc.cpp new file mode 100644 index 00000000..8747229f --- /dev/null +++ b/src/gc/init_gc.cpp @@ -0,0 +1,34 @@ +/** @file init_gc.cpp + * + * @author Roland Conybeare, Mar 2026 + **/ + +#include "init_gc.hpp" +#include "SetupGc.hpp" +#include + +namespace xo { + using xo::mm::SetupGc; + + void + InitSubsys::init() + { + SetupGc::register_facets(); + } + + InitEvidence + InitSubsys::require() { + InitEvidence retval; + + /* recursive subsystem deps for xo-gc/ */ + retval ^= InitSubsys::require(); + + /* xo-gc/'s own initialization code */ + retval ^= Subsystem::provide("gc", &init); + + return retval; + } + +} /*namespace xo*/ + +/* end init_gc.cpp */ From dfd9f7e6f55f5132fc2e228f65624695ab062eea Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 26 Mar 2026 10:26:43 -0400 Subject: [PATCH 059/174] xo-gc: scaffold verify_ok() method for DX1Collector --- include/xo/gc/DX1Collector.hpp | 66 +++++++++--- include/xo/gc/MutationLogEntry.hpp | 7 ++ src/gc/DX1Collector.cpp | 168 +++++++++++++++++++++++------ 3 files changed, 198 insertions(+), 43 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index cf21cf83..86ee9920 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -49,17 +49,30 @@ namespace xo { * @brief encapsulate state needed while GC is running **/ struct GCRunState { - GCRunState() : gc_upto_{0} {} - explicit GCRunState(generation gc_upto); + enum class Mode { + /** gc not running. X1 available for normal allocation **/ + idle, + /** gc in progress. X1 not available for normal allocation **/ + gc, + /** verify in progress. @ref verify_ok call is on the stack **/ + verify + }; - static GCRunState gc_not_running(); + GCRunState() : gc_upto_{0} {} + GCRunState(Mode mode, generation gc_upto); + + static GCRunState idle(); + static GCRunState verify(); static GCRunState gc_upto(generation g); generation gc_upto() const { return gc_upto_; } - bool is_running() const { return gc_upto_ > 0; } + bool is_running() const { return mode_ == Mode::idle; } + bool is_verify() const { return mode_ == Mode::verify; } private: + /** current collector mode **/ + Mode mode_; /** running gc collecting all generations gi < gc_upto **/ generation gc_upto_; }; @@ -87,6 +100,17 @@ namespace xo { obj * root_ = nullptr; }; + /** @brief info collected during a @ref DX1Collector::verify_ok call + * + **/ + struct VerifyStats { + void clear() { *this = VerifyStats(); } + + std::uint32_t n_ext_ = 0; + std::uint32_t n_from_ = 0; + std::uint32_t n_to_ = 0; + }; + // ----- DX1Collector ----- /** @brief garbage collector 'X1' @@ -111,15 +135,10 @@ namespace xo { template obj ref() { return obj(this); } -#ifdef NOT_YET - /** create instance with default configuration, - * generation size @p gen_z - **/ - static DX1Collector make_std(std::size_t gen_z); -#endif - - std::string_view name() const { return config_.name_; } + // ----- access methods ----- + std::string_view name() const noexcept { return config_.name_; } + GCRunState runstate() const noexcept { return runstate_; } const DArena * get_object_types() const noexcept { return &object_types_; } const RootSet * get_root_set() const noexcept { return &root_set_; } const DArena * get_space(role r, generation g) const noexcept { return space_[r][g]; } @@ -128,6 +147,8 @@ namespace xo { DArena * to_space(generation g) noexcept { return get_space(role::to_space(), g); } DArena * new_space() noexcept { return to_space(generation{0}); } + // ----- basic statistics ----- + /** total reserved memory in bytes, across all {role, generation} **/ size_type reserved_total() const noexcept; /** total size in bytes (same as committed_total()) **/ @@ -139,6 +160,8 @@ namespace xo { /** total allocated memory in bytes, across all {role, generation} **/ size_type allocated_total() const noexcept; + // ----- queries ----- + /** introspection for memory use. * Call @p visitor(info) for each pool owned by this allocator **/ @@ -181,6 +204,9 @@ namespace xo { /** Retreive bookkeeping info for allocation at @p mem. **/ AllocInfo alloc_info(value_type mem) const noexcept; + /** verify that GC state appears consistent **/ + bool verify_ok() noexcept; + // ----- app memory model ----- /** lookup interface from type sequence @@ -194,6 +220,8 @@ namespace xo { **/ bool install_type(const AGCObject & meta) noexcept; + // ------ gc root management ----- + /** add GC root at @p *p_root **/ void add_gc_root_poly(obj * p_root) noexcept; @@ -324,8 +352,16 @@ namespace xo { * no-op if not in gc-space. **/ void * _deep_move_interior(void * from_src, generation upto); - /** common driver for _deep_move_root(), _deep_move_interior() **/ + /** Common driver for _deep_move_root(), _deep_move_interior() **/ void * _deep_move_gc_owned(void * from_src, generation upto); + /** Evacuate object at @p *lhs_data to to-space. + * Replace original with forwarding pointer to new location + **/ + void _forward_inplace_aux(AGCObject * lhs_iface, void ** lhs_data); + /** Verify that pointer {@p iface, @p data} is valid: + * destination either in to-space, or somewhere outside this collector + **/ + void _verify_aux(AGCObject * iface, void * data); public: /** garbage collector configuration **/ @@ -341,6 +377,7 @@ namespace xo { /** gc disabled whenever gc_blocked_ > 0 **/ uint32_t gc_blocked_ = 0; + /** if > 0: need gc for all generations < gc_pending_upto_ **/ generation gc_pending_upto_; @@ -391,6 +428,9 @@ namespace xo { * are reversed each time generation g gets collected. **/ std::array space_[c_n_role]; + + /** counters collected during @ref verify_ok call **/ + VerifyStats verify_stats_; }; } /*namespace mm*/ } /*namespace xo*/ diff --git a/include/xo/gc/MutationLogEntry.hpp b/include/xo/gc/MutationLogEntry.hpp index 74598272..eca086d6 100644 --- a/include/xo/gc/MutationLogEntry.hpp +++ b/include/xo/gc/MutationLogEntry.hpp @@ -24,9 +24,16 @@ namespace xo { * - for collector need to traverse data pointer *data **/ class MutationLogEntry { + public: + using AGCObject = xo::mm::AGCObject; + public: MutationLogEntry(void * parent, void ** p_data, obj snap); + void * parent() const { return parent_; } + void ** p_data() const { return p_data_; } + obj snap() const { return snap_; } + private: /** address of object containing logged mutation **/ void * parent_ = nullptr; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 96f65a0e..03a53f6a 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -29,20 +29,26 @@ namespace xo { // ----- GCRunState ----- - GCRunState::GCRunState(generation gc_upto) - : gc_upto_{gc_upto} + GCRunState::GCRunState(Mode mode, generation gc_upto) + : mode_{mode}, gc_upto_{gc_upto} {} GCRunState - GCRunState::gc_not_running() + GCRunState::idle() { - return GCRunState(generation(0)); + return GCRunState(Mode::idle, generation::sentinel()); + } + + GCRunState + GCRunState::verify() + { + return GCRunState(Mode::verify, generation::sentinel()); } GCRunState GCRunState::gc_upto(generation g) { - return GCRunState(generation(g + 1)); + return GCRunState(Mode::gc, generation(g + 1)); } // ----- DX1Collector ----- @@ -322,6 +328,78 @@ namespace xo { return (vtable != nullptr); } + AllocInfo + DX1Collector::alloc_info(value_type mem) const noexcept { + for (role ri : role::all()) { + for (generation gj{0}; gj < config_.n_generation_; ++gj) { + const DArena * arena = this->get_space(ri, gj); + + assert(arena); + + if (arena->contains(mem)) { + return arena->alloc_info(mem); + } + } + } + + // deliberately attempt on nursery to-space, to capture error info + return sentinel + return this->get_space(role::to_space(), generation{0})->alloc_info(mem); + } + + bool + DX1Collector::verify_ok() noexcept + { + // 1. visit space pointers + // - verify space_[*] points to space_storage_[*] + // - verify mlog_[*] points to mlog_storage_[*] + // + // 2. visit roots: + // for each root, verify that immediate child pointers are in to-space + // + // 3. scan to-space: + // for each object, verify that immediate children are also in to-space + // + // 4. scan mutation logs: + // verify that entries refer to to-space + + // Each AGCObject impl provides a forward_children() method, + // that calls DX1Collector::forward_inplace(iface, &data) + // + // tactical plan: hijack forward_children. + // Add run state so DX1Collector can recognize forward_inplace() + // calls made for the purpose of checking child pointers. + + GCRunState saved_runstate = runstate_; + { + this->runstate_ = GCRunState::verify(); + this->verify_stats_.clear(); + + // 2. visit roots + for (GCRoot & root_slot : root_set_) { + VerifyStats pre = verify_stats_; + + auto gco = *root_slot.root(); + + if (gco) { + // forward_children is hijacked here to verify + // pointer validity + + gco.forward_children(this->ref()); + } + + VerifyStats post = verify_stats_; + + // assert fail -> root contains ptr to from-space + assert(pre.n_from_ == post.n_from_); + } + } + + // restore run state at end of verify cycle + this->runstate_ = saved_runstate; + + return true; + } + const AGCObject * DX1Collector::lookup_type(typeseq tseq) const noexcept { @@ -389,14 +467,19 @@ namespace xo { //auto t0 = std::chrono::steady_clock::now(); - log && log("step 0a : update run state"); + if (config_.sanitize_flag_) { + log && log("step 0a : verify"); + this->verify_ok(); + } + + log && log("step 0b : update run state"); this->runstate_ = GCRunState::gc_upto(upto); - log && log("step 0a : [STUB] snapshot alloc state"); + log && log("step 0c : [STUB] snapshot alloc state"); - log && log("step 0b : [STUB] scan for object statistics"); + log && log("step 0d : [STUB] scan for object statistics"); - log && log("step 1 : swap from/to roles"); + log && log("step 1 : swap from/to roles (now to-space is empty)"); this->swap_roles(upto); log && log(xtag("from_0", get_space(role::from_space(), generation{0})->lo_), @@ -409,9 +492,13 @@ namespace xo { log && log("step 3a : [STUB] run destructors"); log && log("step 3b : [STUB] keep reachable weak pointers"); - log && log("step 4 : [STUB] cleanup"); + log && log("step 4 : cleanup"); this->cleanup_phase(upto); + if (config_.sanitize_flag_) { + log && log("step 4b : verify"); + this->verify_ok(); + } } void @@ -442,7 +529,7 @@ namespace xo { space_[role::from_space()][g]->clear(); } - this->runstate_ = GCRunState::gc_not_running(); + this->runstate_ = GCRunState::idle(); } void * @@ -684,6 +771,22 @@ namespace xo { void DX1Collector::forward_inplace(AGCObject * lhs_iface, void ** lhs_data) + { + if (runstate_.is_running()) { + // called during collection phase + this->_forward_inplace_aux(lhs_iface, lhs_data); + } else if (runstate_.is_verify()) { + // called during verify_ok + this->_verify_aux(lhs_iface, lhs_data); + } else { + // should be unreachable + assert(false); + } + } + + void + DX1Collector::_forward_inplace_aux(AGCObject * lhs_iface, + void ** lhs_data) { scope log(XO_DEBUG(config_.debug_flag_), xtag("lhs_data", lhs_data), @@ -858,7 +961,30 @@ namespace xo { * e.g. incremental collection + object is tenured */ } - } /*forward_inplace*/ + } /*_forward_inplace*/ + + void + DX1Collector::_verify_aux(AGCObject * iface, void * data) + { + (void)iface; + (void)data; + + generation g = this->generation_of(role::to_space(), data); + + if (g.is_sentinel()) { + g = this->generation_of(role::from_space(), data); + + if (!g.is_sentinel()) { + // verify failure - live pointer still refers to from-space + + ++(verify_stats_.n_from_); + } else { + ++(verify_stats_.n_ext_); + } + } else { + ++(verify_stats_.n_to_); + } + } void * DX1Collector::shallow_move(const AGCObject * iface, void * from_src) @@ -939,24 +1065,6 @@ namespace xo { return false; } - AllocInfo - DX1Collector::alloc_info(value_type mem) const noexcept { - for (role ri : role::all()) { - for (generation gj{0}; gj < config_.n_generation_; ++gj) { - const DArena * arena = this->get_space(ri, gj); - - assert(arena); - - if (arena->contains(mem)) { - return arena->alloc_info(mem); - } - } - } - - // deliberately attempt on nursery to-space, to capture error info + return sentinel - return this->get_space(role::to_space(), generation{0})->alloc_info(mem); - } - // editor bait: write barrier void DX1Collector::assign_member(void * parent, obj * p_lhs, obj rhs) From f4ddc21c2b220fc1be027b31522a9f6a1e69fe05 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 26 Mar 2026 11:30:16 -0400 Subject: [PATCH 060/174] xo-gc: generation -> Generation + bugfix idle test --- include/xo/gc/DX1Collector.hpp | 66 ++++++--------- include/xo/gc/DX1CollectorIterator.hpp | 12 +-- include/xo/gc/X1CollectorConfig.hpp | 4 +- .../xo/gc/detail/ICollector_DX1Collector.hpp | 8 +- src/gc/DX1Collector.cpp | 82 +++++++++---------- src/gc/DX1CollectorIterator.cpp | 4 +- src/gc/ICollector_DX1Collector.cpp | 10 +-- utest/Collector.test.cpp | 8 +- 8 files changed, 87 insertions(+), 107 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 86ee9920..2eead63b 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -23,26 +23,6 @@ namespace xo { template using up = std::unique_ptr; -#ifdef NOT_YET - /** State associated with a single DX1Collector generation - **/ - struct Generation { - Generation(uint8_t gen_id, up from_space, up to_space); - ~Generation() = default; - - /** identity of this generation. Generations are numbered from - * 0 (youngest) to N (oldest), with N <= c_max_generation - **/ - uint8_t gen_id_; - /** from-space. empty between collection episodes. - * During collection holds former to-space - **/ - up from_space_; - /** to-space. New allocations occur here **/ - up to_space_; - }; -#endif - // ----- GCRunState ----- /** @class GCRunState @@ -58,23 +38,23 @@ namespace xo { verify }; - GCRunState() : gc_upto_{0} {} - GCRunState(Mode mode, generation gc_upto); + GCRunState() : mode_{Mode::idle}, gc_upto_{0} {} + GCRunState(Mode mode, Generation gc_upto); static GCRunState idle(); static GCRunState verify(); - static GCRunState gc_upto(generation g); + static GCRunState gc_upto(Generation g); - generation gc_upto() const { return gc_upto_; } + Generation gc_upto() const { return gc_upto_; } - bool is_running() const { return mode_ == Mode::idle; } + bool is_running() const { return mode_ == Mode::gc; } bool is_verify() const { return mode_ == Mode::verify; } private: /** current collector mode **/ - Mode mode_; + Mode mode_ = Mode::idle; /** running gc collecting all generations gi < gc_upto **/ - generation gc_upto_; + Generation gc_upto_; }; struct DX1CollectorIterator; @@ -141,11 +121,11 @@ namespace xo { GCRunState runstate() const noexcept { return runstate_; } const DArena * get_object_types() const noexcept { return &object_types_; } const RootSet * get_root_set() const noexcept { return &root_set_; } - const DArena * get_space(role r, generation g) const noexcept { return space_[r][g]; } - 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}); } + const DArena * get_space(role r, Generation g) const noexcept { return space_[r][g]; } + 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}); } // ----- basic statistics ----- @@ -180,7 +160,7 @@ namespace xo { /** generation to which pointer @p addr belongs, given role @p r; * sentinel if not found in this collector **/ - generation generation_of(role r, const void * addr) const noexcept; + Generation generation_of(role r, const void * addr) const noexcept; /** return details from last error (will be in gen0 to-space) **/ AllocError last_error() const noexcept; @@ -238,10 +218,10 @@ namespace xo { * 3. if collection is currently disabled, * collection will trigger the next time gc is enabled. **/ - void request_gc(generation upto) noexcept; + void request_gc(Generation upto) noexcept; /** Execute gc immediately, for all generations < @p upto **/ - void execute_gc(generation upto) noexcept; + void execute_gc(Generation upto) noexcept; /** Evacuate object at @p *lhs_data to to-space. * Replace original with forwarding pointer to new location @@ -318,7 +298,7 @@ namespace xo { // ----- book-keeping ----- /** reverse to-space and from-space roles for generation g **/ - void reverse_roles(generation g) noexcept; + void reverse_roles(Generation g) noexcept; /** discard all allocated memory **/ void clear() noexcept; @@ -336,24 +316,24 @@ namespace xo { void _init_space(const X1CollectorConfig & cfg); /** swap from- and to- roles for all generations < @p upto **/ - void swap_roles(generation upto) noexcept; + void swap_roles(Generation upto) noexcept; /** copy roots + everything reachable from them, to to-space **/ - void copy_roots(generation upto) noexcept; + void copy_roots(Generation upto) noexcept; /** cleanup after gc **/ - void cleanup_phase(generation upto); + void cleanup_phase(Generation upto); /** move root subgraph at @p from_src to to-space. * If not in gc-space, visit immediate children and move them. * Require: runstate_.is_running() **/ - void * _deep_move_root(obj from_src, generation upto); + void * _deep_move_root(obj from_src, Generation upto); /** move interior subgraph at @p from_src to to-space. * no-op if not in gc-space. **/ - void * _deep_move_interior(void * from_src, generation upto); + void * _deep_move_interior(void * from_src, Generation upto); /** Common driver for _deep_move_root(), _deep_move_interior() **/ - void * _deep_move_gc_owned(void * from_src, generation upto); + void * _deep_move_gc_owned(void * from_src, Generation upto); /** Evacuate object at @p *lhs_data to to-space. * Replace original with forwarding pointer to new location **/ @@ -379,7 +359,7 @@ namespace xo { uint32_t gc_blocked_ = 0; /** if > 0: need gc for all generations < gc_pending_upto_ **/ - generation gc_pending_upto_; + Generation gc_pending_upto_; /** using arena to get extensible list of root objects. * For each root store one address (type obj*) diff --git a/include/xo/gc/DX1CollectorIterator.hpp b/include/xo/gc/DX1CollectorIterator.hpp index 98a1f1d2..82944ff3 100644 --- a/include/xo/gc/DX1CollectorIterator.hpp +++ b/include/xo/gc/DX1CollectorIterator.hpp @@ -22,8 +22,8 @@ namespace xo { struct DX1CollectorIterator { DX1CollectorIterator() = default; DX1CollectorIterator(const DX1Collector * gc, - generation gen_ix, - generation gen_hi, + Generation gen_ix, + Generation gen_hi, DArenaIterator arena_ix, DArenaIterator arena_hi); @@ -42,8 +42,8 @@ namespace xo { bool is_valid() const noexcept { return (gc_ != nullptr); } bool is_invalid() const noexcept { return !is_valid(); } - generation gen_ix() const { return gen_ix_; } - generation gen_hi() const { return gen_hi_; } + Generation gen_ix() const { return gen_ix_; } + Generation gen_hi() const { return gen_hi_; } DArenaIterator arena_ix() const { return arena_ix_; } DArenaIterator arena_hi() const { return arena_hi_; } @@ -70,8 +70,8 @@ namespace xo { * Current position is within arena for @p gen_ix_ to-space, * Provided @p gen_ix_ < @p gen_hi_ **/ - generation gen_ix_; - generation gen_hi_; + Generation gen_ix_; + Generation gen_hi_; /** Iterating over allocs in [@p arena_ix_, @p arena_hi_). * Current position is at @p arena_ix_ **/ diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp index db96a43f..83f03715 100644 --- a/include/xo/gc/X1CollectorConfig.hpp +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -36,8 +36,8 @@ namespace xo { **/ X1CollectorConfig with_sanitize_flag(bool x); - generation age2gen(object_age age) const noexcept { - return generation(age % n_survive_threshold_); + Generation age2gen(object_age age) const noexcept { + return Generation(age % n_survive_threshold_); } public: diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index 6984cd2e..02c0902d 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -40,15 +40,15 @@ namespace xo { // todo: available() - static size_type allocated(const DX1Collector & d, generation g, role r); - static size_type reserved(const DX1Collector & d, generation g, role r); - static size_type committed(const DX1Collector & d, generation g, role r); + static size_type allocated(const DX1Collector & d, Generation g, role r); + static size_type reserved(const DX1Collector & d, Generation g, role r); + static size_type committed(const DX1Collector & d, Generation g, role r); static bool is_type_installed(const DX1Collector & d, typeseq tseq); static bool install_type(DX1Collector & d, const AGCObject & iface); static void add_gc_root_poly(DX1Collector & d, obj * p_root); static void remove_gc_root_poly(DX1Collector & d, obj * p_root); - static void request_gc(DX1Collector & d, generation upto); + static void request_gc(DX1Collector & d, Generation upto); static void assign_member(DX1Collector & d, void * parent, obj * p_lhs, obj & rhs); static void forward_inplace(DX1Collector & d, AGCObject * lhs_iface, void ** lhs_data); diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 03a53f6a..ef419cc1 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -29,26 +29,26 @@ namespace xo { // ----- GCRunState ----- - GCRunState::GCRunState(Mode mode, generation gc_upto) + GCRunState::GCRunState(Mode mode, Generation gc_upto) : mode_{mode}, gc_upto_{gc_upto} {} GCRunState GCRunState::idle() { - return GCRunState(Mode::idle, generation::sentinel()); + return GCRunState(Mode::idle, Generation::sentinel()); } GCRunState GCRunState::verify() { - return GCRunState(Mode::verify, generation::sentinel()); + return GCRunState(Mode::verify, Generation::sentinel()); } GCRunState - GCRunState::gc_upto(generation g) + GCRunState::gc_upto(Generation g) { - return GCRunState(Mode::gc, generation(g + 1)); + return GCRunState(Mode::gc, Generation(g + 1)); } // ----- DX1Collector ----- @@ -197,7 +197,7 @@ namespace xo { bool DX1Collector::contains_allocated(role r, const void * addr) const noexcept { - generation g = this->generation_of(r, addr); + Generation g = this->generation_of(r, addr); if (g.is_sentinel()) return false; @@ -205,17 +205,17 @@ namespace xo { return this->get_space(r, g)->contains_allocated(addr); } - generation + Generation DX1Collector::generation_of(role r, const void * addr) const noexcept { - for (generation gi{0}; gi < config_.n_generation_; ++gi) { + for (Generation gi{0}; gi < config_.n_generation_; ++gi) { const DArena * arena = get_space(r, gi); if (arena->contains(addr)) return gi; } - return generation::sentinel(); + return Generation::sentinel(); } AllocError @@ -225,7 +225,7 @@ namespace xo { // need to adjust here if runtime errors // encountered during gc. - return get_space(role::to_space(), generation::nursery())->last_error_; + return get_space(role::to_space(), Generation::nursery())->last_error_; } namespace { @@ -239,7 +239,7 @@ namespace xo { size_t z3 = 0; for (role ri : role::all()) { - for (generation gj{0}; gj < d.config_.n_generation_; ++gj) { + for (Generation gj{0}; gj < d.config_.n_generation_; ++gj) { const DArena * arena = d.get_space(ri, gj); assert(arena); @@ -331,7 +331,7 @@ namespace xo { AllocInfo DX1Collector::alloc_info(value_type mem) const noexcept { for (role ri : role::all()) { - for (generation gj{0}; gj < config_.n_generation_; ++gj) { + for (Generation gj{0}; gj < config_.n_generation_; ++gj) { const DArena * arena = this->get_space(ri, gj); assert(arena); @@ -343,7 +343,7 @@ namespace xo { } // deliberately attempt on nursery to-space, to capture error info + return sentinel - return this->get_space(role::to_space(), generation{0})->alloc_info(mem); + return this->get_space(role::to_space(), Generation{0})->alloc_info(mem); } bool @@ -445,7 +445,7 @@ namespace xo { } void - DX1Collector::request_gc(generation upto) noexcept + DX1Collector::request_gc(Generation upto) noexcept { if (gc_blocked_ > 0) { if (gc_pending_upto_ < upto) { @@ -459,7 +459,7 @@ namespace xo { } void - DX1Collector::execute_gc(generation upto) noexcept + DX1Collector::execute_gc(Generation upto) noexcept { scope log(XO_DEBUG(true), xtag("upto", upto)); @@ -482,8 +482,8 @@ namespace xo { log && log("step 1 : swap from/to roles (now to-space is empty)"); this->swap_roles(upto); - log && log(xtag("from_0", get_space(role::from_space(), generation{0})->lo_), - xtag("to_0", get_space(role::to_space(), generation{0})->lo_)); + log && log(xtag("from_0", get_space(role::from_space(), Generation{0})->lo_), + xtag("to_0", get_space(role::to_space(), Generation{0})->lo_)); log && log("step 2a : copy roots"); this->copy_roots(upto); @@ -502,11 +502,11 @@ namespace xo { } void - DX1Collector::swap_roles(generation upto) noexcept + DX1Collector::swap_roles(Generation upto) noexcept { scope log(XO_DEBUG(true), xtag("upto", upto)); - for (generation g = generation{0}; g < upto; ++g) { + for (Generation g = Generation{0}; g < upto; ++g) { log && log("swap roles", xtag("g", g)); std::swap(space_[role::to_space()][g], space_[role::from_space()][g]); @@ -514,14 +514,14 @@ namespace xo { } void - DX1Collector::cleanup_phase(generation upto) + DX1Collector::cleanup_phase(Generation upto) { scope log(XO_DEBUG(true), xtag("upto", upto)); // everything live has been copied out of from-space // -> now set to empty // - for (generation g = generation{0}; g < upto; ++g) { + for (Generation g = Generation{0}; g < upto; ++g) { if (config_.sanitize_flag_) { space_[role::from_space()][g]->scrub(); } @@ -534,7 +534,7 @@ namespace xo { void * DX1Collector::_deep_move_root(obj from_src, - generation upto) + Generation upto) { // NOTE: // Some roots are non-gc-owned nodes. @@ -561,7 +561,7 @@ namespace xo { void * DX1Collector::_deep_move_interior(void * from_src, - generation upto) + Generation upto) { scope log(XO_DEBUG(config_.debug_flag_)); @@ -589,7 +589,7 @@ namespace xo { */ void * DX1Collector::_deep_move_gc_owned(void * from_src, - generation upto) + Generation upto) { scope log(XO_DEBUG(config_.debug_flag_)); @@ -686,7 +686,7 @@ namespace xo { std::array gray_lo_v; { for (uint32_t g = 0; g < upto; ++g) { - gray_lo_v[g] = this->to_space(generation{g})->free_; + gray_lo_v[g] = this->to_space(Generation{g})->free_; } } @@ -706,7 +706,7 @@ namespace xo { do { fixup_work = 0; - for (generation g = generation{0}; g < upto; ++g) { + for (Generation g = Generation{0}; g < upto; ++g) { /** object index for this pass **/ size_t i_obj = 0; @@ -749,7 +749,7 @@ namespace xo { } /*_deep_move_gc_owned*/ void - DX1Collector::copy_roots(generation upto) noexcept + DX1Collector::copy_roots(Generation upto) noexcept { scope log(XO_DEBUG(true)); @@ -834,7 +834,7 @@ namespace xo { * allocated object data. * Only using this to get alloc header **/ - DArena * some_arena = this->from_space(generation(0)); + DArena * some_arena = this->from_space(Generation(0)); DArena::header_type * p_header = some_arena->obj2hdr(object_data); @@ -969,7 +969,7 @@ namespace xo { (void)iface; (void)data; - generation g = this->generation_of(role::to_space(), data); + Generation g = this->generation_of(role::to_space(), data); if (g.is_sentinel()) { g = this->generation_of(role::from_space(), data); @@ -1028,7 +1028,7 @@ namespace xo { object_age age = this->header2age(alloc_hdr); - generation g = config_.age2gen(age); + Generation g = config_.age2gen(age); assert(runstate_.is_running()); @@ -1059,8 +1059,8 @@ namespace xo { bool DX1Collector::expand(size_type z) noexcept { - if (with_facet::mkobj(to_space(generation{0})).expand(z)) - return with_facet::mkobj(from_space(generation{0})).expand(z); + if (with_facet::mkobj(to_space(Generation{0})).expand(z)) + return with_facet::mkobj(from_space(Generation{0})).expand(z); return false; } @@ -1091,7 +1091,7 @@ namespace xo { // 1. generation of lhs // 2. generation of rhs - generation src_g = this->generation_of(role::to_space(), p_lhs); + Generation src_g = this->generation_of(role::to_space(), p_lhs); if (src_g.is_sentinel()) { // only need mlog entries for gc-owned pointers. @@ -1099,7 +1099,7 @@ namespace xo { return; } - generation dest_g = this->generation_of(role::to_space(), rhs.data()); + Generation dest_g = this->generation_of(role::to_space(), rhs.data()); if (dest_g.is_sentinel()) { // similarly, don't need mlog entry to non-gc-owned destination @@ -1161,11 +1161,11 @@ namespace xo { const DArena * arena = get_space(role::to_space(), - generation{0}); + Generation{0}); return DX1CollectorIterator(this, - generation{0}, - generation{config_.n_generation_}, + Generation{0}, + Generation{config_.n_generation_}, arena->begin(), arena->end()); } @@ -1174,7 +1174,7 @@ namespace xo { DX1Collector::end() const noexcept { scope log(XO_DEBUG(false)); - generation gen_hi = generation{config_.n_generation_}; + Generation gen_hi = Generation{config_.n_generation_}; /** valid iterator for end points to end of last DArena. * otherwise will interfere with working compare @@ -1183,7 +1183,7 @@ namespace xo { const DArena * arena = get_space(role::to_space(), - generation(config_.n_generation_ - 1)); + Generation(config_.n_generation_ - 1)); DArenaIterator arena_end = arena->end(); return DX1CollectorIterator(this, @@ -1194,7 +1194,7 @@ namespace xo { } void - DX1Collector::reverse_roles(generation g) noexcept { + DX1Collector::reverse_roles(Generation g) noexcept { assert(g < config_.n_generation_); std::swap(space_[role::from_space()][g], space_[role::to_space()][g]); @@ -1203,7 +1203,7 @@ namespace xo { void DX1Collector::clear() noexcept { for (role ri : role::all()) { - for (generation gj{0}; gj < config_.n_generation_; ++gj) { + for (Generation gj{0}; gj < config_.n_generation_; ++gj) { DArena * arena = this->get_space(ri, gj); assert(arena); diff --git a/src/gc/DX1CollectorIterator.cpp b/src/gc/DX1CollectorIterator.cpp index 7980d319..9780aca5 100644 --- a/src/gc/DX1CollectorIterator.cpp +++ b/src/gc/DX1CollectorIterator.cpp @@ -12,8 +12,8 @@ namespace xo { namespace mm { DX1CollectorIterator::DX1CollectorIterator(const DX1Collector * gc, - generation gen_ix, - generation gen_hi, + Generation gen_ix, + Generation gen_hi, DArenaIterator arena_ix, DArenaIterator arena_hi) : gc_{gc}, gen_ix_{gen_ix}, diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp index fd0c811e..09ec8bab 100644 --- a/src/gc/ICollector_DX1Collector.cpp +++ b/src/gc/ICollector_DX1Collector.cpp @@ -17,7 +17,7 @@ namespace xo { size_type stat_helper(const DX1Collector & d, size_type (DArena::* getter)() const, - generation g, + Generation g, role r) { const DArena * arena = d.get_space(r, g); @@ -30,19 +30,19 @@ namespace xo { } size_type - ICollector_DX1Collector::reserved(const DX1Collector & d, generation g, role r) + ICollector_DX1Collector::reserved(const DX1Collector & d, Generation g, role r) { return stat_helper(d, &DArena::reserved, g, r); } size_type - ICollector_DX1Collector::allocated(const DX1Collector & d, generation g, role r) + ICollector_DX1Collector::allocated(const DX1Collector & d, Generation g, role r) { return stat_helper(d, &DArena::allocated, g, r); } size_type - ICollector_DX1Collector::committed(const DX1Collector & d, generation g, role r) + ICollector_DX1Collector::committed(const DX1Collector & d, Generation g, role r) { return stat_helper(d, &DArena::committed, g, r); } @@ -76,7 +76,7 @@ namespace xo { void ICollector_DX1Collector::request_gc(DX1Collector & d, - generation upto) + Generation upto) { d.request_gc(upto); } diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp index cf82fa93..8b464a2c 100644 --- a/utest/Collector.test.cpp +++ b/utest/Collector.test.cpp @@ -27,7 +27,7 @@ namespace xo { using xo::mm::DX1Collector; using xo::mm::ArenaConfig; using xo::mm::AllocHeaderConfig; - using xo::mm::generation; + using xo::mm::Generation; using xo::mm::c_max_generation; using xo::facet::with_facet; using xo::scope; @@ -74,13 +74,13 @@ namespace xo { DX1Collector gc = DX1Collector{cfg}; - generation g0 = generation{0}; + Generation g0 = Generation{0}; REQUIRE(gc.to_space(g0)); REQUIRE(gc.from_space(g0)); REQUIRE(gc.to_space(g0)->is_mapped()); REQUIRE(gc.from_space(g0)->is_mapped()); - generation g1 = generation{1}; + Generation g1 = Generation{1}; REQUIRE(gc.to_space(g1)); REQUIRE(gc.from_space(g1)); REQUIRE(gc.to_space(g1)->is_mapped()); @@ -92,7 +92,7 @@ namespace xo { REQUIRE(gc.from_space(g1) != gc.from_space(g0)); REQUIRE(gc.to_space(g0) != gc.from_space(g1)); - for (generation gi = generation(2); gi < c_max_generation; ++gi) { + for (Generation gi = Generation(2); gi < c_max_generation; ++gi) { INFO(xtag("gi", gi)); REQUIRE(!gc.to_space(gi)); From 85b3b43d46736bf34c8f9d0055d6d76fbcab3f81 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 26 Mar 2026 14:58:14 -0400 Subject: [PATCH 061/174] xo-gc stack: + contains() method --- include/xo/gc/detail/ICollector_DX1Collector.hpp | 1 + src/gc/ICollector_DX1Collector.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index 02c0902d..fe7bb015 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -44,6 +44,7 @@ namespace xo { static size_type reserved(const DX1Collector & d, Generation g, role r); static size_type committed(const DX1Collector & d, Generation g, role r); static bool is_type_installed(const DX1Collector & d, typeseq tseq); + static bool contains(const DX1Collector & d, role r, const void * addr); static bool install_type(DX1Collector & d, const AGCObject & iface); static void add_gc_root_poly(DX1Collector & d, obj * p_root); diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp index 09ec8bab..5503d429 100644 --- a/src/gc/ICollector_DX1Collector.cpp +++ b/src/gc/ICollector_DX1Collector.cpp @@ -47,6 +47,12 @@ namespace xo { return stat_helper(d, &DArena::committed, g, r); } + bool + ICollector_DX1Collector::contains(const DX1Collector & d, role r, const void * addr) + { + return d.contains(r, addr); + } + bool ICollector_DX1Collector::is_type_installed(const DX1Collector & d, typeseq tseq) { From 25b3cf8d4d69f7331e01b2d6697024b98fdade6a Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 26 Mar 2026 15:02:43 -0400 Subject: [PATCH 062/174] xo-gc: bugfix collector forarding for virtual roots --- include/xo/gc/DX1Collector.hpp | 19 ++++- src/gc/DX1Collector.cpp | 129 ++++++++++++++++++++++++--------- 2 files changed, 111 insertions(+), 37 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 2eead63b..a686d45b 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -86,9 +86,10 @@ namespace xo { struct VerifyStats { void clear() { *this = VerifyStats(); } - std::uint32_t n_ext_ = 0; - std::uint32_t n_from_ = 0; - std::uint32_t n_to_ = 0; + std::uint32_t n_gc_root_ = 0; + std::uint32_t n_ext_ = 0; + std::uint32_t n_from_ = 0; + std::uint32_t n_to_ = 0; }; // ----- DX1Collector ----- @@ -98,6 +99,8 @@ namespace xo { struct DX1Collector { public: using RootSet = DArenaVector; + /* TODO: AllocIterator pointing to free pointer instead of std::byte* */ + using GCMoveCheckpoint = std::array; using MutationLog = DArenaVector; using typeseq = xo::facet::typeseq; using size_type = DArena::size_type; @@ -154,6 +157,8 @@ namespace xo { /** true iff address @p addr allocated from this collector and currently live * in role @p r (according to current GC state) + * + * (i.e. in [lo,free) for an arena) **/ bool contains_allocated(role r, const void * addr) const noexcept; @@ -334,6 +339,14 @@ namespace xo { void * _deep_move_interior(void * from_src, Generation upto); /** Common driver for _deep_move_root(), _deep_move_interior() **/ void * _deep_move_gc_owned(void * from_src, Generation upto); + /** snap checkpoint containing allocator state + * use to detect forwarding activity after visiting objects + **/ + GCMoveCheckpoint _snap_move_checkpoint(Generation upto); + /** traverse objects allocated after @p ckp, to make sure their children + * are forwarded. Repeat until traverse doesn't find any unforwarded children + **/ + void _forward_children_until_fixpoint(Generation upto, GCMoveCheckpoint ckp); /** Evacuate object at @p *lhs_data to to-space. * Replace original with forwarding pointer to new location **/ diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index ef419cc1..a2d4d835 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -3,12 +3,13 @@ * @author Roland Conybeare, Dec 2025 **/ +#include "X1Collector.hpp" #include #include -#include "detail/IAllocator_DX1Collector.hpp" -#include "detail/ICollector_DX1Collector.hpp" +//#include "detail/IAllocator_DX1Collector.hpp" +//#include "detail/ICollector_DX1Collector.hpp" #include "arena/IAllocator_DArena.hpp" -#include +//#include #include #include #include "object_age.hpp" @@ -382,15 +383,22 @@ namespace xo { if (gco) { // forward_children is hijacked here to verify - // pointer validity + // pointer validity. + // + // Nested control re-enters + // - X1Collector::forward_inplace() -> _verify_aux() + // gco.forward_children(this->ref()); + } VerifyStats post = verify_stats_; // assert fail -> root contains ptr to from-space assert(pre.n_from_ == post.n_from_); + + ++verify_stats_.n_gc_root_; } } @@ -403,11 +411,23 @@ namespace xo { const AGCObject * DX1Collector::lookup_type(typeseq tseq) const noexcept { + scope log(XO_DEBUG(false)); + AGCObject * v = reinterpret_cast(object_types_.lo_); const AGCObject * target = &(v[tseq.seqno()]); - assert(reinterpret_cast(target) < object_types_.limit_); + if (reinterpret_cast(target) >= object_types_.limit_) { + log.retroactively_enable(xtag("tseq", tseq), xtag("tname", TypeRegistry::id2name(tseq))); + + log(xtag("types.allocated", object_types_.allocated()), + xtag("types.committed", object_types_.committed()), + xtag("types.lo", object_types_.lo_), + xtag("types.limit", object_types_.limit_), + xtag("types.hi", object_types_.hi_)); + + assert(false); + } return target; } @@ -498,6 +518,11 @@ namespace xo { if (config_.sanitize_flag_) { log && log("step 4b : verify"); this->verify_ok(); + + log && log(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_)); } } @@ -553,8 +578,23 @@ namespace xo { if (src_in_from_space) { return _deep_move_gc_owned(from_src.data(), upto); } else { + // we aren't moving from_src, it's not gc-owned. + // However weare moving all its gc-owned children + auto self = this->ref(); + + GCMoveCheckpoint gray_lo_v = this->_snap_move_checkpoint(upto); + from_src.forward_children(self); + + // For each generation g: + // traverse objects newer than gray_lo_v[g], to make sure children + // are forwarded. Fixpoint reached when gray_lo_v[g] doesn't change. + // Remember that forwarding may promote objects to older generation, + // so need multiple passes + // + this->_forward_children_until_fixpoint(upto, gray_lo_v); + return from_src.data(); } } @@ -612,12 +652,48 @@ namespace xo { /* here: object at from_src not already forwarded */ if (!this->check_move_policy(hdr, from_src)) { - /* object at from_src in generation that is not being collected */ + /* object at from_src is in generation that is not being collected */ log && log("disposition: not moving from_src"); return from_src; } + log && log("disposition: move subtree"); + + /* TODO: AllocIterator pointing to free pointer */ + GCMoveCheckpoint gray_lo_v = this->_snap_move_checkpoint(upto); + + obj alloc(this); + const AGCObject * iface = lookup_type(tseq); + + assert(iface->_has_null_vptr() == false); + + void * to_dest = this->shallow_move(iface, from_src); + + this->_forward_children_until_fixpoint(upto, gray_lo_v); + + log && log(xtag("to_dest", to_dest)); + + return to_dest; + } /*_deep_move_gc_owned*/ + + auto + DX1Collector::_snap_move_checkpoint(Generation upto) -> GCMoveCheckpoint + { + GCMoveCheckpoint gray_lo_v; + + for (uint32_t g = 0; g < upto; ++g) { + gray_lo_v[g] = this->to_space(Generation{g})->free_; + } + + return gray_lo_v; + } + + void + DX1Collector::_forward_children_until_fixpoint(Generation upto, GCMoveCheckpoint gray_lo_v) + { + scope log(XO_DEBUG(config_.debug_flag_)); + /** * To-space: * @@ -680,23 +756,6 @@ namespace xo { * **/ - log && log("disposition: move subtree"); - - /* TODO: AllocIterator pointing to free pointer */ - std::array gray_lo_v; - { - for (uint32_t g = 0; g < upto; ++g) { - gray_lo_v[g] = this->to_space(Generation{g})->free_; - } - } - - obj alloc(this); - const AGCObject * iface = lookup_type(tseq); - - assert(iface->_has_null_vptr() == false); - - void * to_dest = this->shallow_move(iface, from_src); - std::size_t fixup_work = 0; /* TODO: @@ -731,7 +790,7 @@ namespace xo { assert(iface->_has_null_vptr() == false); - obj gc(this); + auto gc = this->ref(); //obj gc(this); iface->forward_children(src, gc); @@ -742,11 +801,7 @@ namespace xo { } } } while (fixup_work > 0); - - log && log(xtag("to_dest", to_dest)); - - return to_dest; - } /*_deep_move_gc_owned*/ + } void DX1Collector::copy_roots(Generation upto) noexcept @@ -777,7 +832,7 @@ namespace xo { this->_forward_inplace_aux(lhs_iface, lhs_data); } else if (runstate_.is_verify()) { // called during verify_ok - this->_verify_aux(lhs_iface, lhs_data); + this->_verify_aux(lhs_iface, *lhs_data); } else { // should be unreachable assert(false); @@ -966,15 +1021,19 @@ namespace xo { void DX1Collector::_verify_aux(AGCObject * iface, void * data) { + scope log(XO_DEBUG(config_.debug_flag_), xtag("data", data)); + (void)iface; (void)data; - Generation g = this->generation_of(role::to_space(), data); + Generation g1 = this->generation_of(role::to_space(), data); - if (g.is_sentinel()) { - g = this->generation_of(role::from_space(), data); + if (g1.is_sentinel()) { + assert(this->contains(role::to_space(), data) == false); - if (!g.is_sentinel()) { + 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_); @@ -982,6 +1041,8 @@ namespace xo { ++(verify_stats_.n_ext_); } } else { + assert(this->contains(role::to_space(), data)); + ++(verify_stats_.n_to_); } } From 7ad855cec255d6f2c5321f9b97f1d35ad1c65308 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 27 Mar 2026 12:16:09 -0400 Subject: [PATCH 063/174] xo-gc: header tidy --- src/gc/DX1Collector.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index a2d4d835..031e922f 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -6,10 +6,7 @@ #include "X1Collector.hpp" #include #include -//#include "detail/IAllocator_DX1Collector.hpp" -//#include "detail/ICollector_DX1Collector.hpp" -#include "arena/IAllocator_DArena.hpp" -//#include +#include #include #include #include "object_age.hpp" From eba6e437be424b114ee869442db69168f206908b Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 27 Mar 2026 13:16:10 -0400 Subject: [PATCH 064/174] xo-gc: verify visits+checks to-space contents --- include/xo/gc/DX1Collector.hpp | 19 +++++++++--- src/gc/DX1Collector.cpp | 53 +++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index a686d45b..2c149855 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -84,12 +84,23 @@ namespace xo { * **/ struct VerifyStats { + bool is_ok() const noexcept { + return (n_from_ == 0) && (n_fwd_ == 0) && (n_no_iface_ == 0); + } + void clear() { *this = VerifyStats(); } - std::uint32_t n_gc_root_ = 0; - std::uint32_t n_ext_ = 0; - std::uint32_t n_from_ = 0; - std::uint32_t n_to_ = 0; + /** number of gc roots examined **/ + std::uint32_t n_gc_root_ = 0; + std::uint32_t n_ext_ = 0; + /** number of from-space objects encountered. Fatal if non-zero **/ + std::uint32_t n_from_ = 0; + /** number of to-space objects encountered. **/ + std::uint32_t n_to_ = 0; + /** counts forwarding object encountered in to-space scan. Fatal if non-zero **/ + std::uint32_t n_fwd_ = 0; + /** counts missing GCObject interface. Fatal if non-zero **/ + std::uint32_t n_no_iface_ = 0; }; // ----- DX1Collector ----- diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 031e922f..cd6ba5d7 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -367,6 +367,8 @@ namespace xo { // Add run state so DX1Collector can recognize forward_inplace() // calls made for the purpose of checking child pointers. + auto self = this->ref(); + GCRunState saved_runstate = runstate_; { this->runstate_ = GCRunState::verify(); @@ -386,7 +388,7 @@ namespace xo { // - X1Collector::forward_inplace() -> _verify_aux() // - gco.forward_children(this->ref()); + gco.forward_children(self); } @@ -397,12 +399,49 @@ namespace xo { ++verify_stats_.n_gc_root_; } + + // 3. scan to-space for each generation + for (Generation g(0); g < config_.n_generation_; ++g) { + const DArena * space = this->get_space(role::to_space(), g); + + for (const AllocInfo & info : *space) { + + if (info.is_forwarding_tseq()) { + ++verify_stats_.n_fwd_; + + } else { + typeseq tseq(info.tseq()); + + const AGCObject * iface = this->lookup_type(tseq); + + if (iface) { + const void * data = info.payload().first; + + // 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() + // + gco.forward_children(self); + } else { + ++verify_stats_.n_no_iface_; + continue; + } + } + } + } } // restore run state at end of verify cycle this->runstate_ = saved_runstate; - return true; + bool ok = verify_stats_.is_ok(); + + return ok; } const AGCObject * @@ -424,6 +463,7 @@ namespace xo { xtag("types.hi", object_types_.hi_)); assert(false); + return nullptr; } return target; @@ -487,6 +527,7 @@ namespace xo { if (config_.sanitize_flag_) { log && log("step 0a : verify"); this->verify_ok(); + } log && log("step 0b : update run state"); @@ -514,12 +555,16 @@ namespace xo { if (config_.sanitize_flag_) { log && log("step 4b : verify"); - this->verify_ok(); + bool ok = this->verify_ok(); log && log(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-to", verify_stats_.n_to_), + xtag("n-fwd", verify_stats_.n_fwd_), + xtag("n-no-iface", verify_stats_.n_no_iface_)); + + assert(ok); } } From 2aebb8e187fbb516c11631c9eec9737c6f1c6740 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 27 Mar 2026 22:33:21 -0400 Subject: [PATCH 065/174] xo-gc stack: genfacet for Collector + facet registry bugfix --- include/xo/gc/PolyForwarderUtil.hpp | 8 ++++++-- src/gc/DX1Collector.cpp | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/include/xo/gc/PolyForwarderUtil.hpp b/include/xo/gc/PolyForwarderUtil.hpp index 19f99c9b..6d87159c 100644 --- a/include/xo/gc/PolyForwarderUtil.hpp +++ b/include/xo/gc/PolyForwarderUtil.hpp @@ -51,8 +51,12 @@ namespace xo { * +-------+ **/ - auto gco = FacetRegistry::instance().variant(*p_ptr); - gc.forward_inplace(gco.iface(), (void **)&(p_ptr->data_)); + if (*p_ptr) { + auto gco = FacetRegistry::instance().variant(*p_ptr); + gc.forward_inplace(gco.iface(), (void **)&(p_ptr->data_)); + } else { + // nullptr is trivial to forward + } } }; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index cd6ba5d7..b3b9c8df 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -179,7 +179,7 @@ namespace xo { } } - for (uint32_t j = 1; j < config_.n_generation_; ++j) { + for (uint32_t j = 0; j + 1 < config_.n_generation_; ++j) { for (uint32_t i = 0; i < c_n_role + 1; ++i) { mlog_storage_[i][j].visit_pools(visitor); } From 9ebd67a72bdf0c489736394af9dbbccf15c7fb3c Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 27 Mar 2026 22:39:58 -0400 Subject: [PATCH 066/174] xo-gc: drop a DX1Collector debug --- src/gc/DX1Collector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index b3b9c8df..5119d2aa 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -1063,7 +1063,7 @@ namespace xo { void DX1Collector::_verify_aux(AGCObject * iface, void * data) { - scope log(XO_DEBUG(config_.debug_flag_), xtag("data", data)); + //scope log(XO_DEBUG(config_.debug_flag_), xtag("data", data)); (void)iface; (void)data; From c16deab464974e72f6a845e760ff7e4a11698440 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 28 Mar 2026 09:43:43 -0400 Subject: [PATCH 067/174] xo-reader2: expand utest to run gc [WIP] --- src/gc/DX1Collector.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 5119d2aa..c7bd964b 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -732,7 +732,8 @@ namespace xo { } void - DX1Collector::_forward_children_until_fixpoint(Generation upto, GCMoveCheckpoint gray_lo_v) + DX1Collector::_forward_children_until_fixpoint(Generation upto, + GCMoveCheckpoint gray_lo_v) { scope log(XO_DEBUG(config_.debug_flag_)); @@ -832,7 +833,7 @@ namespace xo { assert(iface->_has_null_vptr() == false); - auto gc = this->ref(); //obj gc(this); + auto gc = this->ref(); iface->forward_children(src, gc); @@ -887,7 +888,7 @@ namespace xo { { scope log(XO_DEBUG(config_.debug_flag_), xtag("lhs_data", lhs_data), - xtag("*lhs_data", *lhs_data)); + xtag("*lhs_data", lhs_data ? *lhs_data : nullptr)); /* coordinates with DX1Collector::_deep_move() */ @@ -909,7 +910,10 @@ namespace xo { void * object_data = (std::byte *)*lhs_data; - if (!this->contains(role::from_space(), object_data)) { + if (!object_data) { + /* trivial to forward nullptr */ + return; + } else if (!this->contains(role::from_space(), object_data)) { /* *lhs_data either: * 1. already in to-space * 2. not in GC-allocated space at all @@ -919,7 +923,10 @@ namespace xo { * Since not allocated from GC, they don't have * an alloc-header. */ - log && log("disposition: not in from-space"); + log && log("disposition: not in from-space. Don't forward, but check children"); + + obj gco(lhs_iface, object_data); + gco.forward_children(this->ref()); return; } From 0000a1d2f2f3e21223a5e1d957b5089178281dbb Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 28 Mar 2026 13:13:30 -0400 Subject: [PATCH 068/174] xo-gc: check for null iface pointers in verify loop --- src/gc/DX1Collector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index c7bd964b..4c5030b3 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -414,7 +414,7 @@ namespace xo { const AGCObject * iface = this->lookup_type(tseq); - if (iface) { + if (iface && !(iface->_has_null_vptr())) { const void * data = info.payload().first; // assembled fop for gc-aware object From 9d419aa6bb7cd9ac651e3b55661146b51059ec0a Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 28 Mar 2026 13:16:29 -0400 Subject: [PATCH 069/174] xo-gc: in verify report memory ranges for gc space when debug on --- src/gc/DX1Collector.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 4c5030b3..af861f11 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -524,6 +524,18 @@ namespace xo { //auto t0 = std::chrono::steady_clock::now(); + log && log("memory"); + auto visitor = [&log](const MemorySizeInfo & info) { + log && log(xtag("resource", info.resource_name_), + xtag("used", info.used_), + xtag("alloc", info.allocated_), + xtag("commit", info.committed_), + xtag("resv", info.reserved_), + xtag("lo", info.lo_), + xtag("hi", info.hi_)); + }; + this->visit_pools(visitor); + if (config_.sanitize_flag_) { log && log("step 0a : verify"); this->verify_ok(); From 5a383e89cef66bdf4251381b1864b308fa5bb2b9 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 28 Mar 2026 17:06:30 -0400 Subject: [PATCH 070/174] xo-gc: add mutation log check in X1Collector.verify_ok() --- include/xo/gc/DX1Collector.hpp | 11 ++++++- src/gc/DX1Collector.cpp | 55 +++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 2c149855..c72b802e 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -99,8 +99,17 @@ namespace xo { std::uint32_t n_to_ = 0; /** counts forwarding object encountered in to-space scan. Fatal if non-zero **/ std::uint32_t n_fwd_ = 0; - /** counts missing GCObject interface. Fatal if non-zero **/ + /** counts missing GCObject interface. Fatal if non-zero **/ std::uint32_t n_no_iface_ = 0; + /** live mlog entry refers to to-space, as expected **/ + std::uint32_t n_mlog_vital_ = 0; + /** stale mlog entry. not troubling to verify these **/ + std::uint32_t n_mlog_stale_ = 0; + /** live mlog entry refers to from-space. Fatal if non-zero **/ + std::uint32_t n_mlog_from_ = 0; + /** live mlog entry refers to either some other generation or outside gc-space. Fatal if non-zero **/ + std::uint32_t n_mlog_wild_ = 0; + }; // ----- DX1Collector ----- diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index af861f11..bdb73ff1 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -424,7 +424,7 @@ namespace xo { // child pointer validity. // // Nested control reenters - // X1Collector::forward_inplace() + // X1Collector::forward_inplace() -> _verify_aux() // gco.forward_children(self); } else { @@ -434,6 +434,49 @@ namespace xo { } } } + + // 4. scan mutation logs + for (Generation g(0); g + 1 < config_.n_generation_; ++g) { + const DArena * space = this->get_space(role::to_space(), g); + const DArena * from = this->get_space(role::from_space(), g); + + // mutation log for generation g records *incoming* pointers + // from more senior generations; includes objects from *this* + // generation that are older (track since source promotes before + // destination) + // + for (const MutationLogEntry & mrecd : *(mlog_[role::to_space()][g])) { + // mutation log entries are only valid until the next assignment + // at the source location. Superseded entry may now point + // somewhere else. The snapshot member must however point + // to this generation, since that's preserved as long as the + // log entry survives. + + void * orig_data = mrecd.snap().data(); + void * curr_data = *mrecd.p_data(); + + if (orig_data == curr_data) { + // live mlog entry must point to to-space + + if (space->contains_allocated(orig_data)) { + ++verify_stats_.n_mlog_vital_; + } else if (from->contains(curr_data)) { + // verify failure. + ++verify_stats_.n_mlog_from_; + } else { + // verify failure. + ++verify_stats_.n_mlog_wild_; + } + } else { + // requirements on superseded log entry: + // - snapshot refers to to-space + // + // no requirements on current data, entry is superseded anyway + // + ++verify_stats_.n_mlog_stale_; + } + } + } } // restore run state at end of verify cycle @@ -574,7 +617,11 @@ namespace xo { 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_)); + xtag("n-no-iface", verify_stats_.n_no_iface_), + xtag("n-mlog-vital", verify_stats_.n_mlog_vital_), + xtag("n-mlog-stale", verify_stats_.n_mlog_stale_), + xtag("n-mlog-from", verify_stats_.n_mlog_from_), + xtag("n-mlog-wild", verify_stats_.n_mlog_wild_)); assert(ok); } @@ -1230,8 +1277,8 @@ namespace xo { if (src_g < dest_g) { // young-to-old pointers don't need to be remembered, - // since a GC cycle that collects the old generation is guarnatted - // to also collect the young generation. + // since a GC cycle that collects an (old) generation is guarnatted + // to also collect all younger generations. return; } From c5da97dea028c0f6f6b8ba917bc13ffa71b1b640 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 29 Mar 2026 13:44:19 -0400 Subject: [PATCH 071/174] xo-gc stack: + request-gc-statistics() primitive 1. xo-gc now depends on xo-object2. 2. use genfacet for ICollector_DX1Collector 3. moves xo-gc utest previously in xo-object2 to more natural location in xo-gc/ --- CMakeLists.txt | 13 + cmake/xo_gcConfig.cmake.in | 1 + idl/ICollector_DX1Collector.json5 | 24 ++ include/xo/gc/DX1Collector.hpp | 31 +- .../xo/gc/detail/ICollector_DX1Collector.hpp | 118 ++++--- src/gc/CMakeLists.txt | 4 +- src/gc/DX1Collector.cpp | 120 ++++++- src/gc/IAllocator_DX1Collector.cpp | 8 +- src/gc/ICollector_DX1Collector.cpp | 118 ------- src/gc/facet/ICollector_DX1Collector.cpp | 88 ++++++ src/gc/init_gc.cpp | 2 + utest/CMakeLists.txt | 2 + utest/Object2.test.cpp | 134 ++++++++ utest/X1Collector.test.cpp | 297 ++++++++++++++++++ 14 files changed, 790 insertions(+), 170 deletions(-) create mode 100644 idl/ICollector_DX1Collector.json5 delete mode 100644 src/gc/ICollector_DX1Collector.cpp create mode 100644 src/gc/facet/ICollector_DX1Collector.cpp create mode 100644 utest/Object2.test.cpp create mode 100644 utest/X1Collector.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2edb0ac3..bacf4f14 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,19 @@ add_definitions(${PROJECT_CXX_FLAGS}) # ---------------------------------------------------------------- +# note: manual target; generated code committed to git +xo_add_genfacetimpl( + TARGET xo-gc-facetimpl-collector-x1collector + FACET_PKG xo_alloc2 + INPUT idl/ICollector_DX1Collector.json5 +) + +# ---------------------------------------------------------------- + +xo_add_genfacet_all(xo-gc-genfacet-all) + +# ---------------------------------------------------------------- + # must complete definition of expression lib before configuring examples add_subdirectory(src/gc) add_subdirectory(utest) diff --git a/cmake/xo_gcConfig.cmake.in b/cmake/xo_gcConfig.cmake.in index 548eb737..29f87480 100644 --- a/cmake/xo_gcConfig.cmake.in +++ b/cmake/xo_gcConfig.cmake.in @@ -1,6 +1,7 @@ @PACKAGE_INIT@ include(CMakeFindDependencyMacro) +find_dependency(xo_object2) find_dependency(xo_alloc2) find_dependency(xo_facet) find_dependency(subsys) diff --git a/idl/ICollector_DX1Collector.json5 b/idl/ICollector_DX1Collector.json5 new file mode 100644 index 00000000..9124288f --- /dev/null +++ b/idl/ICollector_DX1Collector.json5 @@ -0,0 +1,24 @@ +{ + mode: "implementation", + output_cpp_dir: "src/gc/facet", + output_hpp_dir: "include/xo/gc", + 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 DX1Collector", + using_doxygen: true, + repr: "DX1Collector", + doc: [ "implement ACollector for DX1Collector" ], +} diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index c72b802e..d5934611 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -153,15 +153,38 @@ namespace xo { // ----- basic statistics ----- /** total reserved memory in bytes, across all {role, generation} **/ - size_type reserved_total() const noexcept; + size_type reserved() const noexcept; /** total size in bytes (same as committed_total()) **/ size_type size_total() const noexcept; /** total committed memory in bytes, across all {role, generation} **/ - size_type committed_total() const noexcept; + size_type committed() const noexcept; /** total available memory in bytes, across all {role, generation} **/ - size_type available_total() const noexcept; + size_type available() const noexcept; /** total allocated memory in bytes, across all {role, generation} **/ - size_type allocated_total() const noexcept; + size_type allocated() const noexcept; + + /** total number of mutation log entries **/ + size_type mutation_log_entries() const noexcept; + + /** memory allocated for generation @p g in role @p r **/ + size_type allocated(Generation g, role r) const noexcept; + /** memory committed for generation @p g in role @p r **/ + size_type committed(Generation g, role r) const noexcept; + /** memory (virtual addresses) reserved for generation @p g in role @p r **/ + size_type reserved(Generation g, role r) const noexcept; + + // ----- full statistics ----- + + /** Report gc statistics as a dictionary. + * Providing for the same of making GC statistics visible to schematika programs + * + * @p mm allocate stats dictionary from this allocator. May be the same as this collector. + * @p error_mm Allocator for last-report error reporting when out-of-memory. + * @p p_output on exit @p *p_output contains stats dictionary + **/ + bool report_statistics(obj mm, + obj error_mm, + obj * p_output) const noexcept; // ----- queries ----- diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index fe7bb015..a9c8e012 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -1,63 +1,107 @@ /** @file ICollector_DX1Collector.hpp -* - * @author Roland Conybeare, Dec 2025 + * + * Generated automagically from ingredients: + * 1. code generator: + * [xo-facet/codegen/genfacet] + * arguments: + * --input [idl/ICollector_DX1Collector.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [iface_facet_repr.hpp.j2] + * 3. idl for facet methods + * [idl/ICollector_DX1Collector.json5] **/ #pragma once -#include -#include +#include "Collector.hpp" #include "DX1Collector.hpp" -namespace xo { - namespace mm { struct ICollector_DX1Collector; } +namespace xo { namespace mm { class ICollector_DX1Collector; } } +namespace xo { namespace facet { template <> struct FacetImplementation { - using ImplType = xo::mm::ICollector_Xfer; + using ImplType = xo::mm::ICollector_Xfer + ; }; } +} +namespace xo { namespace mm { - /* changes here coordinate with - * ACollector ACollector.hpp - * ICollector_Any ICollector_Any.hpp - * ICollector_Xfer ICollector_Xfer.hpp - * RCollector RCollector.hpp - */ - struct ICollector_DX1Collector { - using size_type = std::size_t; - using header_type = DArena::header_type; - using typeseq = xo::facet::typeseq; + /** @class ICollector_DX1Collector + **/ + class ICollector_DX1Collector { + public: + /** @defgroup mm-collector-dx1collector-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-dx1collector-methods **/ + ///@{ + // const methods + /** memory in use for this collector **/ + static size_type allocated(const DX1Collector & self, Generation g, role r) noexcept; + /** memory committed for this collector **/ + static size_type committed(const DX1Collector & self, Generation g, role r) noexcept; + /** address space reserved for this collector **/ + static size_type reserved(const DX1Collector & self, Generation g, role r) noexcept; + /** true if gc responsible for data at @p addr, and data belongs to role @p r **/ + static bool contains(const DX1Collector & 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 DX1Collector & 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 DX1Collector & self, obj report_mm, obj error_mm, obj * output) noexcept; - static bool check_move_policy(const DX1Collector & d, - header_type alloc_hdr, - void * object_data); + // non-const methods + /** install interface @p iface for representation with typeseq @p tseq +in collector @p d. - // todo: available() +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. - static size_type allocated(const DX1Collector & d, Generation g, role r); - static size_type reserved(const DX1Collector & d, Generation g, role r); - static size_type committed(const DX1Collector & d, Generation g, role r); - static bool is_type_installed(const DX1Collector & d, typeseq tseq); - static bool contains(const DX1Collector & d, role r, const void * addr); +Return false if installation fails (e.g. memory exhausted) **/ + static bool install_type(DX1Collector & 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(DX1Collector & 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(DX1Collector & 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(DX1Collector & self, Generation upto); + /** Assign pointer @p p_lhs to destination @p rhs, within parent allocation @p parent - static bool install_type(DX1Collector & d, const AGCObject & iface); - static void add_gc_root_poly(DX1Collector & d, obj * p_root); - static void remove_gc_root_poly(DX1Collector & d, obj * p_root); - static void request_gc(DX1Collector & d, Generation upto); - static void assign_member(DX1Collector & d, void * parent, - obj * p_lhs, obj & rhs); - static void forward_inplace(DX1Collector & d, AGCObject * lhs_iface, void ** lhs_data); +Require: gc not in progress **/ + static void assign_member(DX1Collector & self, void * parent, obj * p_lhs, obj & rhs); + /** evacuate @p *lhs, that refers to state with interface @p lhs_iface, +to collector @p d's to-space. Replace *lhs_data with forwarding pointer - static int32_t s_typeseq; - static bool _valid; +Require: gc in progress + **/ + static void forward_inplace(DX1Collector & self, AGCObject * lhs_iface, void ** lhs_data); + ///@} }; + } /*namespace mm*/ } /*namespace xo*/ -/* end ICollector_DX1_Collector.hpp */ +/* end */ \ No newline at end of file diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index eee6864b..2dec8735 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -7,10 +7,11 @@ set(SELF_SRCS SetupGc.cpp IAllocator_DX1Collector.cpp - ICollector_DX1Collector.cpp IAllocIterator_DX1CollectorIterator.cpp DX1Collector.cpp + facet/ICollector_DX1Collector.cpp + DX1CollectorIterator.cpp X1CollectorConfig.cpp @@ -20,6 +21,7 @@ set(SELF_SRCS xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) # note: deps here must also appear in cmake/xo_alloc2Config.cmake.in +xo_dependency(${SELF_LIB} xo_object2) xo_dependency(${SELF_LIB} xo_alloc2) xo_dependency(${SELF_LIB} xo_facet) xo_dependency(${SELF_LIB} subsys) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index bdb73ff1..a668b8d4 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -4,10 +4,12 @@ **/ #include "X1Collector.hpp" +#include +#include +#include #include #include #include -#include #include #include "object_age.hpp" #include @@ -18,6 +20,11 @@ #include // for ::getpagesize() namespace xo { + // for report_statistics() + using xo::scm::DDictionary; + using xo::scm::DString; + using xo::scm::DInteger; + using xo::mm::AAllocator; using xo::facet::TypeRegistry; using xo::facet::typeseq; @@ -165,6 +172,10 @@ namespace xo { this->space_[role::to_space()][igen] = nullptr; this->space_[role::from_space()][igen] = nullptr; } + + if (config_.n_generation_ == 2) { + assert(this->get_space(role::to_space(), Generation{2}) == nullptr); + } } void @@ -251,7 +262,7 @@ namespace xo { } size_type - DX1Collector::reserved_total() const noexcept + DX1Collector::reserved() const noexcept { return accumulate_total_aux(*this, &DArena::reserved); } @@ -259,27 +270,124 @@ namespace xo { size_type DX1Collector::size_total() const noexcept { - return committed_total(); + return this->committed(); } size_type - DX1Collector::committed_total() const noexcept + DX1Collector::committed() const noexcept { return accumulate_total_aux(*this, &DArena::committed); } size_type - DX1Collector::available_total() const noexcept + DX1Collector::available() const noexcept { return accumulate_total_aux(*this, &DArena::available); } size_type - DX1Collector::allocated_total() const noexcept + DX1Collector::allocated() const noexcept { return accumulate_total_aux(*this, &DArena::allocated); } + size_type + DX1Collector::mutation_log_entries() const noexcept + { + size_type z = 0; + + for (Generation gj{0}; gj + 1 < config_.n_generation_; ++gj) { + z += mlog_[role::to_space()][gj]->size(); + } + + return z; + } + + namespace { + size_type + stat_helper(const DX1Collector & 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; + } + } + + size_type + DX1Collector::allocated(Generation g, role r) const noexcept + { + return stat_helper(*this, &DArena::allocated, g, r); + } + + size_type + DX1Collector::committed(Generation g, role r) const noexcept + { + return stat_helper(*this, &DArena::committed, g, r); + } + + size_type + DX1Collector::reserved(Generation g, role r) const noexcept + { + return stat_helper(*this, &DArena::reserved, g, r); + } + + // editor bait: report-gc-statistics + bool + DX1Collector::report_statistics(obj mm, + obj error_mm, + obj * p_output) const noexcept + { + (void)error_mm; + + DDictionary * rpt = DDictionary::make(mm); + + bool ok = true; + + // note: totals taken across both roles and generations, + // so counts both from-space and to-space + // + ok &= rpt->upsert_cstr(mm, "n-generation", DInteger::box(mm, config_.n_generation_)); + ok &= rpt->upsert_cstr(mm, "n-survive-threshold", DInteger::box(mm, config_.n_survive_threshold_)); + ok &= rpt->upsert_cstr(mm, "allocated", DInteger::box(mm, this->allocated())); + ok &= rpt->upsert_cstr(mm, "committed", DInteger::box(mm, this->committed())); + ok &= rpt->upsert_cstr(mm, "reserved", DInteger::box(mm, this->reserved())); + ok &= rpt->upsert_cstr(mm, "n-mlog-entry", DInteger::box(mm, this->mutation_log_entries())); + + // per-(generation,role) info + { + for (Generation gi{0}; gi < config_.n_generation_; ++gi) { + for (role rj : role::all()) { + const DArena * arena = this->get_space(rj, gi); + DDictionary * arena_d = DDictionary::make(mm); + + auto lo = reinterpret_cast(arena->lo_); + auto free = reinterpret_cast(arena->free_); + auto limit = reinterpret_cast(arena->limit_); + auto hi = reinterpret_cast(arena->hi_); + + ok &= arena_d->upsert_cstr(mm, "lo", DInteger::box(mm, lo)); + ok &= arena_d->upsert_cstr(mm, "d-free", DInteger::box(mm, free - lo)); + ok &= arena_d->upsert_cstr(mm, "d-limit", DInteger::box(mm, limit - lo)); + ok &= arena_d->upsert_cstr(mm, "d-hi", DInteger::box(mm, hi - lo)); + + const DString * key = DString::from_str(mm, arena->config_.name_); + + rpt->upsert(mm, std::make_pair(key, obj(arena_d))); + } + } + } + + *p_output = obj(rpt); + + return ok; + } + size_type DX1Collector::header2size(header_type hdr) const noexcept { diff --git a/src/gc/IAllocator_DX1Collector.cpp b/src/gc/IAllocator_DX1Collector.cpp index 039e84df..37ed497f 100644 --- a/src/gc/IAllocator_DX1Collector.cpp +++ b/src/gc/IAllocator_DX1Collector.cpp @@ -29,7 +29,7 @@ namespace xo { auto IAllocator_DX1Collector::reserved(const DX1Collector & d) noexcept -> size_type { - return d.reserved_total(); + return d.reserved(); } auto @@ -41,19 +41,19 @@ namespace xo { auto IAllocator_DX1Collector::committed(const DX1Collector & d) noexcept -> size_type { - return d.committed_total(); + return d.committed(); } auto IAllocator_DX1Collector::available(const DX1Collector & d) noexcept -> size_type { - return d.available_total(); + return d.available(); } auto IAllocator_DX1Collector::allocated(const DX1Collector & d) noexcept -> size_type { - return d.allocated_total(); + return d.allocated(); } void diff --git a/src/gc/ICollector_DX1Collector.cpp b/src/gc/ICollector_DX1Collector.cpp deleted file mode 100644 index 5503d429..00000000 --- a/src/gc/ICollector_DX1Collector.cpp +++ /dev/null @@ -1,118 +0,0 @@ -/** @file ICollector_DX1Collector.cpp - * - * @author Roland Conybeare, Dec 2025 - * - * See also IAllocator_DX1Collector.cpp for allocator facet - **/ - -#include "xo/gc/detail/ICollector_DX1Collector.hpp" -#include "GCObject.hpp" - -namespace xo { - namespace mm { - using size_type = ICollector_DX1Collector::size_type; - using std::byte; - - namespace { - size_type - stat_helper(const DX1Collector & 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; - } - } - - size_type - ICollector_DX1Collector::reserved(const DX1Collector & d, Generation g, role r) - { - return stat_helper(d, &DArena::reserved, g, r); - } - - size_type - ICollector_DX1Collector::allocated(const DX1Collector & d, Generation g, role r) - { - return stat_helper(d, &DArena::allocated, g, r); - } - - size_type - ICollector_DX1Collector::committed(const DX1Collector & d, Generation g, role r) - { - return stat_helper(d, &DArena::committed, g, r); - } - - bool - ICollector_DX1Collector::contains(const DX1Collector & d, role r, const void * addr) - { - return d.contains(r, addr); - } - - bool - ICollector_DX1Collector::is_type_installed(const DX1Collector & d, typeseq tseq) - { - return d.is_type_installed(tseq); - } - - bool - ICollector_DX1Collector::install_type(DX1Collector & d, - const AGCObject & iface) - { - return d.install_type(iface); - } - - void - ICollector_DX1Collector::add_gc_root_poly(DX1Collector & d, - obj * p_root) - { - d.add_gc_root_poly(p_root); - } - - void - ICollector_DX1Collector::remove_gc_root_poly(DX1Collector & d, - obj * p_root) - { - d.remove_gc_root_poly(p_root); - } - - void - ICollector_DX1Collector::request_gc(DX1Collector & d, - Generation upto) - { - d.request_gc(upto); - } - - void - ICollector_DX1Collector::assign_member(DX1Collector & d, - void * parent, - obj * p_lhs, - obj & rhs) - { - d.assign_member(parent, p_lhs, rhs); - } - - void - ICollector_DX1Collector::forward_inplace(DX1Collector & d, - AGCObject * lhs_iface, - void ** lhs_data) - { - d.forward_inplace(lhs_iface, lhs_data); - } - - bool - ICollector_DX1Collector::check_move_policy(const DX1Collector & d, - header_type alloc_hdr, - void * object_data) - { - return d.check_move_policy(alloc_hdr, object_data); - } - - } /*namespace mm*/ -} /*namespace xo*/ - -/* end ICollector_DX1Collector.cpp */ diff --git a/src/gc/facet/ICollector_DX1Collector.cpp b/src/gc/facet/ICollector_DX1Collector.cpp new file mode 100644 index 00000000..10b4add5 --- /dev/null +++ b/src/gc/facet/ICollector_DX1Collector.cpp @@ -0,0 +1,88 @@ +/** @file ICollector_DX1Collector.cpp + * + * Generated automagically from ingredients: + * 1. code generator: + * [xo-facet/codegen/genfacet] + * arguments: + * --input [idl/ICollector_DX1Collector.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [iface_facet_any.hpp.j2] + * 3. idl for facet methods + * [idl/ICollector_DX1Collector.json5] +**/ + +#include "detail/ICollector_DX1Collector.hpp" + +namespace xo { + namespace mm { + auto + ICollector_DX1Collector::allocated(const DX1Collector & self, Generation g, role r) noexcept -> size_type + { + return self.allocated(g, r); + } + + auto + ICollector_DX1Collector::committed(const DX1Collector & self, Generation g, role r) noexcept -> size_type + { + return self.committed(g, r); + } + + auto + ICollector_DX1Collector::reserved(const DX1Collector & self, Generation g, role r) noexcept -> size_type + { + return self.reserved(g, r); + } + + auto + ICollector_DX1Collector::contains(const DX1Collector & self, role r, const void * addr) noexcept -> bool + { + return self.contains(r, addr); + } + + auto + ICollector_DX1Collector::is_type_installed(const DX1Collector & self, typeseq tseq) noexcept -> bool + { + return self.is_type_installed(tseq); + } + + auto + ICollector_DX1Collector::report_statistics(const DX1Collector & self, obj report_mm, obj error_mm, obj * output) noexcept -> bool + { + return self.report_statistics(report_mm, error_mm, output); + } + + auto + ICollector_DX1Collector::install_type(DX1Collector & self, const AGCObject & iface) -> bool + { + return self.install_type(iface); + } + auto + ICollector_DX1Collector::add_gc_root_poly(DX1Collector & self, obj * p_root) -> void + { + self.add_gc_root_poly(p_root); + } + auto + ICollector_DX1Collector::remove_gc_root_poly(DX1Collector & self, obj * p_root) -> void + { + self.remove_gc_root_poly(p_root); + } + auto + ICollector_DX1Collector::request_gc(DX1Collector & self, Generation upto) -> void + { + self.request_gc(upto); + } + auto + ICollector_DX1Collector::assign_member(DX1Collector & self, void * parent, obj * p_lhs, obj & rhs) -> void + { + self.assign_member(parent, p_lhs, rhs); + } + auto + ICollector_DX1Collector::forward_inplace(DX1Collector & self, AGCObject * lhs_iface, void ** lhs_data) -> void + { + self.forward_inplace(lhs_iface, lhs_data); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ICollector_DX1Collector.cpp */ diff --git a/src/gc/init_gc.cpp b/src/gc/init_gc.cpp index 8747229f..40dd700a 100644 --- a/src/gc/init_gc.cpp +++ b/src/gc/init_gc.cpp @@ -5,6 +5,7 @@ #include "init_gc.hpp" #include "SetupGc.hpp" +#include #include namespace xo { @@ -21,6 +22,7 @@ namespace xo { InitEvidence retval; /* recursive subsystem deps for xo-gc/ */ + retval ^= InitSubsys::require(); retval ^= InitSubsys::require(); /* xo-gc/'s own initialization code */ diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index c48dbdb5..0f3c9676 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -5,7 +5,9 @@ set(UTEST_EXE utest.gc) set(UTEST_SRCS gc_utest_main.cpp Collector.test.cpp + X1Collector.test.cpp DX1CollectorIterator.test.cpp + Object2.test.cpp random_allocs.cpp ) diff --git a/utest/Object2.test.cpp b/utest/Object2.test.cpp new file mode 100644 index 00000000..fa0baecb --- /dev/null +++ b/utest/Object2.test.cpp @@ -0,0 +1,134 @@ +/** @file Object2.test.cpp + * + * Tests that target xo-object2/ features, but also rely on xo-gc/ + * + * @author Roland Conybeare, Mar 2026 + **/ + +#include +#include +#include +#include +#include +#include + +namespace ut { + using xo::scm::SetupObject2; + using xo::scm::ListOps; + using xo::scm::DList; + using xo::scm::DInteger; + using xo::scm::DString; + using xo::mm::AAllocator; + using xo::mm::ACollector; + using xo::mm::AGCObject; + using xo::mm::X1CollectorConfig; + using xo::mm::DX1Collector; + using xo::mm::ArenaConfig; + using xo::print::APrintable; + using xo::facet::FacetRegistry; + using xo::facet::with_facet; + using xo::facet::obj; + using xo::facet::typeseq; + using xo::print::ppstate_standalone; + using xo::print::ppconfig; + using xo::scope; + using xo::xtag; + using std::string; + + namespace { + struct testcase_pp { + explicit testcase_pp(size_t gc_z, size_t gc_threshold, int z, const std::string & expected) + : gc_gen_size_{gc_z}, gc_trigger_threshold_{gc_threshold}, expected_{expected} { + for (int i = 0; i < z; ++i) { + list_.push_back(1000 + 197 * i); + } + } + + size_t gc_gen_size_ = 0; + size_t gc_trigger_threshold_ = 0; + std::vector list_; + std::string expected_; + }; + + std::vector + s_testcase_v = { + testcase_pp(16384, 8192, 0, "()"), + testcase_pp(16384, 8192, 1, "(01000)"), + testcase_pp(16384, 8192, 2, "(01000 1197)"), + testcase_pp(16384, 8192, 5, "(01000 1197 01394 1591 01788)"), + testcase_pp(16384, 8192, 10, "(01000 1197 01394 1591 01788 1985 02182 2379 02576 2773)"), + testcase_pp(16384, 8192, 20, "(...)"), + }; + } + + TEST_CASE("printable1", "[pp][x1][list]") + { + constexpr bool c_debug_flag = true; + scope log(XO_DEBUG(c_debug_flag)); + + bool ok = SetupObject2::register_facets(); + REQUIRE(ok); + + FacetRegistry::instance().dump(&std::cerr); + + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + log && log("printable1 test:", xtag("i_tc", i_tc)); + + try { + const testcase_pp & tc = s_testcase_v[i_tc]; + + X1CollectorConfig cfg{ .name_ = "pp_test", + .arena_config_ = ArenaConfig{ + .size_ = tc.gc_gen_size_, + .store_header_flag_ = true}, + .object_types_z_ = 16384, + .gc_trigger_v_{{tc.gc_trigger_threshold_, + tc.gc_trigger_threshold_}}, + .debug_flag_ = c_debug_flag + }; + + DX1Collector gc(cfg); + auto gc_o = with_facet::mkobj(&gc); + auto c_o = with_facet::mkobj(&gc); + + bool ok = SetupObject2::register_types(c_o); + REQUIRE(ok); + + auto l0_o = ListOps::nil(); + + c_o.add_gc_root(&l0_o); + + for(int ip1 = tc.list_.size(); ip1 > 0; --ip1) { + obj elt; + + //elt = DInteger::box(gc_o, tc.list_[ip1 - 1]); + + if (ip1 % 2 == 0) { + elt = DInteger::box(gc_o, tc.list_[ip1 - 1]); + } else { + elt = obj(DString::printf(gc_o, 80, "%05d", tc.list_[ip1 - 1])); + } + + l0_o = ListOps::cons(gc_o, elt, l0_o); + } + + // TODO: log_streambuf using DArena + std::stringstream ss; + ppconfig ppc; + ppstate_standalone pps(&ss, 0, &ppc); + + obj l0_po(static_cast(l0_o.data())); + REQUIRE(l0_po._typeseq() == typeseq::id()); + + pps.pretty(l0_po); + + CHECK(ss.str() == string(tc.expected_)); + } catch (std::exception & ex) { + std::cerr << "caught exception: " << ex.what() << std::endl; + REQUIRE(false); + } + } + } /* TEST_CASE(printable1) */ +} /*namespace ut*/ + +/* end Object2.test.cpp */ diff --git a/utest/X1Collector.test.cpp b/utest/X1Collector.test.cpp new file mode 100644 index 00000000..67d477ec --- /dev/null +++ b/utest/X1Collector.test.cpp @@ -0,0 +1,297 @@ +/** @file X1Collector.test.cpp + * + * @author Roland Conybeare, Dec 2025 + **/ + +#include "init_gc.hpp" +#include "ListOps.hpp" +#include "DFloat.hpp" +#include "DInteger.hpp" +#include "DList.hpp" +#include "DArray.hpp" + +#include +//#include "number/IGCObject_DFloat.hpp" +#include "number/IGCObject_DInteger.hpp" +#include "list/IGCObject_DList.hpp" + +#include +#include +#include + +#include +#include + +#include + +#include +#include + +#include + +namespace ut { + using xo::S_gc_tag; + using xo::scm::ListOps; + using xo::scm::DList; + using xo::scm::DArray; + using xo::scm::DFloat; + using xo::scm::DInteger; + using xo::mm::CollectorTypeRegistry; + using xo::mm::AAllocator; + using xo::mm::ACollector; + using xo::mm::AllocHeader; + using xo::mm::AllocInfo; + using xo::mm::AGCObject; + using xo::mm::X1CollectorConfig; + using xo::mm::DX1Collector; + using xo::mm::DArena; + using xo::mm::ArenaConfig; + using xo::mm::Generation; + using xo::mm::role; + using xo::mm::padding; + using xo::facet::obj; + using xo::facet::with_facet; + using xo::facet::typeseq; + using xo::Subsystem; + using xo::InitEvidence; + using xo::InitSubsys; + using xo::scope; + using xo::xtag; + + namespace { + struct testcase_x1 { + testcase_x1(std::size_t nz, + std::size_t tz, + std::size_t n_gct, + std::size_t t_gct) + : nursery_z_{nz}, + tenured_z_{tz}, + incr_gc_threshold_{n_gct}, + full_gc_threshold_{t_gct} {} + + std::size_t nursery_z_; + std::size_t tenured_z_; + std::size_t incr_gc_threshold_; + std::size_t full_gc_threshold_; + }; + + std::vector + s_testcase_v = { + // n_gct: nursery gc threshold + // t_gct: tenured gc threshold + // + // nz tz n_gct t_gct + testcase_x1(4096, 8192, 1024, 1024) + }; + } + + static InitEvidence s_init = (InitSubsys::require()); + + TEST_CASE("x1", "[gc][x1]") + { + // real purpose: ensure s_init survives static linking + REQUIRE(s_init.evidence()); + + Subsystem::initialize_all(); + + /** + * This is a basic Collector test for xo-object2 data types + **/ + + constexpr bool c_debug_flag = true; + scope log(XO_DEBUG(c_debug_flag)); + + 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)); + + try { + const testcase_x1 & tc = s_testcase_v[i_tc]; + + X1CollectorConfig cfg{ .name_ = "x1_test", + .arena_config_ = ArenaConfig{ + .size_ = tc.tenured_z_, + .store_header_flag_ = true}, + .object_types_z_ = 16384, + .gc_trigger_v_{{ + tc.incr_gc_threshold_, + tc.full_gc_threshold_}}, + .debug_flag_ = c_debug_flag, + }; + + DX1Collector gc(cfg); + + CollectorTypeRegistry::instance().install_types(obj(&gc)); + + DArena * to_0 = nullptr; + + /* verify configuration */ + { + REQUIRE(cfg.n_generation_ == 2); + } + + /* verify initial collector state */ + { + REQUIRE(gc.name() == "x1_test"); + + const DArena * otypes = gc.get_object_types(); + + REQUIRE(otypes != nullptr); + REQUIRE(otypes->reserved() >= cfg.object_types_z_); + REQUIRE(otypes->reserved() < cfg.object_types_z_ + otypes->page_z_); + + const DX1Collector::RootSet * roots = gc.get_root_set(); + + REQUIRE(roots != nullptr); + REQUIRE(roots->store()->reserved() >= cfg.object_roots_z_); + REQUIRE(roots->store()->reserved() < cfg.object_roots_z_ + roots->store()->page_z_); + + const DArena * from_0 = gc.get_space(role::from_space(), Generation{0}); + + REQUIRE(from_0 != nullptr); + REQUIRE(from_0->reserved() >= tc.tenured_z_); + REQUIRE(from_0->reserved() < tc.tenured_z_ + from_0->page_z_); + REQUIRE(from_0->reserved() % from_0->page_z_ == 0); + REQUIRE(from_0->allocated() == 0); + + const DArena * from_1 = gc.get_space(role::from_space(), Generation{1}); + + REQUIRE(from_1 != nullptr); + REQUIRE(from_1->reserved() == from_0->reserved()); + REQUIRE(from_1->allocated() == 0); + + to_0 = gc.get_space(role::to_space(), Generation{0}); + + REQUIRE(to_0 != nullptr); + REQUIRE(to_0->reserved() == from_0->reserved()); + REQUIRE(to_0->allocated() == 0); + + const DArena * to_1 = gc.get_space(role::to_space(), Generation{1}); + + REQUIRE(to_1 != nullptr); + REQUIRE(to_1->reserved() == to_0->reserved()); + REQUIRE(to_1->allocated() == 0); + + const DArena * from_2 = gc.get_space(role::from_space(), Generation{2}); + + REQUIRE(from_2 == nullptr); + + const DArena * to_2 = gc.get_space(role::to_space(), Generation{2}); + + REQUIRE(to_2 == nullptr); + + REQUIRE(gc.reserved() + == otypes->reserved() + roots->store()->reserved() + 4 * from_0->reserved()); + + log && log(xtag("from_0", from_0->lo_), xtag("to_0", to_0->lo_)); + } + + /* attempt allocation */ + auto gc_o = with_facet::mkobj(&gc); + auto c_o = with_facet::mkobj(&gc); + + /* register object types */ + bool ok = CollectorTypeRegistry::instance().install_types(c_o); + + REQUIRE(ok); + + ok = c_o.is_type_installed(typeseq::id()); + REQUIRE(ok); + ok = c_o.is_type_installed(typeseq::id()); + REQUIRE(ok); + ok = c_o.is_type_installed(typeseq::id()); + REQUIRE(ok); + ok = c_o.is_type_installed(typeseq::id()); + REQUIRE(ok); + + auto x0_o = DFloat::box(gc_o, 3.1415927); + c_o.add_gc_root(&x0_o); + REQUIRE(to_0->allocated() == sizeof(AllocHeader) + sizeof(DFloat)); + + auto n1_o = DInteger::box(gc_o, 42); + c_o.add_gc_root(&n1_o); + REQUIRE(to_0->allocated() == (sizeof(AllocHeader) + sizeof(DFloat) + + sizeof(AllocHeader) + sizeof(DInteger))); + + //DList * l0 = DList::list(gc_o, x0_o); + //auto l0_o = with_facet::mkobj(l0); + auto l0_o = ListOps::list(gc_o, x0_o); + c_o.add_gc_root(&l0_o); + REQUIRE(to_0->allocated() == (sizeof(AllocHeader) + sizeof(DFloat) + + sizeof(AllocHeader) + sizeof(DInteger) + + sizeof(AllocHeader) + sizeof(DList))); + + { + { + REQUIRE(x0_o.iface() != nullptr); + REQUIRE(x0_o.data() != nullptr); + REQUIRE(gc.contains(role::to_space(), x0_o.data())); + + /* check alloc info for newly-allocated object */ + AllocInfo info = gc.alloc_info((std::byte *)x0_o.data()); + + REQUIRE(info.age() == 0); + REQUIRE(info.tseq() == typeseq::id().seqno()); + REQUIRE(info.size() >= sizeof(DFloat)); + REQUIRE(info.size() < sizeof(DFloat) + padding::c_alloc_alignment); + } + + { + REQUIRE(n1_o.iface() != nullptr); + REQUIRE(n1_o.data() != nullptr); + REQUIRE(gc.contains(role::to_space(), n1_o.data())); + + /* check alloc info for newly-allocated object */ + AllocInfo info = gc.alloc_info((std::byte *)n1_o.data()); + + REQUIRE(info.age() == 0); + REQUIRE(info.tseq() == typeseq::id().seqno()); + REQUIRE(info.size() >= sizeof(DInteger)); + REQUIRE(info.size() < sizeof(DInteger) + padding::c_alloc_alignment); + } + + { + REQUIRE(l0_o.iface() != nullptr); + REQUIRE(l0_o.data() != nullptr); + REQUIRE(gc.contains(role::to_space(), l0_o.data())); + + /* check alloc info for newly-allocated object */ + AllocInfo info = gc.alloc_info((std::byte *)l0_o.data()); + + REQUIRE(info.age() == 0); + REQUIRE(info.tseq() == typeseq::id().seqno()); + REQUIRE(info.size() >= sizeof(DList)); + REQUIRE(info.size() < sizeof(DList) + padding::c_alloc_alignment); + } + } + + /* no GC roots, so GC is trivial */ + c_o.request_gc(Generation{1}); + + log && log(xtag("l0_o.data()", l0_o.data())); + log && log(xtag("l0_o.data()->head_.data()", l0_o.data()->head_.data())); + log && log(xtag("x0_o.data()", x0_o.data())); + + REQUIRE(!gc.contains(role::from_space(), x0_o.data())); + REQUIRE(gc.contains(role::to_space(), x0_o.data())); + REQUIRE(x0_o.data()->value() == 3.1415927); + + REQUIRE(!gc.contains(role::from_space(), n1_o.data())); + REQUIRE(gc.contains(role::to_space(), n1_o.data())); + REQUIRE(n1_o.data()->value() == 42); + + REQUIRE(!gc.contains(role::from_space(), l0_o.data())); + REQUIRE(gc.contains(role::to_space(), l0_o.data())); + REQUIRE(l0_o.data()->is_empty() == false); + + REQUIRE((void*)l0_o.data()->head_.data() == (void*)x0_o.data()); + REQUIRE((void*)l0_o.data()->rest_ == (void*)DList::_nil()); + + } catch (std::exception & ex) { + std::cerr << "caught exception: " << ex.what() << std::endl; + REQUIRE(false); + } + } + } +} + +/* end X1Collector.test.cpp */ From 890fd0fe1ad66ff5df68d7c0bb0e11bff287f91f Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 29 Mar 2026 15:17:31 -0400 Subject: [PATCH 072/174] xo-gc: use DArenaVector for DX1Collector.object_types_ Original implementation predated DArenaVector, using it is more natural --- include/xo/gc/DX1Collector.hpp | 48 ++++++++++++++++++++++- src/gc/DX1Collector.cpp | 70 +++++++++++++++++++++------------- utest/X1Collector.test.cpp | 8 ++-- 3 files changed, 93 insertions(+), 33 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index d5934611..9b6366cb 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -80,6 +80,49 @@ namespace xo { obj * root_ = nullptr; }; + /** @brief Object Interface + * + * GC-object interface for a particular type. + * X1 maintains a table of these (X1Collector::object_types_) + * indexed by typeseq. + * + * Using a wrapper here for searchability + **/ + struct ObjectTypeSlot { + ObjectTypeSlot() {} + explicit ObjectTypeSlot(AGCObject * iface) { + this->store_iface(iface); + } + + /** true iff this slot is empty **/ + bool is_null() const noexcept { + return this->iface()->_has_null_vptr(); + } + + bool is_occupied() const noexcept { + return !this->is_null(); + } + + AGCObject * iface() const noexcept { + return std::launder((AGCObject *)&iface_[0]); + } + + /** Store interface pointer @p iface. + * We just want the vtable here + **/ + void store_iface(const AGCObject * iface) { + ::memcpy((void*)&(this->iface_[0]), (void*)iface, sizeof(AGCObject)); + } + + private: + /** runtime interface for this object. + * We might prefer to declare this as AGCObject, but that's prohibited + * since AGCObject has abstract methods. + * Main downside of this form is it makes the data unintelligible to debugger. + **/ + alignas(AGCObject) std::byte iface_[sizeof(AGCObject)]; + }; + /** @brief info collected during a @ref DX1Collector::verify_ok call * **/ @@ -119,6 +162,7 @@ namespace xo { struct DX1Collector { public: using RootSet = DArenaVector; + using ObjectTypeTable = DArenaVector; /* TODO: AllocIterator pointing to free pointer instead of std::byte* */ using GCMoveCheckpoint = std::array; using MutationLog = DArenaVector; @@ -142,7 +186,7 @@ namespace xo { std::string_view name() const noexcept { return config_.name_; } GCRunState runstate() const noexcept { return runstate_; } - const DArena * get_object_types() const noexcept { return &object_types_; } + const ObjectTypeTable * get_object_types() const noexcept { return &object_types_; } const RootSet * get_root_set() const noexcept { return &root_set_; } const DArena * get_space(role r, Generation g) const noexcept { return space_[r][g]; } DArena * get_space(role r, Generation g) noexcept { return space_[r][g]; } @@ -409,7 +453,7 @@ namespace xo { /** (ab)using arena to get an extensible array of object types. * For each type need to store one (8-byte) IGCObject_Any instance, **/ - DArena object_types_; + ObjectTypeTable object_types_; /** gc disabled whenever gc_blocked_ > 0 **/ uint32_t gc_blocked_ = 0; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index a668b8d4..88ffef90 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -82,10 +82,10 @@ namespace xo { * likely << .size/8 */ this->object_types_ - = DArena::map(ArenaConfig{.name_ = "x1-object-types", - .size_ = cfg.object_types_z_, - .hugepage_z_ = page_z, - .store_header_flag_ = false}); + = ObjectTypeTable::map(ArenaConfig{.name_ = "x1-object-types", + .size_ = cfg.object_types_z_, + .hugepage_z_ = page_z, + .store_header_flag_ = false}); } void @@ -242,7 +242,7 @@ namespace xo { accumulate_total_aux(const DX1Collector & d, size_t (DArena::* get_stat_fn)() const) noexcept { - size_t z1 = (d.object_types_.*get_stat_fn)(); + size_t z1 = (d.object_types_.store()->*get_stat_fn)(); size_t z2 = (d.root_set_.store()->*get_stat_fn)(); size_t z3 = 0; @@ -424,14 +424,14 @@ namespace xo { bool DX1Collector::is_type_installed(typeseq tseq) const noexcept { - if (object_types_.committed() < sizeof(AGCObject) * (tseq.seqno() + 1)) + if (tseq.is_sentinel() + || static_cast(tseq.seqno()) > object_types_.size()) { return false; + } - AGCObject * v = reinterpret_cast(object_types_.lo_); + const ObjectTypeSlot & slot = object_types_[tseq.seqno()]; - void * vtable = *(void **)&(v[tseq.seqno()]); - - return (vtable != nullptr); + return slot.is_occupied(); } AllocInfo @@ -600,24 +600,34 @@ namespace xo { { scope log(XO_DEBUG(false)); - AGCObject * v = reinterpret_cast(object_types_.lo_); + if (tseq.is_sentinel() + || static_cast(tseq.seqno()) > object_types_.size()) { - const AGCObject * target = &(v[tseq.seqno()]); + log.retroactively_enable("out-of-bounds", + xtag("tseq", tseq), xtag("tname", TypeRegistry::id2name(tseq))); - if (reinterpret_cast(target) >= object_types_.limit_) { - log.retroactively_enable(xtag("tseq", tseq), xtag("tname", TypeRegistry::id2name(tseq))); - - log(xtag("types.allocated", object_types_.allocated()), - xtag("types.committed", object_types_.committed()), - xtag("types.lo", object_types_.lo_), - xtag("types.limit", object_types_.limit_), - xtag("types.hi", object_types_.hi_)); + log(xtag("types.size", object_types_.size()), + xtag("types.allocated", object_types_.store()->allocated()), + xtag("types.committed", object_types_.store()->committed()), + xtag("types.lo", object_types_.store()->lo_), + xtag("types.limit", object_types_.store()->limit_), + xtag("types.hi", object_types_.store()->hi_)); assert(false); return nullptr; } - return target; + const ObjectTypeSlot & slot = object_types_[tseq.seqno()]; + + if (slot.is_null()) { + log.retroactively_enable("null-vtable", + xtag("tseq", tseq), xtag("tname", TypeRegistry::id2name(tseq))); + + assert(false); + return nullptr; + } + + return slot.iface(); } /* editor bait: register_type */ @@ -626,14 +636,20 @@ namespace xo { { typeseq tseq = meta._typeseq(); - bool ok = object_types_.expand(sizeof(AGCObject) * (tseq.seqno() + 1)); - if (!ok) - return false; + assert(tseq.seqno() > 0); - AGCObject * v = reinterpret_cast(object_types_.lo_); + auto ix = static_cast(tseq.seqno()); - /* explicitly copying vtable pointer here */ - std::memcpy((void*)&(v[tseq.seqno()]), (void*)&meta, sizeof(AGCObject)); + if (ix >= object_types_.size()) { + if (!object_types_.resize(std::max(2 * object_types_.size(), ix + 1))) + return false; + } + + assert(ix < object_types_.size()); + + ObjectTypeSlot & slot = object_types_[ix]; + + slot.store_iface(&meta); return true; } diff --git a/utest/X1Collector.test.cpp b/utest/X1Collector.test.cpp index 67d477ec..535b64cd 100644 --- a/utest/X1Collector.test.cpp +++ b/utest/X1Collector.test.cpp @@ -133,11 +133,11 @@ namespace ut { { REQUIRE(gc.name() == "x1_test"); - const DArena * otypes = gc.get_object_types(); + const DX1Collector::ObjectTypeTable * otypes = gc.get_object_types(); REQUIRE(otypes != nullptr); - REQUIRE(otypes->reserved() >= cfg.object_types_z_); - REQUIRE(otypes->reserved() < cfg.object_types_z_ + otypes->page_z_); + REQUIRE(otypes->store()->reserved() >= cfg.object_types_z_); + REQUIRE(otypes->store()->reserved() < cfg.object_types_z_ + otypes->store()->page_z_); const DX1Collector::RootSet * roots = gc.get_root_set(); @@ -180,7 +180,7 @@ namespace ut { REQUIRE(to_2 == nullptr); REQUIRE(gc.reserved() - == otypes->reserved() + roots->store()->reserved() + 4 * from_0->reserved()); + == otypes->store()->reserved() + roots->store()->reserved() + 4 * from_0->reserved()); log && log(xtag("from_0", from_0->lo_), xtag("to_0", to_0->lo_)); } From 84e1f4e4d30263f53069183861fffb4128b31bf7 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 29 Mar 2026 17:19:23 -0400 Subject: [PATCH 073/174] xo-gc stack: + gc-report-object-types() primitive --- include/xo/gc/DX1Collector.hpp | 21 ++- .../xo/gc/detail/ICollector_DX1Collector.hpp | 5 + src/gc/DX1Collector.cpp | 121 +++++++++++++++++- src/gc/facet/ICollector_DX1Collector.cpp | 6 + 4 files changed, 150 insertions(+), 3 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 9b6366cb..c1e1669e 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -96,17 +96,23 @@ namespace xo { /** true iff this slot is empty **/ bool is_null() const noexcept { - return this->iface()->_has_null_vptr(); + return this->_iface()->_has_null_vptr(); } bool is_occupied() const noexcept { return !this->is_null(); } - AGCObject * iface() const noexcept { + AGCObject * _iface() const noexcept { return std::launder((AGCObject *)&iface_[0]); } + AGCObject * iface() const noexcept { + AGCObject * x = this->_iface(); + + return (x->_has_null_vptr() ? nullptr : x); + } + /** Store interface pointer @p iface. * We just want the vtable here **/ @@ -230,6 +236,17 @@ namespace xo { obj error_mm, obj * p_output) const noexcept; + /** Report per-object-type information as a dictionary. + * Scans to-space to count per-object-type information + * + * @p mm allocate stats dictionary from this allocator. May be the same as this collector. + * @p error_mm Allocator for last-report error reporting when out-of-memory. + * @p p_output on exit @p *p_output contains stats dictionary + **/ + bool report_object_types(obj mm, + obj error_mm, + obj * p_output) const noexcept; + // ----- queries ----- /** introspection for memory use. diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index a9c8e012..f7db4b90 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -62,6 +62,11 @@ 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 DX1Collector & 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 DX1Collector & self, obj report_mm, obj error_mm, obj * output) noexcept; // non-const methods /** install interface @p iface for representation with typeseq @p tseq diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 88ffef90..d9f09b88 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -6,7 +6,10 @@ #include "X1Collector.hpp" #include #include +#include #include +#include +#include #include #include #include @@ -20,10 +23,12 @@ #include // for ::getpagesize() namespace xo { - // for report_statistics() + // for report_statistics(), report_object_types() using xo::scm::DDictionary; + using xo::scm::DArray; using xo::scm::DString; using xo::scm::DInteger; + using xo::scm::DBoolean; using xo::mm::AAllocator; using xo::facet::TypeRegistry; @@ -347,6 +352,9 @@ namespace xo { DDictionary * rpt = DDictionary::make(mm); + if (!rpt) + return false; + bool ok = true; // note: totals taken across both roles and generations, @@ -388,6 +396,117 @@ namespace xo { return ok; } + bool + DX1Collector::report_object_types(obj mm, + obj error_mm, + obj * p_output) const noexcept + { + scope log(XO_DEBUG(true)); + + (void)error_mm; + + bool ok = true; + + // stats, indexed by tseq + // could use c++ vector in scratch space instead of running on + // boxed types. + // + DArray * stats_v = DArray::empty(mm, object_types_.size()); + + if (!stats_v) + return false; + + stats_v->resize(stats_v->capacity()); + + log && log(xtag("object_types_.size", object_types_.size()), + xtag("stats_v.capacity", stats_v->capacity()), + xtag("stats_v.size", stats_v->size())); + + // count #of occupied type slots + std::uint32_t n_tseq_present = 0; + // largest tseq present with non-null AGCObject* iface + std::int32_t max_tseq = 0; + + for (const ObjectTypeSlot & slot : object_types_) { + AGCObject * iface = slot.iface(); + + if (iface) { + typeseq tseq = iface->_typeseq(); + + ++n_tseq_present; + if (max_tseq < tseq.seqno()) + max_tseq = tseq.seqno(); + + assert(tseq.seqno() >= 0); + + auto tname_sv = TypeRegistry::id2name(tseq); + DString * tname = DString::from_view(mm, tname_sv); + + DDictionary * recd = DDictionary::make(mm); + + if (!recd) + return false; + + recd->upsert_cstr(mm, "name", obj(tname)); + recd->upsert_cstr(mm, "tseq", DInteger::box(mm, tseq.seqno())); + recd->upsert_cstr(mm, "n-live", DInteger::box(mm, 0)); + recd->upsert_cstr(mm, "bytes", DInteger::box(mm, 0)); + + stats_v->assign_at(tseq.seqno(), obj(recd)); + } + } + + // scan to-space, count objects by type + + for (Generation g{0}; g < config_.n_generation_; ++g) { + const DArena * arena = this->get_space(role::to_space(), g); + + for (AllocInfo info : *arena) { + if (info.is_forwarding_tseq()) { + assert(false); + return false; + } + + uint32_t ix = info.tseq(); + size_t z = info.size(); + + auto recd = obj::from(stats_v->at(ix)); + + assert(recd); + + auto n_live_opt = recd->lookup_cstr("n-live"); + assert(n_live_opt); + auto bytes_opt = recd->lookup_cstr("bytes"); + assert(bytes_opt); + + if (n_live_opt && bytes_opt) { + auto n_live_gco = obj::from(n_live_opt.value()); + auto bytes_gco = obj::from(bytes_opt.value()); + + n_live_gco->assign_value(n_live_gco->value() + 1); + bytes_gco->assign_value(bytes_gco->value() + z); + } + } + } + + stats_v->resize(max_tseq + 1); + + DArray * final_stats_v = DArray::empty(mm, n_tseq_present); + + for (std::size_t i = 0, n = stats_v->size(); i < n; ++i) { + auto recd = stats_v->at(i); + + if (recd) { + bool ok = final_stats_v->push_back(recd); + assert(ok); + } + } + + *p_output = obj(final_stats_v); + + return ok; + } + size_type DX1Collector::header2size(header_type hdr) const noexcept { diff --git a/src/gc/facet/ICollector_DX1Collector.cpp b/src/gc/facet/ICollector_DX1Collector.cpp index 10b4add5..2e3f55f4 100644 --- a/src/gc/facet/ICollector_DX1Collector.cpp +++ b/src/gc/facet/ICollector_DX1Collector.cpp @@ -51,6 +51,12 @@ namespace xo { return self.report_statistics(report_mm, error_mm, output); } + auto + ICollector_DX1Collector::report_object_types(const DX1Collector & self, obj report_mm, obj error_mm, obj * output) noexcept -> bool + { + return self.report_object_types(report_mm, error_mm, output); + } + auto ICollector_DX1Collector::install_type(DX1Collector & self, const AGCObject & iface) -> bool { From caf2cb3e9be5afaf186b89b84aaaba8d166ce17a Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 29 Mar 2026 19:47:15 -0400 Subject: [PATCH 074/174] xo-gc stack: + gc-location-of() primitive --- include/xo/gc/DX1Collector.hpp | 3 +++ .../xo/gc/detail/ICollector_DX1Collector.hpp | 4 ++++ src/gc/DX1Collector.cpp | 22 +++++++++++++++++++ src/gc/facet/ICollector_DX1Collector.cpp | 6 +++++ 4 files changed, 35 insertions(+) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index c1e1669e..8279c8e9 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -223,6 +223,9 @@ namespace xo { /** memory (virtual addresses) reserved for generation @p g in role @p r **/ size_type reserved(Generation g, role r) const noexcept; + /** very similar to generation_of(), but satisfies ACollector api **/ + std::int32_t locate_address(const void * addr) const noexcept; + // ----- full statistics ----- /** Report gc statistics as a dictionary. diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index f7db4b90..f73cb012 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -53,6 +53,10 @@ namespace xo { static size_type committed(const DX1Collector & self, Generation g, role r) noexcept; /** address space reserved for this collector **/ static size_type reserved(const DX1Collector & 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 DX1Collector & 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 DX1Collector & self, role r, const void * addr) noexcept; /** true iff gc-aware object of type @p tseq is installed in this collector **/ diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index d9f09b88..8a90a0b5 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -342,6 +342,27 @@ namespace xo { return stat_helper(*this, &DArena::reserved, g, r); } + std::int32_t + DX1Collector::locate_address(const void * addr) const noexcept + { + Generation g; + + g = this->generation_of(role::to_space(), addr); + + if (!g.is_sentinel()) + return g; + + g = this->generation_of(role::from_space(), addr); + + if (!g.is_sentinel()) { + // use negative values for + + return -1 - g; + } + + return -1; + } + // editor bait: report-gc-statistics bool DX1Collector::report_statistics(obj mm, @@ -362,6 +383,7 @@ namespace xo { // ok &= rpt->upsert_cstr(mm, "n-generation", DInteger::box(mm, config_.n_generation_)); ok &= rpt->upsert_cstr(mm, "n-survive-threshold", DInteger::box(mm, config_.n_survive_threshold_)); + ok &= rpt->upsert_cstr(mm, "allow-incremental-gc", DBoolean::box(mm, config_.allow_incremental_gc_)); ok &= rpt->upsert_cstr(mm, "allocated", DInteger::box(mm, this->allocated())); ok &= rpt->upsert_cstr(mm, "committed", DInteger::box(mm, this->committed())); ok &= rpt->upsert_cstr(mm, "reserved", DInteger::box(mm, this->reserved())); diff --git a/src/gc/facet/ICollector_DX1Collector.cpp b/src/gc/facet/ICollector_DX1Collector.cpp index 2e3f55f4..b07f1695 100644 --- a/src/gc/facet/ICollector_DX1Collector.cpp +++ b/src/gc/facet/ICollector_DX1Collector.cpp @@ -33,6 +33,12 @@ namespace xo { return self.reserved(g, r); } + auto + ICollector_DX1Collector::locate_address(const DX1Collector & self, const void * addr) noexcept -> std::int32_t + { + return self.locate_address(addr); + } + auto ICollector_DX1Collector::contains(const DX1Collector & self, role r, const void * addr) noexcept -> bool { From 490cd69902b17ced26881e24136bea489aa2cc56 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 30 Mar 2026 14:51:51 -0400 Subject: [PATCH 075/174] xo-procedure2 stack: + report-gc-object-ages() primitive --- include/xo/gc/DX1Collector.hpp | 13 +++ .../xo/gc/detail/ICollector_DX1Collector.hpp | 6 ++ src/gc/DX1Collector.cpp | 81 +++++++++++++++++++ src/gc/facet/ICollector_DX1Collector.cpp | 6 ++ 4 files changed, 106 insertions(+) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 8279c8e9..c81191d4 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -250,6 +250,19 @@ namespace xo { obj error_mm, obj * p_output) const noexcept; + /** Report per-age-bucket information as an array of dictionaries. + * Scans to-space to count per-age statistics. + * Each dictionary has keys "n-live" and "bytes". + * Array index corresponds to object age. + * + * @p mm allocate stats from this allocator. + * @p error_mm allocator for error reporting when out-of-memory. + * @p p_output on exit @p *p_output contains stats array + **/ + bool report_object_ages(obj mm, + obj error_mm, + obj * p_output) const noexcept; + // ----- queries ----- /** introspection for memory use. diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index f73cb012..1ae5fac0 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -71,6 +71,12 @@ 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 DX1Collector & 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 DX1Collector & self, obj report_mm, obj error_mm, obj * output) noexcept; // non-const methods /** install interface @p iface for representation with typeseq @p tseq diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 8a90a0b5..f8eb7513 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -529,6 +529,87 @@ namespace xo { return ok; } + bool + DX1Collector::report_object_ages(obj mm, + obj error_mm, + obj * p_output) const noexcept + { + scope log(XO_DEBUG(true)); + + (void)error_mm; + + std::uint64_t n_age = config_.arena_config_.header_.max_age() + 1; + + // stats, indexed by age + DArray * stats_v = DArray::empty(mm, n_age); + + if (!stats_v) + return false; + + // pre-populate with empty dictionaries for each age bucket + for (std::uint64_t a = 0; a < n_age; ++a) { + DDictionary * recd = DDictionary::make(mm); + + if (!recd) + return false; + + recd->upsert_cstr(mm, "age", DInteger::box(mm, a)); + recd->upsert_cstr(mm, "n-live", DInteger::box(mm, 0)); + recd->upsert_cstr(mm, "bytes", DInteger::box(mm, 0)); + + stats_v->push_back(obj(recd)); + } + + log && log(xtag("n_age", n_age), + xtag("stats_v.size", stats_v->size())); + + // scan to-space, count objects by age + + // track largest age with at least one object + std::int64_t max_age_present = 0; + + for (Generation g{0}; g < config_.n_generation_; ++g) { + const DArena * arena = this->get_space(role::to_space(), g); + + for (AllocInfo info : *arena) { + if (info.is_forwarding_tseq()) { + assert(false); + return false; + } + + uint32_t age = info.age(); + size_t z = info.size(); + + if (static_cast(age) > max_age_present) + max_age_present = age; + + auto recd = obj::from(stats_v->at(age)); + + assert(recd); + + auto n_live_opt = recd->lookup_cstr("n-live"); + assert(n_live_opt); + auto bytes_opt = recd->lookup_cstr("bytes"); + assert(bytes_opt); + + if (n_live_opt && bytes_opt) { + auto n_live_gco = obj::from(n_live_opt.value()); + auto bytes_gco = obj::from(bytes_opt.value()); + + n_live_gco->assign_value(n_live_gco->value() + 1); + bytes_gco->assign_value(bytes_gco->value() + z); + } + } + } + + // trim to only report ages up to max observed + stats_v->resize(max_age_present + 1); + + *p_output = obj(stats_v); + + return true; + } + size_type DX1Collector::header2size(header_type hdr) const noexcept { diff --git a/src/gc/facet/ICollector_DX1Collector.cpp b/src/gc/facet/ICollector_DX1Collector.cpp index b07f1695..33a28aa9 100644 --- a/src/gc/facet/ICollector_DX1Collector.cpp +++ b/src/gc/facet/ICollector_DX1Collector.cpp @@ -63,6 +63,12 @@ namespace xo { return self.report_object_types(report_mm, error_mm, output); } + auto + ICollector_DX1Collector::report_object_ages(const DX1Collector & self, obj report_mm, obj error_mm, obj * output) noexcept -> bool + { + return self.report_object_ages(report_mm, error_mm, output); + } + auto ICollector_DX1Collector::install_type(DX1Collector & self, const AGCObject & iface) -> bool { From 31c3d2765e55821f7d1a6232e1f9f89a44bf8981 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 30 Mar 2026 17:52:32 -0400 Subject: [PATCH 076/174] xo-gc: add .sanitize flag to gc-report-statistics() primitive --- src/gc/DX1Collector.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index f8eb7513..36082453 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -384,6 +384,7 @@ namespace xo { ok &= rpt->upsert_cstr(mm, "n-generation", DInteger::box(mm, config_.n_generation_)); ok &= rpt->upsert_cstr(mm, "n-survive-threshold", DInteger::box(mm, config_.n_survive_threshold_)); ok &= rpt->upsert_cstr(mm, "allow-incremental-gc", DBoolean::box(mm, config_.allow_incremental_gc_)); + ok &= rpt->upsert_cstr(mm, "sanitize", DBoolean::box(mm, config_.sanitize_flag_)); ok &= rpt->upsert_cstr(mm, "allocated", DInteger::box(mm, this->allocated())); ok &= rpt->upsert_cstr(mm, "committed", DInteger::box(mm, this->committed())); ok &= rpt->upsert_cstr(mm, "reserved", DInteger::box(mm, this->reserved())); @@ -1516,6 +1517,15 @@ namespace xo { xtag("tname", TypeRegistry::id2name(typeseq(info.tseq()))), xtag("age", info.age()), xtag("size", info.size())); + if (config_.sanitize_flag_) { + AllocInfo info_copy = this->alloc_info((std::byte *)to_dest); + + log && log(xtag("age2", info_copy.age()), xtag("size2", info_copy.size())); + + assert((info_copy.age() == config_.arena_config_.header_.max_age()) + || (info_copy.age() == info.age() + 1)); + } + if(to_dest == from_src) { assert(false); } else { From 9a3f78d92dd8b44fbccd80de3e0541af2e311ce3 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 1 Apr 2026 22:26:23 -0400 Subject: [PATCH 077/174] xo-gc: mutation log properly handled during collection cycle. NOT TESTED --- include/xo/gc/DX1Collector.hpp | 63 ++++++ include/xo/gc/MutationLogEntry.hpp | 6 +- include/xo/gc/X1CollectorConfig.hpp | 9 + src/gc/DX1Collector.cpp | 288 +++++++++++++++++++++++++++- 4 files changed, 361 insertions(+), 5 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index c81191d4..fa7dcdd6 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -129,6 +129,31 @@ namespace xo { alignas(AGCObject) std::byte iface_[sizeof(AGCObject)]; }; + /** @brief statistics from mlog forwarding **/ + class MutationLogStatistics { + public: + MutationLogStatistics() = default; + + MutationLogStatistics & operator+=(const MutationLogStatistics & x) { + n_stale_ += x.n_stale_; + n_live_parent_ += x.n_live_parent_; + n_rescue_ += x.n_rescue_; + n_triage_ += x.n_triage_; + + return *this; + } + + public: + /** count superseded mlog entries **/ + std::size_t n_stale_ = 0; + /** count live parents encountered during mlog scan **/ + std::size_t n_live_parent_ = 0; + /** count child subgraphs rescued during mlog scan **/ + std::size_t n_rescue_ = 0; + /** count triaged mlog entries **/ + std::size_t n_triage_ = 0; + }; + /** @brief info collected during a @ref DX1Collector::verify_ok call * **/ @@ -445,6 +470,44 @@ namespace xo { /** copy roots + everything reachable from them, to to-space **/ void copy_roots(Generation upto) noexcept; + /** cureate new mutation log after copying roots **/ + void forward_mutation_log(Generation upto); + + /** Perform one pass over contents of @p *from_mlog for generation @p gen. + * @p *from_mlog contains all {xgen,xage} pointers that target generation @p gen. + * Surviving mlog entries are moved to either @p *to_mlog or @p *triage_mlog, + * (generation < @p upto being collected this cycle). + * + * Each mlog entry gets one of the following outcomes. + * 1. skip. mlog entry has been superseded by another mut at target site. + * 2. keep. mlog entry is live. destination has been evacuated, + * so source must be updated as well. + * 3. triage. source of incoming object belongs to a generation that was collected, + * and has not been evacuated. Although appears to be garbage, it may + * be live after all if reachable from the destination of some other + * mlog entry in @p *to_mlog. Store these mlog entries in @p *triage_mlog. + * + * @return number of mlog entries moved, whether to @p *to_mlog or @p *triage_mlog. + **/ + MutationLogStatistics _forward_mutation_log_phase(Generation upto, + Generation gen, + MutationLog * from_mlog, + MutationLog * to_mlog, + MutationLog * triage_mlog); + + MutationLogStatistics _preserve_child_of_live_parent(Generation upto, + Generation parent_gen, + const MutationLogEntry & from_entry, + MutationLog * keep_mlog); + + /** helper function to decide whether to keep a mutation log entry + * @return true iff mlog entry appended to @p keep_mlog + **/ + bool _check_keep_mutation_aux(const MutationLogEntry & from_entry, + Generation parent_gen_to, + void * child_to, + MutationLog * keep_mlog); + /** cleanup after gc **/ void cleanup_phase(Generation upto); diff --git a/include/xo/gc/MutationLogEntry.hpp b/include/xo/gc/MutationLogEntry.hpp index eca086d6..c76427a3 100644 --- a/include/xo/gc/MutationLogEntry.hpp +++ b/include/xo/gc/MutationLogEntry.hpp @@ -28,12 +28,16 @@ namespace xo { using AGCObject = xo::mm::AGCObject; public: + MutationLogEntry() = default; MutationLogEntry(void * parent, void ** p_data, obj snap); void * parent() const { return parent_; } void ** p_data() const { return p_data_; } obj snap() const { return snap_; } + /** true if child pointer has been altered since this mlog entry created **/ + bool is_superseded() const noexcept { return *p_data_ != snap_.data(); } + private: /** address of object containing logged mutation **/ void * parent_ = nullptr; @@ -41,7 +45,7 @@ namespace xo { * driving this log entry. **/ void ** p_data_ = nullptr; - /** AGCObject i/face pointer, asof assignment responsible for this log entry. + /** AGCObject child pointer, asof assignment responsible for this log entry. * If *p_data_ matches snap_.data(), then AGCObject interface is snap_.iface(). * Otherwise log entry has been superseded by another assignment. **/ diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp index 83f03715..155a1e5d 100644 --- a/include/xo/gc/X1CollectorConfig.hpp +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -40,6 +40,15 @@ namespace xo { return Generation(age % n_survive_threshold_); } + /** age threshold for promotion to generation @p g **/ + uint32_t promotion_threshold(Generation g) const noexcept { + // TODO: may consider replacing with table-lookup + // Require: if two distinct ages promote to some gen g at the same time, + // then they also promote to gen g+k at the same time for all k>0. + + return g * n_survive_threshold_; + } + public: // ----- Instance Variables ----- diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 36082453..9ad19487 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -949,14 +949,18 @@ namespace xo { this->copy_roots(upto); log && log("step 2b : [STUB] copy pinned"); - log && log("step 3a : [STUB] run destructors"); - log && log("step 3b : [STUB] keep reachable weak pointers"); - log && log("step 4 : cleanup"); + log && log("step 3 : [STUB] forward mutation log"); + this->forward_mutation_log(upto); + + log && log("step 4a : [STUB] run destructors"); + log && log("step 4b : [STUB] keep reachable weak pointers"); + + log && log("step 5 : cleanup"); this->cleanup_phase(upto); if (config_.sanitize_flag_) { - log && log("step 4b : verify"); + log && log("step 5b : verify"); bool ok = this->verify_ok(); log && log(xtag("n-gc-root", verify_stats_.n_gc_root_), @@ -983,6 +987,282 @@ namespace xo { log && log("swap roles", xtag("g", g)); std::swap(space_[role::to_space()][g], space_[role::from_space()][g]); + std::swap(mlog_[role::to_space()][g], mlog_[role::from_space()][g]); + } + } + + void + DX1Collector::forward_mutation_log(Generation upto) + { + /** non-zero if at least one object was rescued (from any generation) + * by mutation log scan + **/ + std::size_t work = 0; + + do { + // on 1st iteration, for all generations: + // - to_mlog, triage_mlog are empty + + for (Generation child_gen{0}; child_gen + 2 < config_.n_generation_; ++child_gen) { + + MutationLog * from_mlog = this->mlog_[role::from_space()][child_gen]; + + if (!from_mlog->empty()) { + MutationLog * to_mlog = this->mlog_[role::to_space()][child_gen]; + MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; + + auto stats = this->_forward_mutation_log_phase(upto, + child_gen, + from_mlog, + to_mlog, + triage_mlog); + + from_mlog->clear(); + + // {from_mlog, triage_mlog} reverse roles + + std::swap(this->mlog_[role::from_space()][child_gen], + this->mlog_[c_n_role][child_gen]); + + work += stats.n_rescue_; + } + } + } while (work > 0); + + // here: reached fixpoints, any remaining triaged mlogs can be discarded + for (Generation child_gen{0}; child_gen + 2 < config_.n_generation_; ++child_gen) { + MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; + + triage_mlog->clear(); + } + } + + MutationLogStatistics + DX1Collector::_forward_mutation_log_phase(Generation upto, + Generation child_gen, + MutationLog * from_mlog, + MutationLog * keep_mlog, + MutationLog * triage_mlog) + { + scope log(XO_DEBUG(config_.debug_flag_), + xtag("child_gen", child_gen), + xtag("mlog.size", from_mlog->size())); + + /* categorize each mlog entry based on combination of {src, dest}. + * In each case we care about {gen, age} of {src, dest} + * objects. + * + * Enough cases to deserve a table: + * + * Legend: + * - P : parent object + * - P' : parent object after this gc phase + * - g(P) : generation of parent P. + * '+' if gen > child_gen (parent gen not collected this cycle) + * - age(P) : age of parent P. + * + * - C : child object + * - C' : child object after this gc phase + * - g(C) : generation of child C. + * - age(C) : age of child C. + * + * - 0 : *from_mlog targets this object's generation. + * object not eligible for promotion. + * Write self* for objects eligible that promote + * if they survive this gc cycle. + * Write + for 'any generation senior to target' + * - 1 : *from_mlog target this object's generation; + * object promotes if it survives + * + * - role : 'to' this phase evacuated + * (or in generation not eligible for collection) + * 'fr' otherwise + * + * | mlog | par | | | mlog | upd + * case | cur | g(P) | P C | C' | action | P | move + * -------+------+-------+--------------+------+---------+--------+----- + * MLOG0 | no | | | | discard | | - + * | | | | | | | + * MLOG1 | yes | * | to:+ fwd:* | to | keep | P->C' | - + * MLOG2 | yes | | fr:0 | to | keep | P->C' | C->to + * MLOG3 | yes | * | fwd:* - | to | update | P'->C' | - + * MLOG4 | yes | | fr:* - | - | triage | - | - + + * notes: + * MLOG1 : child C already forwarded (whether or not promoted) + * MLOG2 : child C survives (and perhaps promoted). + * kept alive by parent in more-senior generation + * MLOG3 : parent has been forwarded. + * update mlog entry for new parent location + * MLOG4 : parent provisionally garbage. triage mlog entry until + * definite outcome. + */ + + MutationLogStatistics counters; + // index of current mlog entry during evac + std::uint32_t i_from = 0; + + for (MutationLogEntry & from_entry : *from_mlog) { + if (log) { + log(xtag("i_from", i_from)); + } + + if (from_entry.is_superseded()) { + // there must be a second mlog entry that refers to + // the new child. Rely on that second entry, + // skipping this one. + + // [MLOG0] obsolete mutation -> skip + ++counters.n_stale_; + continue; + } + + /* here: mlog current */ + + Generation parent_gen_to = this->generation_of(role::to_space(), + from_entry.parent()); + + if (parent_gen_to.is_sentinel()) { + void * parent_fr = *from_entry.p_data(); + + AllocInfo parent_info = this->alloc_info((std::byte *)parent_fr); + + if (parent_info.is_forwarding_tseq()) { + /* [MLOG3] */ + + ++counters.n_live_parent_; + + // new parent location in to-space + // TODO: method on AllocInfo to streamline this + void * parent_to = *(void **)parent_fr; + + parent_gen_to = this->generation_of(role::to_space(), + parent_to); + parent_info = this->alloc_info((std::byte *)parent_to); + + assert(!parent_gen_to.sentinel()); + + // Since parent already forwarded, we don't have to preserve child + // or update parent object. + // + // Do need to replace mlog entry to reflect new parent location. + + std::size_t offset + = ((std::byte *)from_entry.p_data() + - (std::byte *)from_entry.parent()); + + void ** p_data_to = (void **)((std::byte *)(parent_to) + offset); + void * child_to = *p_data_to; + + MutationLogEntry to_entry(parent_to, p_data_to, from_entry.snap()); + + this->_check_keep_mutation_aux(to_entry, + parent_gen_to, + child_to, + keep_mlog); + + + } else { + ++counters.n_triage_; + + // parent hasn't been collected and may be garbage. + // However this is only provisional, since + // parent could turn out to be reachable via some other mutation. + + triage_mlog->push_back(from_entry); + } + } else { + /* [MLOG1, MLOG2] */ + + counters += this->_preserve_child_of_live_parent(upto, + parent_gen_to, + from_entry, + keep_mlog); + } + } + + return counters; + } + + MutationLogStatistics + DX1Collector::_preserve_child_of_live_parent(Generation upto, + Generation parent_gen, + const MutationLogEntry & from_entry, + MutationLog * keep_mlog) + { + void * child_fr = *from_entry.p_data(); + AllocInfo child_info = this->alloc_info((std::byte *)(child_fr)); + + MutationLogStatistics counters; + + // if child collected: new child location in to-space + void * child_to = nullptr; + + // parent is alive: gc must ensure child remains alive + + ++counters.n_live_parent_; + + // Parent already recognized as alive. Either not subject to collection + // or already evacuated. + // (+ remember this need not be 1st pass over mlog entries) + + if (child_info.is_forwarding_tseq()) { + // [MLOG1] + + // child already forwarded. + // TODO: make this a method on AllocInfo + child_to = *(void **)child_fr; + + // assigning through address of P->C pointer + // also makes mlog entry current + + } else { + // [MLOG2] + + ++counters.n_rescue_; + + child_to = this->_deep_move_interior(child_fr, upto); + + // update child pointer in parent object + *from_entry.p_data() = child_to; + } + + // child_to generation in {gen, gen+1} + + this->_check_keep_mutation_aux(from_entry, parent_gen, child_to, keep_mlog); + + return counters; + } + + bool + DX1Collector::_check_keep_mutation_aux(const MutationLogEntry & from_entry, + Generation parent_gen_to, + void * child_to, + MutationLog * keep_mlog) + { + Generation child_gen_to + = this->generation_of(role::to_space(), child_to); + + bool need_mlog_entry + = ((child_gen_to + 1 < config_.n_generation_) + && (config_.promotion_threshold(parent_gen_to) + > config_.promotion_threshold(child_gen_to))); + + if (need_mlog_entry) { + // 1. P->C pointer is still cross-age (xage), and + // 2. this matters; in future P will promote before C + // + // Need to keep entry because parent will be eligible for promotion + // before child + + keep_mlog->push_back(from_entry); + + return true; + } else { + // child now in final generation, + // no longer need to track incoming mutations. + + return false; } } From 486de5d39701e0ddf56271ac5ec3b077b85f86c8 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 2 Apr 2026 00:09:08 -0400 Subject: [PATCH 078/174] xo-gc: move mlog handling to separate tru MutationLogState.*pp Still entangled with DX1Collector for object spaces --- include/xo/gc/DX1Collector.hpp | 136 ++------ include/xo/gc/DX1CollectorIterator.hpp | 2 +- include/xo/gc/MutationLogState.hpp | 162 +++++++++ include/xo/gc/MutationLogStatistics.hpp | 41 +++ include/xo/gc/X1CollectorConfig.hpp | 1 + include/xo/gc/X1VerifyStats.hpp | 51 +++ src/gc/CMakeLists.txt | 1 + src/gc/DX1Collector.cpp | 274 ++------------- src/gc/MutationLogState.cpp | 444 ++++++++++++++++++++++++ 9 files changed, 752 insertions(+), 360 deletions(-) create mode 100644 include/xo/gc/MutationLogState.hpp create mode 100644 include/xo/gc/MutationLogStatistics.hpp create mode 100644 include/xo/gc/X1VerifyStats.hpp create mode 100644 src/gc/MutationLogState.cpp diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index fa7dcdd6..135ebc1a 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -7,7 +7,8 @@ #include "X1CollectorConfig.hpp" #include "GCObject.hpp" -#include "MutationLogEntry.hpp" +#include "MutationLogState.hpp" +#include "X1VerifyStats.hpp" #include "generation.hpp" #include "object_age.hpp" #include "role.hpp" @@ -129,68 +130,11 @@ namespace xo { alignas(AGCObject) std::byte iface_[sizeof(AGCObject)]; }; - /** @brief statistics from mlog forwarding **/ - class MutationLogStatistics { - public: - MutationLogStatistics() = default; - - MutationLogStatistics & operator+=(const MutationLogStatistics & x) { - n_stale_ += x.n_stale_; - n_live_parent_ += x.n_live_parent_; - n_rescue_ += x.n_rescue_; - n_triage_ += x.n_triage_; - - return *this; - } - - public: - /** count superseded mlog entries **/ - std::size_t n_stale_ = 0; - /** count live parents encountered during mlog scan **/ - std::size_t n_live_parent_ = 0; - /** count child subgraphs rescued during mlog scan **/ - std::size_t n_rescue_ = 0; - /** count triaged mlog entries **/ - std::size_t n_triage_ = 0; - }; - - /** @brief info collected during a @ref DX1Collector::verify_ok call - * - **/ - struct VerifyStats { - bool is_ok() const noexcept { - return (n_from_ == 0) && (n_fwd_ == 0) && (n_no_iface_ == 0); - } - - void clear() { *this = VerifyStats(); } - - /** number of gc roots examined **/ - std::uint32_t n_gc_root_ = 0; - std::uint32_t n_ext_ = 0; - /** number of from-space objects encountered. Fatal if non-zero **/ - std::uint32_t n_from_ = 0; - /** number of to-space objects encountered. **/ - std::uint32_t n_to_ = 0; - /** counts forwarding object encountered in to-space scan. Fatal if non-zero **/ - std::uint32_t n_fwd_ = 0; - /** counts missing GCObject interface. Fatal if non-zero **/ - std::uint32_t n_no_iface_ = 0; - /** live mlog entry refers to to-space, as expected **/ - std::uint32_t n_mlog_vital_ = 0; - /** stale mlog entry. not troubling to verify these **/ - std::uint32_t n_mlog_stale_ = 0; - /** live mlog entry refers to from-space. Fatal if non-zero **/ - std::uint32_t n_mlog_from_ = 0; - /** live mlog entry refers to either some other generation or outside gc-space. Fatal if non-zero **/ - std::uint32_t n_mlog_wild_ = 0; - - }; - // ----- DX1Collector ----- /** @brief garbage collector 'X1' **/ - struct DX1Collector { + class DX1Collector { public: using RootSet = DArenaVector; using ObjectTypeTable = DArenaVector; @@ -215,6 +159,7 @@ namespace xo { // ----- access methods ----- + const X1CollectorConfig & config() const noexcept { return config_; } std::string_view name() const noexcept { return config_.name_; } GCRunState runstate() const noexcept { return runstate_; } const ObjectTypeTable * get_object_types() const noexcept { return &object_types_; } @@ -390,6 +335,11 @@ namespace xo { **/ bool check_move_policy(header_type alloc_hdr, void * object_data) const noexcept; + /** move interior subgraph at @p from_src to to-space. + * no-op if not in gc-space. + **/ + void * deep_move_interior(void * from_src, Generation upto); + // ----- allocation ----- /** simple allocation. allocate @p z bytes of memory @@ -460,8 +410,10 @@ namespace xo { void _init_gc_roots(const X1CollectorConfig & cfg, std::size_t page_z); /** aux init function: initialize @ref mlog_storage_[][] arenas **/ void _init_mlogs(const X1CollectorConfig & cfg, std::size_t page_z); +#ifdef MOVED /** aux init function: create mutation log **/ MutationLog _make_mlog(uint32_t igen, char tag_char, size_t mlog_z, std::size_t page_z); +#endif /** aux init function: initialize @ref space_storage_[][] arenas **/ void _init_space(const X1CollectorConfig & cfg); @@ -473,53 +425,14 @@ namespace xo { /** cureate new mutation log after copying roots **/ void forward_mutation_log(Generation upto); - /** Perform one pass over contents of @p *from_mlog for generation @p gen. - * @p *from_mlog contains all {xgen,xage} pointers that target generation @p gen. - * Surviving mlog entries are moved to either @p *to_mlog or @p *triage_mlog, - * (generation < @p upto being collected this cycle). - * - * Each mlog entry gets one of the following outcomes. - * 1. skip. mlog entry has been superseded by another mut at target site. - * 2. keep. mlog entry is live. destination has been evacuated, - * so source must be updated as well. - * 3. triage. source of incoming object belongs to a generation that was collected, - * and has not been evacuated. Although appears to be garbage, it may - * be live after all if reachable from the destination of some other - * mlog entry in @p *to_mlog. Store these mlog entries in @p *triage_mlog. - * - * @return number of mlog entries moved, whether to @p *to_mlog or @p *triage_mlog. - **/ - MutationLogStatistics _forward_mutation_log_phase(Generation upto, - Generation gen, - MutationLog * from_mlog, - MutationLog * to_mlog, - MutationLog * triage_mlog); - - MutationLogStatistics _preserve_child_of_live_parent(Generation upto, - Generation parent_gen, - const MutationLogEntry & from_entry, - MutationLog * keep_mlog); - - /** helper function to decide whether to keep a mutation log entry - * @return true iff mlog entry appended to @p keep_mlog - **/ - bool _check_keep_mutation_aux(const MutationLogEntry & from_entry, - Generation parent_gen_to, - void * child_to, - MutationLog * keep_mlog); - /** cleanup after gc **/ - void cleanup_phase(Generation upto); + void _cleanup_phase(Generation upto); /** move root subgraph at @p from_src to to-space. * If not in gc-space, visit immediate children and move them. * Require: runstate_.is_running() **/ void * _deep_move_root(obj from_src, Generation upto); - /** move interior subgraph at @p from_src to to-space. - * no-op if not in gc-space. - **/ - void * _deep_move_interior(void * from_src, Generation upto); /** Common driver for _deep_move_root(), _deep_move_interior() **/ void * _deep_move_gc_owned(void * from_src, Generation upto); /** snap checkpoint containing allocator state @@ -570,24 +483,15 @@ namespace xo { **/ RootSet root_set_; - /** Cross-generational mutations tracked in MutationLogs. - * We need three logs per generation: - * A. one to observe and remember mutations in to-space - * during normal operation (between GC cycles) - * B. during GC: 2nd mlog to hold entries from from-mlog - * that will still be needed post-GC (because ptr direction - * from higher gen to lower gen after cycle). - * C. during GC: 3rd mlog to triage entries for which - * liveness of pointer source isn't yet established. - * - * NOTE: indexed on generation of pointer *destination* + /** "remembered sets": track pointers P->C that require special handling + * during a gc cycle where either: + * 1. xgen pointers g(P) > g(C): + * P in a more senior generation than C + * 2. xage pointers g(P) = g(C), age(P) > age(C): + * {P,C} in same generation, but in fuutre suriving P would + * get promoted before C. **/ - std::array mlog_storage_[c_n_role + 1]; - - /** mlog pointers. The roles of mlog_storage_[*][g] get permuted - * as each collection cycle proceeds - **/ - std::array mlog_[c_n_role + 1]; + MutationLogState mlog_state_; /** collector-managed memory here. * - space_[1] is from-space diff --git a/include/xo/gc/DX1CollectorIterator.hpp b/include/xo/gc/DX1CollectorIterator.hpp index 82944ff3..d96d9ab6 100644 --- a/include/xo/gc/DX1CollectorIterator.hpp +++ b/include/xo/gc/DX1CollectorIterator.hpp @@ -12,7 +12,7 @@ namespace xo { namespace mm { - struct DX1Collector; + class DX1Collector; /** @class DX1CollectorIterator * @brief Representation for alloc iterator over X1 collector diff --git a/include/xo/gc/MutationLogState.hpp b/include/xo/gc/MutationLogState.hpp new file mode 100644 index 00000000..dd206557 --- /dev/null +++ b/include/xo/gc/MutationLogState.hpp @@ -0,0 +1,162 @@ +/** @file MutationLogState.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include "X1CollectorConfig.hpp" +#include "MutationLogStatistics.hpp" +#include "MutationLogEntry.hpp" +#include +#include + +namespace xo { + namespace mm { + class DX1Collector; + class VerifyStats; + + class MutationLogState { + public: + using MutationLog = DArenaVector; + using size_type = DArena::size_type; + + public: + MutationLogState(uint32_t ngen, bool debug_flag); + + /** Initialize mlog state for configuration @p cfg + * with o/s page size @p page_z + **/ + void init_mlogs(const X1CollectorConfig & cfg, std::size_t page_z); + + /** total number of active mlog entries (across all generations) + **/ + size_type mutation_log_entries() const noexcept; + + void visit_pools(const MemorySizeVisitor & visitor) const; + + /** verify consistent mlog state, + * on behalf of collector @p gc. + * (using gc to identify location of objects). + * Update counters in @p *p_verify_stats. + **/ + void verify_ok(DX1Collector * gc, + VerifyStats * p_verify_stats) noexcept; + + /** Append a single mutation to log for generation @p dest_g + * Mutation modifies @p parent at address @p addr, + * to refer to @p rhs. + * + * Require: mutation is from older->newer, + * see validation in DX1Collector::assign_member. + * + * NOTE: rhs can probably be dropped. Initially thought + * helpful to keep wrapped obj version. On closer look + * not necessary. Important to remember that gc can't change + * any interface pointers, it strictly preserves them. + * + * Since mutation log entries are specific to a particular + * rhs pointer value, they commit corresponding interface + * pointer. This means can alway recover that pointer + * by consulting the AllocHeader for the pointer target + */ + void append_mutation(Generation dest_g, + void * parent, + void ** addr, + obj rhs); + + /** swap {to, from} roles **/ + void swap_roles(Generation upto) noexcept; + + /** On behalf of collector @p gc: + * + * forward mutation logs, for generations 0 <= g < @p upto, + * from from-space to to-space. + **/ + void forward_mutation_log(DX1Collector * gc, + Generation upto); + + private: + /** aux init function: create mutation log **/ + MutationLog _make_mlog(uint32_t igen, char tag_char, + size_t mlog_z, std::size_t page_z); + + /** On behalf of collctor @p gc: + * + * Perform one pass over contents of @p *from_mlog for generation @p gen. + * @p *from_mlog contains all {xgen,xage} pointers that target generation @p gen. + * Surviving mlog entries are moved to either @p *to_mlog or @p *triage_mlog, + * (generation < @p upto being collected this cycle). + * + * Each mlog entry gets one of the following outcomes. + * 1. skip. mlog entry has been superseded by another mut at target site. + * 2. keep. mlog entry is live. destination has been evacuated, + * so source must be updated as well. + * 3. triage. source of incoming object belongs to a generation that was collected, + * and has not been evacuated. Although appears to be garbage, it may + * be live after all if reachable from the destination of some other + * mlog entry in @p *to_mlog. Store these mlog entries in @p *triage_mlog. + * + * @return number of mlog entries moved, whether to @p *to_mlog or @p *triage_mlog. + **/ + MutationLogStatistics _forward_mutation_log_phase(DX1Collector * gc, + Generation upto, + Generation gen, + MutationLog * from_mlog, + MutationLog * to_mlog, + MutationLog * triage_mlog); + + /** On behalf of collector @p gc: + * + * During gc of generations g < @p upto, + * with a P->C edge represented by mlog entry @p from_entry, + * with parent P in generation @p parent_gen: + * ensure child C is evacuated, and append @p from_entry to + * @p keep_mlog. + **/ + MutationLogStatistics _preserve_child_of_live_parent(DX1Collector * gc, + Generation upto, + Generation parent_gen, + const MutationLogEntry & from_entry, + MutationLog * keep_mlog); + + /** On behalf of collector @p gc: + * + * helper function to decide whether to keep a mutation log entry + * @return true iff mlog entry appended to @p keep_mlog + **/ + bool _check_keep_mutation_aux(DX1Collector * gc, + const MutationLogEntry & from_entry, + Generation parent_gen_to, + void * child_to, + MutationLog * keep_mlog); + + + public: + uint32_t n_generation_ = 0; + bool debug_flag_ = false; + + /** Cross-generational mutations tracked in MutationLogs. + * We need three logs per generation: + * A. one to observe and remember mutations in to-space + * during normal operation (between GC cycles) + * B. during GC: 2nd mlog to hold entries from from-mlog + * that will still be needed post-GC (because ptr direction + * from higher gen to lower gen after cycle). + * C. during GC: 3rd mlog to triage entries for which + * liveness of pointer source isn't yet established. + * + * NOTE: indexed on generation of pointer *destination* + **/ + std::array mlog_storage_[c_n_role + 1]; + + /** mlog pointers. The roles of mlog_storage_[*][g] get permuted + * as each collection cycle proceeds + **/ + std::array mlog_[c_n_role + 1]; + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end MutationLogState.hpp */ diff --git a/include/xo/gc/MutationLogStatistics.hpp b/include/xo/gc/MutationLogStatistics.hpp new file mode 100644 index 00000000..986064fc --- /dev/null +++ b/include/xo/gc/MutationLogStatistics.hpp @@ -0,0 +1,41 @@ +/** @file MutationLogStatistics.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include + +namespace xo { + namespace mm { + + /** @brief statistics from mlog forwarding **/ + class MutationLogStatistics { + public: + MutationLogStatistics() = default; + + MutationLogStatistics & operator+=(const MutationLogStatistics & x) { + n_stale_ += x.n_stale_; + n_live_parent_ += x.n_live_parent_; + n_rescue_ += x.n_rescue_; + n_triage_ += x.n_triage_; + + return *this; + } + + public: + /** count superseded mlog entries **/ + std::size_t n_stale_ = 0; + /** count live parents encountered during mlog scan **/ + std::size_t n_live_parent_ = 0; + /** count child subgraphs rescued during mlog scan **/ + std::size_t n_rescue_ = 0; + /** count triaged mlog entries **/ + std::size_t n_triage_ = 0; + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end MutationLogStatistics.hpp */ diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp index 155a1e5d..1c795795 100644 --- a/include/xo/gc/X1CollectorConfig.hpp +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -42,6 +42,7 @@ namespace xo { /** age threshold for promotion to generation @p g **/ uint32_t promotion_threshold(Generation g) const noexcept { + // TODO: may consider replacing with table-lookup // Require: if two distinct ages promote to some gen g at the same time, // then they also promote to gen g+k at the same time for all k>0. diff --git a/include/xo/gc/X1VerifyStats.hpp b/include/xo/gc/X1VerifyStats.hpp new file mode 100644 index 00000000..d5fbc5a7 --- /dev/null +++ b/include/xo/gc/X1VerifyStats.hpp @@ -0,0 +1,51 @@ +/** @file X1VerifyStats.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include + +namespace xo { + namespace mm { + + /** @brief info collected during a @ref DX1Collector::verify_ok call + * (or @ref MutationLogState::verify_ok call) + **/ + class VerifyStats { + public: + bool is_ok() const noexcept { + return (n_from_ == 0) && (n_fwd_ == 0) && (n_no_iface_ == 0); + } + + void clear() { *this = VerifyStats(); } + + /** number of gc roots examined **/ + std::uint32_t n_gc_root_ = 0; + std::uint32_t n_ext_ = 0; + /** number of from-space objects encountered. Fatal if non-zero **/ + std::uint32_t n_from_ = 0; + /** number of to-space objects encountered. **/ + std::uint32_t n_to_ = 0; + /** counts forwarding object encountered in to-space scan. Fatal if non-zero **/ + std::uint32_t n_fwd_ = 0; + /** counts missing GCObject interface. Fatal if non-zero **/ + std::uint32_t n_no_iface_ = 0; + /** live mlog entry refers to to-space, as expected **/ + std::uint32_t n_mlog_vital_ = 0; + /** stale mlog entry. not troubling to verify these **/ + std::uint32_t n_mlog_stale_ = 0; + /** live mlog entry refers to from-space. Fatal if non-zero **/ + std::uint32_t n_mlog_from_ = 0; + /** live mlog entry refers to either some other generation or outside gc-space. + * Fatal if non-zero + **/ + std::uint32_t n_mlog_wild_ = 0; + + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end X1VerifyStats.hpp */ diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index 2dec8735..d30acf0e 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -15,6 +15,7 @@ set(SELF_SRCS DX1CollectorIterator.cpp X1CollectorConfig.cpp + MutationLogState.cpp MutationLogEntry.cpp ) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 9ad19487..342da1c6 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -65,7 +65,8 @@ namespace xo { using size_type = xo::mm::DX1Collector::size_type; - DX1Collector::DX1Collector(const X1CollectorConfig & cfg) : config_{cfg} + DX1Collector::DX1Collector(const X1CollectorConfig & cfg) + : config_{cfg}, mlog_state_{cfg.n_generation_, cfg.debug_flag_} { assert(config_.arena_config_.header_.size_bits_ + config_.arena_config_.header_.age_bits_ + @@ -106,6 +107,9 @@ namespace xo { void DX1Collector::_init_mlogs(const X1CollectorConfig & cfg, std::size_t page_z) { + this->mlog_state_.init_mlogs(cfg, page_z); + +#ifdef MOVED for (uint32_t igen = 0, ngen = cfg.n_generation_; igen + 1 < ngen; ++igen) { // special case: no use for mutation log for youngest generation, // so don't trouble to allocate one @@ -132,8 +136,10 @@ namespace xo { } else { assert(false); } +#endif } +#ifdef MOVED auto DX1Collector::_make_mlog(uint32_t igen, char tag_char, size_t mlog_z, size_t page_z) -> MutationLog { @@ -145,6 +151,7 @@ namespace xo { .hugepage_z_ = page_z, .store_header_flag_ = false}); } +#endif void DX1Collector::_init_space(const X1CollectorConfig & cfg) @@ -195,11 +202,15 @@ namespace xo { } } + mlog_state_.visit_pools(visitor); + +#ifdef MOVED for (uint32_t j = 0; j + 1 < config_.n_generation_; ++j) { for (uint32_t i = 0; i < c_n_role + 1; ++i) { mlog_storage_[i][j].visit_pools(visitor); } } +#endif } bool @@ -299,6 +310,9 @@ namespace xo { size_type DX1Collector::mutation_log_entries() const noexcept { + return mlog_state_.mutation_log_entries(); + +#ifdef MOVED size_type z = 0; for (Generation gj{0}; gj + 1 < config_.n_generation_; ++gj) { @@ -306,6 +320,7 @@ namespace xo { } return z; +#endif } namespace { @@ -767,47 +782,8 @@ namespace xo { } // 4. scan mutation logs - for (Generation g(0); g + 1 < config_.n_generation_; ++g) { - const DArena * space = this->get_space(role::to_space(), g); - const DArena * from = this->get_space(role::from_space(), g); - - // mutation log for generation g records *incoming* pointers - // from more senior generations; includes objects from *this* - // generation that are older (track since source promotes before - // destination) - // - for (const MutationLogEntry & mrecd : *(mlog_[role::to_space()][g])) { - // mutation log entries are only valid until the next assignment - // at the source location. Superseded entry may now point - // somewhere else. The snapshot member must however point - // to this generation, since that's preserved as long as the - // log entry survives. - - void * orig_data = mrecd.snap().data(); - void * curr_data = *mrecd.p_data(); - - if (orig_data == curr_data) { - // live mlog entry must point to to-space - - if (space->contains_allocated(orig_data)) { - ++verify_stats_.n_mlog_vital_; - } else if (from->contains(curr_data)) { - // verify failure. - ++verify_stats_.n_mlog_from_; - } else { - // verify failure. - ++verify_stats_.n_mlog_wild_; - } - } else { - // requirements on superseded log entry: - // - snapshot refers to to-space - // - // no requirements on current data, entry is superseded anyway - // - ++verify_stats_.n_mlog_stale_; - } - } - } + mlog_state_.verify_ok(this, + &(this->verify_stats_)); } // restore run state at end of verify cycle @@ -957,7 +933,7 @@ namespace xo { log && log("step 4b : [STUB] keep reachable weak pointers"); log && log("step 5 : cleanup"); - this->cleanup_phase(upto); + this->_cleanup_phase(upto); if (config_.sanitize_flag_) { log && log("step 5b : verify"); @@ -987,203 +963,18 @@ namespace xo { log && log("swap roles", xtag("g", g)); std::swap(space_[role::to_space()][g], space_[role::from_space()][g]); - std::swap(mlog_[role::to_space()][g], mlog_[role::from_space()][g]); } + + mlog_state_.swap_roles(upto); } void DX1Collector::forward_mutation_log(Generation upto) { - /** non-zero if at least one object was rescued (from any generation) - * by mutation log scan - **/ - std::size_t work = 0; - - do { - // on 1st iteration, for all generations: - // - to_mlog, triage_mlog are empty - - for (Generation child_gen{0}; child_gen + 2 < config_.n_generation_; ++child_gen) { - - MutationLog * from_mlog = this->mlog_[role::from_space()][child_gen]; - - if (!from_mlog->empty()) { - MutationLog * to_mlog = this->mlog_[role::to_space()][child_gen]; - MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; - - auto stats = this->_forward_mutation_log_phase(upto, - child_gen, - from_mlog, - to_mlog, - triage_mlog); - - from_mlog->clear(); - - // {from_mlog, triage_mlog} reverse roles - - std::swap(this->mlog_[role::from_space()][child_gen], - this->mlog_[c_n_role][child_gen]); - - work += stats.n_rescue_; - } - } - } while (work > 0); - - // here: reached fixpoints, any remaining triaged mlogs can be discarded - for (Generation child_gen{0}; child_gen + 2 < config_.n_generation_; ++child_gen) { - MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; - - triage_mlog->clear(); - } - } - - MutationLogStatistics - DX1Collector::_forward_mutation_log_phase(Generation upto, - Generation child_gen, - MutationLog * from_mlog, - MutationLog * keep_mlog, - MutationLog * triage_mlog) - { - scope log(XO_DEBUG(config_.debug_flag_), - xtag("child_gen", child_gen), - xtag("mlog.size", from_mlog->size())); - - /* categorize each mlog entry based on combination of {src, dest}. - * In each case we care about {gen, age} of {src, dest} - * objects. - * - * Enough cases to deserve a table: - * - * Legend: - * - P : parent object - * - P' : parent object after this gc phase - * - g(P) : generation of parent P. - * '+' if gen > child_gen (parent gen not collected this cycle) - * - age(P) : age of parent P. - * - * - C : child object - * - C' : child object after this gc phase - * - g(C) : generation of child C. - * - age(C) : age of child C. - * - * - 0 : *from_mlog targets this object's generation. - * object not eligible for promotion. - * Write self* for objects eligible that promote - * if they survive this gc cycle. - * Write + for 'any generation senior to target' - * - 1 : *from_mlog target this object's generation; - * object promotes if it survives - * - * - role : 'to' this phase evacuated - * (or in generation not eligible for collection) - * 'fr' otherwise - * - * | mlog | par | | | mlog | upd - * case | cur | g(P) | P C | C' | action | P | move - * -------+------+-------+--------------+------+---------+--------+----- - * MLOG0 | no | | | | discard | | - - * | | | | | | | - * MLOG1 | yes | * | to:+ fwd:* | to | keep | P->C' | - - * MLOG2 | yes | | fr:0 | to | keep | P->C' | C->to - * MLOG3 | yes | * | fwd:* - | to | update | P'->C' | - - * MLOG4 | yes | | fr:* - | - | triage | - | - - - * notes: - * MLOG1 : child C already forwarded (whether or not promoted) - * MLOG2 : child C survives (and perhaps promoted). - * kept alive by parent in more-senior generation - * MLOG3 : parent has been forwarded. - * update mlog entry for new parent location - * MLOG4 : parent provisionally garbage. triage mlog entry until - * definite outcome. - */ - - MutationLogStatistics counters; - // index of current mlog entry during evac - std::uint32_t i_from = 0; - - for (MutationLogEntry & from_entry : *from_mlog) { - if (log) { - log(xtag("i_from", i_from)); - } - - if (from_entry.is_superseded()) { - // there must be a second mlog entry that refers to - // the new child. Rely on that second entry, - // skipping this one. - - // [MLOG0] obsolete mutation -> skip - ++counters.n_stale_; - continue; - } - - /* here: mlog current */ - - Generation parent_gen_to = this->generation_of(role::to_space(), - from_entry.parent()); - - if (parent_gen_to.is_sentinel()) { - void * parent_fr = *from_entry.p_data(); - - AllocInfo parent_info = this->alloc_info((std::byte *)parent_fr); - - if (parent_info.is_forwarding_tseq()) { - /* [MLOG3] */ - - ++counters.n_live_parent_; - - // new parent location in to-space - // TODO: method on AllocInfo to streamline this - void * parent_to = *(void **)parent_fr; - - parent_gen_to = this->generation_of(role::to_space(), - parent_to); - parent_info = this->alloc_info((std::byte *)parent_to); - - assert(!parent_gen_to.sentinel()); - - // Since parent already forwarded, we don't have to preserve child - // or update parent object. - // - // Do need to replace mlog entry to reflect new parent location. - - std::size_t offset - = ((std::byte *)from_entry.p_data() - - (std::byte *)from_entry.parent()); - - void ** p_data_to = (void **)((std::byte *)(parent_to) + offset); - void * child_to = *p_data_to; - - MutationLogEntry to_entry(parent_to, p_data_to, from_entry.snap()); - - this->_check_keep_mutation_aux(to_entry, - parent_gen_to, - child_to, - keep_mlog); - - - } else { - ++counters.n_triage_; - - // parent hasn't been collected and may be garbage. - // However this is only provisional, since - // parent could turn out to be reachable via some other mutation. - - triage_mlog->push_back(from_entry); - } - } else { - /* [MLOG1, MLOG2] */ - - counters += this->_preserve_child_of_live_parent(upto, - parent_gen_to, - from_entry, - keep_mlog); - } - } - - return counters; + mlog_state_.forward_mutation_log(this, upto); } +#ifdef MOVED MutationLogStatistics DX1Collector::_preserve_child_of_live_parent(Generation upto, Generation parent_gen, @@ -1233,7 +1024,9 @@ namespace xo { return counters; } +#endif +#ifdef MOVED bool DX1Collector::_check_keep_mutation_aux(const MutationLogEntry & from_entry, Generation parent_gen_to, @@ -1265,9 +1058,10 @@ namespace xo { return false; } } +#endif void - DX1Collector::cleanup_phase(Generation upto) + DX1Collector::_cleanup_phase(Generation upto) { scope log(XO_DEBUG(true), xtag("upto", upto)); @@ -1328,7 +1122,7 @@ namespace xo { } void * - DX1Collector::_deep_move_interior(void * from_src, + DX1Collector::deep_move_interior(void * from_src, Generation upto) { scope log(XO_DEBUG(config_.debug_flag_)); @@ -1948,16 +1742,10 @@ namespace xo { // control here: we have an older->younger pointer, need to log it - // mlog keyed by generation in which pointer _destination_ resides: - // collection that moves destination generation around needs to also - // update pointers such as this one - // - MutationLog * mlog = this->mlog_[role::to_space()][dest_g]; + void ** lhs_addr = reinterpret_cast(&(p_lhs->data_)); - mlog->push_back(MutationLogEntry(parent, - reinterpret_cast(&(p_lhs->data_)), - rhs)); - } + mlog_state_.append_mutation(dest_g, parent, lhs_addr, rhs); + } /*assign_member*/ DX1CollectorIterator DX1Collector::begin() const noexcept diff --git a/src/gc/MutationLogState.cpp b/src/gc/MutationLogState.cpp new file mode 100644 index 00000000..18c22d30 --- /dev/null +++ b/src/gc/MutationLogState.cpp @@ -0,0 +1,444 @@ +/** @file MutationLogState.cpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#include "MutationLogState.hpp" +#include "DX1Collector.hpp" + +namespace xo { + namespace mm { + + MutationLogState::MutationLogState(uint32_t ngen, bool debug_flag) + : n_generation_{ngen}, debug_flag_{debug_flag} + {} + + void + MutationLogState::init_mlogs(const X1CollectorConfig & cfg, + std::size_t page_z) + { + for (uint32_t igen = 0, ngen = cfg.n_generation_; igen + 1 < ngen; ++igen) { + // special case: no use for mutation log for youngest generation, + // so don't trouble to allocate one + + if (igen + 1 < c_max_generation) { + this->mlog_storage_[0][igen] = _make_mlog(igen, 'a', cfg.mutation_log_z_, page_z); + this->mlog_storage_[1][igen] = _make_mlog(igen, 'b', cfg.mutation_log_z_, page_z); + this->mlog_storage_[2][igen] = _make_mlog(igen, 'c', cfg.mutation_log_z_, page_z); + + this->mlog_[0][igen] = &mlog_storage_[0][igen]; + this->mlog_[1][igen] = &mlog_storage_[1][igen]; + this->mlog_[2][igen] = &mlog_storage_[2][igen]; + } else { + assert(false); + } + } + + if (cfg.n_generation_ > 0) { + for (uint32_t igen = cfg.n_generation_ - 1; igen + 1 < c_max_generation; ++igen) { + this->mlog_[0][igen] = nullptr; + this->mlog_[1][igen] = nullptr; + this->mlog_[2][igen] = nullptr; + } + } else { + assert(false); + } + } + + auto + MutationLogState::_make_mlog(uint32_t igen, char tag_char, + size_t mlog_z, size_t page_z) -> MutationLog + { + char buf[40]; + snprintf(buf, sizeof(buf), "x1-mlog-G%u-%c", igen, tag_char); + + return MutationLog::map(ArenaConfig{.name_ = std::string(buf), + .size_ = mlog_z, + .hugepage_z_ = page_z, + .store_header_flag_ = false}); + } + + auto + MutationLogState::mutation_log_entries() const noexcept -> size_type + { + size_type z = 0; + + for (Generation gj{0}; gj + 1 < n_generation_; ++gj) { + z += mlog_[role::to_space()][gj]->size(); + } + + return z; + } + + void + MutationLogState::visit_pools(const MemorySizeVisitor & visitor) const + { + for (uint32_t j = 0; j + 1 < n_generation_; ++j) { + for (uint32_t i = 0; i < c_n_role + 1; ++i) { + mlog_storage_[i][j].visit_pools(visitor); + } + } + } + + void + MutationLogState::verify_ok(DX1Collector * gc, + VerifyStats * p_verify_stats) noexcept + { + // 4. scan mutation logs + for (Generation g(0); g + 1 < n_generation_; ++g) { + const DArena * space = gc->get_space(role::to_space(), g); + const DArena * from = gc->get_space(role::from_space(), g); + + // mutation log for generation g records *incoming* pointers + // from more senior generations; includes objects from *this* + // generation that are older (track since source promotes before + // destination) + // + for (const MutationLogEntry & mrecd : *(mlog_[role::to_space()][g])) { + // mutation log entries are only valid until the next assignment + // at the source location. Superseded entry may now point + // somewhere else. The snapshot member must however point + // to this generation, since that's preserved as long as the + // log entry survives. + + void * orig_data = mrecd.snap().data(); + void * curr_data = *mrecd.p_data(); + + if (orig_data == curr_data) { + // live mlog entry must point to to-space + + if (space->contains_allocated(orig_data)) { + ++(p_verify_stats->n_mlog_vital_); + } else if (from->contains(curr_data)) { + // verify failure. + ++(p_verify_stats->n_mlog_from_); + } else { + // verify failure. + ++(p_verify_stats->n_mlog_wild_); + } + } else { + // requirements on superseded log entry: + // - snapshot refers to to-space + // + // no requirements on current data, entry is superseded anyway + // + ++(p_verify_stats->n_mlog_stale_); + } + } + } + } /*verify_ok*/ + + void + MutationLogState::append_mutation(Generation dest_g, + void * parent, + void ** addr, + obj rhs) + { + // mlog keyed by generation in which pointer _destination_ resides: + // collection that moves destination generation around needs to also + // update pointers such as this one + // + MutationLog * mlog = this->mlog_[role::to_space()][dest_g]; + + mlog->push_back(MutationLogEntry(parent, addr, rhs)); + } + + void + MutationLogState::swap_roles(Generation upto) noexcept + { + scope log(XO_DEBUG(true), xtag("upto", upto)); + + for (Generation g = Generation{0}; g < upto; ++g) { + log && log("swap roles", xtag("g", g)); + + std::swap(mlog_[role::to_space()][g], mlog_[role::from_space()][g]); + } + } + + void + MutationLogState::forward_mutation_log(DX1Collector * gc, + Generation upto) + { + /** non-zero if at least one object was rescued (from any generation) + * by mutation log scan + **/ + std::size_t work = 0; + + do { + // on 1st iteration, for all generations: + // - to_mlog, triage_mlog are empty + + for (Generation child_gen{0}; child_gen + 2 < n_generation_; ++child_gen) { + + MutationLog * from_mlog = this->mlog_[role::from_space()][child_gen]; + + if (!from_mlog->empty()) { + MutationLog * to_mlog = this->mlog_[role::to_space()][child_gen]; + MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; + + auto stats = this->_forward_mutation_log_phase(gc, + upto, + child_gen, + from_mlog, + to_mlog, + triage_mlog); + + from_mlog->clear(); + + // {from_mlog, triage_mlog} reverse roles + + std::swap(this->mlog_[role::from_space()][child_gen], + this->mlog_[c_n_role][child_gen]); + + work += stats.n_rescue_; + } + } + } while (work > 0); + + // here: reached fixpoints, any remaining triaged mlogs can be discarded + for (Generation child_gen{0}; child_gen + 2 < n_generation_; ++child_gen) { + MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; + + triage_mlog->clear(); + } + } + + MutationLogStatistics + MutationLogState::_forward_mutation_log_phase(DX1Collector * gc, + Generation upto, + Generation child_gen, + MutationLog * from_mlog, + MutationLog * keep_mlog, + MutationLog * triage_mlog) + { + scope log(XO_DEBUG(debug_flag_), + xtag("child_gen", child_gen), + xtag("mlog.size", from_mlog->size())); + + /* categorize each mlog entry based on combination of {src, dest}. + * In each case we care about {gen, age} of {src, dest} + * objects. + * + * Enough cases to deserve a table: + * + * Legend: + * - P : parent object + * - P' : parent object after this gc phase + * - g(P) : generation of parent P. + * '+' if gen > child_gen (parent gen not collected this cycle) + * - age(P) : age of parent P. + * + * - C : child object + * - C' : child object after this gc phase + * - g(C) : generation of child C. + * - age(C) : age of child C. + * + * - 0 : *from_mlog targets this object's generation. + * object not eligible for promotion. + * Write self* for objects eligible that promote + * if they survive this gc cycle. + * Write + for 'any generation senior to target' + * - 1 : *from_mlog target this object's generation; + * object promotes if it survives + * + * - role : 'to' this phase evacuated + * (or in generation not eligible for collection) + * 'fr' otherwise + * + * | mlog | par | | | mlog | upd + * case | cur | g(P) | P C | C' | action | P | move + * -------+------+-------+--------------+------+---------+--------+----- + * MLOG0 | no | | | | discard | | - + * | | | | | | | + * MLOG1 | yes | * | to:+ fwd:* | to | keep | P->C' | - + * MLOG2 | yes | | fr:0 | to | keep | P->C' | C->to + * MLOG3 | yes | * | fwd:* - | to | update | P'->C' | - + * MLOG4 | yes | | fr:* - | - | triage | - | - + + * notes: + * MLOG1 : child C already forwarded (whether or not promoted) + * MLOG2 : child C survives (and perhaps promoted). + * kept alive by parent in more-senior generation + * MLOG3 : parent has been forwarded. + * update mlog entry for new parent location + * MLOG4 : parent provisionally garbage. triage mlog entry until + * definite outcome. + */ + + MutationLogStatistics counters; + // index of current mlog entry during evac + std::uint32_t i_from = 0; + + for (MutationLogEntry & from_entry : *from_mlog) { + if (log) { + log(xtag("i_from", i_from)); + } + + if (from_entry.is_superseded()) { + // there must be a second mlog entry that refers to + // the new child. Rely on that second entry, + // skipping this one. + + // [MLOG0] obsolete mutation -> skip + ++counters.n_stale_; + continue; + } + + /* here: mlog current */ + + Generation parent_gen_to = gc->generation_of(role::to_space(), + from_entry.parent()); + + if (parent_gen_to.is_sentinel()) { + void * parent_fr = *from_entry.p_data(); + + AllocInfo parent_info = gc->alloc_info((std::byte *)parent_fr); + + if (parent_info.is_forwarding_tseq()) { + /* [MLOG3] */ + + ++counters.n_live_parent_; + + // new parent location in to-space + // TODO: method on AllocInfo to streamline this + void * parent_to = *(void **)parent_fr; + + parent_gen_to = gc->generation_of(role::to_space(), + parent_to); + parent_info = gc->alloc_info((std::byte *)parent_to); + + assert(!parent_gen_to.sentinel()); + + // Since parent already forwarded, we don't have to preserve child + // or update parent object. + // + // Do need to replace mlog entry to reflect new parent location. + + std::size_t offset + = ((std::byte *)from_entry.p_data() + - (std::byte *)from_entry.parent()); + + void ** p_data_to = (void **)((std::byte *)(parent_to) + offset); + void * child_to = *p_data_to; + + MutationLogEntry to_entry(parent_to, p_data_to, from_entry.snap()); + + this->_check_keep_mutation_aux(gc, + to_entry, + parent_gen_to, + child_to, + keep_mlog); + + + } else { + ++counters.n_triage_; + + // parent hasn't been collected and may be garbage. + // However this is only provisional, since + // parent could turn out to be reachable via some other mutation. + + triage_mlog->push_back(from_entry); + } + } else { + /* [MLOG1, MLOG2] */ + + counters += this->_preserve_child_of_live_parent(gc, + upto, + parent_gen_to, + from_entry, + keep_mlog); + } + } + + return counters; + } + + MutationLogStatistics + MutationLogState::_preserve_child_of_live_parent(DX1Collector * gc, + Generation upto, + Generation parent_gen, + const MutationLogEntry & from_entry, + MutationLog * keep_mlog) + { + void * child_fr = *from_entry.p_data(); + AllocInfo child_info = gc->alloc_info((std::byte *)(child_fr)); + + MutationLogStatistics counters; + + // if child collected: new child location in to-space + void * child_to = nullptr; + + // parent is alive: gc must ensure child remains alive + + ++counters.n_live_parent_; + + // Parent already recognized as alive. Either not subject to collection + // or already evacuated. + // (+ remember this need not be 1st pass over mlog entries) + + if (child_info.is_forwarding_tseq()) { + // [MLOG1] + + // child already forwarded. + // TODO: make this a method on AllocInfo + child_to = *(void **)child_fr; + + // assigning through address of P->C pointer + // also makes mlog entry current + + } else { + // [MLOG2] + + ++counters.n_rescue_; + + child_to = gc->deep_move_interior(child_fr, upto); + + // update child pointer in parent object + *from_entry.p_data() = child_to; + } + + // child_to generation in {gen, gen+1} + + this->_check_keep_mutation_aux(gc, from_entry, parent_gen, child_to, keep_mlog); + + return counters; + } + + bool + MutationLogState::_check_keep_mutation_aux(DX1Collector * gc, + const MutationLogEntry & from_entry, + Generation parent_gen_to, + void * child_to, + MutationLog * keep_mlog) + { + Generation child_gen_to + = gc->generation_of(role::to_space(), child_to); + + bool need_mlog_entry + = ((child_gen_to + 1 < n_generation_) + && (gc->config().promotion_threshold(parent_gen_to) + > gc->config().promotion_threshold(child_gen_to))); + + if (need_mlog_entry) { + // 1. P->C pointer is still cross-age (xage), and + // 2. this matters; in future P will promote before C + // + // Need to keep entry because parent will be eligible for promotion + // before child + + keep_mlog->push_back(from_entry); + + return true; + } else { + // child now in final generation, + // no longer need to track incoming mutations. + + return false; + } + } + + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end MutationLogState.cpp */ From 0f01c97c6beb6205e81e281cc14c5cb60196fd53 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 2 Apr 2026 18:00:09 -0400 Subject: [PATCH 079/174] xo-gc: tiny: + comments --- include/xo/gc/MutationLogState.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/xo/gc/MutationLogState.hpp b/include/xo/gc/MutationLogState.hpp index dd206557..ef7e96aa 100644 --- a/include/xo/gc/MutationLogState.hpp +++ b/include/xo/gc/MutationLogState.hpp @@ -133,7 +133,9 @@ namespace xo { public: + /** number of generations in use. Same as @ref X1CollectorConfig.n_generation_ **/ uint32_t n_generation_ = 0; + /** true to enable debug logging **/ bool debug_flag_ = false; /** Cross-generational mutations tracked in MutationLogs. From 9803709df8d1d9ba80eefb4cdc72a6497452b7b3 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 2 Apr 2026 18:55:46 -0400 Subject: [PATCH 080/174] xo-gc: refactor: move X1 object storage to new cls GCObjectStore. --- include/xo/gc/DX1Collector.hpp | 33 ++--- include/xo/gc/GCObjectStore.hpp | 81 +++++++++++ include/xo/gc/MutationLogState.hpp | 4 +- src/gc/CMakeLists.txt | 1 + src/gc/DX1Collector.cpp | 208 ++--------------------------- src/gc/GCObjectStore.cpp | 108 +++++++++++++++ utest/Collector.test.cpp | 14 +- 7 files changed, 224 insertions(+), 225 deletions(-) create mode 100644 include/xo/gc/GCObjectStore.hpp create mode 100644 src/gc/GCObjectStore.cpp diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 135ebc1a..7992f478 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -7,6 +7,7 @@ #include "X1CollectorConfig.hpp" #include "GCObject.hpp" +#include "GCObjectStore.hpp" #include "MutationLogState.hpp" #include "X1VerifyStats.hpp" #include "generation.hpp" @@ -164,11 +165,11 @@ namespace xo { GCRunState runstate() const noexcept { return runstate_; } const ObjectTypeTable * get_object_types() const noexcept { return &object_types_; } const RootSet * get_root_set() const noexcept { return &root_set_; } - const DArena * get_space(role r, Generation g) const noexcept { return space_[r][g]; } - 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}); } + const DArena * get_space(role r, Generation g) const noexcept { return gco_store_.get_space(r, g); } + DArena * get_space(role r, Generation g) noexcept { return gco_store_.get_space(r, g); } + 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->to_space(Generation{0}); } // ----- basic statistics ----- @@ -397,8 +398,10 @@ namespace xo { // ----- book-keeping ----- +#ifdef OBSOLETE // see swap_roles() /** reverse to-space and from-space roles for generation g **/ void reverse_roles(Generation g) noexcept; +#endif /** discard all allocated memory **/ void clear() noexcept; @@ -410,10 +413,6 @@ namespace xo { void _init_gc_roots(const X1CollectorConfig & cfg, std::size_t page_z); /** aux init function: initialize @ref mlog_storage_[][] arenas **/ void _init_mlogs(const X1CollectorConfig & cfg, std::size_t page_z); -#ifdef MOVED - /** aux init function: create mutation log **/ - MutationLog _make_mlog(uint32_t igen, char tag_char, size_t mlog_z, std::size_t page_z); -#endif /** aux init function: initialize @ref space_storage_[][] arenas **/ void _init_space(const X1CollectorConfig & cfg); @@ -493,21 +492,9 @@ namespace xo { **/ MutationLogState mlog_state_; - /** collector-managed memory here. - * - space_[1] is from-space - * - space_[0] is to-space - * coordinates with role in gc/role.hpp, see also. + /** Collector-managed memory. **/ - - /** arena objects for collector managed memory - * 1:1 with roles, but polarity reverses for each collection - **/ - std::array space_storage_[c_n_role]; - - /** arena pointers. The roles of space_storage_[0][g] and space_storage_[1][g] - * are reversed each time generation g gets collected. - **/ - std::array space_[c_n_role]; + GCObjectStore gco_store_; /** counters collected during @ref verify_ok call **/ VerifyStats verify_stats_; diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp new file mode 100644 index 00000000..35b15d49 --- /dev/null +++ b/include/xo/gc/GCObjectStore.hpp @@ -0,0 +1,81 @@ +/** @file GCObjectStore.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include "generation.hpp" +#include +#include +#include +#include + +namespace xo { + namespace mm { + + /** @brief container to hold gc-aware objects for X1 collector + **/ + class GCObjectStore { + public: + GCObjectStore(const ArenaConfig & arena_cfg, uint32_t ngen, bool debug_flag); + + const DArena * get_space(role r, Generation g) const noexcept { return space_[r][g]; } + 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}); } + + /** Call @p visitor for each memory pool owned by this store **/ + void visit_pools(const MemorySizeVisitor & visitor) const; + + /** For each generation g in [0 ,.., upto) + * swap arenas assigned to {to-space, from-space}. + * Invoked once at the beginning of each gc cycle. + **/ + void swap_roles(Generation upto) noexcept; + + /** Cleanup at the end of a gc cycle. + * Reset from-space + * (current from-space is former to-space, + * relabeled at the beginning of collector cycle) + * for generations in [0 ,.., upto) + **/ + void cleanup_phase(Generation upto, + bool sanitize_flag); + + private: + /** auxiliary init function **/ + void _init_space(); + + private: + /** Configuration for collector spaces. + * Will have (2 x G) of these, + * where G is @ref n_generation_. + * Not using name_ member. + * + * REQUIRE: + * - arena_config_.store_header_flag_ must be true + **/ + ArenaConfig arena_config_; + /** number of generations in use. Same as @ref X1CollectorConfig::n_generation_ **/ + uint32_t n_generation_ = 0; + /** true to enable debug logging **/ + bool debug_flag_ = false; + + /** arena objects for collector managed memory + * 1:1 with roles, but polarity reverses for each collection + **/ + std::array space_storage_[c_n_role]; + + /** arena pointers. The roles of space_storage_[0][g] and space_storage_[1][g] + * are reversed each time generation g gets collected. + **/ + std::array space_[c_n_role]; + + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end GCObjectStore.hpp */ diff --git a/include/xo/gc/MutationLogState.hpp b/include/xo/gc/MutationLogState.hpp index ef7e96aa..57c93146 100644 --- a/include/xo/gc/MutationLogState.hpp +++ b/include/xo/gc/MutationLogState.hpp @@ -16,6 +16,8 @@ namespace xo { class DX1Collector; class VerifyStats; + /** @brief container for X1 collector mutation logs + **/ class MutationLogState { public: using MutationLog = DArenaVector; @@ -133,7 +135,7 @@ namespace xo { public: - /** number of generations in use. Same as @ref X1CollectorConfig.n_generation_ **/ + /** number of generations in use. Same as @ref X1CollectorConfig::n_generation_ **/ uint32_t n_generation_ = 0; /** true to enable debug logging **/ bool debug_flag_ = false; diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index d30acf0e..3f613351 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -15,6 +15,7 @@ set(SELF_SRCS DX1CollectorIterator.cpp X1CollectorConfig.cpp + GCObjectStore.cpp MutationLogState.cpp MutationLogEntry.cpp diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 342da1c6..de9af1e1 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -66,7 +66,9 @@ namespace xo { using size_type = xo::mm::DX1Collector::size_type; DX1Collector::DX1Collector(const X1CollectorConfig & cfg) - : config_{cfg}, mlog_state_{cfg.n_generation_, cfg.debug_flag_} + : config_{cfg}, + mlog_state_{cfg.n_generation_, cfg.debug_flag_}, + gco_store_{cfg.arena_config_, cfg.n_generation_, cfg.debug_flag_} { assert(config_.arena_config_.header_.size_bits_ + config_.arena_config_.header_.age_bits_ + @@ -77,7 +79,9 @@ namespace xo { this->_init_object_types(cfg, page_z); this->_init_gc_roots(cfg, page_z); this->_init_mlogs(cfg, page_z); +#ifdef MOVED this->_init_space(cfg); +#endif } void @@ -108,86 +112,6 @@ namespace xo { DX1Collector::_init_mlogs(const X1CollectorConfig & cfg, std::size_t page_z) { this->mlog_state_.init_mlogs(cfg, page_z); - -#ifdef MOVED - for (uint32_t igen = 0, ngen = cfg.n_generation_; igen + 1 < ngen; ++igen) { - // special case: no use for mutation log for youngest generation, - // so don't trouble to allocate one - - if (igen + 1 < c_max_generation) { - this->mlog_storage_[0][igen] = _make_mlog(igen, 'a', cfg.mutation_log_z_, page_z); - this->mlog_storage_[1][igen] = _make_mlog(igen, 'b', cfg.mutation_log_z_, page_z); - this->mlog_storage_[2][igen] = _make_mlog(igen, 'c', cfg.mutation_log_z_, page_z); - - this->mlog_[0][igen] = &mlog_storage_[0][igen]; - this->mlog_[1][igen] = &mlog_storage_[1][igen]; - this->mlog_[2][igen] = &mlog_storage_[2][igen]; - } else { - assert(false); - } - } - - if (cfg.n_generation_ > 0) { - for (uint32_t igen = cfg.n_generation_ - 1; igen + 1 < c_max_generation; ++igen) { - this->mlog_[0][igen] = nullptr; - this->mlog_[1][igen] = nullptr; - this->mlog_[2][igen] = nullptr; - } - } else { - assert(false); - } -#endif - } - -#ifdef MOVED - auto - DX1Collector::_make_mlog(uint32_t igen, char tag_char, size_t mlog_z, size_t page_z) -> MutationLog - { - char buf[40]; - snprintf(buf, sizeof(buf), "x1-mlog-G%u-%c", igen, tag_char); - - return MutationLog::map(ArenaConfig{.name_ = std::string(buf), - .size_ = mlog_z, - .hugepage_z_ = page_z, - .store_header_flag_ = false}); - } -#endif - - void - DX1Collector::_init_space(const X1CollectorConfig & cfg) - { - assert(c_n_role == 2); - - for (uint32_t igen = 0, ngen = cfg.n_generation_; igen < ngen; ++igen) { - if (igen < c_max_generation) { - { - char buf[40]; - snprintf(buf, sizeof(buf), "x1-space-G%u-a", igen); - - this->space_storage_[0][igen] = DArena::map(cfg.arena_config_.with_name(std::string(buf))); - } - { - char buf[40]; - snprintf(buf, sizeof(buf), "x1-space-G%u-b", igen); - - this->space_storage_[1][igen] = DArena::map(cfg.arena_config_.with_name(std::string(buf))); - } - - this->space_[role::to_space()][igen] = &space_storage_[0][igen]; - this->space_[role::from_space()][igen] = &space_storage_[1][igen]; - } else { - assert(false); - } - } - - for (uint32_t igen = cfg.n_generation_; igen < c_max_generation; ++igen) { - this->space_[role::to_space()][igen] = nullptr; - this->space_[role::from_space()][igen] = nullptr; - } - - if (config_.n_generation_ == 2) { - assert(this->get_space(role::to_space(), Generation{2}) == nullptr); - } } void @@ -196,21 +120,8 @@ namespace xo { object_types_.visit_pools(visitor); root_set_.visit_pools(visitor); - for (uint32_t j = 0; j < config_.n_generation_; ++j) { - for (uint32_t i = 0; i < c_n_role; ++i) { - space_storage_[i][j].visit_pools(visitor); - } - } - + gco_store_.visit_pools(visitor); mlog_state_.visit_pools(visitor); - -#ifdef MOVED - for (uint32_t j = 0; j + 1 < config_.n_generation_; ++j) { - for (uint32_t i = 0; i < c_n_role + 1; ++i) { - mlog_storage_[i][j].visit_pools(visitor); - } - } -#endif } bool @@ -959,12 +870,7 @@ namespace xo { { scope log(XO_DEBUG(true), xtag("upto", upto)); - for (Generation g = Generation{0}; g < upto; ++g) { - log && log("swap roles", xtag("g", g)); - - std::swap(space_[role::to_space()][g], space_[role::from_space()][g]); - } - + gco_store_.swap_roles(upto); mlog_state_.swap_roles(upto); } @@ -974,108 +880,12 @@ namespace xo { mlog_state_.forward_mutation_log(this, upto); } -#ifdef MOVED - MutationLogStatistics - DX1Collector::_preserve_child_of_live_parent(Generation upto, - Generation parent_gen, - const MutationLogEntry & from_entry, - MutationLog * keep_mlog) - { - void * child_fr = *from_entry.p_data(); - AllocInfo child_info = this->alloc_info((std::byte *)(child_fr)); - - MutationLogStatistics counters; - - // if child collected: new child location in to-space - void * child_to = nullptr; - - // parent is alive: gc must ensure child remains alive - - ++counters.n_live_parent_; - - // Parent already recognized as alive. Either not subject to collection - // or already evacuated. - // (+ remember this need not be 1st pass over mlog entries) - - if (child_info.is_forwarding_tseq()) { - // [MLOG1] - - // child already forwarded. - // TODO: make this a method on AllocInfo - child_to = *(void **)child_fr; - - // assigning through address of P->C pointer - // also makes mlog entry current - - } else { - // [MLOG2] - - ++counters.n_rescue_; - - child_to = this->_deep_move_interior(child_fr, upto); - - // update child pointer in parent object - *from_entry.p_data() = child_to; - } - - // child_to generation in {gen, gen+1} - - this->_check_keep_mutation_aux(from_entry, parent_gen, child_to, keep_mlog); - - return counters; - } -#endif - -#ifdef MOVED - bool - DX1Collector::_check_keep_mutation_aux(const MutationLogEntry & from_entry, - Generation parent_gen_to, - void * child_to, - MutationLog * keep_mlog) - { - Generation child_gen_to - = this->generation_of(role::to_space(), child_to); - - bool need_mlog_entry - = ((child_gen_to + 1 < config_.n_generation_) - && (config_.promotion_threshold(parent_gen_to) - > config_.promotion_threshold(child_gen_to))); - - if (need_mlog_entry) { - // 1. P->C pointer is still cross-age (xage), and - // 2. this matters; in future P will promote before C - // - // Need to keep entry because parent will be eligible for promotion - // before child - - keep_mlog->push_back(from_entry); - - return true; - } else { - // child now in final generation, - // no longer need to track incoming mutations. - - return false; - } - } -#endif - void DX1Collector::_cleanup_phase(Generation upto) { scope log(XO_DEBUG(true), xtag("upto", upto)); - // everything live has been copied out of from-space - // -> now set to empty - // - for (Generation g = Generation{0}; g < upto; ++g) { - if (config_.sanitize_flag_) { - space_[role::from_space()][g]->scrub(); - } - - space_[role::from_space()][g]->clear(); - } - + this->gco_store_.cleanup_phase(upto, config_.sanitize_flag_); this->runstate_ = GCRunState::idle(); } @@ -1786,12 +1596,14 @@ namespace xo { arena_end); } +#ifdef MOVED void DX1Collector::reverse_roles(Generation g) noexcept { assert(g < config_.n_generation_); std::swap(space_[role::from_space()][g], space_[role::to_space()][g]); } +#endif void DX1Collector::clear() noexcept { diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp new file mode 100644 index 00000000..7819f216 --- /dev/null +++ b/src/gc/GCObjectStore.cpp @@ -0,0 +1,108 @@ +/** @file GCObjectStore.cpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#include "GCObjectStore.hpp" +#include +#include + +namespace xo { + namespace mm { + + GCObjectStore::GCObjectStore(const ArenaConfig & arena_cfg, + uint32_t ngen, bool debug_flag) + : arena_config_{arena_cfg}, + n_generation_{ngen}, + debug_flag_{debug_flag} + { + assert(arena_config_.header_.size_bits_ + + arena_config_.header_.age_bits_ + + arena_config_.header_.tseq_bits_ <= 64); + + this->_init_space(); + } + + void + GCObjectStore::_init_space() + { + assert(c_n_role == 2); + + for (uint32_t igen = 0, ngen = n_generation_; igen < ngen; ++igen) { + if (igen < c_max_generation) { + { + char buf[40]; + snprintf(buf, sizeof(buf), "x1-space-G%u-a", igen); + + this->space_storage_[0][igen] + = DArena::map(arena_config_.with_name(std::string(buf))); + } + { + char buf[40]; + snprintf(buf, sizeof(buf), "x1-space-G%u-b", igen); + + this->space_storage_[1][igen] + = DArena::map(arena_config_.with_name(std::string(buf))); + } + + this->space_[role::to_space()][igen] = &space_storage_[0][igen]; + this->space_[role::from_space()][igen] = &space_storage_[1][igen]; + } else { + assert(false); + } + } + + for (uint32_t igen = n_generation_; igen < c_max_generation; ++igen) { + this->space_[role::to_space()][igen] = nullptr; + this->space_[role::from_space()][igen] = nullptr; + } + + if (n_generation_ == 2) { + assert(this->get_space(role::to_space(), Generation{2}) == nullptr); + } + } + + void + GCObjectStore::visit_pools(const MemorySizeVisitor & visitor) const + { + for (uint32_t j = 0; j < n_generation_; ++j) { + for (uint32_t i = 0; i < c_n_role; ++i) { + space_storage_[i][j].visit_pools(visitor); + } + } + } + + void + GCObjectStore::swap_roles(Generation upto) noexcept + { + scope log(XO_DEBUG(true), xtag("upto", upto)); + + for (Generation g = Generation{0}; g < upto; ++g) { + log && log("swap roles", xtag("g", g)); + + std::swap(space_[role::to_space()][g], space_[role::from_space()][g]); + } + } + + void + GCObjectStore::cleanup_phase(Generation upto, + bool sanitize_flag) + { + scope log(XO_DEBUG(true), xtag("upto", upto)); + + // everything live has been copied out of from-space + // -> now set to empty + // + for (Generation g = Generation{0}; g < upto; ++g) { + if (sanitize_flag) { + space_[role::from_space()][g]->scrub(); + } + + space_[role::from_space()][g]->clear(); + } + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end GCObjectStore.cpp */ diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp index 8b464a2c..225a5ab6 100644 --- a/utest/Collector.test.cpp +++ b/utest/Collector.test.cpp @@ -91,15 +91,23 @@ namespace xo { REQUIRE(gc.to_space(g1) != gc.to_space(g0)); REQUIRE(gc.from_space(g1) != gc.from_space(g0)); REQUIRE(gc.to_space(g0) != gc.from_space(g1)); + REQUIRE(gc.to_space(g1) != gc.from_space(g1)); + + for (Generation gi{0}; gi < 2; ++gi) { + INFO(xtag("gi", gi)); + + REQUIRE(gc.to_space(gi)); + REQUIRE(gc.from_space(gi)); + + REQUIRE(gc.from_space(gi)->is_mapped()); + REQUIRE(gc.to_space(gi)->is_mapped()); + } for (Generation gi = Generation(2); gi < c_max_generation; ++gi) { INFO(xtag("gi", gi)); REQUIRE(!gc.to_space(gi)); REQUIRE(!gc.from_space(gi)); - - REQUIRE(!gc.space_storage_[0][gi].is_mapped()); - REQUIRE(!gc.space_storage_[1][gi].is_mapped()); } } From 1b3ecf956100945d77e8be5762f8acec27a0555f Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 2 Apr 2026 20:14:52 -0400 Subject: [PATCH 081/174] xo-gc: refactor to prune mlog store dep on DX1Collector --- include/xo/gc/DX1Collector.hpp | 2 +- include/xo/gc/MutationLogConfig.hpp | 38 ++++++++++++++++++ include/xo/gc/MutationLogState.hpp | 19 ++++----- src/gc/CMakeLists.txt | 2 + src/gc/DX1Collector.cpp | 19 ++++----- src/gc/GCObjectStore.cpp | 4 +- src/gc/MutationLogConfig.cpp | 22 +++++++++++ src/gc/MutationLogState.cpp | 61 +++++++++++++++++------------ 8 files changed, 120 insertions(+), 47 deletions(-) create mode 100644 include/xo/gc/MutationLogConfig.hpp create mode 100644 src/gc/MutationLogConfig.cpp diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 7992f478..6fd48b98 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -412,7 +412,7 @@ namespace xo { /** aux init function: initialize @ref roots_ arena **/ void _init_gc_roots(const X1CollectorConfig & cfg, std::size_t page_z); /** aux init function: initialize @ref mlog_storage_[][] arenas **/ - void _init_mlogs(const X1CollectorConfig & cfg, std::size_t page_z); + void _init_mlogs(std::size_t page_z); /** aux init function: initialize @ref space_storage_[][] arenas **/ void _init_space(const X1CollectorConfig & cfg); diff --git a/include/xo/gc/MutationLogConfig.hpp b/include/xo/gc/MutationLogConfig.hpp new file mode 100644 index 00000000..c8cd37b6 --- /dev/null +++ b/include/xo/gc/MutationLogConfig.hpp @@ -0,0 +1,38 @@ +/** @file MutationLogConfig.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace mm { + + class MutationLogConfig { + public: + MutationLogConfig(std::uint32_t ngen, + std::size_t mlog_z, + bool debug_flag); + + public: + /** number of generations in use. + * Same as @ref X1CollectorConfig::n_generation_ + **/ + std::uint32_t n_generation_ = 0; + + /** storage for xgen pointer bookkeeping (aka remembered sets). + * Use 3x this value per generation + **/ + std::size_t mutation_log_z_ = 1024; + + /** true to enable debug logging **/ + bool debug_flag_ = false; + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end MutationLogConfig.hpp */ diff --git a/include/xo/gc/MutationLogState.hpp b/include/xo/gc/MutationLogState.hpp index 57c93146..7ca0d186 100644 --- a/include/xo/gc/MutationLogState.hpp +++ b/include/xo/gc/MutationLogState.hpp @@ -5,7 +5,9 @@ #pragma once +#include "MutationLogConfig.hpp" #include "X1CollectorConfig.hpp" +#include "GCObjectStore.hpp" #include "MutationLogStatistics.hpp" #include "MutationLogEntry.hpp" #include @@ -24,12 +26,12 @@ namespace xo { using size_type = DArena::size_type; public: - MutationLogState(uint32_t ngen, bool debug_flag); + explicit MutationLogState(const MutationLogConfig & config); - /** Initialize mlog state for configuration @p cfg + /** Initialize mlog state * with o/s page size @p page_z **/ - void init_mlogs(const X1CollectorConfig & cfg, std::size_t page_z); + void init_mlogs(std::size_t page_z); /** total number of active mlog entries (across all generations) **/ @@ -42,7 +44,7 @@ namespace xo { * (using gc to identify location of objects). * Update counters in @p *p_verify_stats. **/ - void verify_ok(DX1Collector * gc, + void verify_ok(GCObjectStore * gc, VerifyStats * p_verify_stats) noexcept; /** Append a single mutation to log for generation @p dest_g @@ -67,7 +69,8 @@ namespace xo { void ** addr, obj rhs); - /** swap {to, from} roles **/ + /** swap {to, from} roles + **/ void swap_roles(Generation upto) noexcept; /** On behalf of collector @p gc: @@ -135,10 +138,8 @@ namespace xo { public: - /** number of generations in use. Same as @ref X1CollectorConfig::n_generation_ **/ - uint32_t n_generation_ = 0; - /** true to enable debug logging **/ - bool debug_flag_ = false; + /** configuration for mlog store **/ + MutationLogConfig config_; /** Cross-generational mutations tracked in MutationLogs. * We need three logs per generation: diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index 3f613351..ec49a414 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -16,6 +16,8 @@ set(SELF_SRCS X1CollectorConfig.cpp GCObjectStore.cpp + + MutationLogConfig.cpp MutationLogState.cpp MutationLogEntry.cpp diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index de9af1e1..5e653eb2 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -67,7 +67,11 @@ namespace xo { DX1Collector::DX1Collector(const X1CollectorConfig & cfg) : config_{cfg}, - mlog_state_{cfg.n_generation_, cfg.debug_flag_}, + mlog_state_{ + MutationLogConfig{ + cfg.n_generation_, + cfg.mutation_log_z_, + cfg.debug_flag_}}, gco_store_{cfg.arena_config_, cfg.n_generation_, cfg.debug_flag_} { assert(config_.arena_config_.header_.size_bits_ + @@ -78,10 +82,7 @@ namespace xo { this->_init_object_types(cfg, page_z); this->_init_gc_roots(cfg, page_z); - this->_init_mlogs(cfg, page_z); -#ifdef MOVED - this->_init_space(cfg); -#endif + this->_init_mlogs(page_z); } void @@ -109,9 +110,9 @@ namespace xo { } void - DX1Collector::_init_mlogs(const X1CollectorConfig & cfg, std::size_t page_z) + DX1Collector::_init_mlogs(std::size_t page_z) { - this->mlog_state_.init_mlogs(cfg, page_z); + this->mlog_state_.init_mlogs(page_z); } void @@ -145,7 +146,7 @@ namespace xo { DX1Collector::generation_of(role r, const void * addr) const noexcept { for (Generation gi{0}; gi < config_.n_generation_; ++gi) { - const DArena * arena = get_space(r, gi); + const DArena * arena = this->get_space(r, gi); if (arena->contains(addr)) return gi; @@ -693,7 +694,7 @@ namespace xo { } // 4. scan mutation logs - mlog_state_.verify_ok(this, + mlog_state_.verify_ok(&gco_store_, &(this->verify_stats_)); } diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 7819f216..1537d6b5 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -75,7 +75,7 @@ namespace xo { void GCObjectStore::swap_roles(Generation upto) noexcept { - scope log(XO_DEBUG(true), xtag("upto", upto)); + scope log(XO_DEBUG(debug_flag_), xtag("upto", upto)); for (Generation g = Generation{0}; g < upto; ++g) { log && log("swap roles", xtag("g", g)); @@ -88,7 +88,7 @@ namespace xo { GCObjectStore::cleanup_phase(Generation upto, bool sanitize_flag) { - scope log(XO_DEBUG(true), xtag("upto", upto)); + scope log(XO_DEBUG(debug_flag_), xtag("upto", upto)); // everything live has been copied out of from-space // -> now set to empty diff --git a/src/gc/MutationLogConfig.cpp b/src/gc/MutationLogConfig.cpp new file mode 100644 index 00000000..5e2275dd --- /dev/null +++ b/src/gc/MutationLogConfig.cpp @@ -0,0 +1,22 @@ +/** @file MutationLogConfig.cpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#include "MutationLogConfig.hpp" + +namespace xo { + namespace mm { + + MutationLogConfig::MutationLogConfig(std::uint32_t ngen, + std::size_t mlog_z, + bool debug_flag) + : n_generation_{ngen}, + mutation_log_z_{mlog_z}, + debug_flag_{debug_flag} + {} + + } +} + +/* end MutationLogConfig.cpp */ diff --git a/src/gc/MutationLogState.cpp b/src/gc/MutationLogState.cpp index 18c22d30..ba2d51f1 100644 --- a/src/gc/MutationLogState.cpp +++ b/src/gc/MutationLogState.cpp @@ -9,36 +9,43 @@ namespace xo { namespace mm { - MutationLogState::MutationLogState(uint32_t ngen, bool debug_flag) - : n_generation_{ngen}, debug_flag_{debug_flag} + MutationLogState::MutationLogState(const MutationLogConfig & config) + : config_{config} {} void - MutationLogState::init_mlogs(const X1CollectorConfig & cfg, - std::size_t page_z) + MutationLogState::init_mlogs(std::size_t page_z) { - for (uint32_t igen = 0, ngen = cfg.n_generation_; igen + 1 < ngen; ++igen) { + assert(c_n_role + 1 == 3); + + for (uint32_t igen = 0, ngen = config_.n_generation_; igen + 1 < ngen; ++igen) { // special case: no use for mutation log for youngest generation, // so don't trouble to allocate one if (igen + 1 < c_max_generation) { - this->mlog_storage_[0][igen] = _make_mlog(igen, 'a', cfg.mutation_log_z_, page_z); - this->mlog_storage_[1][igen] = _make_mlog(igen, 'b', cfg.mutation_log_z_, page_z); - this->mlog_storage_[2][igen] = _make_mlog(igen, 'c', cfg.mutation_log_z_, page_z); + std::array label_v{'a', 'b', 'c'}; - this->mlog_[0][igen] = &mlog_storage_[0][igen]; - this->mlog_[1][igen] = &mlog_storage_[1][igen]; - this->mlog_[2][igen] = &mlog_storage_[2][igen]; + for (std::uint32_t mlog_role = 0; mlog_role < c_n_role + 1; ++mlog_role) { + this->mlog_storage_[mlog_role][igen] + = _make_mlog(igen, + label_v[mlog_role], + config_.mutation_log_z_, + page_z); + + this->mlog_[mlog_role][igen] + = &(mlog_storage_[mlog_role][igen]); + } } else { assert(false); } } - if (cfg.n_generation_ > 0) { - for (uint32_t igen = cfg.n_generation_ - 1; igen + 1 < c_max_generation; ++igen) { - this->mlog_[0][igen] = nullptr; - this->mlog_[1][igen] = nullptr; - this->mlog_[2][igen] = nullptr; + if (config_.n_generation_ > 0) { + for (std::uint32_t igen = config_.n_generation_ - 1; + igen + 1 < c_max_generation; ++igen) { + + for (std::uint32_t mlog_role = 0; mlog_role < c_n_role + 1; ++mlog_role) + this->mlog_[mlog_role][igen] = nullptr; } } else { assert(false); @@ -63,7 +70,7 @@ namespace xo { { size_type z = 0; - for (Generation gj{0}; gj + 1 < n_generation_; ++gj) { + for (Generation gj{0}; gj + 1 < config_.n_generation_; ++gj) { z += mlog_[role::to_space()][gj]->size(); } @@ -73,7 +80,7 @@ namespace xo { void MutationLogState::visit_pools(const MemorySizeVisitor & visitor) const { - for (uint32_t j = 0; j + 1 < n_generation_; ++j) { + for (uint32_t j = 0; j + 1 < config_.n_generation_; ++j) { for (uint32_t i = 0; i < c_n_role + 1; ++i) { mlog_storage_[i][j].visit_pools(visitor); } @@ -81,13 +88,13 @@ namespace xo { } void - MutationLogState::verify_ok(DX1Collector * gc, + MutationLogState::verify_ok(GCObjectStore * gco_store, VerifyStats * p_verify_stats) noexcept { // 4. scan mutation logs - for (Generation g(0); g + 1 < n_generation_; ++g) { - const DArena * space = gc->get_space(role::to_space(), g); - const DArena * from = gc->get_space(role::from_space(), g); + 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); // mutation log for generation g records *incoming* pointers // from more senior generations; includes objects from *this* @@ -168,7 +175,9 @@ namespace xo { // on 1st iteration, for all generations: // - to_mlog, triage_mlog are empty - for (Generation child_gen{0}; child_gen + 2 < n_generation_; ++child_gen) { + for (Generation child_gen{0}; + child_gen + 2 < config_.n_generation_; + ++child_gen) { MutationLog * from_mlog = this->mlog_[role::from_space()][child_gen]; @@ -196,7 +205,7 @@ namespace xo { } while (work > 0); // here: reached fixpoints, any remaining triaged mlogs can be discarded - for (Generation child_gen{0}; child_gen + 2 < n_generation_; ++child_gen) { + for (Generation child_gen{0}; child_gen + 2 < config_.n_generation_; ++child_gen) { MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; triage_mlog->clear(); @@ -211,7 +220,7 @@ namespace xo { MutationLog * keep_mlog, MutationLog * triage_mlog) { - scope log(XO_DEBUG(debug_flag_), + scope log(XO_DEBUG(config_.debug_flag_), xtag("child_gen", child_gen), xtag("mlog.size", from_mlog->size())); @@ -415,7 +424,7 @@ namespace xo { = gc->generation_of(role::to_space(), child_to); bool need_mlog_entry - = ((child_gen_to + 1 < n_generation_) + = ((child_gen_to + 1 < config_.n_generation_) && (gc->config().promotion_threshold(parent_gen_to) > gc->config().promotion_threshold(child_gen_to))); From 57b6d2380baf7ada2dd3f6581e438326b76ae192 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 2 Apr 2026 20:28:07 -0400 Subject: [PATCH 082/174] xo-gc: move DX1Collector.generation_of() impl -> GCObjectStore --- include/xo/gc/GCObjectStore.hpp | 5 +++++ src/gc/DX1Collector.cpp | 9 +-------- src/gc/GCObjectStore.cpp | 13 +++++++++++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 35b15d49..e1a3b940 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -26,6 +26,11 @@ namespace xo { DArena * to_space(Generation g) noexcept { return get_space(role::to_space(), g); } DArena * new_space() noexcept { return to_space(Generation{0}); } + /** generation to which pointer @p addr belongs, given role @p r; + * sentinel if not found in this collector + **/ + Generation generation_of(role r, const void * addr) const noexcept; + /** Call @p visitor for each memory pool owned by this store **/ void visit_pools(const MemorySizeVisitor & visitor) const; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 5e653eb2..deac4bfb 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -145,14 +145,7 @@ namespace xo { Generation DX1Collector::generation_of(role r, const void * addr) const noexcept { - for (Generation gi{0}; gi < config_.n_generation_; ++gi) { - const DArena * arena = this->get_space(r, gi); - - if (arena->contains(addr)) - return gi; - } - - return Generation::sentinel(); + return gco_store_.generation_of(r, addr); } AllocError diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 1537d6b5..7e4d80a5 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -62,6 +62,19 @@ namespace xo { } } + Generation + GCObjectStore::generation_of(role r, const void * addr) const noexcept + { + for (Generation gi{0}; gi < n_generation_; ++gi) { + const DArena * arena = this->get_space(r, gi); + + if (arena->contains(addr)) + return gi; + } + + return Generation::sentinel(); + } + void GCObjectStore::visit_pools(const MemorySizeVisitor & visitor) const { From 9f87d453d09f164f6172816e6e706a7515345749 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 2 Apr 2026 20:35:22 -0400 Subject: [PATCH 083/174] xo-gc: move header query aux method impls to GCObjectStore --- include/xo/gc/DX1Collector.hpp | 6 +++--- include/xo/gc/GCObjectStore.hpp | 16 ++++++++++++++++ src/gc/DX1Collector.cpp | 19 +++++-------------- src/gc/GCObjectStore.cpp | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 6fd48b98..6a53b2b8 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -143,9 +143,9 @@ namespace xo { using GCMoveCheckpoint = std::array; using MutationLog = DArenaVector; using typeseq = xo::facet::typeseq; - using size_type = DArena::size_type; - using value_type = DArena::value_type; - using header_type = DArena::header_type; + using size_type = GCObjectStore::size_type; + using value_type = GCObjectStore::value_type; + using header_type = GCObjectStore::header_type; /** hard max typeseq for collector-registered types **/ static constexpr size_t c_max_typeseq = 4096; diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index e1a3b940..44dc579b 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -6,6 +6,7 @@ #pragma once #include "generation.hpp" +#include "object_age.hpp" #include #include #include @@ -17,6 +18,11 @@ namespace xo { /** @brief container to hold gc-aware objects for X1 collector **/ class GCObjectStore { + public: + using header_type = DArena::header_type; + using value_type = DArena::value_type; + using size_type = DArena::size_type; + public: GCObjectStore(const ArenaConfig & arena_cfg, uint32_t ngen, bool debug_flag); @@ -31,6 +37,16 @@ namespace xo { **/ Generation generation_of(role r, const void * addr) const noexcept; + /** get allocation size from header **/ + std::size_t header2size(header_type hdr) const noexcept; + /** get generation counter from alloc header **/ + object_age header2age(header_type hdr) const noexcept; + /** get tseq from alloc header **/ + uint32_t header2tseq(header_type hdr) const noexcept; + + /** true iff original alloc has been replaced by a forwarding pointer **/ + bool is_forwarding_header(header_type hdr) const noexcept; + /** Call @p visitor for each memory pool owned by this store **/ void visit_pools(const MemorySizeVisitor & visitor) const; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index deac4bfb..091734e9 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -534,34 +534,25 @@ namespace xo { size_type DX1Collector::header2size(header_type hdr) const noexcept { - uint32_t z = config_.arena_config_.header_.size(hdr); - - return z; + return gco_store_.header2size(hdr); } object_age DX1Collector::header2age(header_type hdr) const noexcept { - uint32_t age = config_.arena_config_.header_.age(hdr); - - assert(age < c_max_object_age); - - return object_age(age); + return gco_store_.header2age(hdr); } uint32_t DX1Collector::header2tseq(header_type hdr) const noexcept { - uint32_t tseq = config_.arena_config_.header_.tseq(hdr); - - return tseq; + return gco_store_.header2tseq(hdr); } bool DX1Collector::is_forwarding_header(header_type hdr) const noexcept { - /** forwarding pointer encoded as sentinel tseq **/ - return config_.arena_config_.header_.is_forwarding_tseq(hdr); + return gco_store_.is_forwarding_header(hdr); } bool @@ -1527,7 +1518,7 @@ namespace xo { assert(src_hdr && dest_hdr); - if (header2age(*src_hdr) <= header2age(*dest_hdr)) { + if (this->header2age(*src_hdr) <= this->header2age(*dest_hdr)) { // source and destination have the same age; // therefore are always collected on the same set of GC cycles // -> no need to remember separately. diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 7e4d80a5..b0948eb7 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -75,6 +75,39 @@ namespace xo { return Generation::sentinel(); } + auto + GCObjectStore::header2size(header_type hdr) const noexcept -> size_type + { + uint32_t z = arena_config_.header_.size(hdr); + + return z; + } + + object_age + GCObjectStore::header2age(header_type hdr) const noexcept + { + uint32_t age = arena_config_.header_.age(hdr); + + assert(age < c_max_object_age); + + return object_age(age); + } + + uint32_t + GCObjectStore::header2tseq(header_type hdr) const noexcept + { + uint32_t tseq = arena_config_.header_.tseq(hdr); + + return tseq; + } + + bool + GCObjectStore::is_forwarding_header(header_type hdr) const noexcept + { + /** forwarding pointer encoded as sentinel tseq **/ + return arena_config_.header_.is_forwarding_tseq(hdr); + } + void GCObjectStore::visit_pools(const MemorySizeVisitor & visitor) const { From ae9e97acc2859bae42cd2d62a0345ae221805bd9 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 2 Apr 2026 21:23:46 -0400 Subject: [PATCH 084/174] xo-gc: refactor, focus on DX1Collector+MutationLogState --- include/xo/gc/DX1Collector.hpp | 2 ++ include/xo/gc/GCObjectStore.hpp | 21 ++++----------- include/xo/gc/GCObjectStoreConfig.hpp | 39 +++++++++++++++++++++++++++ include/xo/gc/MutationLogConfig.hpp | 20 ++++++++++++++ include/xo/gc/MutationLogState.hpp | 2 +- include/xo/gc/X1CollectorConfig.hpp | 24 ++++++++++++----- src/gc/CMakeLists.txt | 5 +++- src/gc/DX1Collector.cpp | 8 ++---- src/gc/GCObjectStore.cpp | 39 +++++++++++++-------------- src/gc/GCObjectStoreConfig.cpp | 22 +++++++++++++++ src/gc/MutationLogConfig.cpp | 2 ++ src/gc/MutationLogState.cpp | 13 ++++----- 12 files changed, 140 insertions(+), 57 deletions(-) create mode 100644 include/xo/gc/GCObjectStoreConfig.hpp create mode 100644 src/gc/GCObjectStoreConfig.cpp diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 6a53b2b8..2addb90a 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -161,6 +161,8 @@ namespace xo { // ----- access methods ----- const X1CollectorConfig & config() const noexcept { return config_; } + const GCObjectStore & gco_store() const noexcept { return gco_store_; } + std::string_view name() const noexcept { return config_.name_; } GCRunState runstate() const noexcept { return runstate_; } const ObjectTypeTable * get_object_types() const noexcept { return &object_types_; } diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 44dc579b..18dbee8f 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -5,11 +5,11 @@ #pragma once +#include "GCObjectStoreConfig.hpp" #include "generation.hpp" #include "object_age.hpp" #include -#include -#include +//#include #include namespace xo { @@ -24,7 +24,7 @@ namespace xo { using size_type = DArena::size_type; public: - GCObjectStore(const ArenaConfig & arena_cfg, uint32_t ngen, bool debug_flag); + explicit GCObjectStore(const GCObjectStoreConfig & cfg); const DArena * get_space(role r, Generation g) const noexcept { return space_[r][g]; } DArena * get_space(role r, Generation g) noexcept { return space_[r][g]; } @@ -70,19 +70,8 @@ namespace xo { void _init_space(); private: - /** Configuration for collector spaces. - * Will have (2 x G) of these, - * where G is @ref n_generation_. - * Not using name_ member. - * - * REQUIRE: - * - arena_config_.store_header_flag_ must be true - **/ - ArenaConfig arena_config_; - /** number of generations in use. Same as @ref X1CollectorConfig::n_generation_ **/ - uint32_t n_generation_ = 0; - /** true to enable debug logging **/ - bool debug_flag_ = false; + /** configuration for gc-aware object store **/ + GCObjectStoreConfig config_; /** arena objects for collector managed memory * 1:1 with roles, but polarity reverses for each collection diff --git a/include/xo/gc/GCObjectStoreConfig.hpp b/include/xo/gc/GCObjectStoreConfig.hpp new file mode 100644 index 00000000..40e4483d --- /dev/null +++ b/include/xo/gc/GCObjectStoreConfig.hpp @@ -0,0 +1,39 @@ +/** @file GCObjectStoreConfig.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include + +namespace xo { + namespace mm { + + /** @brief record GCObjectStore configuration **/ + class GCObjectStoreConfig { + public: + GCObjectStoreConfig(const ArenaConfig & arena_cfg, + std::uint32_t ngen, + bool debug_flag); + + public: + /** Configuration for collector spaces. + * Will have (2 x G) of these, + * where G is @ref n_generation_. + * Not using name_ member. + * + * REQUIRE: + * - arena_config_.store_header_flag_ must be true + **/ + ArenaConfig arena_config_; + /** number of generations in use. Same as @ref X1CollectorConfig::n_generation_ **/ + std::uint32_t n_generation_ = 0; + /** true to enable debug logging **/ + bool debug_flag_ = false; + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end GCObjectStoreConfig.hpp */ diff --git a/include/xo/gc/MutationLogConfig.hpp b/include/xo/gc/MutationLogConfig.hpp index c8cd37b6..89d7041d 100644 --- a/include/xo/gc/MutationLogConfig.hpp +++ b/include/xo/gc/MutationLogConfig.hpp @@ -5,24 +5,44 @@ #pragma once +#include "generation.hpp" #include #include namespace xo { namespace mm { + /** @brief configuration for MutationLogState **/ class MutationLogConfig { public: MutationLogConfig(std::uint32_t ngen, + std::uint32_t survive, std::size_t mlog_z, bool debug_flag); + /** age threshold for promotion to generation @p g **/ + uint32_t promotion_threshold(Generation g) const noexcept { + + // TODO: may consider replacing with table-lookup + // Require: if two distinct ages promote to some gen g at the same time, + // then they also promote to gen g+k at the same time for all k>0. + + return g * n_survive_threshold_; + } + + public: /** number of generations in use. * Same as @ref X1CollectorConfig::n_generation_ **/ std::uint32_t n_generation_ = 0; + /** Number of promotion steps. + * An object that survives this number of collections + * advances to the next generation. + **/ + uint32_t n_survive_threshold_ = 2; + /** storage for xgen pointer bookkeeping (aka remembered sets). * Use 3x this value per generation **/ diff --git a/include/xo/gc/MutationLogState.hpp b/include/xo/gc/MutationLogState.hpp index 7ca0d186..0fb5d624 100644 --- a/include/xo/gc/MutationLogState.hpp +++ b/include/xo/gc/MutationLogState.hpp @@ -130,7 +130,7 @@ namespace xo { * helper function to decide whether to keep a mutation log entry * @return true iff mlog entry appended to @p keep_mlog **/ - bool _check_keep_mutation_aux(DX1Collector * gc, + bool _check_keep_mutation_aux(const GCObjectStore & gco_store, const MutationLogEntry & from_entry, Generation parent_gen_to, void * child_to, diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp index 1c795795..03a4b11c 100644 --- a/include/xo/gc/X1CollectorConfig.hpp +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -5,6 +5,8 @@ #pragma once +#include "GCObjectStoreConfig.hpp" +#include "MutationLogConfig.hpp" #include "object_age.hpp" #include "generation.hpp" #include @@ -36,18 +38,28 @@ namespace xo { **/ X1CollectorConfig with_sanitize_flag(bool x); + /** fetch configuration for gc object store **/ + GCObjectStoreConfig gco_store_config() const noexcept { + return GCObjectStoreConfig(arena_config_, + n_generation_, + debug_flag_); + } + + /** fetch configuration for mutation log store **/ + MutationLogConfig mlog_config() const noexcept { + return MutationLogConfig(n_generation_, + n_survive_threshold_, + mutation_log_z_, + debug_flag_); + } + Generation age2gen(object_age age) const noexcept { return Generation(age % n_survive_threshold_); } /** age threshold for promotion to generation @p g **/ uint32_t promotion_threshold(Generation g) const noexcept { - - // TODO: may consider replacing with table-lookup - // Require: if two distinct ages promote to some gen g at the same time, - // then they also promote to gen g+k at the same time for all k>0. - - return g * n_survive_threshold_; + return mlog_config().promotion_threshold(g); } public: diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index ec49a414..42ce4edd 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -9,18 +9,21 @@ set(SELF_SRCS IAllocator_DX1Collector.cpp IAllocIterator_DX1CollectorIterator.cpp + X1CollectorConfig.cpp DX1Collector.cpp facet/ICollector_DX1Collector.cpp DX1CollectorIterator.cpp - X1CollectorConfig.cpp + GCObjectStoreConfig.cpp GCObjectStore.cpp MutationLogConfig.cpp MutationLogState.cpp MutationLogEntry.cpp + + ) xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS}) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 091734e9..e5045aa0 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -67,12 +67,8 @@ namespace xo { DX1Collector::DX1Collector(const X1CollectorConfig & cfg) : config_{cfg}, - mlog_state_{ - MutationLogConfig{ - cfg.n_generation_, - cfg.mutation_log_z_, - cfg.debug_flag_}}, - gco_store_{cfg.arena_config_, cfg.n_generation_, cfg.debug_flag_} + mlog_state_{cfg.mlog_config()}, + gco_store_{cfg.gco_store_config()} { assert(config_.arena_config_.header_.size_bits_ + config_.arena_config_.header_.age_bits_ + diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index b0948eb7..aa71f8ba 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -10,15 +10,12 @@ namespace xo { namespace mm { - GCObjectStore::GCObjectStore(const ArenaConfig & arena_cfg, - uint32_t ngen, bool debug_flag) - : arena_config_{arena_cfg}, - n_generation_{ngen}, - debug_flag_{debug_flag} + GCObjectStore::GCObjectStore(const GCObjectStoreConfig & cfg) + : config_{cfg} { - assert(arena_config_.header_.size_bits_ + - arena_config_.header_.age_bits_ + - arena_config_.header_.tseq_bits_ <= 64); + assert(config_.arena_config_.header_.size_bits_ + + config_.arena_config_.header_.age_bits_ + + config_.arena_config_.header_.tseq_bits_ <= 64); this->_init_space(); } @@ -28,21 +25,21 @@ namespace xo { { assert(c_n_role == 2); - for (uint32_t igen = 0, ngen = n_generation_; igen < ngen; ++igen) { + for (uint32_t igen = 0, ngen = config_.n_generation_; igen < ngen; ++igen) { if (igen < c_max_generation) { { char buf[40]; snprintf(buf, sizeof(buf), "x1-space-G%u-a", igen); this->space_storage_[0][igen] - = DArena::map(arena_config_.with_name(std::string(buf))); + = DArena::map(config_.arena_config_.with_name(std::string(buf))); } { char buf[40]; snprintf(buf, sizeof(buf), "x1-space-G%u-b", igen); this->space_storage_[1][igen] - = DArena::map(arena_config_.with_name(std::string(buf))); + = DArena::map(config_.arena_config_.with_name(std::string(buf))); } this->space_[role::to_space()][igen] = &space_storage_[0][igen]; @@ -52,12 +49,12 @@ namespace xo { } } - for (uint32_t igen = n_generation_; igen < c_max_generation; ++igen) { + for (uint32_t igen = config_.n_generation_; igen < c_max_generation; ++igen) { this->space_[role::to_space()][igen] = nullptr; this->space_[role::from_space()][igen] = nullptr; } - if (n_generation_ == 2) { + if (config_.n_generation_ == 2) { assert(this->get_space(role::to_space(), Generation{2}) == nullptr); } } @@ -65,7 +62,7 @@ namespace xo { Generation GCObjectStore::generation_of(role r, const void * addr) const noexcept { - for (Generation gi{0}; gi < n_generation_; ++gi) { + for (Generation gi{0}; gi < config_.n_generation_; ++gi) { const DArena * arena = this->get_space(r, gi); if (arena->contains(addr)) @@ -78,7 +75,7 @@ namespace xo { auto GCObjectStore::header2size(header_type hdr) const noexcept -> size_type { - uint32_t z = arena_config_.header_.size(hdr); + uint32_t z = config_.arena_config_.header_.size(hdr); return z; } @@ -86,7 +83,7 @@ namespace xo { object_age GCObjectStore::header2age(header_type hdr) const noexcept { - uint32_t age = arena_config_.header_.age(hdr); + uint32_t age = config_.arena_config_.header_.age(hdr); assert(age < c_max_object_age); @@ -96,7 +93,7 @@ namespace xo { uint32_t GCObjectStore::header2tseq(header_type hdr) const noexcept { - uint32_t tseq = arena_config_.header_.tseq(hdr); + uint32_t tseq = config_.arena_config_.header_.tseq(hdr); return tseq; } @@ -105,13 +102,13 @@ namespace xo { GCObjectStore::is_forwarding_header(header_type hdr) const noexcept { /** forwarding pointer encoded as sentinel tseq **/ - return arena_config_.header_.is_forwarding_tseq(hdr); + return config_.arena_config_.header_.is_forwarding_tseq(hdr); } void GCObjectStore::visit_pools(const MemorySizeVisitor & visitor) const { - for (uint32_t j = 0; j < n_generation_; ++j) { + for (uint32_t j = 0; j < config_.n_generation_; ++j) { for (uint32_t i = 0; i < c_n_role; ++i) { space_storage_[i][j].visit_pools(visitor); } @@ -121,7 +118,7 @@ namespace xo { void GCObjectStore::swap_roles(Generation upto) noexcept { - scope log(XO_DEBUG(debug_flag_), 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)); @@ -134,7 +131,7 @@ namespace xo { GCObjectStore::cleanup_phase(Generation upto, bool sanitize_flag) { - scope log(XO_DEBUG(debug_flag_), xtag("upto", upto)); + scope log(XO_DEBUG(config_.debug_flag_), xtag("upto", upto)); // everything live has been copied out of from-space // -> now set to empty diff --git a/src/gc/GCObjectStoreConfig.cpp b/src/gc/GCObjectStoreConfig.cpp new file mode 100644 index 00000000..1d765a8b --- /dev/null +++ b/src/gc/GCObjectStoreConfig.cpp @@ -0,0 +1,22 @@ +/** @file GCObjectStore.cpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#include "GCObjectStore.hpp" + +namespace xo { + namespace mm { + + GCObjectStoreConfig::GCObjectStoreConfig(const ArenaConfig & arena_cfg, + std::uint32_t ngen, + bool debug_flag) + : arena_config_{arena_cfg}, + n_generation_{ngen}, + debug_flag_{debug_flag} + {} + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end GCObjectStore.cpp */ diff --git a/src/gc/MutationLogConfig.cpp b/src/gc/MutationLogConfig.cpp index 5e2275dd..ff2af63f 100644 --- a/src/gc/MutationLogConfig.cpp +++ b/src/gc/MutationLogConfig.cpp @@ -9,9 +9,11 @@ namespace xo { namespace mm { MutationLogConfig::MutationLogConfig(std::uint32_t ngen, + std::uint32_t survive, std::size_t mlog_z, bool debug_flag) : n_generation_{ngen}, + n_survive_threshold_{survive}, mutation_log_z_{mlog_z}, debug_flag_{debug_flag} {} diff --git a/src/gc/MutationLogState.cpp b/src/gc/MutationLogState.cpp index ba2d51f1..b6198f2c 100644 --- a/src/gc/MutationLogState.cpp +++ b/src/gc/MutationLogState.cpp @@ -332,7 +332,7 @@ namespace xo { MutationLogEntry to_entry(parent_to, p_data_to, from_entry.snap()); - this->_check_keep_mutation_aux(gc, + this->_check_keep_mutation_aux(gc->gco_store(), to_entry, parent_gen_to, child_to, @@ -408,25 +408,26 @@ namespace xo { // child_to generation in {gen, gen+1} - this->_check_keep_mutation_aux(gc, from_entry, parent_gen, child_to, keep_mlog); + this->_check_keep_mutation_aux(gc->gco_store(), + from_entry, parent_gen, child_to, keep_mlog); return counters; } bool - MutationLogState::_check_keep_mutation_aux(DX1Collector * gc, + MutationLogState::_check_keep_mutation_aux(const GCObjectStore & gco_store, const MutationLogEntry & from_entry, Generation parent_gen_to, void * child_to, MutationLog * keep_mlog) { Generation child_gen_to - = gc->generation_of(role::to_space(), child_to); + = gco_store.generation_of(role::to_space(), child_to); bool need_mlog_entry = ((child_gen_to + 1 < config_.n_generation_) - && (gc->config().promotion_threshold(parent_gen_to) - > gc->config().promotion_threshold(child_gen_to))); + && (config_.promotion_threshold(parent_gen_to) + > config_.promotion_threshold(child_gen_to))); if (need_mlog_entry) { // 1. P->C pointer is still cross-age (xage), and From 55c5838f4a30afe35e63f329cbc83607d0e361a7 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 2 Apr 2026 21:27:04 -0400 Subject: [PATCH 085/174] xo-gc: refactor: move alloc_info() impl -> GCObjectStore --- include/xo/gc/DX1Collector.hpp | 6 +++--- include/xo/gc/GCObjectStore.hpp | 3 +++ src/gc/DX1Collector.cpp | 23 +++++------------------ src/gc/GCObjectStore.cpp | 18 ++++++++++++++++++ 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 2addb90a..9a0f2811 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -273,15 +273,15 @@ namespace xo { /** true iff original alloc has been replaced by a forwarding pointer **/ bool is_forwarding_header(header_type hdr) const noexcept; + /** Retreive bookkeeping info for allocation at @p mem. **/ + AllocInfo alloc_info(value_type mem) const noexcept; + /** true iff type with id @p tseq has known metadata * (i.e. has appeared in preceding call to install_type * for this collector) **/ bool is_type_installed(typeseq tseq) const noexcept; - /** Retreive bookkeeping info for allocation at @p mem. **/ - AllocInfo alloc_info(value_type mem) const noexcept; - /** verify that GC state appears consistent **/ bool verify_ok() noexcept; diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 18dbee8f..0b0d33a4 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -47,6 +47,9 @@ namespace xo { /** true iff original alloc has been replaced by a forwarding pointer **/ bool is_forwarding_header(header_type hdr) const noexcept; + /** Retreive bookkeeping info for allocation at @p mem. **/ + AllocInfo alloc_info(value_type mem) const noexcept; + /** Call @p visitor for each memory pool owned by this store **/ void visit_pools(const MemorySizeVisitor & visitor) const; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index e5045aa0..12070bb6 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -551,6 +551,11 @@ namespace xo { return gco_store_.is_forwarding_header(hdr); } + AllocInfo + DX1Collector::alloc_info(value_type mem) const noexcept { + return gco_store_.alloc_info(mem); + } + bool DX1Collector::is_type_installed(typeseq tseq) const noexcept { @@ -564,24 +569,6 @@ namespace xo { return slot.is_occupied(); } - AllocInfo - DX1Collector::alloc_info(value_type mem) const noexcept { - for (role ri : role::all()) { - for (Generation gj{0}; gj < config_.n_generation_; ++gj) { - const DArena * arena = this->get_space(ri, gj); - - assert(arena); - - if (arena->contains(mem)) { - return arena->alloc_info(mem); - } - } - } - - // deliberately attempt on nursery to-space, to capture error info + return sentinel - return this->get_space(role::to_space(), Generation{0})->alloc_info(mem); - } - bool DX1Collector::verify_ok() noexcept { diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index aa71f8ba..576534fa 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -105,6 +105,24 @@ namespace xo { return config_.arena_config_.header_.is_forwarding_tseq(hdr); } + AllocInfo + GCObjectStore::alloc_info(value_type mem) const noexcept { + for (role ri : role::all()) { + for (Generation gj{0}; gj < config_.n_generation_; ++gj) { + const DArena * arena = this->get_space(ri, gj); + + assert(arena); + + if (arena->contains(mem)) { + return arena->alloc_info(mem); + } + } + } + + // deliberately attempt on nursery to-space, to capture error info + return sentinel + return this->get_space(role::to_space(), Generation{0})->alloc_info(mem); + } + void GCObjectStore::visit_pools(const MemorySizeVisitor & visitor) const { From 08b313f25c4d5ff708dc95bf455ab9228bc08d02 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 2 Apr 2026 21:56:12 -0400 Subject: [PATCH 086/174] xo-gc: refactor: move some aux method impls to GCObjectStore --- include/xo/gc/GCObjectStore.hpp | 23 +++++++++++++++ include/xo/gc/GCObjectStoreConfig.hpp | 32 +++++++++++++++++++++ include/xo/gc/MutationLogConfig.hpp | 20 +++++++++++++- include/xo/gc/X1CollectorConfig.hpp | 7 +++-- src/gc/DX1Collector.cpp | 33 ++++------------------ src/gc/GCObjectStore.cpp | 40 ++++++++++++++++++++++++++- src/gc/GCObjectStoreConfig.cpp | 2 ++ src/gc/MutationLogConfig.cpp | 4 +++ src/gc/MutationLogState.cpp | 4 +-- 9 files changed, 131 insertions(+), 34 deletions(-) diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 0b0d33a4..dff223c3 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -26,6 +26,8 @@ namespace xo { public: explicit GCObjectStore(const GCObjectStoreConfig & cfg); + const GCObjectStoreConfig & config() const noexcept { return config_; } + const DArena * get_space(role r, Generation g) const noexcept { return space_[r][g]; } 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); } @@ -53,6 +55,27 @@ namespace xo { /** Call @p visitor for each memory pool owned by this store **/ void visit_pools(const MemorySizeVisitor & visitor) const; + /** true iff address @p addr allocated from this collector + * in role @p r (according to current GC state) + **/ + bool contains(role r, const void * addr) const noexcept; + + /** true iff address @p addr allocated from this collector and currently live + * in role @p r (according to current GC state) + * + * (i.e. in [lo,free) for an arena) + **/ + bool contains_allocated(role r, const void * addr) const noexcept; + + /** true iff {@p alloc_hdr, @p object_data} should move for + * a collection of all generations strictly younger than @p upto. + * + * Require: runstate_.is_running() + **/ + bool check_move_policy(Generation upto, + header_type alloc_hdr, + void * gco_data) const noexcept; + /** For each generation g in [0 ,.., upto) * swap arenas assigned to {to-space, from-space}. * Invoked once at the beginning of each gc cycle. diff --git a/include/xo/gc/GCObjectStoreConfig.hpp b/include/xo/gc/GCObjectStoreConfig.hpp index 40e4483d..a5512d1e 100644 --- a/include/xo/gc/GCObjectStoreConfig.hpp +++ b/include/xo/gc/GCObjectStoreConfig.hpp @@ -5,6 +5,8 @@ #pragma once +#include "generation.hpp" +#include "object_age.hpp" #include namespace xo { @@ -15,8 +17,30 @@ namespace xo { public: GCObjectStoreConfig(const ArenaConfig & arena_cfg, std::uint32_t ngen, + std::uint32_t nsurvive, bool debug_flag); + /** generation that would contain an object that has survived + * @p age collections. Equals the number of times object + * has been promoted. + * + * Must be consistent + **/ + Generation age2gen(object_age age) const noexcept { + return Generation(age % n_survive_threshold_); + } + + /** age threshold for promotion to generation @p g **/ + uint32_t promotion_threshold(Generation g) const noexcept { + + // TODO: may consider replacing with table-lookup + // Require: if two distinct ages promote to some gen g at the same time, + // then they also promote to gen g+k at the same time for all k>0. + + return g * n_survive_threshold_; + } + + public: /** Configuration for collector spaces. * Will have (2 x G) of these, @@ -27,8 +51,16 @@ namespace xo { * - arena_config_.store_header_flag_ must be true **/ ArenaConfig arena_config_; + /** number of generations in use. Same as @ref X1CollectorConfig::n_generation_ **/ std::uint32_t n_generation_ = 0; + + /** Number of promotion steps. + * An object that survives this number of collections + * advances to the next generation. + **/ + std::uint32_t n_survive_threshold_ = 2; + /** true to enable debug logging **/ bool debug_flag_ = false; }; diff --git a/include/xo/gc/MutationLogConfig.hpp b/include/xo/gc/MutationLogConfig.hpp index 89d7041d..b499045a 100644 --- a/include/xo/gc/MutationLogConfig.hpp +++ b/include/xo/gc/MutationLogConfig.hpp @@ -5,6 +5,7 @@ #pragma once +#include "object_age.hpp" #include "generation.hpp" #include #include @@ -16,10 +17,25 @@ namespace xo { class MutationLogConfig { public: MutationLogConfig(std::uint32_t ngen, +#ifdef OBSOLETE // in GCObjectStore std::uint32_t survive, +#endif std::size_t mlog_z, bool debug_flag); +#ifdef OBSOLETE + /** generation that would contain an object that has survived + * @p age collections. Equals the number of times object + * has been promoted. + * + * Must be consistent + **/ + Generation age2gen(object_age age) const noexcept { + return Generation(age % n_survive_threshold_); + } +#endif + +#ifdef OBSOLETE /** age threshold for promotion to generation @p g **/ uint32_t promotion_threshold(Generation g) const noexcept { @@ -29,7 +45,7 @@ namespace xo { return g * n_survive_threshold_; } - +#endif public: /** number of generations in use. @@ -37,11 +53,13 @@ namespace xo { **/ std::uint32_t n_generation_ = 0; +#ifdef OBSOLETE /** Number of promotion steps. * An object that survives this number of collections * advances to the next generation. **/ uint32_t n_survive_threshold_ = 2; +#endif /** storage for xgen pointer bookkeeping (aka remembered sets). * Use 3x this value per generation diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp index 03a4b11c..3e46b67e 100644 --- a/include/xo/gc/X1CollectorConfig.hpp +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -42,24 +42,27 @@ namespace xo { GCObjectStoreConfig gco_store_config() const noexcept { return GCObjectStoreConfig(arena_config_, n_generation_, + n_survive_threshold_, debug_flag_); } /** fetch configuration for mutation log store **/ MutationLogConfig mlog_config() const noexcept { return MutationLogConfig(n_generation_, +#ifdef OBSOLETE n_survive_threshold_, +#endif mutation_log_z_, debug_flag_); } Generation age2gen(object_age age) const noexcept { - return Generation(age % n_survive_threshold_); + return this->gco_store_config().age2gen(age); } /** age threshold for promotion to generation @p g **/ uint32_t promotion_threshold(Generation g) const noexcept { - return mlog_config().promotion_threshold(g); + return this->gco_store_config().promotion_threshold(g); } public: diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 12070bb6..2df998f4 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -124,18 +124,13 @@ namespace xo { bool DX1Collector::contains(role r, const void * addr) const noexcept { - return !(this->generation_of(r, addr).is_sentinel()); + return gco_store_.contains(r, addr); } bool DX1Collector::contains_allocated(role r, const void * addr) const noexcept { - Generation g = this->generation_of(r, addr); - - if (g.is_sentinel()) - return false; - - return this->get_space(r, g)->contains_allocated(addr); + return gco_store_.contains_allocated(r, addr); } Generation @@ -212,16 +207,6 @@ namespace xo { DX1Collector::mutation_log_entries() const noexcept { return mlog_state_.mutation_log_entries(); - -#ifdef MOVED - size_type z = 0; - - for (Generation gj{0}; gj + 1 < config_.n_generation_; ++gj) { - z += mlog_[role::to_space()][gj]->size(); - } - - return z; -#endif } namespace { @@ -1397,19 +1382,11 @@ namespace xo { DX1Collector::check_move_policy(header_type alloc_hdr, void * object_data) const noexcept { - (void)object_data; - - // when gc is moving objects, to- and from- spaces have been - // reversed: forwarding pointers are located in from-space and - // refer to to-space. - - object_age age = this->header2age(alloc_hdr); - - Generation g = config_.age2gen(age); - assert(runstate_.is_running()); - return (g < runstate_.gc_upto()); + return gco_store_.check_move_policy(runstate_.gc_upto(), + alloc_hdr, + object_data); } auto diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 576534fa..3ba98e84 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -133,10 +133,48 @@ namespace xo { } } + bool + GCObjectStore::contains(role r, const void * addr) const noexcept + { + return !(this->generation_of(r, addr).is_sentinel()); + } + + bool + GCObjectStore::contains_allocated(role r, const void * addr) const noexcept + { + Generation g = this->generation_of(r, addr); + + if (g.is_sentinel()) + return false; + + return this->get_space(r, g)->contains_allocated(addr); + } + + bool + GCObjectStore::check_move_policy(Generation upto, + header_type alloc_hdr, + void * object_data) const noexcept + { + (void)object_data; + + // when gc is moving objects, to- and from- spaces have been + // reversed: forwarding pointers are located in from-space and + // refer to to-space. + + object_age age = this->header2age(alloc_hdr); + + Generation g = config_.age2gen(age); + + //assert(runstate_.is_running()); + + return (g < upto); + } + void GCObjectStore::swap_roles(Generation upto) noexcept { - scope log(XO_DEBUG(config_.debug_flag_), 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)); diff --git a/src/gc/GCObjectStoreConfig.cpp b/src/gc/GCObjectStoreConfig.cpp index 1d765a8b..560d7c35 100644 --- a/src/gc/GCObjectStoreConfig.cpp +++ b/src/gc/GCObjectStoreConfig.cpp @@ -10,9 +10,11 @@ namespace xo { GCObjectStoreConfig::GCObjectStoreConfig(const ArenaConfig & arena_cfg, std::uint32_t ngen, + std::uint32_t nsurvive, bool debug_flag) : arena_config_{arena_cfg}, n_generation_{ngen}, + n_survive_threshold_{nsurvive}, debug_flag_{debug_flag} {} diff --git a/src/gc/MutationLogConfig.cpp b/src/gc/MutationLogConfig.cpp index ff2af63f..ed18a17f 100644 --- a/src/gc/MutationLogConfig.cpp +++ b/src/gc/MutationLogConfig.cpp @@ -9,11 +9,15 @@ namespace xo { namespace mm { MutationLogConfig::MutationLogConfig(std::uint32_t ngen, +#ifdef OBSOLETE std::uint32_t survive, +#endif std::size_t mlog_z, bool debug_flag) : n_generation_{ngen}, +#ifdef OBSOLETE n_survive_threshold_{survive}, +#endif mutation_log_z_{mlog_z}, debug_flag_{debug_flag} {} diff --git a/src/gc/MutationLogState.cpp b/src/gc/MutationLogState.cpp index b6198f2c..33c1ca74 100644 --- a/src/gc/MutationLogState.cpp +++ b/src/gc/MutationLogState.cpp @@ -426,8 +426,8 @@ namespace xo { bool need_mlog_entry = ((child_gen_to + 1 < config_.n_generation_) - && (config_.promotion_threshold(parent_gen_to) - > config_.promotion_threshold(child_gen_to))); + && (gco_store.config().promotion_threshold(parent_gen_to) + > gco_store.config().promotion_threshold(child_gen_to))); if (need_mlog_entry) { // 1. P->C pointer is still cross-age (xage), and From 63857a302596ce15f9846351ba47dfbc093fa8cd Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 2 Apr 2026 23:58:22 -0400 Subject: [PATCH 087/174] xo-gc: refactor: move object type table -> GCObjectStore --- include/xo/gc/DX1Collector.hpp | 71 +---- include/xo/gc/GCObjectStore.hpp | 66 ++++- include/xo/gc/GCObjectStoreConfig.hpp | 4 + include/xo/gc/ObjectTypeSlot.hpp | 65 +++++ include/xo/gc/X1CollectorConfig.hpp | 6 +- src/gc/DX1Collector.cpp | 96 +++---- src/gc/GCObjectStore.cpp | 362 +++++++++++++++++++++++++- src/gc/GCObjectStoreConfig.cpp | 2 + 8 files changed, 548 insertions(+), 124 deletions(-) create mode 100644 include/xo/gc/ObjectTypeSlot.hpp diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 9a0f2811..6e7720ee 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -6,7 +6,8 @@ #pragma once #include "X1CollectorConfig.hpp" -#include "GCObject.hpp" +//#include "ObjectTypeSlot.hpp" +//#include "GCObject.hpp" #include "GCObjectStore.hpp" #include "MutationLogState.hpp" #include "X1VerifyStats.hpp" @@ -82,55 +83,6 @@ namespace xo { obj * root_ = nullptr; }; - /** @brief Object Interface - * - * GC-object interface for a particular type. - * X1 maintains a table of these (X1Collector::object_types_) - * indexed by typeseq. - * - * Using a wrapper here for searchability - **/ - struct ObjectTypeSlot { - ObjectTypeSlot() {} - explicit ObjectTypeSlot(AGCObject * iface) { - this->store_iface(iface); - } - - /** true iff this slot is empty **/ - bool is_null() const noexcept { - return this->_iface()->_has_null_vptr(); - } - - bool is_occupied() const noexcept { - return !this->is_null(); - } - - AGCObject * _iface() const noexcept { - return std::launder((AGCObject *)&iface_[0]); - } - - AGCObject * iface() const noexcept { - AGCObject * x = this->_iface(); - - return (x->_has_null_vptr() ? nullptr : x); - } - - /** Store interface pointer @p iface. - * We just want the vtable here - **/ - void store_iface(const AGCObject * iface) { - ::memcpy((void*)&(this->iface_[0]), (void*)iface, sizeof(AGCObject)); - } - - private: - /** runtime interface for this object. - * We might prefer to declare this as AGCObject, but that's prohibited - * since AGCObject has abstract methods. - * Main downside of this form is it makes the data unintelligible to debugger. - **/ - alignas(AGCObject) std::byte iface_[sizeof(AGCObject)]; - }; - // ----- DX1Collector ----- /** @brief garbage collector 'X1' @@ -165,7 +117,7 @@ namespace xo { std::string_view name() const noexcept { return config_.name_; } GCRunState runstate() const noexcept { return runstate_; } - const ObjectTypeTable * get_object_types() const noexcept { return &object_types_; } + 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); } DArena * get_space(role r, Generation g) noexcept { return gco_store_.get_space(r, g); } @@ -204,7 +156,8 @@ namespace xo { /** Report gc statistics as a dictionary. * Providing for the same of making GC statistics visible to schematika programs * - * @p mm allocate stats dictionary from this allocator. May be the same as this collector. + * @p mm allocate stats dictionary from this allocator. + * May be the same as this collector. * @p error_mm Allocator for last-report error reporting when out-of-memory. * @p p_output on exit @p *p_output contains stats dictionary **/ @@ -215,7 +168,8 @@ namespace xo { /** Report per-object-type information as a dictionary. * Scans to-space to count per-object-type information * - * @p mm allocate stats dictionary from this allocator. May be the same as this collector. + * @p mm allocate stats dictionary from this allocator. + * May be the same as this collector. * @p error_mm Allocator for last-report error reporting when out-of-memory. * @p p_output on exit @p *p_output contains stats dictionary **/ @@ -409,8 +363,10 @@ namespace xo { void clear() noexcept; private: +#ifdef OBSOLETE /** aux init function: initialize @ref object_types_ arena **/ void _init_object_types(const X1CollectorConfig & cfg, std::size_t page_z); +#endif /** aux init function: initialize @ref roots_ arena **/ void _init_gc_roots(const X1CollectorConfig & cfg, std::size_t page_z); /** aux init function: initialize @ref mlog_storage_[][] arenas **/ @@ -443,7 +399,8 @@ namespace xo { /** traverse objects allocated after @p ckp, to make sure their children * are forwarded. Repeat until traverse doesn't find any unforwarded children **/ - void _forward_children_until_fixpoint(Generation upto, GCMoveCheckpoint ckp); + void _forward_children_until_fixpoint(Generation upto, + GCMoveCheckpoint ckp); /** Evacuate object at @p *lhs_data to to-space. * Replace original with forwarding pointer to new location **/ @@ -460,10 +417,10 @@ namespace xo { /** current gc state **/ GCRunState runstate_; - /** (ab)using arena to get an extensible array of object types. - * For each type need to store one (8-byte) IGCObject_Any instance, - **/ +#ifdef MARKED + /** gc-aware object types **/ ObjectTypeTable object_types_; +#endif /** gc disabled whenever gc_blocked_ > 0 **/ uint32_t gc_blocked_ = 0; diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index dff223c3..b1d96796 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -6,34 +6,51 @@ #pragma once #include "GCObjectStoreConfig.hpp" +#include "ObjectTypeSlot.hpp" #include "generation.hpp" #include "object_age.hpp" #include -//#include #include namespace xo { namespace mm { + class DX1Collector; /** @brief container to hold gc-aware objects for X1 collector **/ class GCObjectStore { public: + using ObjectTypeTable = DArenaVector; + /* TODO: AllocIterator pointing to free pointer instead of std::byte* */ + using GCMoveCheckpoint = std::array; using header_type = DArena::header_type; using value_type = DArena::value_type; using size_type = DArena::size_type; + using typeseq = xo::reflect::typeseq; public: explicit GCObjectStore(const GCObjectStoreConfig & cfg); 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]; } 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}); } + /** true iff type with id @p tseq has known metadata + * (i.e. has appeared in preceding call to install_type + * for this collector) + **/ + bool is_type_installed(typeseq tseq) const noexcept; + + /** lookup interface from type sequence + * (can use tseq = typeseq::id() for type T) + **/ + const AGCObject * lookup_type(typeseq tseq) const noexcept; + /** generation to which pointer @p addr belongs, given role @p r; * sentinel if not found in this collector **/ @@ -67,14 +84,48 @@ namespace xo { **/ bool contains_allocated(role r, const void * addr) const noexcept; + /** Report per-age-bucket information as an array of dictionaries. + * Scans to-space to count per-age statistics. + * Each dictionary has keys "n-live" and "bytes". + * Array index corresponds to object age. + * + * @p mm allocate stats from this allocator. + * @p error_mm allocator for error reporting when out-of-memory. + * @p p_output on exit @p *p_output contains stats array + **/ + bool report_object_types(obj mm, + obj error_mm, + obj * p_output) const noexcept; + + /** snap checkpoint containing allocator state + * use to detect forwarding activity after visiting objects + **/ + GCMoveCheckpoint snap_move_checkpoint(Generation upto); + + /** Register object type with this collector. + * Provides shallow copy and pointer forwarding for instances of this + * type. + **/ + bool install_type(const AGCObject & meta) noexcept; + + /** traverse objects allocated after @p ckp, to make sure their children + * are forwarded. Repeat until traverse doesn't find any unforwarded children. + * + * 1. Breadth-first implementation, bad for memory locality + * 2. Need @p gc for per-object-type forward_children api + **/ + void _forward_children_until_fixpoint(DX1Collector * gc, + Generation upto, + const GCMoveCheckpoint & gray_lo_v); + /** true iff {@p alloc_hdr, @p object_data} should move for * a collection of all generations strictly younger than @p upto. * * Require: runstate_.is_running() **/ - bool check_move_policy(Generation upto, - header_type alloc_hdr, - void * gco_data) const noexcept; + bool _check_move_policy(header_type alloc_hdr, + void * gco_data, + Generation upto) const noexcept; /** For each generation g in [0 ,.., upto) * swap arenas assigned to {to-space, from-space}. @@ -92,6 +143,10 @@ namespace xo { bool sanitize_flag); private: + + /** configure @ref object_types_, using @p page_z **/ + void _init_object_types(std::size_t page_z); + /** auxiliary init function **/ void _init_space(); @@ -99,6 +154,9 @@ namespace xo { /** configuration for gc-aware object store **/ GCObjectStoreConfig config_; + /** gc-aware object types **/ + ObjectTypeTable object_types_; + /** arena objects for collector managed memory * 1:1 with roles, but polarity reverses for each collection **/ diff --git a/include/xo/gc/GCObjectStoreConfig.hpp b/include/xo/gc/GCObjectStoreConfig.hpp index a5512d1e..08cfd370 100644 --- a/include/xo/gc/GCObjectStoreConfig.hpp +++ b/include/xo/gc/GCObjectStoreConfig.hpp @@ -18,6 +18,7 @@ namespace xo { GCObjectStoreConfig(const ArenaConfig & arena_cfg, std::uint32_t ngen, std::uint32_t nsurvive, + std::size_t object_types_z, bool debug_flag); /** generation that would contain an object that has survived @@ -61,6 +62,9 @@ namespace xo { **/ std::uint32_t n_survive_threshold_ = 2; + /** storage for N object types requires 8*N bytes **/ + std::size_t object_types_z_ = 2*1024*1024; + /** true to enable debug logging **/ bool debug_flag_ = false; }; diff --git a/include/xo/gc/ObjectTypeSlot.hpp b/include/xo/gc/ObjectTypeSlot.hpp new file mode 100644 index 00000000..214d31c2 --- /dev/null +++ b/include/xo/gc/ObjectTypeSlot.hpp @@ -0,0 +1,65 @@ +/** @file ObjectTypeSlot.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include "GCObject.hpp" + +namespace xo { + namespace mm { + + /** @brief Object Interface + * + * GC-object interface for a particular type. + * X1 maintains a table of these (X1Collector::object_types_) + * indexed by typeseq. + * + * Using a wrapper here for searchability + **/ + struct ObjectTypeSlot { + ObjectTypeSlot() {} + explicit ObjectTypeSlot(AGCObject * iface) { + this->store_iface(iface); + } + + /** true iff this slot is empty **/ + bool is_null() const noexcept { + return this->_iface()->_has_null_vptr(); + } + + bool is_occupied() const noexcept { + return !this->is_null(); + } + + AGCObject * _iface() const noexcept { + return std::launder((AGCObject *)&iface_[0]); + } + + AGCObject * iface() const noexcept { + AGCObject * x = this->_iface(); + + return (x->_has_null_vptr() ? nullptr : x); + } + + /** Store interface pointer @p iface. + * We just want the vtable here + **/ + void store_iface(const AGCObject * iface) { + ::memcpy((void*)&(this->iface_[0]), (void*)iface, sizeof(AGCObject)); + } + + private: + /** runtime interface for this object. + * We might prefer to declare this as AGCObject, but that's prohibited + * since AGCObject has abstract methods. + * Main downside of this form is it makes the data unintelligible to debugger. + **/ + alignas(AGCObject) std::byte iface_[sizeof(AGCObject)]; + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end ObjectTypeSlot.hpp */ diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp index 3e46b67e..6ea9562b 100644 --- a/include/xo/gc/X1CollectorConfig.hpp +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -15,6 +15,8 @@ namespace xo { namespace mm { + /** @brief configuration for X1 collector + **/ struct X1CollectorConfig { using size_type = std::size_t; @@ -43,15 +45,13 @@ namespace xo { return GCObjectStoreConfig(arena_config_, n_generation_, n_survive_threshold_, + object_types_z_, debug_flag_); } /** fetch configuration for mutation log store **/ MutationLogConfig mlog_config() const noexcept { return MutationLogConfig(n_generation_, -#ifdef OBSOLETE - n_survive_threshold_, -#endif mutation_log_z_, debug_flag_); } diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 2df998f4..e09fb100 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -5,11 +5,13 @@ #include "X1Collector.hpp" #include + #include #include #include #include #include + #include #include #include @@ -76,14 +78,18 @@ namespace xo { size_t page_z = getpagesize(); - this->_init_object_types(cfg, page_z); + //this->_init_object_types(cfg, page_z); this->_init_gc_roots(cfg, page_z); this->_init_mlogs(page_z); } +#ifdef OBSOLETE // called from GCObjectStore ctor void DX1Collector::_init_object_types(const X1CollectorConfig & cfg, std::size_t page_z) { + gco_state_._init_object_types(); + +#ifdef MOVED /* 1MB reserved address space enough for up to 128k distinct types. * In this case don't want to use hugepages since actual #of types * likely << .size/8 @@ -93,7 +99,9 @@ namespace xo { .size_ = cfg.object_types_z_, .hugepage_z_ = page_z, .store_header_flag_ = false}); +#endif } +#endif void DX1Collector::_init_gc_roots(const X1CollectorConfig & cfg, std::size_t page_z) @@ -114,7 +122,7 @@ namespace xo { void DX1Collector::visit_pools(const MemorySizeVisitor & visitor) const { - object_types_.visit_pools(visitor); + //object_types_.visit_pools(visitor); root_set_.visit_pools(visitor); gco_store_.visit_pools(visitor); @@ -154,7 +162,8 @@ namespace xo { accumulate_total_aux(const DX1Collector & d, size_t (DArena::* get_stat_fn)() const) noexcept { - size_t z1 = (d.object_types_.store()->*get_stat_fn)(); + //size_t z1 = (d.object_types_.store()->*get_stat_fn)(); + size_t z1 = (d.gco_store_.get_object_types()->store()->*get_stat_fn)(); size_t z2 = (d.root_set_.store()->*get_stat_fn)(); size_t z3 = 0; @@ -325,6 +334,9 @@ namespace xo { obj error_mm, obj * p_output) const noexcept { + return gco_store_.report_object_types(mm, error_mm, p_output); + +#ifdef MOVED scope log(XO_DEBUG(true)); (void)error_mm; @@ -429,6 +441,7 @@ namespace xo { *p_output = obj(final_stats_v); return ok; +#endif } bool @@ -436,6 +449,8 @@ namespace xo { obj error_mm, obj * p_output) const noexcept { + //return gco_store_.report_object_ages(mm, error_mm, p_output); + scope log(XO_DEBUG(true)); (void)error_mm; @@ -544,14 +559,7 @@ namespace xo { bool DX1Collector::is_type_installed(typeseq tseq) const noexcept { - if (tseq.is_sentinel() - || static_cast(tseq.seqno()) > object_types_.size()) { - return false; - } - - const ObjectTypeSlot & slot = object_types_[tseq.seqno()]; - - return slot.is_occupied(); + return gco_store_.is_type_installed(tseq); } bool @@ -661,42 +669,16 @@ namespace xo { const AGCObject * DX1Collector::lookup_type(typeseq tseq) const noexcept { - scope log(XO_DEBUG(false)); - - if (tseq.is_sentinel() - || static_cast(tseq.seqno()) > object_types_.size()) { - - log.retroactively_enable("out-of-bounds", - xtag("tseq", tseq), xtag("tname", TypeRegistry::id2name(tseq))); - - log(xtag("types.size", object_types_.size()), - xtag("types.allocated", object_types_.store()->allocated()), - xtag("types.committed", object_types_.store()->committed()), - xtag("types.lo", object_types_.store()->lo_), - xtag("types.limit", object_types_.store()->limit_), - xtag("types.hi", object_types_.store()->hi_)); - - assert(false); - return nullptr; - } - - const ObjectTypeSlot & slot = object_types_[tseq.seqno()]; - - if (slot.is_null()) { - log.retroactively_enable("null-vtable", - xtag("tseq", tseq), xtag("tname", TypeRegistry::id2name(tseq))); - - assert(false); - return nullptr; - } - - return slot.iface(); + return gco_store_.lookup_type(tseq); } /* editor bait: register_type */ bool DX1Collector::install_type(const AGCObject & meta) noexcept { + return gco_store_.install_type(meta); + +#ifdef MARKED typeseq tseq = meta._typeseq(); assert(tseq.seqno() > 0); @@ -715,6 +697,7 @@ namespace xo { slot.store_iface(&meta); return true; +#endif } void @@ -916,15 +899,15 @@ namespace xo { DX1Collector::_deep_move_gc_owned(void * from_src, Generation upto) { - scope log(XO_DEBUG(config_.debug_flag_)); + scope log(XO_DEBUG(gco_store_.config().debug_flag_)); - AllocInfo info = this->alloc_info((std::byte *)from_src); + AllocInfo info = gco_store_.alloc_info((std::byte *)from_src); AllocHeader hdr = info.header(); typeseq tseq(info.tseq()); - assert(this->contains_allocated(role::from_space(), from_src)); + assert(gco_store_.contains_allocated(role::from_space(), from_src)); - if (is_forwarding_header(hdr)) { + if (gco_store_.is_forwarding_header(hdr)) { /* already forwarded - pickup destination * * Coordinates with forward_inplace() @@ -936,7 +919,7 @@ namespace xo { /* here: object at from_src not already forwarded */ - if (!this->check_move_policy(hdr, from_src)) { + if (!gco_store_._check_move_policy(hdr, from_src, upto)) { /* object at from_src is in generation that is not being collected */ log && log("disposition: not moving from_src"); @@ -946,7 +929,7 @@ namespace xo { log && log("disposition: move subtree"); /* TODO: AllocIterator pointing to free pointer */ - GCMoveCheckpoint gray_lo_v = this->_snap_move_checkpoint(upto); + GCMoveCheckpoint gray_lo_v = gco_store_.snap_move_checkpoint(upto); obj alloc(this); const AGCObject * iface = lookup_type(tseq); @@ -965,19 +948,18 @@ namespace xo { auto DX1Collector::_snap_move_checkpoint(Generation upto) -> GCMoveCheckpoint { - GCMoveCheckpoint gray_lo_v; - - for (uint32_t g = 0; g < upto; ++g) { - gray_lo_v[g] = this->to_space(Generation{g})->free_; - } - - return gray_lo_v; + return gco_store_.snap_move_checkpoint(upto); } void DX1Collector::_forward_children_until_fixpoint(Generation upto, GCMoveCheckpoint gray_lo_v) { + // problem -- need object type lookup +#ifdef NOT_YET + gco_store_._forward_children_until_fixpoint(upto, gray_lo_v); +#endif + scope log(XO_DEBUG(config_.debug_flag_)); /** @@ -1384,9 +1366,9 @@ namespace xo { { assert(runstate_.is_running()); - return gco_store_.check_move_policy(runstate_.gc_upto(), - alloc_hdr, - object_data); + return gco_store_._check_move_policy(alloc_hdr, + object_data, + runstate_.gc_upto()); } auto diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 3ba98e84..2b118825 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -4,10 +4,30 @@ **/ #include "GCObjectStore.hpp" + +#include +#include +#include +#include +#include + +#include + +#include #include #include +#include // for ::getpagesize() namespace xo { + using xo::scm::DDictionary; + using xo::scm::DArray; + using xo::scm::DString; + using xo::scm::DInteger; + + using xo::mm::DArena; + using xo::facet::TypeRegistry; + using xo::reflect::typeseq; + namespace mm { GCObjectStore::GCObjectStore(const GCObjectStoreConfig & cfg) @@ -17,9 +37,26 @@ namespace xo { config_.arena_config_.header_.age_bits_ + config_.arena_config_.header_.tseq_bits_ <= 64); + size_t page_z = getpagesize(); + + this->_init_object_types(page_z); this->_init_space(); } + void + GCObjectStore::_init_object_types(std::size_t page_z) + { + /* 1MB reserved address space enough for up to 128k distinct types. + * In this case don't want to use hugepages since actual #of types + * likely << .size/8 + */ + this->object_types_ + = ObjectTypeTable::map(ArenaConfig{.name_ = "x1-object-types", + .size_ = config_.object_types_z_, + .hugepage_z_ = page_z, + .store_header_flag_ = false}); + } + void GCObjectStore::_init_space() { @@ -59,6 +96,57 @@ namespace xo { } } + bool + GCObjectStore::is_type_installed(typeseq tseq) const noexcept + { + if (tseq.is_sentinel() + || static_cast(tseq.seqno()) > object_types_.size()) { + return false; + } + + const ObjectTypeSlot & slot = object_types_[tseq.seqno()]; + + return slot.is_occupied(); + } + + const AGCObject * + GCObjectStore::lookup_type(typeseq tseq) const noexcept + { + scope log(XO_DEBUG(false)); + + if (tseq.is_sentinel() + || (static_cast(tseq.seqno()) + > object_types_.size())) { + + log.retroactively_enable("out-of-bounds", + xtag("tseq", tseq), + xtag("tname", TypeRegistry::id2name(tseq))); + + log(xtag("types.size", object_types_.size()), + xtag("types.allocated", object_types_.store()->allocated()), + xtag("types.committed", object_types_.store()->committed()), + xtag("types.lo", object_types_.store()->lo_), + xtag("types.limit", object_types_.store()->limit_), + xtag("types.hi", object_types_.store()->hi_)); + + assert(false); + return nullptr; + } + + const ObjectTypeSlot & slot = object_types_[tseq.seqno()]; + + if (slot.is_null()) { + log.retroactively_enable("null-vtable", + xtag("tseq", tseq), + xtag("tname", TypeRegistry::id2name(tseq))); + + assert(false); + return nullptr; + } + + return slot.iface(); + } + Generation GCObjectStore::generation_of(role r, const void * addr) const noexcept { @@ -131,6 +219,8 @@ namespace xo { space_storage_[i][j].visit_pools(visitor); } } + + object_types_.visit_pools(visitor); } bool @@ -151,9 +241,120 @@ namespace xo { } bool - GCObjectStore::check_move_policy(Generation upto, - header_type alloc_hdr, - void * object_data) const noexcept + GCObjectStore::report_object_types(obj mm, + obj error_mm, + obj * p_output) const noexcept + { + scope log(XO_DEBUG(true)); + + (void)error_mm; + + bool ok = true; + + // stats, indexed by tseq + // could use c++ vector in scratch space instead of running on + // boxed types. + // + DArray * stats_v = DArray::empty(mm, object_types_.size()); + + if (!stats_v) + return false; + + stats_v->resize(stats_v->capacity()); + + log && log(xtag("object_types_.size", object_types_.size()), + xtag("stats_v.capacity", stats_v->capacity()), + xtag("stats_v.size", stats_v->size())); + + // count #of occupied type slots + std::uint32_t n_tseq_present = 0; + // largest tseq present with non-null AGCObject* iface + std::int32_t max_tseq = 0; + + for (const ObjectTypeSlot & slot : object_types_) { + AGCObject * iface = slot.iface(); + + if (iface) { + typeseq tseq = iface->_typeseq(); + + ++n_tseq_present; + if (max_tseq < tseq.seqno()) + max_tseq = tseq.seqno(); + + assert(tseq.seqno() >= 0); + + auto tname_sv = TypeRegistry::id2name(tseq); + DString * tname = DString::from_view(mm, tname_sv); + + DDictionary * recd = DDictionary::make(mm); + + if (!recd) + return false; + + recd->upsert_cstr(mm, "name", obj(tname)); + recd->upsert_cstr(mm, "tseq", DInteger::box(mm, tseq.seqno())); + recd->upsert_cstr(mm, "n-live", DInteger::box(mm, 0)); + recd->upsert_cstr(mm, "bytes", DInteger::box(mm, 0)); + + stats_v->assign_at(tseq.seqno(), obj(recd)); + } + } + + // scan to-space, count objects by type + + for (Generation g{0}; g < config_.n_generation_; ++g) { + const DArena * arena = this->get_space(role::to_space(), g); + + for (AllocInfo info : *arena) { + if (info.is_forwarding_tseq()) { + assert(false); + return false; + } + + uint32_t ix = info.tseq(); + size_t z = info.size(); + + auto recd = obj::from(stats_v->at(ix)); + + assert(recd); + + auto n_live_opt = recd->lookup_cstr("n-live"); + assert(n_live_opt); + auto bytes_opt = recd->lookup_cstr("bytes"); + assert(bytes_opt); + + if (n_live_opt && bytes_opt) { + auto n_live_gco = obj::from(n_live_opt.value()); + auto bytes_gco = obj::from(bytes_opt.value()); + + n_live_gco->assign_value(n_live_gco->value() + 1); + bytes_gco->assign_value(bytes_gco->value() + z); + } + } + } + + stats_v->resize(max_tseq + 1); + + DArray * final_stats_v = DArray::empty(mm, n_tseq_present); + + for (std::size_t i = 0, n = stats_v->size(); i < n; ++i) { + auto recd = stats_v->at(i); + + if (recd) { + bool ok = final_stats_v->push_back(recd); + assert(ok); + } + } + + *p_output = obj(final_stats_v); + + return ok; + } + + bool + GCObjectStore::_check_move_policy(header_type alloc_hdr, + void * object_data, + Generation upto) const noexcept { (void)object_data; @@ -201,6 +402,161 @@ namespace xo { } } + auto + GCObjectStore::snap_move_checkpoint(Generation upto) -> GCMoveCheckpoint + { + GCMoveCheckpoint gray_lo_v; + + for (uint32_t g = 0; g < upto; ++g) { + gray_lo_v[g] = this->to_space(Generation{g})->free_; + } + + return gray_lo_v; + } + + /* editor bait: register_type */ + bool + GCObjectStore::install_type(const AGCObject & meta) noexcept + { + typeseq tseq = meta._typeseq(); + + assert(tseq.seqno() > 0); + + auto ix = static_cast(tseq.seqno()); + + if (ix >= object_types_.size()) { + if (!object_types_.resize(std::max(2 * object_types_.size(), ix + 1))) + return false; + } + + assert(ix < object_types_.size()); + + ObjectTypeSlot & slot = object_types_[ix]; + + slot.store_iface(&meta); + + return true; + } + +#ifdef MARKED + void + GCObjectStore::_forward_children_until_fixpoint(DX1Collector * gc, + Generation upto, + const GCMoveCheckpoint & gray_lo_v) + { + scope log(XO_DEBUG(config_.debug_flag_)); + + /** + * To-space: + * + * to_lo = start of to-space + * w,W = white objects. An object x is white if x + * + all immediate children of x are in to-space + * (also implies this GC cycle put it there) + * g,G = grey objects. An object x is gray if it's in to-space, + * but possibly has >0 black children + * _ = free to-space memory + * N = nursery space (generation{0}) + * T = tenured space (generation{1}) + * + * wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________... + * ^ ^ ^ + * to_lo grey_lo(N) free_ptr(N) + * + * After moving children of one object, + * advancing {nursery_grey_lo, nursery_free_ptr} + * + * wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG______... + * ^ ^ ^ + * to_lo grey_lo(N) free_ptr(N) + * + * Invariant: + * + * objects in [to_lo, gray_lo) are white. + * all gray objects are in [gray_lo, free_ptr) + * memory starting at free_ptr is free. + * + * deep_move terminates when gray_lo catches up to free_ptr + * + * Above is simplified. Complication is that GC (including incremental) may + * promote objects from nursery (N) to tenured (T) + * + * So more accurate before/after picture + * + * N wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________... + * ^ ^ ^ + * to_lo(N) grey_lo(N) free_ptr(N) + * + * T wwwwwwwwwwwwwwgggggggggggg_______________________________... + * ^ ^ ^ + * to_lo(T) grey_lo(T) free_ptr(N) + * + * After moving children of one object, + * advancing {nursery_grey_lo, nursery_free_ptr} + * + * N wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG_____... + * ^ ^ ^ + * to_lo(N) grey_lo(N) free_ptr(N) + * + * T wwwwwwwwwwwwwwggggggggggggGGGGG_________________________... + * ^ ^ ^ + * to_lo(T) grey_lo(T) free_ptr(T) + * + * deep_move terminates when both: + * - gray_lo(N) catches up with free_ptr(N) + * - gray_lo(T) catches up with free_ptr(T) + * + **/ + + std::size_t fixup_work = 0; + + /* TODO: + * - loop here is bad for memory locality + * - replace with depth-first traversal + */ + do { + fixup_work = 0; + + for (Generation g = Generation{0}; g < upto; ++g) { + /** object index for this pass **/ + size_t i_obj = 0; + + /* TODO: use AllocIterator here */ + while(gray_lo_v[g] < to_space(g)->free_) { + AllocHeader * hdr = (AllocHeader *)gray_lo_v[g]; + void * src = (hdr + 1); + + const auto & hdr_cfg = config_.arena_config_.header_; + typeseq tseq = typeseq(hdr_cfg.tseq(*hdr)); + size_t z = hdr_cfg.size_with_padding(*hdr); + + log && log("deep_move_gc_owned: fwd to-space children", + xtag("g", g), + xtag("i_obj", i_obj), + xtag("src", src), + xtag("tseq", tseq), + xtag("tname", TypeRegistry::id2name(tseq)), + xtag("z", z)); + + const AGCObject * iface = this->lookup_type(tseq); + + assert(iface->_has_null_vptr() == false); + + auto gc = this->ref(); + + iface->forward_children(src, gc); + + gray_lo_v[g] = ((std::byte *)src) + z; + + ++i_obj; + ++fixup_work; + } + } + } while (fixup_work > 0); + } /*_forward_children_until_fixpoint*/ +#endif + + } /*namespace mm*/ } /*namespace xo*/ diff --git a/src/gc/GCObjectStoreConfig.cpp b/src/gc/GCObjectStoreConfig.cpp index 560d7c35..132b3fb2 100644 --- a/src/gc/GCObjectStoreConfig.cpp +++ b/src/gc/GCObjectStoreConfig.cpp @@ -11,10 +11,12 @@ namespace xo { GCObjectStoreConfig::GCObjectStoreConfig(const ArenaConfig & arena_cfg, std::uint32_t ngen, std::uint32_t nsurvive, + std::size_t object_types_z, bool debug_flag) : arena_config_{arena_cfg}, n_generation_{ngen}, n_survive_threshold_{nsurvive}, + object_types_z_{object_types_z}, debug_flag_{debug_flag} {} From a7dac96be3f2b3c92c3fe6092c09eff2f06696bf Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 00:04:34 -0400 Subject: [PATCH 088/174] xo-alloc2: tidy: generation.hpp -> Generation.hpp --- include/xo/gc/DX1Collector.hpp | 2 +- include/xo/gc/DX1CollectorIterator.hpp | 2 +- include/xo/gc/GCObjectStore.hpp | 2 +- include/xo/gc/GCObjectStoreConfig.hpp | 2 +- include/xo/gc/MutationLogConfig.hpp | 2 +- include/xo/gc/X1CollectorConfig.hpp | 2 +- src/gc/DX1Collector.cpp | 1 - 7 files changed, 6 insertions(+), 7 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 6e7720ee..e7a958c1 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -11,7 +11,7 @@ #include "GCObjectStore.hpp" #include "MutationLogState.hpp" #include "X1VerifyStats.hpp" -#include "generation.hpp" +//#include "generation.hpp" #include "object_age.hpp" #include "role.hpp" #include diff --git a/include/xo/gc/DX1CollectorIterator.hpp b/include/xo/gc/DX1CollectorIterator.hpp index d96d9ab6..da065c1b 100644 --- a/include/xo/gc/DX1CollectorIterator.hpp +++ b/include/xo/gc/DX1CollectorIterator.hpp @@ -6,7 +6,7 @@ #pragma once #include "AllocInfo.hpp" -#include "generation.hpp" +#include "Generation.hpp" #include #include diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index b1d96796..7c4a6cfc 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -7,7 +7,7 @@ #include "GCObjectStoreConfig.hpp" #include "ObjectTypeSlot.hpp" -#include "generation.hpp" +//#include "Generation.hpp" #include "object_age.hpp" #include #include diff --git a/include/xo/gc/GCObjectStoreConfig.hpp b/include/xo/gc/GCObjectStoreConfig.hpp index 08cfd370..9185721f 100644 --- a/include/xo/gc/GCObjectStoreConfig.hpp +++ b/include/xo/gc/GCObjectStoreConfig.hpp @@ -5,7 +5,7 @@ #pragma once -#include "generation.hpp" +#include "Generation.hpp" #include "object_age.hpp" #include diff --git a/include/xo/gc/MutationLogConfig.hpp b/include/xo/gc/MutationLogConfig.hpp index b499045a..e10f228c 100644 --- a/include/xo/gc/MutationLogConfig.hpp +++ b/include/xo/gc/MutationLogConfig.hpp @@ -6,7 +6,7 @@ #pragma once #include "object_age.hpp" -#include "generation.hpp" +#include "Generation.hpp" #include #include diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp index 6ea9562b..38f2d5f3 100644 --- a/include/xo/gc/X1CollectorConfig.hpp +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -8,7 +8,7 @@ #include "GCObjectStoreConfig.hpp" #include "MutationLogConfig.hpp" #include "object_age.hpp" -#include "generation.hpp" +//#include "Generation.hpp" #include #include #include diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index e09fb100..6c30d752 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include "object_age.hpp" #include #include From 7d8061e91b545644b64a1bfc266aabdc99e39b19 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 15:22:52 -0400 Subject: [PATCH 089/174] xo-gc: refactor: rename MutationLogState -> MutationLogStore --- include/xo/gc/DX1Collector.hpp | 21 ++----------- ...ationLogState.hpp => MutationLogStore.hpp} | 8 ++--- src/gc/CMakeLists.txt | 2 +- src/gc/DX1Collector.cpp | 16 +++++----- ...ationLogState.cpp => MutationLogStore.cpp} | 30 +++++++++---------- 5 files changed, 30 insertions(+), 47 deletions(-) rename include/xo/gc/{MutationLogState.hpp => MutationLogStore.hpp} (97%) rename src/gc/{MutationLogState.cpp => MutationLogStore.cpp} (95%) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index e7a958c1..32e4030a 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -6,12 +6,9 @@ #pragma once #include "X1CollectorConfig.hpp" -//#include "ObjectTypeSlot.hpp" -//#include "GCObject.hpp" #include "GCObjectStore.hpp" -#include "MutationLogState.hpp" +#include "MutationLogStore.hpp" #include "X1VerifyStats.hpp" -//#include "generation.hpp" #include "object_age.hpp" #include "role.hpp" #include @@ -354,19 +351,10 @@ namespace xo { // ----- book-keeping ----- -#ifdef OBSOLETE // see swap_roles() - /** reverse to-space and from-space roles for generation g **/ - void reverse_roles(Generation g) noexcept; -#endif - /** discard all allocated memory **/ void clear() noexcept; private: -#ifdef OBSOLETE - /** aux init function: initialize @ref object_types_ arena **/ - void _init_object_types(const X1CollectorConfig & cfg, std::size_t page_z); -#endif /** aux init function: initialize @ref roots_ arena **/ void _init_gc_roots(const X1CollectorConfig & cfg, std::size_t page_z); /** aux init function: initialize @ref mlog_storage_[][] arenas **/ @@ -417,11 +405,6 @@ namespace xo { /** current gc state **/ GCRunState runstate_; -#ifdef MARKED - /** gc-aware object types **/ - ObjectTypeTable object_types_; -#endif - /** gc disabled whenever gc_blocked_ > 0 **/ uint32_t gc_blocked_ = 0; @@ -449,7 +432,7 @@ namespace xo { * {P,C} in same generation, but in fuutre suriving P would * get promoted before C. **/ - MutationLogState mlog_state_; + MutationLogStore mlog_store_; /** Collector-managed memory. **/ diff --git a/include/xo/gc/MutationLogState.hpp b/include/xo/gc/MutationLogStore.hpp similarity index 97% rename from include/xo/gc/MutationLogState.hpp rename to include/xo/gc/MutationLogStore.hpp index 0fb5d624..31d65e01 100644 --- a/include/xo/gc/MutationLogState.hpp +++ b/include/xo/gc/MutationLogStore.hpp @@ -1,4 +1,4 @@ -/** @file MutationLogState.hpp +/** @file MutationLogStore.hpp * * @author Roland Conybeare, Apr 2026 **/ @@ -20,13 +20,13 @@ namespace xo { /** @brief container for X1 collector mutation logs **/ - class MutationLogState { + class MutationLogStore { public: using MutationLog = DArenaVector; using size_type = DArena::size_type; public: - explicit MutationLogState(const MutationLogConfig & config); + explicit MutationLogStore(const MutationLogConfig & config); /** Initialize mlog state * with o/s page size @p page_z @@ -164,4 +164,4 @@ namespace xo { } /*namespace mm*/ } /*namespace xo*/ -/* end MutationLogState.hpp */ +/* end MutationLogStore.hpp */ diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index 42ce4edd..9539bcdc 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -19,7 +19,7 @@ set(SELF_SRCS GCObjectStore.cpp MutationLogConfig.cpp - MutationLogState.cpp + MutationLogStore.cpp MutationLogEntry.cpp diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 6c30d752..b1001f08 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -68,7 +68,7 @@ namespace xo { DX1Collector::DX1Collector(const X1CollectorConfig & cfg) : config_{cfg}, - mlog_state_{cfg.mlog_config()}, + mlog_store_{cfg.mlog_config()}, gco_store_{cfg.gco_store_config()} { assert(config_.arena_config_.header_.size_bits_ + @@ -115,7 +115,7 @@ namespace xo { void DX1Collector::_init_mlogs(std::size_t page_z) { - this->mlog_state_.init_mlogs(page_z); + this->mlog_store_.init_mlogs(page_z); } void @@ -125,7 +125,7 @@ namespace xo { root_set_.visit_pools(visitor); gco_store_.visit_pools(visitor); - mlog_state_.visit_pools(visitor); + mlog_store_.visit_pools(visitor); } bool @@ -214,7 +214,7 @@ namespace xo { size_type DX1Collector::mutation_log_entries() const noexcept { - return mlog_state_.mutation_log_entries(); + return mlog_store_.mutation_log_entries(); } namespace { @@ -653,7 +653,7 @@ namespace xo { } // 4. scan mutation logs - mlog_state_.verify_ok(&gco_store_, + mlog_store_.verify_ok(&gco_store_, &(this->verify_stats_)); } @@ -806,13 +806,13 @@ namespace xo { scope log(XO_DEBUG(true), xtag("upto", upto)); gco_store_.swap_roles(upto); - mlog_state_.swap_roles(upto); + mlog_store_.swap_roles(upto); } void DX1Collector::forward_mutation_log(Generation upto) { - mlog_state_.forward_mutation_log(this, upto); + mlog_store_.forward_mutation_log(this, upto); } void @@ -1480,7 +1480,7 @@ namespace xo { void ** lhs_addr = reinterpret_cast(&(p_lhs->data_)); - mlog_state_.append_mutation(dest_g, parent, lhs_addr, rhs); + mlog_store_.append_mutation(dest_g, parent, lhs_addr, rhs); } /*assign_member*/ DX1CollectorIterator diff --git a/src/gc/MutationLogState.cpp b/src/gc/MutationLogStore.cpp similarity index 95% rename from src/gc/MutationLogState.cpp rename to src/gc/MutationLogStore.cpp index 33c1ca74..563d4bda 100644 --- a/src/gc/MutationLogState.cpp +++ b/src/gc/MutationLogStore.cpp @@ -1,20 +1,20 @@ -/** @file MutationLogState.cpp +/** @file MutationLogStore.cpp * * @author Roland Conybeare, Apr 2026 **/ -#include "MutationLogState.hpp" +#include "MutationLogStore.hpp" #include "DX1Collector.hpp" namespace xo { namespace mm { - MutationLogState::MutationLogState(const MutationLogConfig & config) + MutationLogStore::MutationLogStore(const MutationLogConfig & config) : config_{config} {} void - MutationLogState::init_mlogs(std::size_t page_z) + MutationLogStore::init_mlogs(std::size_t page_z) { assert(c_n_role + 1 == 3); @@ -53,7 +53,7 @@ namespace xo { } auto - MutationLogState::_make_mlog(uint32_t igen, char tag_char, + MutationLogStore::_make_mlog(uint32_t igen, char tag_char, size_t mlog_z, size_t page_z) -> MutationLog { char buf[40]; @@ -66,7 +66,7 @@ namespace xo { } auto - MutationLogState::mutation_log_entries() const noexcept -> size_type + MutationLogStore::mutation_log_entries() const noexcept -> size_type { size_type z = 0; @@ -78,7 +78,7 @@ namespace xo { } void - MutationLogState::visit_pools(const MemorySizeVisitor & visitor) const + MutationLogStore::visit_pools(const MemorySizeVisitor & visitor) const { for (uint32_t j = 0; j + 1 < config_.n_generation_; ++j) { for (uint32_t i = 0; i < c_n_role + 1; ++i) { @@ -88,7 +88,7 @@ namespace xo { } void - MutationLogState::verify_ok(GCObjectStore * gco_store, + MutationLogStore::verify_ok(GCObjectStore * gco_store, VerifyStats * p_verify_stats) noexcept { // 4. scan mutation logs @@ -136,7 +136,7 @@ namespace xo { } /*verify_ok*/ void - MutationLogState::append_mutation(Generation dest_g, + MutationLogStore::append_mutation(Generation dest_g, void * parent, void ** addr, obj rhs) @@ -151,7 +151,7 @@ namespace xo { } void - MutationLogState::swap_roles(Generation upto) noexcept + MutationLogStore::swap_roles(Generation upto) noexcept { scope log(XO_DEBUG(true), xtag("upto", upto)); @@ -163,7 +163,7 @@ namespace xo { } void - MutationLogState::forward_mutation_log(DX1Collector * gc, + MutationLogStore::forward_mutation_log(DX1Collector * gc, Generation upto) { /** non-zero if at least one object was rescued (from any generation) @@ -213,7 +213,7 @@ namespace xo { } MutationLogStatistics - MutationLogState::_forward_mutation_log_phase(DX1Collector * gc, + MutationLogStore::_forward_mutation_log_phase(DX1Collector * gc, Generation upto, Generation child_gen, MutationLog * from_mlog, @@ -363,7 +363,7 @@ namespace xo { } MutationLogStatistics - MutationLogState::_preserve_child_of_live_parent(DX1Collector * gc, + MutationLogStore::_preserve_child_of_live_parent(DX1Collector * gc, Generation upto, Generation parent_gen, const MutationLogEntry & from_entry, @@ -415,7 +415,7 @@ namespace xo { } bool - MutationLogState::_check_keep_mutation_aux(const GCObjectStore & gco_store, + MutationLogStore::_check_keep_mutation_aux(const GCObjectStore & gco_store, const MutationLogEntry & from_entry, Generation parent_gen_to, void * child_to, @@ -451,4 +451,4 @@ namespace xo { } /*namespace mm*/ } /*namespace xo*/ -/* end MutationLogState.cpp */ +/* end MutationLogStore.cpp */ From 1f0aa5f6a675e2aa7ceb9da6299b4f5577571f47 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 15:37:15 -0400 Subject: [PATCH 090/174] xo-gc: refactor: move _shallow_move() to GCObjectStore --- include/xo/gc/DX1Collector.hpp | 2 +- include/xo/gc/GCObjectStore.hpp | 8 ++++++ src/gc/DX1Collector.cpp | 41 ++++--------------------------- src/gc/GCObjectStore.cpp | 43 +++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 37 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 32e4030a..b3d1ba7b 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -280,7 +280,7 @@ namespace xo { /** evacuate object with type @p iface at address @p from_src * to to-space. Return new to-space location. **/ - void * shallow_move(const AGCObject * iface, void * from_src); + void * _shallow_move(const AGCObject * iface, void * from_src); /** true iff {alloc_hdr, object_data} should move for * currently-running collection. diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 7c4a6cfc..7cb4e713 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -108,6 +108,14 @@ namespace xo { **/ bool install_type(const AGCObject & meta) noexcept; + /** during a gc cycle: + * evacuate object @p from_src, with gc-object interface @p iface. + * Shallow: does not traverse children + **/ + void * _shallow_move(obj mm, + const AGCObject * iface, + void * from_src); + /** traverse objects allocated after @p ckp, to make sure their children * are forwarded. Repeat until traverse doesn't find any unforwarded children. * diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index b1001f08..fc4acdc5 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -868,7 +868,7 @@ namespace xo { void * DX1Collector::deep_move_interior(void * from_src, - Generation upto) + Generation upto) { scope log(XO_DEBUG(config_.debug_flag_)); @@ -935,7 +935,7 @@ namespace xo { assert(iface->_has_null_vptr() == false); - void * to_dest = this->shallow_move(iface, from_src); + void * to_dest = this->_shallow_move(iface, from_src); this->_forward_children_until_fixpoint(upto, gray_lo_v); @@ -1261,7 +1261,7 @@ namespace xo { * +----------+ */ - *lhs_data = this->shallow_move(lhs_iface, *lhs_data); + *lhs_data = this->_shallow_move(lhs_iface, *lhs_data); /* * lhs obj (from-space) @@ -1321,42 +1321,11 @@ namespace xo { } void * - DX1Collector::shallow_move(const AGCObject * iface, void * from_src) + DX1Collector::_shallow_move(const AGCObject * iface, void * from_src) { - scope log(XO_DEBUG(config_.debug_flag_)); - - AllocInfo info = this->alloc_info((std::byte *)from_src); obj alloc(this); - void * to_dest = iface->shallow_copy(from_src, alloc); - - log && log(xtag("from_src", from_src), xtag("to_dest", to_dest)); - log && log(xtag("tseq", info.tseq()), - xtag("tname", TypeRegistry::id2name(typeseq(info.tseq()))), - xtag("age", info.age()), xtag("size", info.size())); - - if (config_.sanitize_flag_) { - AllocInfo info_copy = this->alloc_info((std::byte *)to_dest); - - log && log(xtag("age2", info_copy.age()), xtag("size2", info_copy.size())); - - assert((info_copy.age() == config_.arena_config_.header_.max_age()) - || (info_copy.age() == info.age() + 1)); - } - - if(to_dest == from_src) { - assert(false); - } else { - *(const_cast(info.p_header_)) - = AllocHeader(config_ - .arena_config_ - .header_ - .mark_forwarding_tseq(*info.p_header_)); - - *(void **)from_src = to_dest; - } - - return to_dest; + return gco_store_._shallow_move(alloc, iface, from_src); } bool diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 2b118825..13475dd1 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -438,6 +438,49 @@ namespace xo { return true; } + void * + GCObjectStore::_shallow_move(obj mm, + const AGCObject * iface, + void * from_src) + { + scope log(XO_DEBUG(config_.debug_flag_)); + + AllocInfo info = this->alloc_info((std::byte *)from_src); + + //obj gc_gco(gc); + + void * to_dest = iface->shallow_copy(from_src, mm); + + log && log(xtag("from_src", from_src), xtag("to_dest", to_dest)); + log && log(xtag("tseq", info.tseq()), + xtag("tname", TypeRegistry::id2name(typeseq(info.tseq()))), + xtag("age", info.age()), + xtag("size", info.size())); + + if (config_.debug_flag_) { + AllocInfo info_copy = this->alloc_info((std::byte *)to_dest); + + log && log(xtag("age2", info_copy.age()), xtag("size2", info_copy.size())); + + assert((info_copy.age() == config_.arena_config_.header_.max_age()) + || (info_copy.age() == info.age() + 1)); + } + + if(to_dest == from_src) { + assert(false); + } else { + *(const_cast(info.p_header_)) + = AllocHeader(config_ + .arena_config_ + .header_ + .mark_forwarding_tseq(*info.p_header_)); + + *(void **)from_src = to_dest; + } + + return to_dest; + } /*shallow_move*/ + #ifdef MARKED void GCObjectStore::_forward_children_until_fixpoint(DX1Collector * gc, From 99593c4d724d7a0d78b608fa29c2bfe71ec12e0c Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 15:53:25 -0400 Subject: [PATCH 091/174] xo-gc: refactor: move forward_inplace_aux() to GCObjectStore --- include/xo/gc/DX1Collector.hpp | 3 +- include/xo/gc/GCObjectStore.hpp | 12 ++ src/gc/DX1Collector.cpp | 23 ++-- src/gc/GCObjectStore.cpp | 187 ++++++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 8 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index b3d1ba7b..c41284ab 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -392,7 +392,8 @@ namespace xo { /** Evacuate object at @p *lhs_data to to-space. * Replace original with forwarding pointer to new location **/ - void _forward_inplace_aux(AGCObject * lhs_iface, void ** lhs_data); + void _forward_inplace_aux(AGCObject * lhs_iface, void ** lhs_data, Generation upto); + /** Verify that pointer {@p iface, @p data} is valid: * destination either in to-space, or somewhere outside this collector **/ diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 7cb4e713..147cec6a 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -135,6 +135,18 @@ namespace xo { void * gco_data, Generation upto) const noexcept; + /** 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 + * forward_children() + * + * Replace original with forwarding pointer to new location + **/ + void _forward_inplace_aux(DX1Collector * gc, + AGCObject * lhs_iface, + void ** lhs_data, + Generation upto); + /** For each generation g in [0 ,.., upto) * swap arenas assigned to {to-space, from-space}. * Invoked once at the beginning of each gc cycle. diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index fc4acdc5..39525b36 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -1094,9 +1094,11 @@ namespace xo { DX1Collector::forward_inplace(AGCObject * lhs_iface, void ** lhs_data) { + Generation upto = runstate_.gc_upto(); + if (runstate_.is_running()) { // called during collection phase - this->_forward_inplace_aux(lhs_iface, lhs_data); + this->_forward_inplace_aux(lhs_iface, lhs_data, upto); } else if (runstate_.is_verify()) { // called during verify_ok this->_verify_aux(lhs_iface, *lhs_data); @@ -1108,8 +1110,14 @@ namespace xo { void DX1Collector::_forward_inplace_aux(AGCObject * lhs_iface, - void ** lhs_data) + void ** lhs_data, + Generation upto) { + // upto == runstate_.gc_upto() + + gco_store_._forward_inplace_aux(this, lhs_iface, lhs_data, upto); + +#ifdef MARKED scope log(XO_DEBUG(config_.debug_flag_), xtag("lhs_data", lhs_data), xtag("*lhs_data", lhs_data ? *lhs_data : nullptr)); @@ -1137,7 +1145,7 @@ namespace xo { if (!object_data) { /* trivial to forward nullptr */ return; - } else if (!this->contains(role::from_space(), object_data)) { + } else if (!gco_store_.contains(role::from_space(), object_data)) { /* *lhs_data either: * 1. already in to-space * 2. not in GC-allocated space at all @@ -1162,7 +1170,7 @@ namespace xo { * allocated object data. * Only using this to get alloc header **/ - DArena * some_arena = this->from_space(Generation(0)); + DArena * some_arena = gco_store_.from_space(Generation(0)); DArena::header_type * p_header = some_arena->obj2hdr(object_data); @@ -1192,7 +1200,7 @@ namespace xo { */ assert(alloc_z >= sizeof(uintptr_t)); - if (this->is_forwarding_header(alloc_hdr)) { + if (gco_store_.is_forwarding_header(alloc_hdr)) { /* *lhs_data already refers to a forwarding pointer */ /* @@ -1245,7 +1253,7 @@ namespace xo { xtag("tname", TypeRegistry::id2name(typeseq(info.tseq()))), xtag("age", info.age()), xtag("size", info.size())); } - } else if (this->check_move_policy(alloc_hdr, object_data)) { + } else if (gco_store_._check_move_policy(alloc_hdr, object_data, upto)) { /* copy object *lhs + replace with forwarding pointer */ log && log("forward object now"); @@ -1289,7 +1297,8 @@ namespace xo { * e.g. incremental collection + object is tenured */ } - } /*_forward_inplace*/ +#endif + } /*_forward_inplace_aux*/ void DX1Collector::_verify_aux(AGCObject * iface, void * data) diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 13475dd1..118530db 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -4,6 +4,7 @@ **/ #include "GCObjectStore.hpp" +#include "X1Collector.hpp" #include #include @@ -371,6 +372,192 @@ namespace xo { return (g < upto); } + void + GCObjectStore::_forward_inplace_aux(DX1Collector * gc, + AGCObject * lhs_iface, + void ** lhs_data, + Generation upto) + { + // upto == runstate_.gc_upto() + + scope log(XO_DEBUG(config_.debug_flag_), + xtag("lhs_data", lhs_data), + xtag("*lhs_data", lhs_data ? *lhs_data : nullptr)); + + /* coordinates with DX1Collector::_deep_move() */ + + /* + * lhs obj + * | +---------+ +---+-+----+ + * \--->| .iface | | T |G|size| header + * +---------+ object_data +---+-+----+ + * | .data x----------------->| alloc | + * +---------+ | data | + * | for | + * | instance | + * | ... | + * +----------+ + */ + + void * object_data = (std::byte *)*lhs_data; + + if (!object_data) { + /* trivial to forward nullptr */ + return; + } else if (!this->contains(role::from_space(), object_data)) { + /* *lhs_data either: + * 1. already in to-space + * 2. not in GC-allocated space at all + * (small number of niche examples of this) + * + * It's important we recognize case (2) up front. + * Since not allocated from GC, they don't have + * an alloc-header. + */ + log && log("disposition: not in from-space. Don't forward, but check children"); + + obj gco(lhs_iface, object_data); + gco.forward_children(gc->ref()); + + return; + } + + log && log("disposition: in from-space"); + + /** NOTE: for form's sake: + * lookup actual arena that + * allocated object data. + * Only using this to get alloc header + **/ + DArena * some_arena = this->from_space(Generation(0)); + + DArena::header_type * p_header + = some_arena->obj2hdr(object_data); + + DArena::header_type alloc_hdr = *p_header; + + /* recover allocation size */ + std::size_t alloc_z = some_arena->config_.header_.size_with_padding(alloc_hdr); + + if (log) { + log(xtag("some_arena.lo", some_arena->lo_), + xtag("p_header", p_header), + xtag("alloc_z", alloc_z)); + + AllocInfo info = this->alloc_info((std::byte *)object_data); + log(xtag("tseq", info.tseq()), + xtag("tname", TypeRegistry::id2name(typeseq(info.tseq()))), + xtag("is_forwarding_tseq", info.is_forwarding_tseq()), + xtag("age", info.age()), + xtag("size", info.size())); + } + + /* need to be able to fit forwarding pointer + * in place of forwarded object. + * + * This is guaranteed anyway, by alignment rules + */ + assert(alloc_z >= sizeof(uintptr_t)); + + if (this->is_forwarding_header(alloc_hdr)) { + /* *lhs_data already refers to a forwarding pointer */ + + /* + * lhs obj (from-space) + * | +---------+ +---+-+----+ + * \--->| .iface | |FWD|G|size| alloc_hdr + * +---------+ object_data +---+-+----+ + * | .data x----------------->| x--------\ + * +---------+ | | | dest + * | | | + * +----------+ | + * | + * (to-space) | + * +---+-+----+ | + * |TSQ|G|size| | + * +---+-+----+ | + * | | <-/ + * | | + * | | + * +----------+ + */ + void * dest = *(void**)object_data; + + *lhs_data = dest; + /* + * lhs obj + * | +---------+ + * \--->| .iface | + * +---------+ + * | .data x------------\ + * +---------+ | + * | dest + * | + * | + * | (to-space) + * | +---+-+----+ + * | |TSQ|G|size| + * | +---+-+----+ + * \---> | | + * | | + * | | + * +----------+ + */ + + if (log) { + log("lhs_data already forwarded", xtag("dest", dest)); + + AllocInfo info = this->alloc_info((std::byte *)dest); + log(xtag("tseq", info.tseq()), + xtag("tname", TypeRegistry::id2name(typeseq(info.tseq()))), + xtag("age", info.age()), xtag("size", info.size())); + } + } else if (this->_check_move_policy(alloc_hdr, object_data, upto)) { + /* copy object *lhs + replace with forwarding pointer */ + + log && log("forward object now"); + + /* + * lhs obj (from-space) + * | +---------+ +---+-+----+ + * \--->| .iface | |TSQ|G|size| alloc_hdr + * +---------+ object_data +---+-+----+ + * | .data x----------------> | | + * +---------+ | | + * | | + * +----------+ + */ + + *lhs_data = this->_shallow_move(gc->ref(), lhs_iface, *lhs_data); + + /* + * lhs obj (from-space) + * | +---------+ +---+-+----+ + * \--->| .iface | |FWD|G|SIZE| + * +---------+ +---+-+----+ + * | .data x------------\ | x--------\ + * +---------+ | | | | + * | | | | + * dest | +----------+ | + * | | + * | (to-space) | + * | +---+-+----+ | + * | |TSQ|G|size| | + * | +---+-+----+ | + * \---> | | <-/ + * | | + * | | + * +----------+ + */ + } else { + log && log("object not eligible/required to forward"); + + /* object doesn't need to move. + * e.g. incremental collection + object is tenured + */ + } + } /*_forward_inplace_aux*/ + void GCObjectStore::swap_roles(Generation upto) noexcept { From d44f1e9024f9a781055bf0f6a9f10be560f5fde8 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 16:24:45 -0400 Subject: [PATCH 092/174] xo-gc: refactor: move deep_move_gc_owned() to GCObjectStore --- include/xo/gc/DX1Collector.hpp | 4 +- include/xo/gc/GCObjectStore.hpp | 12 +- src/gc/DX1Collector.cpp | 367 +------------------------------- src/gc/GCObjectStore.cpp | 68 +++++- 4 files changed, 75 insertions(+), 376 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index c41284ab..78f17114 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -384,11 +384,13 @@ namespace xo { * use to detect forwarding activity after visiting objects **/ GCMoveCheckpoint _snap_move_checkpoint(Generation upto); + /** traverse objects allocated after @p ckp, to make sure their children * are forwarded. Repeat until traverse doesn't find any unforwarded children **/ void _forward_children_until_fixpoint(Generation upto, - GCMoveCheckpoint ckp); + const GCMoveCheckpoint & ckp); + /** Evacuate object at @p *lhs_data to to-space. * Replace original with forwarding pointer to new location **/ diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 147cec6a..d07be565 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -108,11 +108,19 @@ namespace xo { **/ bool install_type(const AGCObject & meta) noexcept; + /** Common driver for _deep_move_root(), _deep_move_interior(). + * Move object subgraph @p from_src on behalf of @p gc collection cycle, + * covering generations in [0 ,.., upto). + **/ + void * _deep_move_gc_owned(DX1Collector * gc, + void * from_src, + Generation upto); + /** during a gc cycle: * evacuate object @p from_src, with gc-object interface @p iface. * Shallow: does not traverse children **/ - void * _shallow_move(obj mm, + void * _shallow_move(DX1Collector * gc, const AGCObject * iface, void * from_src); @@ -124,7 +132,7 @@ namespace xo { **/ void _forward_children_until_fixpoint(DX1Collector * gc, Generation upto, - const GCMoveCheckpoint & gray_lo_v); + GCMoveCheckpoint gray_lo_v); /** true iff {@p alloc_hdr, @p object_data} should move for * a collection of all generations strictly younger than @p upto. diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 39525b36..1b4fbfcc 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -676,27 +676,6 @@ namespace xo { DX1Collector::install_type(const AGCObject & meta) noexcept { return gco_store_.install_type(meta); - -#ifdef MARKED - typeseq tseq = meta._typeseq(); - - assert(tseq.seqno() > 0); - - auto ix = static_cast(tseq.seqno()); - - if (ix >= object_types_.size()) { - if (!object_types_.resize(std::max(2 * object_types_.size(), ix + 1))) - return false; - } - - assert(ix < object_types_.size()); - - ObjectTypeSlot & slot = object_types_[ix]; - - slot.store_iface(&meta); - - return true; -#endif } void @@ -898,50 +877,7 @@ namespace xo { DX1Collector::_deep_move_gc_owned(void * from_src, Generation upto) { - scope log(XO_DEBUG(gco_store_.config().debug_flag_)); - - AllocInfo info = gco_store_.alloc_info((std::byte *)from_src); - AllocHeader hdr = info.header(); - typeseq tseq(info.tseq()); - - assert(gco_store_.contains_allocated(role::from_space(), from_src)); - - if (gco_store_.is_forwarding_header(hdr)) { - /* already forwarded - pickup destination - * - * Coordinates with forward_inplace() - */ - log && log("disposition: already forwarded"); - - return *(void **)from_src; - } - - /* here: object at from_src not already forwarded */ - - if (!gco_store_._check_move_policy(hdr, from_src, upto)) { - /* object at from_src is in generation that is not being collected */ - log && log("disposition: not moving from_src"); - - return from_src; - } - - log && log("disposition: move subtree"); - - /* TODO: AllocIterator pointing to free pointer */ - GCMoveCheckpoint gray_lo_v = gco_store_.snap_move_checkpoint(upto); - - obj alloc(this); - const AGCObject * iface = lookup_type(tseq); - - assert(iface->_has_null_vptr() == false); - - void * to_dest = this->_shallow_move(iface, from_src); - - this->_forward_children_until_fixpoint(upto, gray_lo_v); - - log && log(xtag("to_dest", to_dest)); - - return to_dest; + return gco_store_._deep_move_gc_owned(this, from_src, upto); } /*_deep_move_gc_owned*/ auto @@ -952,122 +888,11 @@ namespace xo { void DX1Collector::_forward_children_until_fixpoint(Generation upto, - GCMoveCheckpoint gray_lo_v) + const GCMoveCheckpoint & gray_lo_v) { // problem -- need object type lookup -#ifdef NOT_YET - gco_store_._forward_children_until_fixpoint(upto, gray_lo_v); -#endif - scope log(XO_DEBUG(config_.debug_flag_)); - - /** - * To-space: - * - * to_lo = start of to-space - * w,W = white objects. An object x is white if x - * + all immediate children of x are in to-space - * (also implies this GC cycle put it there) - * g,G = grey objects. An object x is gray if it's in to-space, - * but possibly has >0 black children - * _ = free to-space memory - * N = nursery space (generation{0}) - * T = tenured space (generation{1}) - * - * wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________... - * ^ ^ ^ - * to_lo grey_lo(N) free_ptr(N) - * - * After moving children of one object, - * advancing {nursery_grey_lo, nursery_free_ptr} - * - * wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG______... - * ^ ^ ^ - * to_lo grey_lo(N) free_ptr(N) - * - * Invariant: - * - * objects in [to_lo, gray_lo) are white. - * all gray objects are in [gray_lo, free_ptr) - * memory starting at free_ptr is free. - * - * deep_move terminates when gray_lo catches up to free_ptr - * - * Above is simplified. Complication is that GC (including incremental) may - * promote objects from nursery (N) to tenured (T) - * - * So more accurate before/after picture - * - * N wwwwwwwwwwwwwwwwwwwggggggggggggggggggggg_________________... - * ^ ^ ^ - * to_lo(N) grey_lo(N) free_ptr(N) - * - * T wwwwwwwwwwwwwwgggggggggggg_______________________________... - * ^ ^ ^ - * to_lo(T) grey_lo(T) free_ptr(N) - * - * After moving children of one object, - * advancing {nursery_grey_lo, nursery_free_ptr} - * - * N wwwwwwwwwwwwwwwwwwwWWWWgggggggggggggggggGGGGGGGGGGG_____... - * ^ ^ ^ - * to_lo(N) grey_lo(N) free_ptr(N) - * - * T wwwwwwwwwwwwwwggggggggggggGGGGG_________________________... - * ^ ^ ^ - * to_lo(T) grey_lo(T) free_ptr(T) - * - * deep_move terminates when both: - * - gray_lo(N) catches up with free_ptr(N) - * - gray_lo(T) catches up with free_ptr(T) - * - **/ - - std::size_t fixup_work = 0; - - /* TODO: - * - loop here is bad for memory locality - * - replace with depth-first traversal - */ - do { - fixup_work = 0; - - for (Generation g = Generation{0}; g < upto; ++g) { - /** object index for this pass **/ - size_t i_obj = 0; - - /* TODO: use AllocIterator here */ - while(gray_lo_v[g] < to_space(g)->free_) { - AllocHeader * hdr = (AllocHeader *)gray_lo_v[g]; - void * src = (hdr + 1); - - const auto & hdr_cfg = config_.arena_config_.header_; - typeseq tseq = typeseq(hdr_cfg.tseq(*hdr)); - size_t z = hdr_cfg.size_with_padding(*hdr); - - log && log("deep_move_gc_owned: fwd to-space children", - xtag("g", g), - xtag("i_obj", i_obj), - xtag("src", src), - xtag("tseq", tseq), - xtag("tname", TypeRegistry::id2name(tseq)), - xtag("z", z)); - - const AGCObject * iface = this->lookup_type(tseq); - - assert(iface->_has_null_vptr() == false); - - auto gc = this->ref(); - - iface->forward_children(src, gc); - - gray_lo_v[g] = ((std::byte *)src) + z; - - ++i_obj; - ++fixup_work; - } - } - } while (fixup_work > 0); + gco_store_._forward_children_until_fixpoint(this, upto, gray_lo_v); } void @@ -1116,188 +941,6 @@ namespace xo { // upto == runstate_.gc_upto() gco_store_._forward_inplace_aux(this, lhs_iface, lhs_data, upto); - -#ifdef MARKED - scope log(XO_DEBUG(config_.debug_flag_), - xtag("lhs_data", lhs_data), - xtag("*lhs_data", lhs_data ? *lhs_data : nullptr)); - - /* coordinates with DX1Collector::_deep_move() */ - - (void)lhs_iface; - assert(runstate_.is_running()); - - /* - * lhs obj - * | +---------+ +---+-+----+ - * \--->| .iface | | T |G|size| header - * +---------+ object_data +---+-+----+ - * | .data x----------------->| alloc | - * +---------+ | data | - * | for | - * | instance | - * | ... | - * +----------+ - */ - - void * object_data = (std::byte *)*lhs_data; - - if (!object_data) { - /* trivial to forward nullptr */ - return; - } else if (!gco_store_.contains(role::from_space(), object_data)) { - /* *lhs_data either: - * 1. already in to-space - * 2. not in GC-allocated space at all - * (small number of niche examples of this) - * - * It's important we recognize case (2) up front. - * Since not allocated from GC, they don't have - * an alloc-header. - */ - log && log("disposition: not in from-space. Don't forward, but check children"); - - obj gco(lhs_iface, object_data); - gco.forward_children(this->ref()); - - return; - } - - log && log("disposition: in from-space"); - - /** NOTE: for form's sake: - * lookup actual arena that - * allocated object data. - * Only using this to get alloc header - **/ - DArena * some_arena = gco_store_.from_space(Generation(0)); - - DArena::header_type * p_header - = some_arena->obj2hdr(object_data); - - DArena::header_type alloc_hdr = *p_header; - - /* recover allocation size */ - std::size_t alloc_z = some_arena->config_.header_.size_with_padding(alloc_hdr); - - if (log) { - log(xtag("some_arena.lo", some_arena->lo_), - xtag("p_header", p_header), - xtag("alloc_z", alloc_z)); - - AllocInfo info = this->alloc_info((std::byte *)object_data); - log(xtag("tseq", info.tseq()), - xtag("tname", TypeRegistry::id2name(typeseq(info.tseq()))), - xtag("is_forwarding_tseq", info.is_forwarding_tseq()), - xtag("age", info.age()), - xtag("size", info.size())); - } - - /* need to be able to fit forwarding pointer - * in place of forwarded object. - * - * This is guaranteed anyway, by alignment rules - */ - assert(alloc_z >= sizeof(uintptr_t)); - - if (gco_store_.is_forwarding_header(alloc_hdr)) { - /* *lhs_data already refers to a forwarding pointer */ - - /* - * lhs obj (from-space) - * | +---------+ +---+-+----+ - * \--->| .iface | |FWD|G|size| alloc_hdr - * +---------+ object_data +---+-+----+ - * | .data x----------------->| x--------\ - * +---------+ | | | dest - * | | | - * +----------+ | - * | - * (to-space) | - * +---+-+----+ | - * |TSQ|G|size| | - * +---+-+----+ | - * | | <-/ - * | | - * | | - * +----------+ - */ - void * dest = *(void**)object_data; - - *lhs_data = dest; - /* - * lhs obj - * | +---------+ - * \--->| .iface | - * +---------+ - * | .data x------------\ - * +---------+ | - * | dest - * | - * | - * | (to-space) - * | +---+-+----+ - * | |TSQ|G|size| - * | +---+-+----+ - * \---> | | - * | | - * | | - * +----------+ - */ - - if (log) { - log("lhs_data already forwarded", xtag("dest", dest)); - - AllocInfo info = this->alloc_info((std::byte *)dest); - log(xtag("tseq", info.tseq()), - xtag("tname", TypeRegistry::id2name(typeseq(info.tseq()))), - xtag("age", info.age()), xtag("size", info.size())); - } - } else if (gco_store_._check_move_policy(alloc_hdr, object_data, upto)) { - /* copy object *lhs + replace with forwarding pointer */ - - log && log("forward object now"); - - /* - * lhs obj (from-space) - * | +---------+ +---+-+----+ - * \--->| .iface | |TSQ|G|size| alloc_hdr - * +---------+ object_data +---+-+----+ - * | .data x----------------> | | - * +---------+ | | - * | | - * +----------+ - */ - - *lhs_data = this->_shallow_move(lhs_iface, *lhs_data); - - /* - * lhs obj (from-space) - * | +---------+ +---+-+----+ - * \--->| .iface | |FWD|G|SIZE| - * +---------+ +---+-+----+ - * | .data x------------\ | x--------\ - * +---------+ | | | | - * | | | | - * dest | +----------+ | - * | | - * | (to-space) | - * | +---+-+----+ | - * | |TSQ|G|size| | - * | +---+-+----+ | - * \---> | | <-/ - * | | - * | | - * +----------+ - */ - } else { - log && log("object not eligible/required to forward"); - - /* object doesn't need to move. - * e.g. incremental collection + object is tenured - */ - } -#endif } /*_forward_inplace_aux*/ void @@ -1332,9 +975,7 @@ namespace xo { void * DX1Collector::_shallow_move(const AGCObject * iface, void * from_src) { - obj alloc(this); - - return gco_store_._shallow_move(alloc, iface, from_src); + return gco_store_._shallow_move(this, iface, from_src); } bool diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 118530db..874b518a 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -528,7 +528,7 @@ namespace xo { * +----------+ */ - *lhs_data = this->_shallow_move(gc->ref(), lhs_iface, *lhs_data); + *lhs_data = this->_shallow_move(gc, lhs_iface, *lhs_data); /* * lhs obj (from-space) @@ -626,7 +626,58 @@ namespace xo { } void * - GCObjectStore::_shallow_move(obj mm, + GCObjectStore::_deep_move_gc_owned(DX1Collector * gc, + void * from_src, + Generation upto) + { + scope log(XO_DEBUG(config_.debug_flag_)); + + AllocInfo info = this->alloc_info((std::byte *)from_src); + AllocHeader hdr = info.header(); + typeseq tseq(info.tseq()); + + assert(this->contains_allocated(role::from_space(), from_src)); + + if (this->is_forwarding_header(hdr)) { + /* already forwarded - pickup destination + * + * Coordinates with forward_inplace() + */ + log && log("disposition: already forwarded"); + + return *(void **)from_src; + } + + /* here: object at from_src not already forwarded */ + + if (!this->_check_move_policy(hdr, from_src, upto)) { + /* object at from_src is in generation that is not being collected */ + log && log("disposition: not moving from_src"); + + return from_src; + } + + log && log("disposition: move subtree"); + + /* TODO: AllocIterator pointing to free pointer */ + GCMoveCheckpoint gray_lo_v = this->snap_move_checkpoint(upto); + + //obj alloc(this); + const AGCObject * iface = lookup_type(tseq); + + assert(iface->_has_null_vptr() == false); + + void * to_dest = this->_shallow_move(gc, iface, from_src); + + this->_forward_children_until_fixpoint(gc, upto, gray_lo_v); + + log && log(xtag("to_dest", to_dest)); + + return to_dest; + } /*_deep_move_gc_owned*/ + + void * + GCObjectStore::_shallow_move(DX1Collector * gc, const AGCObject * iface, void * from_src) { @@ -636,7 +687,7 @@ namespace xo { //obj gc_gco(gc); - void * to_dest = iface->shallow_copy(from_src, mm); + void * to_dest = iface->shallow_copy(from_src, gc->ref()); log && log(xtag("from_src", from_src), xtag("to_dest", to_dest)); log && log(xtag("tseq", info.tseq()), @@ -666,13 +717,12 @@ namespace xo { } return to_dest; - } /*shallow_move*/ + } /*_shallow_move*/ -#ifdef MARKED void GCObjectStore::_forward_children_until_fixpoint(DX1Collector * gc, Generation upto, - const GCMoveCheckpoint & gray_lo_v) + GCMoveCheckpoint gray_lo_v) { scope log(XO_DEBUG(config_.debug_flag_)); @@ -772,9 +822,9 @@ namespace xo { assert(iface->_has_null_vptr() == false); - auto gc = this->ref(); + auto gc_gco = gc->ref(); - iface->forward_children(src, gc); + iface->forward_children(src, gc_gco); gray_lo_v[g] = ((std::byte *)src) + z; @@ -784,8 +834,6 @@ namespace xo { } } while (fixup_work > 0); } /*_forward_children_until_fixpoint*/ -#endif - } /*namespace mm*/ } /*namespace xo*/ From d24f75b9ef468c5933374b0c9d17965d95e6b0e5 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 16:32:15 -0400 Subject: [PATCH 093/174] xo-gc: move deep_move_interior() aux to GCObjectStore --- include/xo/gc/DX1Collector.hpp | 7 ++----- include/xo/gc/GCObjectStore.hpp | 7 +++++++ src/gc/DX1Collector.cpp | 2 ++ src/gc/GCObjectStore.cpp | 18 ++++++++++++++++++ src/gc/MutationLogStore.cpp | 2 +- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 78f17114..1f2cebad 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -110,7 +110,9 @@ namespace xo { // ----- access methods ----- const X1CollectorConfig & config() const noexcept { return config_; } + const GCObjectStore & gco_store() const noexcept { return gco_store_; } + GCObjectStore & gco_store() noexcept { return gco_store_; } std::string_view name() const noexcept { return config_.name_; } GCRunState runstate() const noexcept { return runstate_; } @@ -289,11 +291,6 @@ namespace xo { **/ bool check_move_policy(header_type alloc_hdr, void * object_data) const noexcept; - /** move interior subgraph at @p from_src to to-space. - * no-op if not in gc-space. - **/ - void * deep_move_interior(void * from_src, Generation upto); - // ----- allocation ----- /** simple allocation. allocate @p z bytes of memory diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index d07be565..67a5506a 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -108,6 +108,13 @@ namespace xo { **/ bool install_type(const AGCObject & meta) noexcept; + /** move interior subgraph at @p from_src to to-space. + * no-op if not in gc-space. + **/ + void * deep_move_interior(DX1Collector * gc, + void * from_src, + Generation upto); + /** Common driver for _deep_move_root(), _deep_move_interior(). * Move object subgraph @p from_src on behalf of @p gc collection cycle, * covering generations in [0 ,.., upto). diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 1b4fbfcc..56f5a743 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -845,6 +845,7 @@ namespace xo { } } +#ifdef OBSOLETE void * DX1Collector::deep_move_interior(void * from_src, Generation upto) @@ -861,6 +862,7 @@ namespace xo { return _deep_move_gc_owned(from_src, upto); } +#endif /* * rules: diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 874b518a..6b096784 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -625,6 +625,24 @@ namespace xo { return true; } + void * + GCObjectStore::deep_move_interior(DX1Collector * gc, + void * from_src, + Generation upto) + { + scope log(XO_DEBUG(config_.debug_flag_)); + + if (!from_src) + return nullptr; + + bool src_in_from_space = this->contains(role::from_space(), from_src); + + if (!src_in_from_space) + return from_src; + + return this->_deep_move_gc_owned(gc, from_src, upto); + } + void * GCObjectStore::_deep_move_gc_owned(DX1Collector * gc, void * from_src, diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 563d4bda..04950353 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -400,7 +400,7 @@ namespace xo { ++counters.n_rescue_; - child_to = gc->deep_move_interior(child_fr, upto); + child_to = gc->gco_store().deep_move_interior(gc, child_fr, upto); // update child pointer in parent object *from_entry.p_data() = child_to; From 177430d8a44432b1fe608484d1fb6a5154ba72c5 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 16:42:04 -0400 Subject: [PATCH 094/174] xo-gc: refactor: move _deep_move_root() to GCObjectStore --- include/xo/gc/GCObjectStore.hpp | 16 ++++++++-- src/gc/DX1Collector.cpp | 56 +-------------------------------- src/gc/GCObjectStore.cpp | 51 ++++++++++++++++++++++++++++-- src/gc/MutationLogStore.cpp | 4 ++- 4 files changed, 65 insertions(+), 62 deletions(-) diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 67a5506a..f10381bd 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -108,12 +108,22 @@ namespace xo { **/ bool install_type(const AGCObject & meta) noexcept; + /** 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 (!). + + * Require: runstate_.is_running() + **/ + void * _deep_move_root(DX1Collector * gc, + obj from_src, + Generation upto); + /** move interior subgraph at @p from_src to to-space. * no-op if not in gc-space. **/ - void * deep_move_interior(DX1Collector * gc, - void * from_src, - Generation upto); + void * _deep_move_interior(DX1Collector * gc, + void * from_src, + Generation upto); /** Common driver for _deep_move_root(), _deep_move_interior(). * Move object subgraph @p from_src on behalf of @p gc collection cycle, diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 56f5a743..3dd7c165 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -807,63 +807,9 @@ namespace xo { DX1Collector::_deep_move_root(obj from_src, Generation upto) { - // NOTE: - // Some roots are non-gc-owned nodes. - // GC must still visit immediate children of these nodes - // to move gc-owned children. - // This implements virtual root node feature, - // intended to mitigate mutation log churn. - - scope log(XO_DEBUG(config_.debug_flag_)); - - if (!from_src) - return nullptr; - - bool src_in_from_space = this->contains(role::from_space(), from_src.data()); - - if (src_in_from_space) { - return _deep_move_gc_owned(from_src.data(), upto); - } else { - // we aren't moving from_src, it's not gc-owned. - // However weare moving all its gc-owned children - - auto self = this->ref(); - - GCMoveCheckpoint gray_lo_v = this->_snap_move_checkpoint(upto); - - from_src.forward_children(self); - - // For each generation g: - // traverse objects newer than gray_lo_v[g], to make sure children - // are forwarded. Fixpoint reached when gray_lo_v[g] doesn't change. - // Remember that forwarding may promote objects to older generation, - // so need multiple passes - // - this->_forward_children_until_fixpoint(upto, gray_lo_v); - - return from_src.data(); - } + return gco_store_._deep_move_root(this, from_src, upto); } -#ifdef OBSOLETE - void * - DX1Collector::deep_move_interior(void * from_src, - Generation upto) - { - scope log(XO_DEBUG(config_.debug_flag_)); - - if (!from_src) - return nullptr; - - bool src_in_from_space = this->contains(role::from_space(), from_src); - - if (!src_in_from_space) - return from_src; - - return _deep_move_gc_owned(from_src, upto); - } -#endif - /* * rules: * - from_src must be in from-space diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 6b096784..27f70a74 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -626,9 +626,54 @@ namespace xo { } void * - GCObjectStore::deep_move_interior(DX1Collector * gc, - void * from_src, - Generation upto) + GCObjectStore::_deep_move_root(DX1Collector * gc, + obj from_src, + Generation upto) + { + // NOTE: + // Some roots are non-gc-owned nodes. + // GC must still visit immediate children of these nodes + // to move gc-owned children. + // This implements virtual root node feature, + // intended to mitigate mutation log churn. + + scope log(XO_DEBUG(config_.debug_flag_)); + + if (!from_src) + return nullptr; + + bool src_in_from_space = this->contains(role::from_space(), + from_src.data()); + + if (src_in_from_space) { + return this->_deep_move_gc_owned(gc, from_src.data(), upto); + } else { + // we aren't moving from_src, it's not gc-owned. + // However weare moving all its gc-owned children + + auto gc_obj = gc->ref(); + + GCMoveCheckpoint gray_lo_v + = this->snap_move_checkpoint(upto); + + from_src.forward_children(gc_obj); + + // For each generation g: + // traverse objects newer than gray_lo_v[g], to make sure children + // are forwarded. Fixpoint reached when gray_lo_v[g] doesn't change. + // Remember that forwarding may promote objects to older generation, + // so need multiple passes + // + this->_forward_children_until_fixpoint(gc, upto, gray_lo_v); + + return from_src.data(); + } + } + + void * + GCObjectStore::_deep_move_interior(DX1Collector * gc, + void * from_src, + Generation upto) { scope log(XO_DEBUG(config_.debug_flag_)); diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 04950353..d957fe62 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -400,7 +400,9 @@ namespace xo { ++counters.n_rescue_; - child_to = gc->gco_store().deep_move_interior(gc, child_fr, upto); + GCObjectStore & gco_store = gc->gco_store(); + + child_to = gco_store._deep_move_interior(gc, child_fr, upto); // update child pointer in parent object *from_entry.p_data() = child_to; From 9a2c2772c26eff3ed424ba3848d82e7be9553955 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 16:46:14 -0400 Subject: [PATCH 095/174] xo-gc: refactor: retire DX1Collector._deep_move_root() --- include/xo/gc/DX1Collector.hpp | 2 ++ src/gc/DX1Collector.cpp | 24 +++--------------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 1f2cebad..0bc3594a 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -370,11 +370,13 @@ namespace xo { /** cleanup after gc **/ void _cleanup_phase(Generation upto); +#ifdef OBSOLETE /** move root subgraph at @p from_src to to-space. * If not in gc-space, visit immediate children and move them. * Require: runstate_.is_running() **/ void * _deep_move_root(obj from_src, Generation upto); +#endif /** Common driver for _deep_move_root(), _deep_move_interior() **/ void * _deep_move_gc_owned(void * from_src, Generation upto); /** snap checkpoint containing allocator state diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 3dd7c165..2d7dc6ab 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -82,26 +82,6 @@ namespace xo { this->_init_mlogs(page_z); } -#ifdef OBSOLETE // called from GCObjectStore ctor - void - DX1Collector::_init_object_types(const X1CollectorConfig & cfg, std::size_t page_z) - { - gco_state_._init_object_types(); - -#ifdef MOVED - /* 1MB reserved address space enough for up to 128k distinct types. - * In this case don't want to use hugepages since actual #of types - * likely << .size/8 - */ - this->object_types_ - = ObjectTypeTable::map(ArenaConfig{.name_ = "x1-object-types", - .size_ = cfg.object_types_z_, - .hugepage_z_ = page_z, - .store_header_flag_ = false}); -#endif - } -#endif - void DX1Collector::_init_gc_roots(const X1CollectorConfig & cfg, std::size_t page_z) { @@ -803,12 +783,14 @@ namespace xo { this->runstate_ = GCRunState::idle(); } +#ifdef OBSOLETE void * DX1Collector::_deep_move_root(obj from_src, Generation upto) { return gco_store_._deep_move_root(this, from_src, upto); } +#endif /* * rules: @@ -855,7 +837,7 @@ namespace xo { xtag("slot.root()", slot.root()), xtag("slot.root()->data_", slot.root()->data_)); - void * root_to = this->_deep_move_root(*slot.root(), upto); + void * root_to = gco_store_._deep_move_root(this, *slot.root(), upto); slot.root()->reset_opaque(root_to); From 405f0ddba7789573783b6405008960c4a54488b3 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 16:47:50 -0400 Subject: [PATCH 096/174] xo-gc: refactor: retire DX1Collector._deep_move_gc_owned --- include/xo/gc/DX1Collector.hpp | 2 +- src/gc/DX1Collector.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 0bc3594a..52c9647d 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -376,9 +376,9 @@ namespace xo { * Require: runstate_.is_running() **/ void * _deep_move_root(obj from_src, Generation upto); -#endif /** Common driver for _deep_move_root(), _deep_move_interior() **/ void * _deep_move_gc_owned(void * from_src, Generation upto); +#endif /** snap checkpoint containing allocator state * use to detect forwarding activity after visiting objects **/ diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 2d7dc6ab..9a427de1 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -792,6 +792,7 @@ namespace xo { } #endif +#ifdef OBSOLETE /* * rules: * - from_src must be in from-space @@ -809,6 +810,7 @@ namespace xo { { return gco_store_._deep_move_gc_owned(this, from_src, upto); } /*_deep_move_gc_owned*/ +#endif auto DX1Collector::_snap_move_checkpoint(Generation upto) -> GCMoveCheckpoint From 0fdf5badd562508a0a711fe823983cebab712d33 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 16:48:43 -0400 Subject: [PATCH 097/174] xo-gc: retire DX1Collector._snap_move_checkpoint() --- include/xo/gc/DX1Collector.hpp | 2 +- src/gc/DX1Collector.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 52c9647d..195569d0 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -378,11 +378,11 @@ namespace xo { void * _deep_move_root(obj from_src, Generation upto); /** Common driver for _deep_move_root(), _deep_move_interior() **/ void * _deep_move_gc_owned(void * from_src, Generation upto); -#endif /** snap checkpoint containing allocator state * use to detect forwarding activity after visiting objects **/ GCMoveCheckpoint _snap_move_checkpoint(Generation upto); +#endif /** traverse objects allocated after @p ckp, to make sure their children * are forwarded. Repeat until traverse doesn't find any unforwarded children diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 9a427de1..d6c24ca0 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -812,11 +812,13 @@ namespace xo { } /*_deep_move_gc_owned*/ #endif +#ifdef OBSOLETE auto DX1Collector::_snap_move_checkpoint(Generation upto) -> GCMoveCheckpoint { return gco_store_.snap_move_checkpoint(upto); } +#endif void DX1Collector::_forward_children_until_fixpoint(Generation upto, From 041eae0c0e41e022e28d880bc749a22f46a95a61 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 16:49:49 -0400 Subject: [PATCH 098/174] xo-gc: retire DX1Collector._forward_children_until_fixpoint() --- include/xo/gc/DX1Collector.hpp | 2 +- src/gc/DX1Collector.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 195569d0..555ef89a 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -382,13 +382,13 @@ namespace xo { * use to detect forwarding activity after visiting objects **/ GCMoveCheckpoint _snap_move_checkpoint(Generation upto); -#endif /** traverse objects allocated after @p ckp, to make sure their children * are forwarded. Repeat until traverse doesn't find any unforwarded children **/ void _forward_children_until_fixpoint(Generation upto, const GCMoveCheckpoint & ckp); +#endif /** Evacuate object at @p *lhs_data to to-space. * Replace original with forwarding pointer to new location diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index d6c24ca0..e1fbe591 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -820,6 +820,7 @@ namespace xo { } #endif +#ifdef OSBOLETE void DX1Collector::_forward_children_until_fixpoint(Generation upto, const GCMoveCheckpoint & gray_lo_v) @@ -828,6 +829,7 @@ namespace xo { gco_store_._forward_children_until_fixpoint(this, upto, gray_lo_v); } +#endif void DX1Collector::copy_roots(Generation upto) noexcept From 9cd39aef7b6e2b96dd0c2bb8b4b7c864a59e5f97 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 16:52:56 -0400 Subject: [PATCH 099/174] xo-gc: refactor: retire DX1Collector._forward_inplace_aux() --- include/xo/gc/DX1Collector.hpp | 2 +- src/gc/DX1Collector.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 555ef89a..ccc27536 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -388,12 +388,12 @@ namespace xo { **/ void _forward_children_until_fixpoint(Generation upto, const GCMoveCheckpoint & ckp); -#endif /** Evacuate object at @p *lhs_data to to-space. * Replace original with forwarding pointer to new location **/ void _forward_inplace_aux(AGCObject * lhs_iface, void ** lhs_data, Generation upto); +#endif /** Verify that pointer {@p iface, @p data} is valid: * destination either in to-space, or somewhere outside this collector diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index e1fbe591..015492b7 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -859,7 +859,7 @@ namespace xo { if (runstate_.is_running()) { // called during collection phase - this->_forward_inplace_aux(lhs_iface, lhs_data, upto); + gco_store_._forward_inplace_aux(this, lhs_iface, lhs_data, upto); } else if (runstate_.is_verify()) { // called during verify_ok this->_verify_aux(lhs_iface, *lhs_data); @@ -869,6 +869,7 @@ namespace xo { } } +#ifdef OBSOLETE void DX1Collector::_forward_inplace_aux(AGCObject * lhs_iface, void ** lhs_data, @@ -878,6 +879,7 @@ namespace xo { gco_store_._forward_inplace_aux(this, lhs_iface, lhs_data, upto); } /*_forward_inplace_aux*/ +#endif void DX1Collector::_verify_aux(AGCObject * iface, void * data) From 29cb4c46160d4426fc33b500920cfe5a63806cf1 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 16:58:25 -0400 Subject: [PATCH 100/174] xo-gc: refactor: move .report_object_ages() to GCObjectStore --- include/xo/gc/DX1Collector.hpp | 25 ---- include/xo/gc/GCObjectStore.hpp | 13 ++ src/gc/DX1Collector.cpp | 244 +------------------------------- src/gc/GCObjectStore.cpp | 81 +++++++++++ 4 files changed, 95 insertions(+), 268 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index ccc27536..514cc3da 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -370,31 +370,6 @@ namespace xo { /** cleanup after gc **/ void _cleanup_phase(Generation upto); -#ifdef OBSOLETE - /** move root subgraph at @p from_src to to-space. - * If not in gc-space, visit immediate children and move them. - * Require: runstate_.is_running() - **/ - void * _deep_move_root(obj from_src, Generation upto); - /** Common driver for _deep_move_root(), _deep_move_interior() **/ - void * _deep_move_gc_owned(void * from_src, Generation upto); - /** snap checkpoint containing allocator state - * use to detect forwarding activity after visiting objects - **/ - GCMoveCheckpoint _snap_move_checkpoint(Generation upto); - - /** traverse objects allocated after @p ckp, to make sure their children - * are forwarded. Repeat until traverse doesn't find any unforwarded children - **/ - void _forward_children_until_fixpoint(Generation upto, - const GCMoveCheckpoint & ckp); - - /** Evacuate object at @p *lhs_data to to-space. - * Replace original with forwarding pointer to new location - **/ - void _forward_inplace_aux(AGCObject * lhs_iface, void ** lhs_data, Generation upto); -#endif - /** Verify that pointer {@p iface, @p data} is valid: * destination either in to-space, or somewhere outside this collector **/ diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index f10381bd..b5be3dd1 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -97,6 +97,19 @@ namespace xo { obj error_mm, obj * p_output) const noexcept; + /** Report per-age-bucket information as an array of dictionaries. + * Scans to-space to count per-age statistics. + * Each dictionary has keys "n-live" and "bytes". + * Array index corresponds to object age. + * + * @p mm allocate stats from this allocator. + * @p error_mm allocator for error reporting when out-of-memory. + * @p p_output on exit @p *p_output contains stats array + **/ + bool report_object_ages(obj mm, + obj error_mm, + obj * p_output) const noexcept; + /** snap checkpoint containing allocator state * use to detect forwarding activity after visiting objects **/ diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 015492b7..3b2eefd8 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -314,113 +314,6 @@ namespace xo { obj * p_output) const noexcept { return gco_store_.report_object_types(mm, error_mm, p_output); - -#ifdef MOVED - scope log(XO_DEBUG(true)); - - (void)error_mm; - - bool ok = true; - - // stats, indexed by tseq - // could use c++ vector in scratch space instead of running on - // boxed types. - // - DArray * stats_v = DArray::empty(mm, object_types_.size()); - - if (!stats_v) - return false; - - stats_v->resize(stats_v->capacity()); - - log && log(xtag("object_types_.size", object_types_.size()), - xtag("stats_v.capacity", stats_v->capacity()), - xtag("stats_v.size", stats_v->size())); - - // count #of occupied type slots - std::uint32_t n_tseq_present = 0; - // largest tseq present with non-null AGCObject* iface - std::int32_t max_tseq = 0; - - for (const ObjectTypeSlot & slot : object_types_) { - AGCObject * iface = slot.iface(); - - if (iface) { - typeseq tseq = iface->_typeseq(); - - ++n_tseq_present; - if (max_tseq < tseq.seqno()) - max_tseq = tseq.seqno(); - - assert(tseq.seqno() >= 0); - - auto tname_sv = TypeRegistry::id2name(tseq); - DString * tname = DString::from_view(mm, tname_sv); - - DDictionary * recd = DDictionary::make(mm); - - if (!recd) - return false; - - recd->upsert_cstr(mm, "name", obj(tname)); - recd->upsert_cstr(mm, "tseq", DInteger::box(mm, tseq.seqno())); - recd->upsert_cstr(mm, "n-live", DInteger::box(mm, 0)); - recd->upsert_cstr(mm, "bytes", DInteger::box(mm, 0)); - - stats_v->assign_at(tseq.seqno(), obj(recd)); - } - } - - // scan to-space, count objects by type - - for (Generation g{0}; g < config_.n_generation_; ++g) { - const DArena * arena = this->get_space(role::to_space(), g); - - for (AllocInfo info : *arena) { - if (info.is_forwarding_tseq()) { - assert(false); - return false; - } - - uint32_t ix = info.tseq(); - size_t z = info.size(); - - auto recd = obj::from(stats_v->at(ix)); - - assert(recd); - - auto n_live_opt = recd->lookup_cstr("n-live"); - assert(n_live_opt); - auto bytes_opt = recd->lookup_cstr("bytes"); - assert(bytes_opt); - - if (n_live_opt && bytes_opt) { - auto n_live_gco = obj::from(n_live_opt.value()); - auto bytes_gco = obj::from(bytes_opt.value()); - - n_live_gco->assign_value(n_live_gco->value() + 1); - bytes_gco->assign_value(bytes_gco->value() + z); - } - } - } - - stats_v->resize(max_tseq + 1); - - DArray * final_stats_v = DArray::empty(mm, n_tseq_present); - - for (std::size_t i = 0, n = stats_v->size(); i < n; ++i) { - auto recd = stats_v->at(i); - - if (recd) { - bool ok = final_stats_v->push_back(recd); - assert(ok); - } - } - - *p_output = obj(final_stats_v); - - return ok; -#endif } bool @@ -428,82 +321,7 @@ namespace xo { obj error_mm, obj * p_output) const noexcept { - //return gco_store_.report_object_ages(mm, error_mm, p_output); - - scope log(XO_DEBUG(true)); - - (void)error_mm; - - std::uint64_t n_age = config_.arena_config_.header_.max_age() + 1; - - // stats, indexed by age - DArray * stats_v = DArray::empty(mm, n_age); - - if (!stats_v) - return false; - - // pre-populate with empty dictionaries for each age bucket - for (std::uint64_t a = 0; a < n_age; ++a) { - DDictionary * recd = DDictionary::make(mm); - - if (!recd) - return false; - - recd->upsert_cstr(mm, "age", DInteger::box(mm, a)); - recd->upsert_cstr(mm, "n-live", DInteger::box(mm, 0)); - recd->upsert_cstr(mm, "bytes", DInteger::box(mm, 0)); - - stats_v->push_back(obj(recd)); - } - - log && log(xtag("n_age", n_age), - xtag("stats_v.size", stats_v->size())); - - // scan to-space, count objects by age - - // track largest age with at least one object - std::int64_t max_age_present = 0; - - for (Generation g{0}; g < config_.n_generation_; ++g) { - const DArena * arena = this->get_space(role::to_space(), g); - - for (AllocInfo info : *arena) { - if (info.is_forwarding_tseq()) { - assert(false); - return false; - } - - uint32_t age = info.age(); - size_t z = info.size(); - - if (static_cast(age) > max_age_present) - max_age_present = age; - - auto recd = obj::from(stats_v->at(age)); - - assert(recd); - - auto n_live_opt = recd->lookup_cstr("n-live"); - assert(n_live_opt); - auto bytes_opt = recd->lookup_cstr("bytes"); - assert(bytes_opt); - - if (n_live_opt && bytes_opt) { - auto n_live_gco = obj::from(n_live_opt.value()); - auto bytes_gco = obj::from(bytes_opt.value()); - - n_live_gco->assign_value(n_live_gco->value() + 1); - bytes_gco->assign_value(bytes_gco->value() + z); - } - } - } - - // trim to only report ages up to max observed - stats_v->resize(max_age_present + 1); - - *p_output = obj(stats_v); - - return true; + return gco_store_.report_object_types(mm, error_mm, p_output); } size_type @@ -783,54 +601,6 @@ namespace xo { this->runstate_ = GCRunState::idle(); } -#ifdef OBSOLETE - void * - DX1Collector::_deep_move_root(obj from_src, - Generation upto) - { - return gco_store_._deep_move_root(this, from_src, upto); - } -#endif - -#ifdef OBSOLETE - /* - * rules: - * - from_src must be in from-space - * - object type stored in alloc header - * - return value is new location in to-space - * - * - preserving i/face pointer - * - replace destination with forwarding pointer - * - * EDITOR: gc -> self - */ - void * - DX1Collector::_deep_move_gc_owned(void * from_src, - Generation upto) - { - return gco_store_._deep_move_gc_owned(this, from_src, upto); - } /*_deep_move_gc_owned*/ -#endif - -#ifdef OBSOLETE - auto - DX1Collector::_snap_move_checkpoint(Generation upto) -> GCMoveCheckpoint - { - return gco_store_.snap_move_checkpoint(upto); - } -#endif - -#ifdef OSBOLETE - void - DX1Collector::_forward_children_until_fixpoint(Generation upto, - const GCMoveCheckpoint & gray_lo_v) - { - // problem -- need object type lookup - - gco_store_._forward_children_until_fixpoint(this, upto, gray_lo_v); - } -#endif - void DX1Collector::copy_roots(Generation upto) noexcept { @@ -869,18 +639,6 @@ namespace xo { } } -#ifdef OBSOLETE - void - DX1Collector::_forward_inplace_aux(AGCObject * lhs_iface, - void ** lhs_data, - Generation upto) - { - // upto == runstate_.gc_upto() - - gco_store_._forward_inplace_aux(this, lhs_iface, lhs_data, upto); - } /*_forward_inplace_aux*/ -#endif - void DX1Collector::_verify_aux(AGCObject * iface, void * data) { diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 27f70a74..9bbd2107 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -352,6 +352,87 @@ namespace xo { return ok; } + bool + GCObjectStore::report_object_ages(obj mm, + obj error_mm, + obj * p_output) const noexcept + { + scope log(XO_DEBUG(true)); + + (void)error_mm; + + std::uint64_t n_age = config_.arena_config_.header_.max_age() + 1; + + // stats, indexed by age + DArray * stats_v = DArray::empty(mm, n_age); + + if (!stats_v) + return false; + + // pre-populate with empty dictionaries for each age bucket + for (std::uint64_t a = 0; a < n_age; ++a) { + DDictionary * recd = DDictionary::make(mm); + + if (!recd) + return false; + + recd->upsert_cstr(mm, "age", DInteger::box(mm, a)); + recd->upsert_cstr(mm, "n-live", DInteger::box(mm, 0)); + recd->upsert_cstr(mm, "bytes", DInteger::box(mm, 0)); + + stats_v->push_back(obj(recd)); + } + + log && log(xtag("n_age", n_age), + xtag("stats_v.size", stats_v->size())); + + // scan to-space, count objects by age + + // track largest age with at least one object + std::int64_t max_age_present = 0; + + for (Generation g{0}; g < config_.n_generation_; ++g) { + const DArena * arena = this->get_space(role::to_space(), g); + + for (AllocInfo info : *arena) { + if (info.is_forwarding_tseq()) { + assert(false); + return false; + } + + uint32_t age = info.age(); + size_t z = info.size(); + + if (static_cast(age) > max_age_present) + max_age_present = age; + + auto recd = obj::from(stats_v->at(age)); + + assert(recd); + + auto n_live_opt = recd->lookup_cstr("n-live"); + assert(n_live_opt); + auto bytes_opt = recd->lookup_cstr("bytes"); + assert(bytes_opt); + + if (n_live_opt && bytes_opt) { + auto n_live_gco = obj::from(n_live_opt.value()); + auto bytes_gco = obj::from(bytes_opt.value()); + + n_live_gco->assign_value(n_live_gco->value() + 1); + bytes_gco->assign_value(bytes_gco->value() + z); + } + } + } + + // trim to only report ages up to max observed + stats_v->resize(max_age_present + 1); + + *p_output = obj(stats_v); + + return true; + } + bool GCObjectStore::_check_move_policy(header_type alloc_hdr, void * object_data, From 7524f30bf819c80d8717a139be6f1df92e2ae095 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 17:01:20 -0400 Subject: [PATCH 101/174] xo-gc: refactor: retire DX1Collector.forward_mutation_log() --- include/xo/gc/DX1Collector.hpp | 3 --- src/gc/DX1Collector.cpp | 8 +------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 514cc3da..40c69dfd 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -364,9 +364,6 @@ namespace xo { /** copy roots + everything reachable from them, to to-space **/ void copy_roots(Generation upto) noexcept; - /** cureate new mutation log after copying roots **/ - void forward_mutation_log(Generation upto); - /** cleanup after gc **/ void _cleanup_phase(Generation upto); diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 3b2eefd8..2fa284e1 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -550,7 +550,7 @@ namespace xo { log && log("step 2b : [STUB] copy pinned"); log && log("step 3 : [STUB] forward mutation log"); - this->forward_mutation_log(upto); + mlog_store_.forward_mutation_log(this, upto); log && log("step 4a : [STUB] run destructors"); log && log("step 4b : [STUB] keep reachable weak pointers"); @@ -586,12 +586,6 @@ namespace xo { mlog_store_.swap_roles(upto); } - void - DX1Collector::forward_mutation_log(Generation upto) - { - mlog_store_.forward_mutation_log(this, upto); - } - void DX1Collector::_cleanup_phase(Generation upto) { From cd7adc3bb95ba632dd9b8fa58b47be75a1adc3ed Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 17:02:26 -0400 Subject: [PATCH 102/174] xo-gc: refactor: retire DX1Collector._shallow_move() --- include/xo/gc/DX1Collector.hpp | 2 ++ src/gc/DX1Collector.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 40c69dfd..612092e8 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -279,10 +279,12 @@ namespace xo { **/ void forward_inplace(AGCObject * lhs_iface, void ** lhs_data); +#ifdef OBSOLETE /** evacuate object with type @p iface at address @p from_src * to to-space. Return new to-space location. **/ void * _shallow_move(const AGCObject * iface, void * from_src); +#endif /** true iff {alloc_hdr, object_data} should move for * currently-running collection. diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 2fa284e1..b17185e0 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -662,11 +662,13 @@ namespace xo { } } +#ifdef OBSOLETE void * DX1Collector::_shallow_move(const AGCObject * iface, void * from_src) { return gco_store_._shallow_move(this, iface, from_src); } +#endif bool DX1Collector::check_move_policy(header_type alloc_hdr, From 033b83418cbec2162f27f48ab101e9c0c5f98b5f Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 17:05:33 -0400 Subject: [PATCH 103/174] xo-gc: refactor: streamline GCObjectStore .forward_inplace_aux() --- include/xo/gc/DX1Collector.hpp | 7 ------- include/xo/gc/GCObjectStore.hpp | 15 ++++++++------- src/gc/DX1Collector.cpp | 10 +--------- src/gc/GCObjectStore.cpp | 8 ++++---- 4 files changed, 13 insertions(+), 27 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 612092e8..85b0c18e 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -279,13 +279,6 @@ namespace xo { **/ void forward_inplace(AGCObject * lhs_iface, void ** lhs_data); -#ifdef OBSOLETE - /** evacuate object with type @p iface at address @p from_src - * to to-space. Return new to-space location. - **/ - void * _shallow_move(const AGCObject * iface, void * from_src); -#endif - /** true iff {alloc_hdr, object_data} should move for * currently-running collection. * diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index b5be3dd1..d801112f 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -173,6 +173,13 @@ namespace xo { void * gco_data, Generation upto) const noexcept; + public: + /** For each generation g in [0 ,.., upto) + * swap arenas assigned to {to-space, from-space}. + * Invoked once at the beginning of each gc cycle. + **/ + void swap_roles(Generation upto) noexcept; + /** 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 @@ -180,17 +187,11 @@ namespace xo { * * Replace original with forwarding pointer to new location **/ - void _forward_inplace_aux(DX1Collector * gc, + void forward_inplace_aux(DX1Collector * gc, AGCObject * lhs_iface, void ** lhs_data, Generation upto); - /** For each generation g in [0 ,.., upto) - * swap arenas assigned to {to-space, from-space}. - * Invoked once at the beginning of each gc cycle. - **/ - void swap_roles(Generation upto) noexcept; - /** Cleanup at the end of a gc cycle. * Reset from-space * (current from-space is former to-space, diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index b17185e0..4dd9a1ee 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -623,7 +623,7 @@ namespace xo { if (runstate_.is_running()) { // called during collection phase - gco_store_._forward_inplace_aux(this, lhs_iface, lhs_data, upto); + gco_store_.forward_inplace_aux(this, lhs_iface, lhs_data, upto); } else if (runstate_.is_verify()) { // called during verify_ok this->_verify_aux(lhs_iface, *lhs_data); @@ -662,14 +662,6 @@ namespace xo { } } -#ifdef OBSOLETE - void * - DX1Collector::_shallow_move(const AGCObject * iface, void * from_src) - { - return gco_store_._shallow_move(this, iface, from_src); - } -#endif - bool DX1Collector::check_move_policy(header_type alloc_hdr, void * object_data) const noexcept diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 9bbd2107..7828cbfe 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -454,10 +454,10 @@ namespace xo { } void - GCObjectStore::_forward_inplace_aux(DX1Collector * gc, - AGCObject * lhs_iface, - void ** lhs_data, - Generation upto) + GCObjectStore::forward_inplace_aux(DX1Collector * gc, + AGCObject * lhs_iface, + void ** lhs_data, + Generation upto) { // upto == runstate_.gc_upto() From 5e90e5f3efcf27051daa38258cd0c350914133e4 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 17:07:15 -0400 Subject: [PATCH 104/174] xo-gc: refactor: retire DX1Collector.check_move_policy() --- include/xo/gc/DX1Collector.hpp | 2 ++ src/gc/DX1Collector.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 85b0c18e..6eb4e3ac 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -279,12 +279,14 @@ namespace xo { **/ void forward_inplace(AGCObject * lhs_iface, void ** lhs_data); +#ifdef OBSOLETE /** true iff {alloc_hdr, object_data} should move for * currently-running collection. * * Require: runstate_.is_running() **/ bool check_move_policy(header_type alloc_hdr, void * object_data) const noexcept; +#endif // ----- allocation ----- diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 4dd9a1ee..baa775bf 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -662,6 +662,7 @@ namespace xo { } } +#ifdef OBSOLETE bool DX1Collector::check_move_policy(header_type alloc_hdr, void * object_data) const noexcept @@ -672,6 +673,7 @@ namespace xo { object_data, runstate_.gc_upto()); } +#endif auto DX1Collector::alloc(typeseq t, size_type z) noexcept -> value_type From ab0c9a5faddb792ae223d35c6ab6f876105ebdd2 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 17:08:57 -0400 Subject: [PATCH 105/174] xo-gc: GCObjectStore._check_move_policy() -> private --- include/xo/gc/GCObjectStore.hpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index d801112f..3c8bcc64 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -164,15 +164,6 @@ namespace xo { Generation upto, GCMoveCheckpoint gray_lo_v); - /** true iff {@p alloc_hdr, @p object_data} should move for - * a collection of all generations strictly younger than @p upto. - * - * Require: runstate_.is_running() - **/ - bool _check_move_policy(header_type alloc_hdr, - void * gco_data, - Generation upto) const noexcept; - public: /** For each generation g in [0 ,.., upto) * swap arenas assigned to {to-space, from-space}. @@ -209,6 +200,15 @@ namespace xo { /** auxiliary init function **/ void _init_space(); + /** true iff {@p alloc_hdr, @p object_data} should move for + * a collection of all generations strictly younger than @p upto. + * + * Require: runstate_.is_running() + **/ + bool _check_move_policy(header_type alloc_hdr, + void * gco_data, + Generation upto) const noexcept; + private: /** configuration for gc-aware object store **/ GCObjectStoreConfig config_; From 4dee53307e1676acde08df6bb0729b2a3aa6a639 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 17:10:04 -0400 Subject: [PATCH 106/174] xo-gc: privatise GCObjectStore._forward_children_until_fixpoint --- include/xo/gc/GCObjectStore.hpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 3c8bcc64..126933d6 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -154,16 +154,6 @@ namespace xo { const AGCObject * iface, void * from_src); - /** traverse objects allocated after @p ckp, to make sure their children - * are forwarded. Repeat until traverse doesn't find any unforwarded children. - * - * 1. Breadth-first implementation, bad for memory locality - * 2. Need @p gc for per-object-type forward_children api - **/ - void _forward_children_until_fixpoint(DX1Collector * gc, - Generation upto, - GCMoveCheckpoint gray_lo_v); - public: /** For each generation g in [0 ,.., upto) * swap arenas assigned to {to-space, from-space}. @@ -209,6 +199,16 @@ namespace xo { void * gco_data, Generation upto) const noexcept; + /** traverse objects allocated after @p ckp, to make sure their children + * are forwarded. Repeat until traverse doesn't find any unforwarded children. + * + * 1. Breadth-first implementation, bad for memory locality + * 2. Need @p gc for per-object-type forward_children api + **/ + void _forward_children_until_fixpoint(DX1Collector * gc, + Generation upto, + GCMoveCheckpoint gray_lo_v); + private: /** configuration for gc-aware object store **/ GCObjectStoreConfig config_; From 2f46ac6aa50f8c37e67a2d596dd74262b0678367 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 17:11:23 -0400 Subject: [PATCH 107/174] xo-gc: privatise GCObjectStore._shallow_move() --- include/xo/gc/GCObjectStore.hpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 126933d6..625745e4 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -146,14 +146,7 @@ namespace xo { void * from_src, Generation upto); - /** during a gc cycle: - * evacuate object @p from_src, with gc-object interface @p iface. - * Shallow: does not traverse children - **/ - void * _shallow_move(DX1Collector * gc, - const AGCObject * iface, - void * from_src); - + private: public: /** For each generation g in [0 ,.., upto) * swap arenas assigned to {to-space, from-space}. @@ -209,6 +202,14 @@ namespace xo { Generation upto, GCMoveCheckpoint gray_lo_v); + /** during a gc cycle: + * evacuate object @p from_src, with gc-object interface @p iface. + * Shallow: does not traverse children + **/ + void * _shallow_move(DX1Collector * gc, + const AGCObject * iface, + void * from_src); + private: /** configuration for gc-aware object store **/ GCObjectStoreConfig config_; From eba721959337967854ea8cdcefafa1143883aa82 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 17:29:18 -0400 Subject: [PATCH 108/174] xo-gc: streamlining/simplifying --- include/xo/gc/DX1Collector.hpp | 6 ++---- include/xo/gc/GCObjectStore.hpp | 33 +++++++++++++++--------------- include/xo/gc/MutationLogStore.hpp | 4 ++-- include/xo/gc/X1VerifyStats.hpp | 4 ++-- src/gc/DX1Collector.cpp | 17 ++------------- src/gc/GCObjectStore.cpp | 6 +++--- src/gc/MutationLogStore.cpp | 4 ++-- 7 files changed, 30 insertions(+), 44 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 6eb4e3ac..76e90d21 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -382,9 +382,7 @@ namespace xo { /** if > 0: need gc for all generations < gc_pending_upto_ **/ Generation gc_pending_upto_; - /** using arena to get extensible list of root objects. - * For each root store one address (type obj*) - * + /** * An Object x that supports AGCObject, but doesn't live in gc-space, * will get special treatment if it appears in root_set_: * collector will traverse x to forward pointers to gc-owned @@ -410,7 +408,7 @@ namespace xo { GCObjectStore gco_store_; /** counters collected during @ref verify_ok call **/ - VerifyStats verify_stats_; + X1VerifyStats verify_stats_; }; } /*namespace mm*/ } /*namespace xo*/ diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 625745e4..ab365a46 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -131,22 +131,6 @@ namespace xo { obj from_src, Generation upto); - /** move interior subgraph at @p from_src to to-space. - * no-op if not in gc-space. - **/ - void * _deep_move_interior(DX1Collector * gc, - void * from_src, - Generation upto); - - /** Common driver for _deep_move_root(), _deep_move_interior(). - * Move object subgraph @p from_src on behalf of @p gc collection cycle, - * covering generations in [0 ,.., upto). - **/ - void * _deep_move_gc_owned(DX1Collector * gc, - void * from_src, - Generation upto); - - private: public: /** For each generation g in [0 ,.., upto) * swap arenas assigned to {to-space, from-space}. @@ -154,6 +138,15 @@ namespace xo { **/ void swap_roles(Generation upto) noexcept; + /** move interior subgraph at @p from_src to to-space. + * no-op if not in gc-space. + * + * NOTE: load-bearing for MutationLogStore + **/ + void * deep_move_interior(DX1Collector * gc, + void * from_src, + Generation upto); + /** 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 @@ -192,6 +185,14 @@ namespace xo { void * gco_data, Generation upto) const noexcept; + /** Common driver for _deep_move_root(), _deep_move_interior(). + * Move object subgraph @p from_src on behalf of @p gc collection cycle, + * covering generations in [0 ,.., upto). + **/ + void * _deep_move_gc_owned(DX1Collector * gc, + void * from_src, + Generation upto); + /** traverse objects allocated after @p ckp, to make sure their children * are forwarded. Repeat until traverse doesn't find any unforwarded children. * diff --git a/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index 31d65e01..677baeb7 100644 --- a/include/xo/gc/MutationLogStore.hpp +++ b/include/xo/gc/MutationLogStore.hpp @@ -16,7 +16,7 @@ namespace xo { namespace mm { class DX1Collector; - class VerifyStats; + class X1VerifyStats; /** @brief container for X1 collector mutation logs **/ @@ -45,7 +45,7 @@ namespace xo { * Update counters in @p *p_verify_stats. **/ void verify_ok(GCObjectStore * gc, - VerifyStats * p_verify_stats) noexcept; + X1VerifyStats * p_verify_stats) noexcept; /** Append a single mutation to log for generation @p dest_g * Mutation modifies @p parent at address @p addr, diff --git a/include/xo/gc/X1VerifyStats.hpp b/include/xo/gc/X1VerifyStats.hpp index d5fbc5a7..0e2fa783 100644 --- a/include/xo/gc/X1VerifyStats.hpp +++ b/include/xo/gc/X1VerifyStats.hpp @@ -13,13 +13,13 @@ namespace xo { /** @brief info collected during a @ref DX1Collector::verify_ok call * (or @ref MutationLogState::verify_ok call) **/ - class VerifyStats { + class X1VerifyStats { public: bool is_ok() const noexcept { return (n_from_ == 0) && (n_fwd_ == 0) && (n_no_iface_ == 0); } - void clear() { *this = VerifyStats(); } + void clear() { *this = X1VerifyStats(); } /** number of gc roots examined **/ std::uint32_t n_gc_root_ = 0; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index baa775bf..95641dd0 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -391,7 +391,7 @@ namespace xo { // 2. visit roots for (GCRoot & root_slot : root_set_) { - VerifyStats pre = verify_stats_; + X1VerifyStats pre = verify_stats_; auto gco = *root_slot.root(); @@ -407,7 +407,7 @@ namespace xo { } - VerifyStats post = verify_stats_; + X1VerifyStats post = verify_stats_; // assert fail -> root contains ptr to from-space assert(pre.n_from_ == post.n_from_); @@ -662,19 +662,6 @@ namespace xo { } } -#ifdef OBSOLETE - bool - DX1Collector::check_move_policy(header_type alloc_hdr, - void * object_data) const noexcept - { - assert(runstate_.is_running()); - - return gco_store_._check_move_policy(alloc_hdr, - object_data, - runstate_.gc_upto()); - } -#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 7828cbfe..c933faf0 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -752,9 +752,9 @@ namespace xo { } void * - GCObjectStore::_deep_move_interior(DX1Collector * gc, - void * from_src, - Generation upto) + GCObjectStore::deep_move_interior(DX1Collector * gc, + void * from_src, + Generation upto) { scope log(XO_DEBUG(config_.debug_flag_)); diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index d957fe62..c22ef718 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -89,7 +89,7 @@ namespace xo { void MutationLogStore::verify_ok(GCObjectStore * gco_store, - VerifyStats * p_verify_stats) noexcept + X1VerifyStats * p_verify_stats) noexcept { // 4. scan mutation logs for (Generation g(0); g + 1 < config_.n_generation_; ++g) { @@ -402,7 +402,7 @@ namespace xo { GCObjectStore & gco_store = gc->gco_store(); - child_to = gco_store._deep_move_interior(gc, child_fr, upto); + child_to = gco_store.deep_move_interior(gc, child_fr, upto); // update child pointer in parent object *from_entry.p_data() = child_to; From f71d40e4a0e18249008eda5e6d901238ac13c91b Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 17:31:02 -0400 Subject: [PATCH 109/174] xo-gc: streamline naming --- include/xo/gc/DX1Collector.hpp | 4 ++-- src/gc/DX1Collector.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 76e90d21..5fe82353 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -357,9 +357,9 @@ namespace xo { void _init_space(const X1CollectorConfig & cfg); /** swap from- and to- roles for all generations < @p upto **/ - void swap_roles(Generation upto) noexcept; + void _swap_roles(Generation upto) noexcept; /** copy roots + everything reachable from them, to to-space **/ - void copy_roots(Generation upto) noexcept; + void _copy_roots(Generation upto) noexcept; /** cleanup after gc **/ void _cleanup_phase(Generation upto); diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 95641dd0..de2ea71e 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -539,13 +539,13 @@ namespace xo { log && log("step 0d : [STUB] scan for object statistics"); log && log("step 1 : swap from/to roles (now to-space is empty)"); - this->swap_roles(upto); + this->_swap_roles(upto); log && log(xtag("from_0", get_space(role::from_space(), Generation{0})->lo_), xtag("to_0", get_space(role::to_space(), Generation{0})->lo_)); log && log("step 2a : copy roots"); - this->copy_roots(upto); + this->_copy_roots(upto); log && log("step 2b : [STUB] copy pinned"); @@ -578,7 +578,7 @@ namespace xo { } void - DX1Collector::swap_roles(Generation upto) noexcept + DX1Collector::_swap_roles(Generation upto) noexcept { scope log(XO_DEBUG(true), xtag("upto", upto)); @@ -596,7 +596,7 @@ namespace xo { } void - DX1Collector::copy_roots(Generation upto) noexcept + DX1Collector::_copy_roots(Generation upto) noexcept { scope log(XO_DEBUG(true)); From 5d3c088ba7ef8c887536a61e4dba29bbec8647fd Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 17:47:06 -0400 Subject: [PATCH 110/174] xo-gc: improve public/private between MutationLogStore and X1 --- include/xo/gc/MutationLogConfig.hpp | 41 ++------------ include/xo/gc/MutationLogStore.hpp | 54 ++++++++++-------- include/xo/gc/X1CollectorConfig.hpp | 3 + src/gc/DX1Collector.cpp | 84 +++------------------------- src/gc/MutationLogConfig.cpp | 8 +-- src/gc/MutationLogStore.cpp | 87 +++++++++++++++++++++++++++-- 6 files changed, 134 insertions(+), 143 deletions(-) diff --git a/include/xo/gc/MutationLogConfig.hpp b/include/xo/gc/MutationLogConfig.hpp index e10f228c..08b75e12 100644 --- a/include/xo/gc/MutationLogConfig.hpp +++ b/include/xo/gc/MutationLogConfig.hpp @@ -17,55 +17,26 @@ namespace xo { class MutationLogConfig { public: MutationLogConfig(std::uint32_t ngen, -#ifdef OBSOLETE // in GCObjectStore - std::uint32_t survive, -#endif std::size_t mlog_z, + bool enabled_flag, bool debug_flag); -#ifdef OBSOLETE - /** generation that would contain an object that has survived - * @p age collections. Equals the number of times object - * has been promoted. - * - * Must be consistent - **/ - Generation age2gen(object_age age) const noexcept { - return Generation(age % n_survive_threshold_); - } -#endif - -#ifdef OBSOLETE - /** age threshold for promotion to generation @p g **/ - uint32_t promotion_threshold(Generation g) const noexcept { - - // TODO: may consider replacing with table-lookup - // Require: if two distinct ages promote to some gen g at the same time, - // then they also promote to gen g+k at the same time for all k>0. - - return g * n_survive_threshold_; - } -#endif - public: /** number of generations in use. * Same as @ref X1CollectorConfig::n_generation_ **/ std::uint32_t n_generation_ = 0; -#ifdef OBSOLETE - /** Number of promotion steps. - * An object that survives this number of collections - * advances to the next generation. - **/ - uint32_t n_survive_threshold_ = 2; -#endif - /** storage for xgen pointer bookkeeping (aka remembered sets). * Use 3x this value per generation **/ std::size_t mutation_log_z_ = 1024; + /** true if mlog feature enabled (i.e. incremental gc enabled). + * false to disable (in which case only full gc supported) + **/ + bool enabled_flag_ = false; + /** true to enable debug logging **/ bool debug_flag_ = false; }; diff --git a/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index 677baeb7..6d2604bc 100644 --- a/include/xo/gc/MutationLogStore.hpp +++ b/include/xo/gc/MutationLogStore.hpp @@ -40,13 +40,40 @@ namespace xo { void visit_pools(const MemorySizeVisitor & visitor) const; /** verify consistent mlog state, - * on behalf of collector @p gc. + * on behalf of gc-aware object store @p gc. * (using gc to identify location of objects). * Update counters in @p *p_verify_stats. **/ void verify_ok(GCObjectStore * gc, X1VerifyStats * p_verify_stats) noexcept; + /** on behalf of gc-aware object store @p gc, + * change the value of a child pointer at @p p_lhs + * with parent object @p parent. p_lhs and parent must belong + * to the same allocation. + **/ + void assign_member(GCObjectStore * gc, + void * parent, + obj * p_lhs, + obj rhs); + + /** swap {to, from} roles + **/ + void swap_roles(Generation upto) noexcept; + + /** On behalf of collector @p gc: + * + * forward mutation logs, for generations 0 <= g < @p upto, + * from from-space to to-space. + **/ + void forward_mutation_log(DX1Collector * gc, + Generation upto); + + private: + /** aux init function: create mutation log **/ + MutationLog _make_mlog(uint32_t igen, char tag_char, + size_t mlog_z, std::size_t page_z); + /** Append a single mutation to log for generation @p dest_g * Mutation modifies @p parent at address @p addr, * to refer to @p rhs. @@ -64,27 +91,10 @@ namespace xo { * pointer. This means can alway recover that pointer * by consulting the AllocHeader for the pointer target */ - void append_mutation(Generation dest_g, - void * parent, - void ** addr, - obj rhs); - - /** swap {to, from} roles - **/ - void swap_roles(Generation upto) noexcept; - - /** On behalf of collector @p gc: - * - * forward mutation logs, for generations 0 <= g < @p upto, - * from from-space to to-space. - **/ - void forward_mutation_log(DX1Collector * gc, - Generation upto); - - private: - /** aux init function: create mutation log **/ - MutationLog _make_mlog(uint32_t igen, char tag_char, - size_t mlog_z, std::size_t page_z); + void _append_mutation(Generation dest_g, + void * parent, + void ** addr, + obj rhs); /** On behalf of collctor @p gc: * diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp index 38f2d5f3..5bbc377a 100644 --- a/include/xo/gc/X1CollectorConfig.hpp +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -51,8 +51,11 @@ namespace xo { /** fetch configuration for mutation log store **/ MutationLogConfig mlog_config() const noexcept { + bool mlog_enabled_flag = allow_incremental_gc_; + return MutationLogConfig(n_generation_, mutation_log_z_, + mlog_enabled_flag, debug_flag_); } diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index de2ea71e..c4e754c1 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -701,78 +701,19 @@ namespace xo { // ++ stats.n_mutation_; - *p_lhs = rhs; - if (runstate_.is_running()) { + *p_lhs = rhs; + // for removal of all doubt: - // don't log mutations during GC cycle + // don't log mutations during GC cycle. + // That said: should not be happening! + assert(false); + return; - } - - if (!config_.allow_incremental_gc_) { - // only need to log mutations when incremental gc is enabled - return; - } - - // logging policy depends on: - // 1. generation of lhs - // 2. generation of rhs - - Generation src_g = this->generation_of(role::to_space(), p_lhs); - - if (src_g.is_sentinel()) { - // only need mlog entries for gc-owned pointers. - // In this case pointer does not originate in gc-owned space - return; - } - - Generation dest_g = this->generation_of(role::to_space(), rhs.data()); - - if (dest_g.is_sentinel()) { - // similarly, don't need mlog entry to non-gc-owned destination - return; - } - - if (src_g < dest_g) { - // young-to-old pointers don't need to be remembered, - // since a GC cycle that collects an (old) generation is guarnatted - // to also collect all younger generations. - return; - } - - if (src_g == dest_g) { - // for pointers within the same generation, need to log - // if source is older than destination. - - const DArena * arena = this->get_space(role::to_space(), src_g); - - const DArena::header_type * src_hdr = arena->obj2hdr(parent); - const DArena::header_type * dest_hdr = arena->obj2hdr(rhs.data()); - - assert(src_hdr && dest_hdr); - - if (this->header2age(*src_hdr) <= this->header2age(*dest_hdr)) { - // source and destination have the same age; - // therefore are always collected on the same set of GC cycles - // -> no need to remember separately. - return; - } else { - // even though {src,dest} belong to the same generation: - // source will be eligible for promotion before destination. - // At that point pointer would become a cross-generational pointer, - // so need to track it now. - - log && log("xage ptr -> must log"); - } } else { - log && log("xgen ptr -> must log"); + mlog_store_.assign_member(&gco_store_, parent, p_lhs, rhs); + } - - // control here: we have an older->younger pointer, need to log it - - void ** lhs_addr = reinterpret_cast(&(p_lhs->data_)); - - mlog_store_.append_mutation(dest_g, parent, lhs_addr, rhs); } /*assign_member*/ DX1CollectorIterator @@ -814,15 +755,6 @@ namespace xo { arena_end); } -#ifdef MOVED - void - DX1Collector::reverse_roles(Generation g) noexcept { - assert(g < config_.n_generation_); - - std::swap(space_[role::from_space()][g], space_[role::to_space()][g]); - } -#endif - void DX1Collector::clear() noexcept { for (role ri : role::all()) { diff --git a/src/gc/MutationLogConfig.cpp b/src/gc/MutationLogConfig.cpp index ed18a17f..dd1357a1 100644 --- a/src/gc/MutationLogConfig.cpp +++ b/src/gc/MutationLogConfig.cpp @@ -9,16 +9,12 @@ namespace xo { namespace mm { MutationLogConfig::MutationLogConfig(std::uint32_t ngen, -#ifdef OBSOLETE - std::uint32_t survive, -#endif std::size_t mlog_z, + bool enabled_flag, bool debug_flag) : n_generation_{ngen}, -#ifdef OBSOLETE - n_survive_threshold_{survive}, -#endif mutation_log_z_{mlog_z}, + enabled_flag_{enabled_flag}, debug_flag_{debug_flag} {} diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index c22ef718..20d5254c 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -136,10 +136,89 @@ namespace xo { } /*verify_ok*/ void - MutationLogStore::append_mutation(Generation dest_g, - void * parent, - void ** addr, - obj rhs) + MutationLogStore::assign_member(GCObjectStore * gco_store, + void * parent, + obj * p_lhs, + obj rhs) + { + scope log(XO_DEBUG(config_.debug_flag_), + xtag("parent", parent), xtag("lhs", p_lhs), xtag("rhs", rhs.data())); + + // ++ stats.n_mutation_; + + *p_lhs = rhs; + + if (!config_.enabled_flag_) { + // only need to log mutations when incremental gc is enabled + return; + } + + // logging policy depends on: + // 1. generation of lhs + // 2. generation of rhs + + Generation src_g = gco_store->generation_of(role::to_space(), p_lhs); + + if (src_g.is_sentinel()) { + // only need mlog entries for gc-owned pointers. + // In this case pointer does not originate in gc-owned space + return; + } + + Generation dest_g = gco_store->generation_of(role::to_space(), rhs.data()); + + if (dest_g.is_sentinel()) { + // similarly, don't need mlog entry to non-gc-owned destination + return; + } + + if (src_g < dest_g) { + // young-to-old pointers don't need to be remembered, + // since a GC cycle that collects an (old) generation is guarnatted + // to also collect all younger generations. + return; + } + + if (src_g == dest_g) { + // for pointers within the same generation, need to log + // if source is older than destination. + + const DArena * arena = gco_store->get_space(role::to_space(), src_g); + + const DArena::header_type * src_hdr = arena->obj2hdr(parent); + const DArena::header_type * dest_hdr = arena->obj2hdr(rhs.data()); + + assert(src_hdr && dest_hdr); + + if (gco_store->header2age(*src_hdr) <= gco_store->header2age(*dest_hdr)) { + // source and destination have the same age; + // therefore are always collected on the same set of GC cycles + // -> no need to remember separately. + return; + } else { + // even though {src,dest} belong to the same generation: + // source will be eligible for promotion before destination. + // At that point pointer would become a cross-generational pointer, + // so need to track it now. + + log && log("xage ptr -> must log"); + } + } else { + log && log("xgen ptr -> must log"); + } + + // control here: we have an older->younger pointer, need to log it + + void ** lhs_addr = reinterpret_cast(&(p_lhs->data_)); + + this->_append_mutation(dest_g, parent, lhs_addr, rhs); + } + + void + MutationLogStore::_append_mutation(Generation dest_g, + void * parent, + void ** addr, + obj rhs) { // mlog keyed by generation in which pointer _destination_ resides: // collection that moves destination generation around needs to also From 723ea7e3df9c7c76d4077bdd8b897b22a14e0ab9 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 17:49:00 -0400 Subject: [PATCH 111/174] xo-gc: tidy: drop _ prefix to GCObjectStore._deep_move_root. --- include/xo/gc/GCObjectStore.hpp | 7 +++---- src/gc/DX1Collector.cpp | 2 +- src/gc/GCObjectStore.cpp | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index ab365a46..9e9c5607 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -127,11 +127,10 @@ namespace xo { * Require: runstate_.is_running() **/ - void * _deep_move_root(DX1Collector * gc, - obj from_src, - Generation upto); + void * deep_move_root(DX1Collector * gc, + obj from_src, + Generation upto); - public: /** For each generation g in [0 ,.., upto) * swap arenas assigned to {to-space, from-space}. * Invoked once at the beginning of each gc cycle. diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index c4e754c1..795cd2f8 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -607,7 +607,7 @@ namespace xo { xtag("slot.root()", slot.root()), xtag("slot.root()->data_", slot.root()->data_)); - void * root_to = gco_store_._deep_move_root(this, *slot.root(), upto); + void * root_to = gco_store_.deep_move_root(this, *slot.root(), upto); slot.root()->reset_opaque(root_to); diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index c933faf0..573af2ea 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -707,9 +707,9 @@ namespace xo { } void * - GCObjectStore::_deep_move_root(DX1Collector * gc, - obj from_src, - Generation upto) + GCObjectStore::deep_move_root(DX1Collector * gc, + obj from_src, + Generation upto) { // NOTE: // Some roots are non-gc-owned nodes. From e08b659a73ae71aef70a2443372955e45c93c4c7 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 18:04:52 -0400 Subject: [PATCH 112/174] xo-gc: refactor: + GCObjectStore.verify_ok() + use in DX1Collector.verify_ok() --- include/xo/gc/GCObjectStore.hpp | 24 ++++++++++++++------ src/gc/DX1Collector.cpp | 34 +--------------------------- src/gc/GCObjectStore.cpp | 39 +++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 40 deletions(-) diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 9e9c5607..83aec4ec 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -7,7 +7,6 @@ #include "GCObjectStoreConfig.hpp" #include "ObjectTypeSlot.hpp" -//#include "Generation.hpp" #include "object_age.hpp" #include #include @@ -15,6 +14,7 @@ namespace xo { namespace mm { class DX1Collector; + class X1VerifyStats; /** @brief container to hold gc-aware objects for X1 collector **/ @@ -115,12 +115,28 @@ namespace xo { **/ GCMoveCheckpoint snap_move_checkpoint(Generation upto); + /** verify consistency of this object store, on behalf of collector @p gc. + * Advancing counters in @p *p_verify_stats. + * + * @p gc argument is load-bearing so we have collector interface + * to call AGCObject visitor method (forward_children()) on each + * object stored here. + **/ + void verify_ok(DX1Collector * gc, + X1VerifyStats * p_verify_stats) noexcept; + /** Register object type with this collector. * Provides shallow copy and pointer forwarding for instances of this * type. **/ bool install_type(const AGCObject & meta) noexcept; + /** For each generation g in [0 ,.., upto) + * swap arenas assigned to {to-space, from-space}. + * Invoked once at the beginning of each gc cycle. + **/ + void swap_roles(Generation upto) noexcept; + /** 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 (!). @@ -131,12 +147,6 @@ namespace xo { obj from_src, Generation upto); - /** For each generation g in [0 ,.., upto) - * swap arenas assigned to {to-space, from-space}. - * Invoked once at the beginning of each gc cycle. - **/ - void swap_roles(Generation upto) noexcept; - /** move interior subgraph at @p from_src to to-space. * no-op if not in gc-space. * diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 795cd2f8..b589bcd1 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -416,39 +416,7 @@ namespace xo { } // 3. scan to-space for each generation - for (Generation g(0); g < config_.n_generation_; ++g) { - const DArena * space = this->get_space(role::to_space(), g); - - for (const AllocInfo & info : *space) { - - if (info.is_forwarding_tseq()) { - ++verify_stats_.n_fwd_; - - } else { - typeseq tseq(info.tseq()); - - const AGCObject * iface = this->lookup_type(tseq); - - if (iface && !(iface->_has_null_vptr())) { - const void * data = info.payload().first; - - // 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.forward_children(self); - } else { - ++verify_stats_.n_no_iface_; - continue; - } - } - } - } + gco_store_.verify_ok(this, &(this->verify_stats_)); // 4. scan mutation logs mlog_store_.verify_ok(&gco_store_, diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 573af2ea..df897e37 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -682,6 +682,45 @@ namespace xo { return gray_lo_v; } + void + GCObjectStore::verify_ok(DX1Collector * gc, + X1VerifyStats * p_verify_stats) noexcept + { + for (Generation g(0); g < config_.n_generation_; ++g) { + const DArena * space = this->get_space(role::to_space(), g); + + for (const AllocInfo & info : *space) { + + if (info.is_forwarding_tseq()) { + ++(p_verify_stats->n_fwd_); + + } else { + typeseq tseq(info.tseq()); + + const AGCObject * iface = this->lookup_type(tseq); + + if (iface && !(iface->_has_null_vptr())) { + const void * data = info.payload().first; + + // 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.forward_children(gc->ref()); + } else { + ++(p_verify_stats->n_no_iface_); + continue; + } + } + } + } + } + /* editor bait: register_type */ bool GCObjectStore::install_type(const AGCObject & meta) noexcept From 0de80c6fc00f9abfa92df7d2fc8279fc73a3a49c Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 3 Apr 2026 18:33:09 -0400 Subject: [PATCH 113/174] xo-gc stack: + ACollector.shallow_copy() --- include/xo/gc/detail/ICollector_DX1Collector.hpp | 4 ++++ src/gc/facet/ICollector_DX1Collector.cpp | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index 1ae5fac0..6f203e77 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -107,6 +107,10 @@ Return false if installation fails (e.g. memory exhausted) **/ Require: gc not in progress **/ static void assign_member(DX1Collector & 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(DX1Collector & self, std::byte * src); /** evacuate @p *lhs, that refers to state with interface @p lhs_iface, to collector @p d's to-space. Replace *lhs_data with forwarding pointer diff --git a/src/gc/facet/ICollector_DX1Collector.cpp b/src/gc/facet/ICollector_DX1Collector.cpp index 33a28aa9..261deaa0 100644 --- a/src/gc/facet/ICollector_DX1Collector.cpp +++ b/src/gc/facet/ICollector_DX1Collector.cpp @@ -95,6 +95,11 @@ namespace xo { self.assign_member(parent, p_lhs, rhs); } auto + ICollector_DX1Collector::alloc_copy(DX1Collector & self, std::byte * src) -> void * + { + return self.alloc_copy(src); + } + auto ICollector_DX1Collector::forward_inplace(DX1Collector & self, AGCObject * lhs_iface, void ** lhs_data) -> void { self.forward_inplace(lhs_iface, lhs_data); From ef23bca2b1a65d949bbd1c42de5b8e3da9c2e307 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 4 Apr 2026 14:38:14 -0400 Subject: [PATCH 114/174] refactor: make AGCObject.shallow_copy() non-const prep for moving to ACollector interface --- include/xo/gc/GCObjectStore.hpp | 4 ++-- src/gc/GCObjectStore.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 83aec4ec..1191e17a 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -49,7 +49,7 @@ namespace xo { /** lookup interface from type sequence * (can use tseq = typeseq::id() for type T) **/ - const AGCObject * lookup_type(typeseq tseq) const noexcept; + AGCObject * lookup_type(typeseq tseq) const noexcept; /** generation to which pointer @p addr belongs, given role @p r; * sentinel if not found in this collector @@ -217,7 +217,7 @@ namespace xo { * Shallow: does not traverse children **/ void * _shallow_move(DX1Collector * gc, - const AGCObject * iface, + AGCObject * iface, void * from_src); private: diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index df897e37..9fb14498 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -110,7 +110,7 @@ namespace xo { return slot.is_occupied(); } - const AGCObject * + AGCObject * GCObjectStore::lookup_type(typeseq tseq) const noexcept { scope log(XO_DEBUG(false)); @@ -846,7 +846,7 @@ namespace xo { GCMoveCheckpoint gray_lo_v = this->snap_move_checkpoint(upto); //obj alloc(this); - const AGCObject * iface = lookup_type(tseq); + AGCObject * iface = this->lookup_type(tseq); assert(iface->_has_null_vptr() == false); @@ -861,7 +861,7 @@ namespace xo { void * GCObjectStore::_shallow_move(DX1Collector * gc, - const AGCObject * iface, + AGCObject * iface, void * from_src) { scope log(XO_DEBUG(config_.debug_flag_)); From 9790a186e6d150130db087c2f8ea9bb902239582 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 4 Apr 2026 15:00:53 -0400 Subject: [PATCH 115/174] refactor: rename GCObject.shallow_copy -> shallow_move resolve conflict since relying on move constructor in std_copy_for --- src/gc/GCObjectStore.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 9fb14498..621a1548 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -870,7 +870,7 @@ namespace xo { //obj gc_gco(gc); - void * to_dest = iface->shallow_copy(from_src, gc->ref()); + void * to_dest = iface->shallow_move(from_src, gc->ref()); log && log(xtag("from_src", from_src), xtag("to_dest", to_dest)); log && log(xtag("tseq", info.tseq()), From 85938f174fd40bde9fe081d2d195d99724fbc14e Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 4 Apr 2026 16:33:35 -0400 Subject: [PATCH 116/174] refactor: rename shallow_copy -> shallow_move + streamline Use RCollector.std_copy_for where appropriate --- src/gc/GCObjectStore.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 621a1548..4076fd90 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -870,7 +870,7 @@ namespace xo { //obj gc_gco(gc); - void * to_dest = iface->shallow_move(from_src, gc->ref()); + void * to_dest = iface->shallow_move(from_src, gc->ref()); log && log(xtag("from_src", from_src), xtag("to_dest", to_dest)); log && log(xtag("tseq", info.tseq()), From 19107fa4857b3699c7acfef299b03c7753466a9b Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 5 Apr 2026 18:07:14 -0400 Subject: [PATCH 117/174] xo-gc stack: refactor: introduce GCObjectVisitor facet Plan using to properly level GCObjectStore and MutationLogStore below Collector. [WIP] not used yet --- CMakeLists.txt | 7 +++ idl/IGCObjectVisitor_DX1Collector.json5 | 27 +++++++++ include/xo/gc/DX1Collector.hpp | 10 +--- .../detail/IGCObjectVisitor_DX1Collector.hpp | 58 +++++++++++++++++++ src/gc/CMakeLists.txt | 1 + src/gc/DX1Collector.cpp | 20 +++++++ .../facet/IGCObjectVisitor_DX1Collector.cpp | 27 +++++++++ 7 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 idl/IGCObjectVisitor_DX1Collector.json5 create mode 100644 include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp create mode 100644 src/gc/facet/IGCObjectVisitor_DX1Collector.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bacf4f14..aa0df8ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,13 @@ xo_add_genfacetimpl( INPUT idl/ICollector_DX1Collector.json5 ) +# note: manual target; generated code committed to git +xo_add_genfacetimpl( + TARGET xo-gc-facetimpl-gcobjectvisitor-x1collector + FACET_PKG xo_alloc2 + INPUT idl/IGCObjectVisitor_DX1Collector.json5 +) + # ---------------------------------------------------------------- xo_add_genfacet_all(xo-gc-genfacet-all) diff --git a/idl/IGCObjectVisitor_DX1Collector.json5 b/idl/IGCObjectVisitor_DX1Collector.json5 new file mode 100644 index 00000000..438aad0a --- /dev/null +++ b/idl/IGCObjectVisitor_DX1Collector.json5 @@ -0,0 +1,27 @@ +{ + mode: "implementation", + output_cpp_dir: "src/gc/facet", + output_hpp_dir: "include/xo/gc", + 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/GCObjectVisitor.json5", + brief: "provide AGCObjectVisitor interface for DX1Collector", + using_doxygen: true, + repr: "DX1Collector", + doc: [ + "Implement AGCObjectVisitor for DX1Collector.", + "Evacuate object pointer (migrate to to-space) during collection phase" + ], +} diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 5fe82353..6139dbb2 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -279,14 +279,10 @@ namespace xo { **/ void forward_inplace(AGCObject * lhs_iface, void ** lhs_data); -#ifdef OBSOLETE - /** true iff {alloc_hdr, object_data} should move for - * currently-running collection. - * - * Require: runstate_.is_running() + /** Supports the GCObjectVisitor facet. + * Synonym for forward_inplace **/ - bool check_move_policy(header_type alloc_hdr, void * object_data) const noexcept; -#endif + void visit_child(AGCObject * lhs_iface, void ** lhs_data); // ----- allocation ----- diff --git a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp b/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp new file mode 100644 index 00000000..c2696a78 --- /dev/null +++ b/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp @@ -0,0 +1,58 @@ +/** @file IGCObjectVisitor_DX1Collector.hpp + * + * Generated automagically from ingredients: + * 1. code generator: + * [xo-facet/codegen/genfacet] + * arguments: + * --input [idl/IGCObjectVisitor_DX1Collector.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [iface_facet_repr.hpp.j2] + * 3. idl for facet methods + * [idl/IGCObjectVisitor_DX1Collector.json5] + **/ + +#pragma once + +#include "GCObjectVisitor.hpp" +#include "DX1Collector.hpp" + +namespace xo { namespace mm { class IGCObjectVisitor_DX1Collector; } } + +namespace xo { + namespace facet { + template <> + struct FacetImplementation + { + using ImplType = xo::mm::IGCObjectVisitor_Xfer + ; + }; + } +} + +namespace xo { + namespace mm { + /** @class IGCObjectVisitor_DX1Collector + **/ + class IGCObjectVisitor_DX1Collector { + public: + /** @defgroup mm-gcobjectvisitor-dx1collector-type-traits **/ + ///@{ + using Copaque = xo::mm::AGCObjectVisitor::Copaque; + using Opaque = xo::mm::AGCObjectVisitor::Opaque; + ///@} + /** @defgroup mm-gcobjectvisitor-dx1collector-methods **/ + ///@{ + // const methods + + // non-const methods + /** visit child of a gc-aware object. May update child in-place! **/ + static void visit_child(DX1Collector & self, AGCObject * iface, void ** pp_data) noexcept; + ///@} + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end */ \ No newline at end of file diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index 9539bcdc..7cbce173 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -12,6 +12,7 @@ set(SELF_SRCS X1CollectorConfig.cpp DX1Collector.cpp facet/ICollector_DX1Collector.cpp + facet/IGCObjectVisitor_DX1Collector.cpp DX1CollectorIterator.cpp diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index b589bcd1..d31c78ca 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -587,6 +587,10 @@ namespace xo { DX1Collector::forward_inplace(AGCObject * lhs_iface, void ** lhs_data) { + // TODO: streamline once GCObject refactored so that + // forward_children takes GCObjectVisitor instead of Collector + // argument. + Generation upto = runstate_.gc_upto(); if (runstate_.is_running()) { @@ -601,6 +605,22 @@ namespace xo { } } + void + DX1Collector::visit_child(AGCObject * lhs_iface, + void ** lhs_data) + { + if (runstate_.is_running()) { + // called during collection phase + this->forward_inplace(lhs_iface, lhs_data); + } else if (runstate_.is_verify()) { + // called during verify_ok + this->_verify_aux(lhs_iface, *lhs_data); + } else { + // should be unreachable + assert(false); + } + } + void DX1Collector::_verify_aux(AGCObject * iface, void * data) { diff --git a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp new file mode 100644 index 00000000..edb35061 --- /dev/null +++ b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp @@ -0,0 +1,27 @@ +/** @file IGCObjectVisitor_DX1Collector.cpp + * + * Generated automagically from ingredients: + * 1. code generator: + * [xo-facet/codegen/genfacet] + * arguments: + * --input [idl/IGCObjectVisitor_DX1Collector.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [iface_facet_any.hpp.j2] + * 3. idl for facet methods + * [idl/IGCObjectVisitor_DX1Collector.json5] +**/ + +#include "detail/IGCObjectVisitor_DX1Collector.hpp" + +namespace xo { + namespace mm { + auto + IGCObjectVisitor_DX1Collector::visit_child(DX1Collector & self, AGCObject * iface, void ** pp_data) noexcept -> void + { + self.visit_child(iface, pp_data); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IGCObjectVisitor_DX1Collector.cpp */ From 86f553408a3dc5d48551009ec8c9edd45e8cc816 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 5 Apr 2026 23:53:02 -0400 Subject: [PATCH 118/174] refactor: + narrower interface for gc pointer forwarding add AGCObjectVisitor, instead of requiring ACollector. --- include/xo/gc/X1Collector.hpp | 1 + src/gc/DX1Collector.cpp | 5 +++-- src/gc/GCObjectStore.cpp | 12 ++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/include/xo/gc/X1Collector.hpp b/include/xo/gc/X1Collector.hpp index 38999844..c35f5251 100644 --- a/include/xo/gc/X1Collector.hpp +++ b/include/xo/gc/X1Collector.hpp @@ -8,5 +8,6 @@ #include "DX1Collector.hpp" #include "detail/ICollector_DX1Collector.hpp" #include "detail/IAllocator_DX1Collector.hpp" +#include "detail/IGCObjectVisitor_DX1Collector.hpp" /* end X1Collector.hpp */ diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index d31c78ca..a74b7f09 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include "object_age.hpp" @@ -382,7 +383,7 @@ namespace xo { // Add run state so DX1Collector can recognize forward_inplace() // calls made for the purpose of checking child pointers. - auto self = this->ref(); + auto self = this->ref(); GCRunState saved_runstate = runstate_; { @@ -403,7 +404,7 @@ namespace xo { // - X1Collector::forward_inplace() -> _verify_aux() // - gco.forward_children(self); + gco.visit_gco_children(self); } diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 4076fd90..ee900b50 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -498,7 +498,7 @@ namespace xo { log && log("disposition: not in from-space. Don't forward, but check children"); obj gco(lhs_iface, object_data); - gco.forward_children(gc->ref()); + gco.visit_gco_children(gc->ref()); return; } @@ -711,7 +711,7 @@ namespace xo { // Nested control reenters // X1Collector::forward_inplace() -> _verify_aux() // - gco.forward_children(gc->ref()); + gco.visit_gco_children(gc->ref()); } else { ++(p_verify_stats->n_no_iface_); continue; @@ -771,12 +771,12 @@ namespace xo { // we aren't moving from_src, it's not gc-owned. // However weare moving all its gc-owned children - auto gc_obj = gc->ref(); + auto gc_obj = gc->ref(); GCMoveCheckpoint gray_lo_v = this->snap_move_checkpoint(upto); - from_src.forward_children(gc_obj); + from_src.visit_gco_children(gc_obj); // For each generation g: // traverse objects newer than gray_lo_v[g], to make sure children @@ -1005,9 +1005,9 @@ namespace xo { assert(iface->_has_null_vptr() == false); - auto gc_gco = gc->ref(); + auto gc_gco = gc->ref(); - iface->forward_children(src, gc_gco); + iface->visit_gco_children(src, gc_gco); gray_lo_v[g] = ((std::byte *)src) + z; From d387355c7cd507da03eee93945cce99b401adf0a Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 6 Apr 2026 00:11:08 -0400 Subject: [PATCH 119/174] refactor: make shallow_move() available from AGCObjectVisitor --- include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp | 4 ++++ src/gc/GCObjectStore.cpp | 10 +++++----- src/gc/facet/IGCObjectVisitor_DX1Collector.cpp | 5 +++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp b/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp index c2696a78..6512f882 100644 --- a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp +++ b/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp @@ -47,6 +47,10 @@ namespace xo { // const methods // non-const methods + /** allocate copy of source object at address @p src. +Source must be owned by this collector. +Increments object age **/ + static void * alloc_copy(DX1Collector & self, std::byte * src); /** visit child of a gc-aware object. May update child in-place! **/ static void visit_child(DX1Collector & self, AGCObject * iface, void ** pp_data) noexcept; ///@} diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index ee900b50..33d2d240 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -454,7 +454,7 @@ namespace xo { } void - GCObjectStore::forward_inplace_aux(DX1Collector * gc, + GCObjectStore::forward_inplace_aux(DX1Collector * x1gc, AGCObject * lhs_iface, void ** lhs_data, Generation upto) @@ -498,7 +498,7 @@ namespace xo { log && log("disposition: not in from-space. Don't forward, but check children"); obj gco(lhs_iface, object_data); - gco.visit_gco_children(gc->ref()); + gco.visit_gco_children(x1gc->ref()); return; } @@ -609,7 +609,7 @@ namespace xo { * +----------+ */ - *lhs_data = this->_shallow_move(gc, lhs_iface, *lhs_data); + *lhs_data = this->_shallow_move(x1gc, lhs_iface, *lhs_data); /* * lhs obj (from-space) @@ -860,7 +860,7 @@ namespace xo { } /*_deep_move_gc_owned*/ void * - GCObjectStore::_shallow_move(DX1Collector * gc, + GCObjectStore::_shallow_move(DX1Collector * x1gc, AGCObject * iface, void * from_src) { @@ -870,7 +870,7 @@ namespace xo { //obj gc_gco(gc); - void * to_dest = iface->shallow_move(from_src, gc->ref()); + void * to_dest = iface->shallow_move(from_src, x1gc->ref()); log && log(xtag("from_src", from_src), xtag("to_dest", to_dest)); log && log(xtag("tseq", info.tseq()), diff --git a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp index edb35061..d4c609c4 100644 --- a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp +++ b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp @@ -15,6 +15,11 @@ namespace xo { namespace mm { + auto + IGCObjectVisitor_DX1Collector::alloc_copy(DX1Collector & self, std::byte * src) -> void * + { + return self.alloc_copy(src); + } auto IGCObjectVisitor_DX1Collector::visit_child(DX1Collector & self, AGCObject * iface, void ** pp_data) noexcept -> void { From 67d3008fe51af2ef1b2b118413ff2e9ce267871e Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 6 Apr 2026 15:21:48 -0400 Subject: [PATCH 120/174] refactor: use GCObjectVisitor api w/ gco_shallow_move --- src/gc/GCObjectStore.cpp | 2 +- src/gc/SetupGc.cpp | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 33d2d240..a3070afb 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -870,7 +870,7 @@ namespace xo { //obj gc_gco(gc); - void * to_dest = iface->shallow_move(from_src, x1gc->ref()); + void * to_dest = iface->gco_shallow_move(from_src, x1gc->ref()); log && log(xtag("from_src", from_src), xtag("to_dest", to_dest)); log && log(xtag("tseq", info.tseq()), diff --git a/src/gc/SetupGc.cpp b/src/gc/SetupGc.cpp index f99ea4e6..fa0ea721 100644 --- a/src/gc/SetupGc.cpp +++ b/src/gc/SetupGc.cpp @@ -24,9 +24,12 @@ namespace xo { FacetRegistry::register_impl(); FacetRegistry::register_impl(); + FacetRegistry::register_impl(); log && log(xtag("DX1Collector.tseq", typeseq::id())); + log && log(xtag("ACollector.tseq", typeseq::id())); + log && log(xtag("AGCObjectVisitor.tseq", typeseq::id())); return true; } From 4a26d16cb3318a7aa89311afa451a61d3eee81ec Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 6 Apr 2026 16:36:48 -0400 Subject: [PATCH 121/174] xo-gc: refactor: GCObjectStore only uses AGCObjectVisitor now previously needed DX1Collector* --- include/xo/gc/GCObjectStore.hpp | 21 ++++++++++---------- src/gc/DX1Collector.cpp | 10 ++++++---- src/gc/GCObjectStore.cpp | 34 +++++++++++++++------------------ src/gc/MutationLogStore.cpp | 16 ++++++++-------- 4 files changed, 39 insertions(+), 42 deletions(-) diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 1191e17a..a3292755 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -13,7 +13,6 @@ namespace xo { namespace mm { - class DX1Collector; class X1VerifyStats; /** @brief container to hold gc-aware objects for X1 collector @@ -122,7 +121,7 @@ namespace xo { * to call AGCObject visitor method (forward_children()) on each * object stored here. **/ - void verify_ok(DX1Collector * gc, + void verify_ok(obj gc, X1VerifyStats * p_verify_stats) noexcept; /** Register object type with this collector. @@ -143,7 +142,7 @@ namespace xo { * Require: runstate_.is_running() **/ - void * deep_move_root(DX1Collector * gc, + void * deep_move_root(obj gc, obj from_src, Generation upto); @@ -152,7 +151,7 @@ namespace xo { * * NOTE: load-bearing for MutationLogStore **/ - void * deep_move_interior(DX1Collector * gc, + void * deep_move_interior(obj gc, void * from_src, Generation upto); @@ -163,10 +162,10 @@ namespace xo { * * Replace original with forwarding pointer to new location **/ - void forward_inplace_aux(DX1Collector * gc, - AGCObject * lhs_iface, - void ** lhs_data, - Generation upto); + void forward_inplace_aux(obj gc, + AGCObject * lhs_iface, + void ** lhs_data, + Generation upto); /** Cleanup at the end of a gc cycle. * Reset from-space @@ -198,7 +197,7 @@ namespace xo { * Move object subgraph @p from_src on behalf of @p gc collection cycle, * covering generations in [0 ,.., upto). **/ - void * _deep_move_gc_owned(DX1Collector * gc, + void * _deep_move_gc_owned(obj gc, void * from_src, Generation upto); @@ -208,7 +207,7 @@ namespace xo { * 1. Breadth-first implementation, bad for memory locality * 2. Need @p gc for per-object-type forward_children api **/ - void _forward_children_until_fixpoint(DX1Collector * gc, + void _forward_children_until_fixpoint(obj gc, Generation upto, GCMoveCheckpoint gray_lo_v); @@ -216,7 +215,7 @@ namespace xo { * evacuate object @p from_src, with gc-object interface @p iface. * Shallow: does not traverse children **/ - void * _shallow_move(DX1Collector * gc, + void * _shallow_move(obj gc, AGCObject * iface, void * from_src); diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index a74b7f09..d6d889fa 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -417,7 +417,7 @@ namespace xo { } // 3. scan to-space for each generation - gco_store_.verify_ok(this, &(this->verify_stats_)); + gco_store_.verify_ok(this->ref(), &(this->verify_stats_)); // 4. scan mutation logs mlog_store_.verify_ok(&gco_store_, @@ -576,7 +576,7 @@ namespace xo { xtag("slot.root()", slot.root()), xtag("slot.root()->data_", slot.root()->data_)); - void * root_to = gco_store_.deep_move_root(this, *slot.root(), upto); + void * root_to = gco_store_.deep_move_root(this->ref(), *slot.root(), upto); slot.root()->reset_opaque(root_to); @@ -596,7 +596,7 @@ namespace xo { if (runstate_.is_running()) { // called during collection phase - gco_store_.forward_inplace_aux(this, lhs_iface, lhs_data, upto); + gco_store_.forward_inplace_aux(this->ref(), lhs_iface, lhs_data, upto); } else if (runstate_.is_verify()) { // called during verify_ok this->_verify_aux(lhs_iface, *lhs_data); @@ -611,8 +611,10 @@ namespace xo { void ** lhs_data) { if (runstate_.is_running()) { + Generation upto = runstate_.gc_upto(); + // called during collection phase - this->forward_inplace(lhs_iface, lhs_data); + gco_store_.forward_inplace_aux(this->ref(), lhs_iface, lhs_data, upto); } else if (runstate_.is_verify()) { // called during verify_ok this->_verify_aux(lhs_iface, *lhs_data); diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index a3070afb..a1cceaa1 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -4,7 +4,7 @@ **/ #include "GCObjectStore.hpp" -#include "X1Collector.hpp" +#include "X1VerifyStats.hpp" #include #include @@ -454,7 +454,7 @@ namespace xo { } void - GCObjectStore::forward_inplace_aux(DX1Collector * x1gc, + GCObjectStore::forward_inplace_aux(obj gc, AGCObject * lhs_iface, void ** lhs_data, Generation upto) @@ -465,7 +465,7 @@ namespace xo { xtag("lhs_data", lhs_data), xtag("*lhs_data", lhs_data ? *lhs_data : nullptr)); - /* coordinates with DX1Collector::_deep_move() */ + /* coordinates with _deep_move() */ /* * lhs obj @@ -498,7 +498,7 @@ namespace xo { log && log("disposition: not in from-space. Don't forward, but check children"); obj gco(lhs_iface, object_data); - gco.visit_gco_children(x1gc->ref()); + gco.visit_gco_children(gc); return; } @@ -609,7 +609,7 @@ namespace xo { * +----------+ */ - *lhs_data = this->_shallow_move(x1gc, lhs_iface, *lhs_data); + *lhs_data = this->_shallow_move(gc, lhs_iface, *lhs_data); /* * lhs obj (from-space) @@ -683,7 +683,7 @@ namespace xo { } void - GCObjectStore::verify_ok(DX1Collector * gc, + GCObjectStore::verify_ok(obj gc, X1VerifyStats * p_verify_stats) noexcept { for (Generation g(0); g < config_.n_generation_; ++g) { @@ -711,7 +711,7 @@ namespace xo { // Nested control reenters // X1Collector::forward_inplace() -> _verify_aux() // - gco.visit_gco_children(gc->ref()); + gco.visit_gco_children(gc); } else { ++(p_verify_stats->n_no_iface_); continue; @@ -746,7 +746,7 @@ namespace xo { } void * - GCObjectStore::deep_move_root(DX1Collector * gc, + GCObjectStore::deep_move_root(obj gc, obj from_src, Generation upto) { @@ -771,12 +771,10 @@ namespace xo { // we aren't moving from_src, it's not gc-owned. // However weare moving all its gc-owned children - auto gc_obj = gc->ref(); - GCMoveCheckpoint gray_lo_v = this->snap_move_checkpoint(upto); - from_src.visit_gco_children(gc_obj); + from_src.visit_gco_children(gc); // For each generation g: // traverse objects newer than gray_lo_v[g], to make sure children @@ -791,7 +789,7 @@ namespace xo { } void * - GCObjectStore::deep_move_interior(DX1Collector * gc, + GCObjectStore::deep_move_interior(obj gc, void * from_src, Generation upto) { @@ -809,7 +807,7 @@ namespace xo { } void * - GCObjectStore::_deep_move_gc_owned(DX1Collector * gc, + GCObjectStore::_deep_move_gc_owned(obj gc, void * from_src, Generation upto) { @@ -860,7 +858,7 @@ namespace xo { } /*_deep_move_gc_owned*/ void * - GCObjectStore::_shallow_move(DX1Collector * x1gc, + GCObjectStore::_shallow_move(obj gc, AGCObject * iface, void * from_src) { @@ -870,7 +868,7 @@ namespace xo { //obj gc_gco(gc); - void * to_dest = iface->gco_shallow_move(from_src, x1gc->ref()); + 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("tseq", info.tseq()), @@ -903,7 +901,7 @@ namespace xo { } /*_shallow_move*/ void - GCObjectStore::_forward_children_until_fixpoint(DX1Collector * gc, + GCObjectStore::_forward_children_until_fixpoint(obj gc, Generation upto, GCMoveCheckpoint gray_lo_v) { @@ -1005,9 +1003,7 @@ namespace xo { assert(iface->_has_null_vptr() == false); - auto gc_gco = gc->ref(); - - iface->visit_gco_children(src, gc_gco); + iface->visit_gco_children(src, gc); gray_lo_v[g] = ((std::byte *)src) + z; diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 20d5254c..9cf7e9b1 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -4,7 +4,7 @@ **/ #include "MutationLogStore.hpp" -#include "DX1Collector.hpp" +#include "X1Collector.hpp" // temporary namespace xo { namespace mm { @@ -439,17 +439,17 @@ namespace xo { } return counters; - } + } /*forward_mutation_log_phase*/ MutationLogStatistics - MutationLogStore::_preserve_child_of_live_parent(DX1Collector * gc, + MutationLogStore::_preserve_child_of_live_parent(DX1Collector * x1gc, Generation upto, Generation parent_gen, const MutationLogEntry & from_entry, MutationLog * keep_mlog) { void * child_fr = *from_entry.p_data(); - AllocInfo child_info = gc->alloc_info((std::byte *)(child_fr)); + AllocInfo child_info = x1gc->alloc_info((std::byte *)(child_fr)); MutationLogStatistics counters; @@ -464,6 +464,8 @@ namespace xo { // or already evacuated. // (+ remember this need not be 1st pass over mlog entries) + GCObjectStore & gco_store = x1gc->gco_store(); + if (child_info.is_forwarding_tseq()) { // [MLOG1] @@ -479,9 +481,7 @@ namespace xo { ++counters.n_rescue_; - GCObjectStore & gco_store = gc->gco_store(); - - child_to = gco_store.deep_move_interior(gc, child_fr, upto); + child_to = gco_store.deep_move_interior(x1gc->ref(), child_fr, upto); // update child pointer in parent object *from_entry.p_data() = child_to; @@ -489,7 +489,7 @@ namespace xo { // child_to generation in {gen, gen+1} - this->_check_keep_mutation_aux(gc->gco_store(), + this->_check_keep_mutation_aux(gco_store, from_entry, parent_gen, child_to, keep_mlog); return counters; From fd548d6816805b47c3347b660c02b3baa2019e37 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 6 Apr 2026 17:15:30 -0400 Subject: [PATCH 122/174] xo-gc stack: refactor: + alloc_info() method replicated form AAllocator facet --- include/xo/gc/DX1Collector.hpp | 2 +- include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp | 3 +++ src/gc/DX1Collector.cpp | 4 ++-- src/gc/facet/IGCObjectVisitor_DX1Collector.cpp | 6 ++++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 6139dbb2..9ffe5fea 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -227,7 +227,7 @@ namespace xo { bool is_forwarding_header(header_type hdr) const noexcept; /** Retreive bookkeeping info for allocation at @p mem. **/ - AllocInfo alloc_info(value_type mem) const noexcept; + AllocInfo alloc_info(void * mem) const noexcept; /** true iff type with id @p tseq has known metadata * (i.e. has appeared in preceding call to install_type diff --git a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp b/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp index 6512f882..ce01500b 100644 --- a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp +++ b/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp @@ -45,6 +45,9 @@ namespace xo { /** @defgroup mm-gcobjectvisitor-dx1collector-methods **/ ///@{ // const methods + /** allocation metadata for gc-aware data at address @p gco. +@p gco must be the result of a call to collector's alloc() function **/ + static AllocInfo alloc_info(const DX1Collector & self, void * addr); // non-const methods /** allocate copy of source object at address @p src. diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index d6d889fa..9075f44c 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -350,8 +350,8 @@ namespace xo { } AllocInfo - DX1Collector::alloc_info(value_type mem) const noexcept { - return gco_store_.alloc_info(mem); + DX1Collector::alloc_info(void * mem) const noexcept { + return gco_store_.alloc_info((std::byte *)(mem)); } bool diff --git a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp index d4c609c4..5dd9d3d6 100644 --- a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp +++ b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp @@ -15,6 +15,12 @@ namespace xo { namespace mm { + auto + IGCObjectVisitor_DX1Collector::alloc_info(const DX1Collector & self, void * addr) -> AllocInfo + { + return self.alloc_info(addr); + } + auto IGCObjectVisitor_DX1Collector::alloc_copy(DX1Collector & self, std::byte * src) -> void * { From 5f9a81e6685a445249d36192b7a73db3dbc6b8d8 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 6 Apr 2026 19:37:11 -0400 Subject: [PATCH 123/174] xo-gc: MutationLogStore -> GCObjectStore --- include/xo/gc/DX1Collector.hpp | 8 ++++---- include/xo/gc/MutationLogStore.hpp | 11 ++++++++--- src/gc/DX1Collector.cpp | 4 ++-- src/gc/MutationLogStore.cpp | 25 ++++++++++++------------- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 9ffe5fea..3140e6be 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -389,6 +389,10 @@ namespace xo { **/ RootSet root_set_; + /** Collector-managed memory. + **/ + GCObjectStore gco_store_; + /** "remembered sets": track pointers P->C that require special handling * during a gc cycle where either: * 1. xgen pointers g(P) > g(C): @@ -399,10 +403,6 @@ namespace xo { **/ MutationLogStore mlog_store_; - /** Collector-managed memory. - **/ - GCObjectStore gco_store_; - /** counters collected during @ref verify_ok call **/ X1VerifyStats verify_stats_; }; diff --git a/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index 6d2604bc..4ef37e30 100644 --- a/include/xo/gc/MutationLogStore.hpp +++ b/include/xo/gc/MutationLogStore.hpp @@ -26,7 +26,8 @@ namespace xo { using size_type = DArena::size_type; public: - explicit MutationLogStore(const MutationLogConfig & config); + explicit MutationLogStore(const MutationLogConfig & config, + GCObjectStore * gco_store); /** Initialize mlog state * with o/s page size @p page_z @@ -140,8 +141,7 @@ namespace xo { * helper function to decide whether to keep a mutation log entry * @return true iff mlog entry appended to @p keep_mlog **/ - bool _check_keep_mutation_aux(const GCObjectStore & gco_store, - const MutationLogEntry & from_entry, + bool _check_keep_mutation_aux(const MutationLogEntry & from_entry, Generation parent_gen_to, void * child_to, MutationLog * keep_mlog); @@ -151,6 +151,11 @@ namespace xo { /** configuration for mlog store **/ MutationLogConfig config_; + /** stores GCOs (gc-aware objects) owned by the incremental collector + * with this mutaiton-log store + **/ + GCObjectStore * gco_store_ = nullptr; + /** Cross-generational mutations tracked in MutationLogs. * We need three logs per generation: * A. one to observe and remember mutations in to-space diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 9075f44c..78a298af 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -69,8 +69,8 @@ namespace xo { DX1Collector::DX1Collector(const X1CollectorConfig & cfg) : config_{cfg}, - mlog_store_{cfg.mlog_config()}, - gco_store_{cfg.gco_store_config()} + gco_store_{cfg.gco_store_config()}, + mlog_store_{cfg.mlog_config(), &gco_store_} { assert(config_.arena_config_.header_.size_bits_ + config_.arena_config_.header_.age_bits_ + diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 9cf7e9b1..46e5c3e4 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -9,8 +9,10 @@ namespace xo { namespace mm { - MutationLogStore::MutationLogStore(const MutationLogConfig & config) - : config_{config} + MutationLogStore::MutationLogStore(const MutationLogConfig & config, + GCObjectStore * gco_store) + : config_{config}, + gco_store_{gco_store} {} void @@ -411,8 +413,7 @@ namespace xo { MutationLogEntry to_entry(parent_to, p_data_to, from_entry.snap()); - this->_check_keep_mutation_aux(gc->gco_store(), - to_entry, + this->_check_keep_mutation_aux(to_entry, parent_gen_to, child_to, keep_mlog); @@ -464,7 +465,7 @@ namespace xo { // or already evacuated. // (+ remember this need not be 1st pass over mlog entries) - GCObjectStore & gco_store = x1gc->gco_store(); + //GCObjectStore & gco_store = x1gc->gco_store(); if (child_info.is_forwarding_tseq()) { // [MLOG1] @@ -481,7 +482,7 @@ namespace xo { ++counters.n_rescue_; - child_to = gco_store.deep_move_interior(x1gc->ref(), child_fr, upto); + child_to = gco_store_->deep_move_interior(x1gc->ref(), child_fr, upto); // update child pointer in parent object *from_entry.p_data() = child_to; @@ -489,26 +490,24 @@ namespace xo { // child_to generation in {gen, gen+1} - this->_check_keep_mutation_aux(gco_store, - from_entry, parent_gen, child_to, keep_mlog); + this->_check_keep_mutation_aux(from_entry, parent_gen, child_to, keep_mlog); return counters; } bool - MutationLogStore::_check_keep_mutation_aux(const GCObjectStore & gco_store, - const MutationLogEntry & from_entry, + MutationLogStore::_check_keep_mutation_aux(const MutationLogEntry & from_entry, Generation parent_gen_to, void * child_to, MutationLog * keep_mlog) { Generation child_gen_to - = gco_store.generation_of(role::to_space(), child_to); + = gco_store_->generation_of(role::to_space(), child_to); bool need_mlog_entry = ((child_gen_to + 1 < config_.n_generation_) - && (gco_store.config().promotion_threshold(parent_gen_to) - > gco_store.config().promotion_threshold(child_gen_to))); + && (gco_store_->config().promotion_threshold(parent_gen_to) + > gco_store_->config().promotion_threshold(child_gen_to))); if (need_mlog_entry) { // 1. P->C pointer is still cross-age (xage), and From 469570e74e33ab20d574bbd07a0f31cc99150da0 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 6 Apr 2026 23:02:15 -0400 Subject: [PATCH 124/174] xo-gc: refactoring to narrow collector<->mlog store interaction [WIP] --- include/xo/gc/MutationLogStore.hpp | 4 ++-- src/gc/MutationLogStore.cpp | 27 ++++++++++++++------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index 4ef37e30..c44aa080 100644 --- a/include/xo/gc/MutationLogStore.hpp +++ b/include/xo/gc/MutationLogStore.hpp @@ -115,7 +115,7 @@ namespace xo { * * @return number of mlog entries moved, whether to @p *to_mlog or @p *triage_mlog. **/ - MutationLogStatistics _forward_mutation_log_phase(DX1Collector * gc, + MutationLogStatistics _forward_mutation_log_phase(obj gc, Generation upto, Generation gen, MutationLog * from_mlog, @@ -130,7 +130,7 @@ namespace xo { * ensure child C is evacuated, and append @p from_entry to * @p keep_mlog. **/ - MutationLogStatistics _preserve_child_of_live_parent(DX1Collector * gc, + MutationLogStatistics _preserve_child_of_live_parent(obj gc, Generation upto, Generation parent_gen, const MutationLogEntry & from_entry, diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 46e5c3e4..b96e2981 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -266,7 +266,7 @@ namespace xo { MutationLog * to_mlog = this->mlog_[role::to_space()][child_gen]; MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; - auto stats = this->_forward_mutation_log_phase(gc, + auto stats = this->_forward_mutation_log_phase(gc->ref(), upto, child_gen, from_mlog, @@ -294,7 +294,7 @@ namespace xo { } MutationLogStatistics - MutationLogStore::_forward_mutation_log_phase(DX1Collector * gc, + MutationLogStore::_forward_mutation_log_phase(obj gc, Generation upto, Generation child_gen, MutationLog * from_mlog, @@ -377,12 +377,12 @@ namespace xo { /* here: mlog current */ Generation parent_gen_to = gc->generation_of(role::to_space(), - from_entry.parent()); + from_entry.parent()); if (parent_gen_to.is_sentinel()) { void * parent_fr = *from_entry.p_data(); - AllocInfo parent_info = gc->alloc_info((std::byte *)parent_fr); + AllocInfo parent_info = gc.alloc_info((std::byte *)parent_fr); if (parent_info.is_forwarding_tseq()) { /* [MLOG3] */ @@ -395,7 +395,7 @@ namespace xo { parent_gen_to = gc->generation_of(role::to_space(), parent_to); - parent_info = gc->alloc_info((std::byte *)parent_to); + parent_info = gc.alloc_info((std::byte *)parent_to); assert(!parent_gen_to.sentinel()); @@ -431,11 +431,12 @@ namespace xo { } else { /* [MLOG1, MLOG2] */ - counters += this->_preserve_child_of_live_parent(gc, - upto, - parent_gen_to, - from_entry, - keep_mlog); + counters + += this->_preserve_child_of_live_parent(gc, + upto, + parent_gen_to, + from_entry, + keep_mlog); } } @@ -443,14 +444,14 @@ namespace xo { } /*forward_mutation_log_phase*/ MutationLogStatistics - MutationLogStore::_preserve_child_of_live_parent(DX1Collector * x1gc, + MutationLogStore::_preserve_child_of_live_parent(obj gc, Generation upto, Generation parent_gen, const MutationLogEntry & from_entry, MutationLog * keep_mlog) { void * child_fr = *from_entry.p_data(); - AllocInfo child_info = x1gc->alloc_info((std::byte *)(child_fr)); + AllocInfo child_info = gc.alloc_info((std::byte *)(child_fr)); MutationLogStatistics counters; @@ -482,7 +483,7 @@ namespace xo { ++counters.n_rescue_; - child_to = gco_store_->deep_move_interior(x1gc->ref(), child_fr, upto); + child_to = gco_store_->deep_move_interior(gc, child_fr, upto); // update child pointer in parent object *from_entry.p_data() = child_to; From b9e4a36d472cb7e9576ed2b53658e1a4af266f5e Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 6 Apr 2026 23:18:45 -0400 Subject: [PATCH 125/174] refactor: xo-gc: + GCObjectVisitor.generation_of() Concession to narrow MutationLogStore to only use GCObjectVisitor instead of assuming X1Collector. --- include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp | 3 +++ src/gc/MutationLogStore.cpp | 8 ++++---- src/gc/facet/IGCObjectVisitor_DX1Collector.cpp | 6 ++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp b/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp index ce01500b..d1c09228 100644 --- a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp +++ b/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp @@ -48,6 +48,9 @@ namespace xo { /** allocation metadata for gc-aware data at address @p gco. @p gco must be the result of a call to collector's alloc() function **/ static AllocInfo alloc_info(const DX1Collector & self, void * addr); + /** generation to which pointer @p addr belongs, given role @p r; +sentinel if @p addr is not owned by collector **/ + static Generation generation_of(const DX1Collector & self, role r, const void * addr) noexcept; // non-const methods /** allocate copy of source object at address @p src. diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index b96e2981..24cbf86c 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -376,8 +376,8 @@ namespace xo { /* here: mlog current */ - Generation parent_gen_to = gc->generation_of(role::to_space(), - from_entry.parent()); + Generation parent_gen_to = gc.generation_of(role::to_space(), + from_entry.parent()); if (parent_gen_to.is_sentinel()) { void * parent_fr = *from_entry.p_data(); @@ -393,8 +393,8 @@ namespace xo { // TODO: method on AllocInfo to streamline this void * parent_to = *(void **)parent_fr; - parent_gen_to = gc->generation_of(role::to_space(), - parent_to); + parent_gen_to = gc.generation_of(role::to_space(), + parent_to); parent_info = gc.alloc_info((std::byte *)parent_to); assert(!parent_gen_to.sentinel()); diff --git a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp index 5dd9d3d6..129ba422 100644 --- a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp +++ b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp @@ -21,6 +21,12 @@ namespace xo { return self.alloc_info(addr); } + auto + IGCObjectVisitor_DX1Collector::generation_of(const DX1Collector & self, role r, const void * addr) noexcept -> Generation + { + return self.generation_of(r, addr); + } + auto IGCObjectVisitor_DX1Collector::alloc_copy(DX1Collector & self, std::byte * src) -> void * { From 006c1830b30407c20ee8a5f5665c985a6439596e Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 6 Apr 2026 23:22:13 -0400 Subject: [PATCH 126/174] xo-gc: MutationLogStore uses only AGCObjectVisitor Previously assumed DX1Collector --- include/xo/gc/MutationLogStore.hpp | 2 +- src/gc/DX1Collector.cpp | 2 +- src/gc/MutationLogStore.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index c44aa080..78042fac 100644 --- a/include/xo/gc/MutationLogStore.hpp +++ b/include/xo/gc/MutationLogStore.hpp @@ -67,7 +67,7 @@ namespace xo { * forward mutation logs, for generations 0 <= g < @p upto, * from from-space to to-space. **/ - void forward_mutation_log(DX1Collector * gc, + void forward_mutation_log(obj gc, Generation upto); private: diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 78a298af..0baddcb7 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -519,7 +519,7 @@ namespace xo { log && log("step 2b : [STUB] copy pinned"); log && log("step 3 : [STUB] forward mutation log"); - mlog_store_.forward_mutation_log(this, upto); + mlog_store_.forward_mutation_log(this->ref(), upto); log && log("step 4a : [STUB] run destructors"); log && log("step 4b : [STUB] keep reachable weak pointers"); diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 24cbf86c..77244878 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -244,7 +244,7 @@ namespace xo { } void - MutationLogStore::forward_mutation_log(DX1Collector * gc, + MutationLogStore::forward_mutation_log(obj gc, Generation upto) { /** non-zero if at least one object was rescued (from any generation) @@ -266,7 +266,7 @@ namespace xo { MutationLog * to_mlog = this->mlog_[role::to_space()][child_gen]; MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; - auto stats = this->_forward_mutation_log_phase(gc->ref(), + auto stats = this->_forward_mutation_log_phase(gc, upto, child_gen, from_mlog, From 268e474cfdee19b4de0cf380e88c9e9b6125d1b1 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 6 Apr 2026 23:33:05 -0400 Subject: [PATCH 127/174] xo-gc: cosmetic: rename xo::mm::role -> xo::mm::Role --- include/xo/gc/DX1Collector.hpp | 42 +++++++++---------- include/xo/gc/GCObjectStore.hpp | 20 ++++----- .../xo/gc/detail/ICollector_DX1Collector.hpp | 10 ++--- .../detail/IGCObjectVisitor_DX1Collector.hpp | 2 +- src/gc/DX1Collector.cpp | 42 +++++++++---------- src/gc/DX1CollectorIterator.cpp | 2 +- src/gc/GCObjectStore.cpp | 40 +++++++++--------- src/gc/IAllocator_DX1Collector.cpp | 2 +- src/gc/MutationLogStore.cpp | 30 ++++++------- src/gc/facet/ICollector_DX1Collector.cpp | 8 ++-- .../facet/IGCObjectVisitor_DX1Collector.cpp | 2 +- utest/X1Collector.test.cpp | 32 +++++++------- 12 files changed, 116 insertions(+), 116 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 3140e6be..aaa2b083 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -118,34 +118,34 @@ namespace xo { GCRunState runstate() const noexcept { return runstate_; } 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); } - DArena * get_space(role r, Generation g) noexcept { return gco_store_.get_space(r, g); } - 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); } + const DArena * get_space(Role r, Generation g) const noexcept { return gco_store_.get_space(r, g); } + DArena * get_space(Role r, Generation g) noexcept { return gco_store_.get_space(r, g); } + 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->to_space(Generation{0}); } // ----- basic statistics ----- - /** total reserved memory in bytes, across all {role, generation} **/ + /** total reserved memory in bytes, across all {Role, generation} **/ size_type reserved() const noexcept; /** total size in bytes (same as committed_total()) **/ size_type size_total() const noexcept; - /** total committed memory in bytes, across all {role, generation} **/ + /** total committed memory in bytes, across all {Role, generation} **/ size_type committed() const noexcept; - /** total available memory in bytes, across all {role, generation} **/ + /** total available memory in bytes, across all {Role, generation} **/ size_type available() const noexcept; - /** total allocated memory in bytes, across all {role, generation} **/ + /** total allocated memory in bytes, across all {Role, generation} **/ size_type allocated() const noexcept; /** total number of mutation log entries **/ size_type mutation_log_entries() const noexcept; - /** memory allocated for generation @p g in role @p r **/ - size_type allocated(Generation g, role r) const noexcept; - /** memory committed for generation @p g in role @p r **/ - size_type committed(Generation g, role r) const noexcept; - /** memory (virtual addresses) reserved for generation @p g in role @p r **/ - size_type reserved(Generation g, role r) const noexcept; + /** memory allocated for generation @p g in Role @p r **/ + size_type allocated(Generation g, Role r) const noexcept; + /** memory committed for generation @p g in Role @p r **/ + size_type committed(Generation g, Role r) const noexcept; + /** memory (virtual addresses) reserved for generation @p g in Role @p r **/ + size_type reserved(Generation g, Role r) const noexcept; /** very similar to generation_of(), but satisfies ACollector api **/ std::int32_t locate_address(const void * addr) const noexcept; @@ -197,21 +197,21 @@ namespace xo { void visit_pools(const MemorySizeVisitor & visitor) const; /** true iff address @p addr allocated from this collector - * in role @p r (according to current GC state) + * in Role @p r (according to current GC state) **/ - bool contains(role r, const void * addr) const noexcept; + bool contains(Role r, const void * addr) const noexcept; /** true iff address @p addr allocated from this collector and currently live - * in role @p r (according to current GC state) + * in Role @p r (according to current GC state) * * (i.e. in [lo,free) for an arena) **/ - bool contains_allocated(role r, const void * addr) const noexcept; + bool contains_allocated(Role r, const void * addr) const noexcept; - /** generation to which pointer @p addr belongs, given role @p r; + /** generation to which pointer @p addr belongs, given Role @p r; * sentinel if not found in this collector **/ - Generation generation_of(role r, const void * addr) const noexcept; + Generation generation_of(Role r, const void * addr) const noexcept; /** return details from last error (will be in gen0 to-space) **/ AllocError last_error() const noexcept; @@ -352,7 +352,7 @@ namespace xo { /** aux init function: initialize @ref space_storage_[][] arenas **/ void _init_space(const X1CollectorConfig & cfg); - /** swap from- and to- roles for all generations < @p upto **/ + /** swap from- and to- Roles for all generations < @p upto **/ void _swap_roles(Generation upto) noexcept; /** copy roots + everything reachable from them, to to-space **/ void _copy_roots(Generation upto) noexcept; diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index a3292755..dbb591c3 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -33,10 +33,10 @@ namespace xo { 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]; } - 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); } + const DArena * get_space(Role r, Generation g) const noexcept { return space_[r][g]; } + 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}); } /** true iff type with id @p tseq has known metadata @@ -50,10 +50,10 @@ namespace xo { **/ AGCObject * lookup_type(typeseq tseq) const noexcept; - /** generation to which pointer @p addr belongs, given role @p r; + /** generation to which pointer @p addr belongs, given Role @p r; * sentinel if not found in this collector **/ - Generation generation_of(role r, const void * addr) const noexcept; + Generation generation_of(Role r, const void * addr) const noexcept; /** get allocation size from header **/ std::size_t header2size(header_type hdr) const noexcept; @@ -72,16 +72,16 @@ namespace xo { void visit_pools(const MemorySizeVisitor & visitor) const; /** true iff address @p addr allocated from this collector - * in role @p r (according to current GC state) + * in Role @p r (according to current GC state) **/ - bool contains(role r, const void * addr) const noexcept; + bool contains(Role r, const void * addr) const noexcept; /** true iff address @p addr allocated from this collector and currently live - * in role @p r (according to current GC state) + * in Role @p r (according to current GC state) * * (i.e. in [lo,free) for an arena) **/ - bool contains_allocated(role r, const void * addr) const noexcept; + bool contains_allocated(Role r, const void * addr) const noexcept; /** Report per-age-bucket information as an array of dictionaries. * Scans to-space to count per-age statistics. diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index 6f203e77..c81513f0 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -48,17 +48,17 @@ namespace xo { ///@{ // const methods /** memory in use for this collector **/ - static size_type allocated(const DX1Collector & self, Generation g, role r) noexcept; + static size_type allocated(const DX1Collector & self, Generation g, Role r) noexcept; /** memory committed for this collector **/ - static size_type committed(const DX1Collector & self, Generation g, role r) noexcept; + static size_type committed(const DX1Collector & self, Generation g, Role r) noexcept; /** address space reserved for this collector **/ - static size_type reserved(const DX1Collector & self, Generation g, role r) noexcept; + static size_type reserved(const DX1Collector & 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 DX1Collector & 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 DX1Collector & self, role r, const void * addr) noexcept; + /** true if gc responsible for data at @p addr, and data belongs to Role @p r **/ + static bool contains(const DX1Collector & 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 DX1Collector & self, typeseq tseq) noexcept; /** Report gc statistics, at discretion of collector implementation. diff --git a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp b/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp index d1c09228..95534db6 100644 --- a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp +++ b/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp @@ -50,7 +50,7 @@ namespace xo { static AllocInfo alloc_info(const DX1Collector & self, void * addr); /** generation to which pointer @p addr belongs, given role @p r; sentinel if @p addr is not owned by collector **/ - static Generation generation_of(const DX1Collector & self, role r, const void * addr) noexcept; + static Generation generation_of(const DX1Collector & self, Role r, const void * addr) noexcept; // non-const methods /** allocate copy of source object at address @p src. diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 0baddcb7..28636368 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -110,19 +110,19 @@ namespace xo { } bool - DX1Collector::contains(role r, const void * addr) const noexcept + DX1Collector::contains(Role r, const void * addr) const noexcept { return gco_store_.contains(r, addr); } bool - DX1Collector::contains_allocated(role r, const void * addr) const noexcept + DX1Collector::contains_allocated(Role r, const void * addr) const noexcept { return gco_store_.contains_allocated(r, addr); } Generation - DX1Collector::generation_of(role r, const void * addr) const noexcept + DX1Collector::generation_of(Role r, const void * addr) const noexcept { return gco_store_.generation_of(r, addr); } @@ -134,7 +134,7 @@ namespace xo { // need to adjust here if runtime errors // encountered during gc. - return get_space(role::to_space(), Generation::nursery())->last_error_; + return get_space(Role::to_space(), Generation::nursery())->last_error_; } namespace { @@ -148,7 +148,7 @@ namespace xo { size_t z3 = 0; - for (role ri : role::all()) { + for (Role ri : Role::all()) { for (Generation gj{0}; gj < d.config_.n_generation_; ++gj) { const DArena * arena = d.get_space(ri, gj); @@ -203,7 +203,7 @@ namespace xo { stat_helper(const DX1Collector & d, size_type (DArena::* getter)() const, Generation g, - role r) + Role r) { const DArena * arena = d.get_space(r, g); @@ -215,19 +215,19 @@ namespace xo { } size_type - DX1Collector::allocated(Generation g, role r) const noexcept + DX1Collector::allocated(Generation g, Role r) const noexcept { return stat_helper(*this, &DArena::allocated, g, r); } size_type - DX1Collector::committed(Generation g, role r) const noexcept + DX1Collector::committed(Generation g, Role r) const noexcept { return stat_helper(*this, &DArena::committed, g, r); } size_type - DX1Collector::reserved(Generation g, role r) const noexcept + DX1Collector::reserved(Generation g, Role r) const noexcept { return stat_helper(*this, &DArena::reserved, g, r); } @@ -237,12 +237,12 @@ namespace xo { { Generation g; - g = this->generation_of(role::to_space(), addr); + g = this->generation_of(Role::to_space(), addr); if (!g.is_sentinel()) return g; - g = this->generation_of(role::from_space(), addr); + g = this->generation_of(Role::from_space(), addr); if (!g.is_sentinel()) { // use negative values for @@ -283,7 +283,7 @@ namespace xo { // per-(generation,role) info { for (Generation gi{0}; gi < config_.n_generation_; ++gi) { - for (role rj : role::all()) { + for (Role rj : Role::all()) { const DArena * arena = this->get_space(rj, gi); DDictionary * arena_d = DDictionary::make(mm); @@ -510,8 +510,8 @@ namespace xo { log && log("step 1 : swap from/to roles (now to-space is empty)"); this->_swap_roles(upto); - log && log(xtag("from_0", get_space(role::from_space(), Generation{0})->lo_), - xtag("to_0", get_space(role::to_space(), Generation{0})->lo_)); + log && log(xtag("from_0", get_space(Role::from_space(), Generation{0})->lo_), + xtag("to_0", get_space(Role::to_space(), Generation{0})->lo_)); log && log("step 2a : copy roots"); this->_copy_roots(upto); @@ -632,12 +632,12 @@ namespace xo { (void)iface; (void)data; - Generation g1 = this->generation_of(role::to_space(), data); + Generation g1 = this->generation_of(Role::to_space(), data); if (g1.is_sentinel()) { - assert(this->contains(role::to_space(), data) == false); + assert(this->contains(Role::to_space(), data) == false); - Generation g2 = this->generation_of(role::from_space(), data); + Generation g2 = this->generation_of(Role::from_space(), data); if (!g2.is_sentinel()) { // verify failure - live pointer still refers to from-space @@ -647,7 +647,7 @@ namespace xo { ++(verify_stats_.n_ext_); } } else { - assert(this->contains(role::to_space(), data)); + assert(this->contains(Role::to_space(), data)); ++(verify_stats_.n_to_); } @@ -713,7 +713,7 @@ namespace xo { scope log(XO_DEBUG(false)); const DArena * arena - = get_space(role::to_space(), + = get_space(Role::to_space(), Generation{0}); return DX1CollectorIterator(this, @@ -735,7 +735,7 @@ namespace xo { **/ const DArena * arena - = get_space(role::to_space(), + = get_space(Role::to_space(), Generation(config_.n_generation_ - 1)); DArenaIterator arena_end = arena->end(); @@ -748,7 +748,7 @@ namespace xo { void DX1Collector::clear() noexcept { - for (role ri : role::all()) { + for (Role ri : Role::all()) { for (Generation gj{0}; gj < config_.n_generation_; ++gj) { DArena * arena = this->get_space(ri, gj); diff --git a/src/gc/DX1CollectorIterator.cpp b/src/gc/DX1CollectorIterator.cpp index 9780aca5..624a3d1f 100644 --- a/src/gc/DX1CollectorIterator.cpp +++ b/src/gc/DX1CollectorIterator.cpp @@ -42,7 +42,7 @@ namespace xo { for (; gen_ix_ < gen_hi_; ++gen_ix_) { const DArena * arena - = gc_->get_space(role::to_space(), gen_ix_); + = gc_->get_space(Role::to_space(), gen_ix_); assert(arena); diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index a1cceaa1..f7ae5497 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -80,20 +80,20 @@ namespace xo { = DArena::map(config_.arena_config_.with_name(std::string(buf))); } - this->space_[role::to_space()][igen] = &space_storage_[0][igen]; - this->space_[role::from_space()][igen] = &space_storage_[1][igen]; + this->space_[Role::to_space()][igen] = &space_storage_[0][igen]; + this->space_[Role::from_space()][igen] = &space_storage_[1][igen]; } else { assert(false); } } for (uint32_t igen = config_.n_generation_; igen < c_max_generation; ++igen) { - this->space_[role::to_space()][igen] = nullptr; - this->space_[role::from_space()][igen] = nullptr; + this->space_[Role::to_space()][igen] = nullptr; + this->space_[Role::from_space()][igen] = nullptr; } if (config_.n_generation_ == 2) { - assert(this->get_space(role::to_space(), Generation{2}) == nullptr); + assert(this->get_space(Role::to_space(), Generation{2}) == nullptr); } } @@ -149,7 +149,7 @@ namespace xo { } Generation - GCObjectStore::generation_of(role r, const void * addr) const noexcept + GCObjectStore::generation_of(Role r, const void * addr) const noexcept { for (Generation gi{0}; gi < config_.n_generation_; ++gi) { const DArena * arena = this->get_space(r, gi); @@ -196,7 +196,7 @@ namespace xo { AllocInfo GCObjectStore::alloc_info(value_type mem) const noexcept { - for (role ri : role::all()) { + for (Role ri : Role::all()) { for (Generation gj{0}; gj < config_.n_generation_; ++gj) { const DArena * arena = this->get_space(ri, gj); @@ -209,7 +209,7 @@ namespace xo { } // deliberately attempt on nursery to-space, to capture error info + return sentinel - return this->get_space(role::to_space(), Generation{0})->alloc_info(mem); + return this->get_space(Role::to_space(), Generation{0})->alloc_info(mem); } void @@ -225,13 +225,13 @@ namespace xo { } bool - GCObjectStore::contains(role r, const void * addr) const noexcept + GCObjectStore::contains(Role r, const void * addr) const noexcept { return !(this->generation_of(r, addr).is_sentinel()); } bool - GCObjectStore::contains_allocated(role r, const void * addr) const noexcept + GCObjectStore::contains_allocated(Role r, const void * addr) const noexcept { Generation g = this->generation_of(r, addr); @@ -304,7 +304,7 @@ namespace xo { // scan to-space, count objects by type for (Generation g{0}; g < config_.n_generation_; ++g) { - const DArena * arena = this->get_space(role::to_space(), g); + const DArena * arena = this->get_space(Role::to_space(), g); for (AllocInfo info : *arena) { if (info.is_forwarding_tseq()) { @@ -392,7 +392,7 @@ namespace xo { std::int64_t max_age_present = 0; for (Generation g{0}; g < config_.n_generation_; ++g) { - const DArena * arena = this->get_space(role::to_space(), g); + const DArena * arena = this->get_space(Role::to_space(), g); for (AllocInfo info : *arena) { if (info.is_forwarding_tseq()) { @@ -485,7 +485,7 @@ namespace xo { if (!object_data) { /* trivial to forward nullptr */ return; - } else if (!this->contains(role::from_space(), object_data)) { + } else if (!this->contains(Role::from_space(), object_data)) { /* *lhs_data either: * 1. already in to-space * 2. not in GC-allocated space at all @@ -648,7 +648,7 @@ namespace xo { for (Generation g = Generation{0}; g < upto; ++g) { log && log("swap roles", xtag("g", g)); - std::swap(space_[role::to_space()][g], space_[role::from_space()][g]); + std::swap(space_[Role::to_space()][g], space_[Role::from_space()][g]); } } @@ -663,10 +663,10 @@ namespace xo { // for (Generation g = Generation{0}; g < upto; ++g) { if (sanitize_flag) { - space_[role::from_space()][g]->scrub(); + space_[Role::from_space()][g]->scrub(); } - space_[role::from_space()][g]->clear(); + space_[Role::from_space()][g]->clear(); } } @@ -687,7 +687,7 @@ namespace xo { X1VerifyStats * p_verify_stats) noexcept { for (Generation g(0); g < config_.n_generation_; ++g) { - const DArena * space = this->get_space(role::to_space(), g); + const DArena * space = this->get_space(Role::to_space(), g); for (const AllocInfo & info : *space) { @@ -762,7 +762,7 @@ namespace xo { if (!from_src) return nullptr; - bool src_in_from_space = this->contains(role::from_space(), + bool src_in_from_space = this->contains(Role::from_space(), from_src.data()); if (src_in_from_space) { @@ -798,7 +798,7 @@ namespace xo { if (!from_src) return nullptr; - bool src_in_from_space = this->contains(role::from_space(), from_src); + bool src_in_from_space = this->contains(Role::from_space(), from_src); if (!src_in_from_space) return from_src; @@ -817,7 +817,7 @@ namespace xo { AllocHeader hdr = info.header(); typeseq tseq(info.tseq()); - assert(this->contains_allocated(role::from_space(), from_src)); + assert(this->contains_allocated(Role::from_space(), from_src)); if (this->is_forwarding_header(hdr)) { /* already forwarded - pickup destination diff --git a/src/gc/IAllocator_DX1Collector.cpp b/src/gc/IAllocator_DX1Collector.cpp index 37ed497f..c0c93d6c 100644 --- a/src/gc/IAllocator_DX1Collector.cpp +++ b/src/gc/IAllocator_DX1Collector.cpp @@ -65,7 +65,7 @@ namespace xo { bool IAllocator_DX1Collector::contains(const DX1Collector & d, const void * addr) noexcept { - return d.contains(role::to_space(), addr); + return d.contains(Role::to_space(), addr); } AllocError diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 77244878..4970aa1c 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -73,7 +73,7 @@ namespace xo { size_type z = 0; for (Generation gj{0}; gj + 1 < config_.n_generation_; ++gj) { - z += mlog_[role::to_space()][gj]->size(); + z += mlog_[Role::to_space()][gj]->size(); } return z; @@ -95,15 +95,15 @@ namespace xo { { // 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* // generation that are older (track since source promotes before // destination) // - for (const MutationLogEntry & mrecd : *(mlog_[role::to_space()][g])) { + for (const MutationLogEntry & mrecd : *(mlog_[Role::to_space()][g])) { // mutation log entries are only valid until the next assignment // at the source location. Superseded entry may now point // somewhere else. The snapshot member must however point @@ -159,7 +159,7 @@ namespace xo { // 1. generation of lhs // 2. generation of rhs - Generation src_g = gco_store->generation_of(role::to_space(), p_lhs); + Generation src_g = gco_store->generation_of(Role::to_space(), p_lhs); if (src_g.is_sentinel()) { // only need mlog entries for gc-owned pointers. @@ -167,7 +167,7 @@ namespace xo { return; } - Generation dest_g = gco_store->generation_of(role::to_space(), rhs.data()); + Generation dest_g = gco_store->generation_of(Role::to_space(), rhs.data()); if (dest_g.is_sentinel()) { // similarly, don't need mlog entry to non-gc-owned destination @@ -185,7 +185,7 @@ namespace xo { // for pointers within the same generation, need to log // if source is older than destination. - const DArena * arena = gco_store->get_space(role::to_space(), src_g); + const DArena * arena = gco_store->get_space(Role::to_space(), src_g); const DArena::header_type * src_hdr = arena->obj2hdr(parent); const DArena::header_type * dest_hdr = arena->obj2hdr(rhs.data()); @@ -226,7 +226,7 @@ namespace xo { // collection that moves destination generation around needs to also // update pointers such as this one // - MutationLog * mlog = this->mlog_[role::to_space()][dest_g]; + MutationLog * mlog = this->mlog_[Role::to_space()][dest_g]; mlog->push_back(MutationLogEntry(parent, addr, rhs)); } @@ -239,7 +239,7 @@ namespace xo { for (Generation g = Generation{0}; g < upto; ++g) { log && log("swap roles", xtag("g", g)); - std::swap(mlog_[role::to_space()][g], mlog_[role::from_space()][g]); + std::swap(mlog_[Role::to_space()][g], mlog_[Role::from_space()][g]); } } @@ -260,10 +260,10 @@ namespace xo { child_gen + 2 < config_.n_generation_; ++child_gen) { - MutationLog * from_mlog = this->mlog_[role::from_space()][child_gen]; + MutationLog * from_mlog = this->mlog_[Role::from_space()][child_gen]; if (!from_mlog->empty()) { - MutationLog * to_mlog = this->mlog_[role::to_space()][child_gen]; + MutationLog * to_mlog = this->mlog_[Role::to_space()][child_gen]; MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; auto stats = this->_forward_mutation_log_phase(gc, @@ -277,7 +277,7 @@ namespace xo { // {from_mlog, triage_mlog} reverse roles - std::swap(this->mlog_[role::from_space()][child_gen], + std::swap(this->mlog_[Role::from_space()][child_gen], this->mlog_[c_n_role][child_gen]); work += stats.n_rescue_; @@ -376,7 +376,7 @@ namespace xo { /* here: mlog current */ - Generation parent_gen_to = gc.generation_of(role::to_space(), + Generation parent_gen_to = gc.generation_of(Role::to_space(), from_entry.parent()); if (parent_gen_to.is_sentinel()) { @@ -393,7 +393,7 @@ namespace xo { // TODO: method on AllocInfo to streamline this void * parent_to = *(void **)parent_fr; - parent_gen_to = gc.generation_of(role::to_space(), + parent_gen_to = gc.generation_of(Role::to_space(), parent_to); parent_info = gc.alloc_info((std::byte *)parent_to); @@ -503,7 +503,7 @@ namespace xo { MutationLog * keep_mlog) { Generation child_gen_to - = gco_store_->generation_of(role::to_space(), child_to); + = gco_store_->generation_of(Role::to_space(), child_to); bool need_mlog_entry = ((child_gen_to + 1 < config_.n_generation_) diff --git a/src/gc/facet/ICollector_DX1Collector.cpp b/src/gc/facet/ICollector_DX1Collector.cpp index 261deaa0..a656ca6b 100644 --- a/src/gc/facet/ICollector_DX1Collector.cpp +++ b/src/gc/facet/ICollector_DX1Collector.cpp @@ -16,19 +16,19 @@ namespace xo { namespace mm { auto - ICollector_DX1Collector::allocated(const DX1Collector & self, Generation g, role r) noexcept -> size_type + ICollector_DX1Collector::allocated(const DX1Collector & self, Generation g, Role r) noexcept -> size_type { return self.allocated(g, r); } auto - ICollector_DX1Collector::committed(const DX1Collector & self, Generation g, role r) noexcept -> size_type + ICollector_DX1Collector::committed(const DX1Collector & self, Generation g, Role r) noexcept -> size_type { return self.committed(g, r); } auto - ICollector_DX1Collector::reserved(const DX1Collector & self, Generation g, role r) noexcept -> size_type + ICollector_DX1Collector::reserved(const DX1Collector & self, Generation g, Role r) noexcept -> size_type { return self.reserved(g, r); } @@ -40,7 +40,7 @@ namespace xo { } auto - ICollector_DX1Collector::contains(const DX1Collector & self, role r, const void * addr) noexcept -> bool + ICollector_DX1Collector::contains(const DX1Collector & self, Role r, const void * addr) noexcept -> bool { return self.contains(r, addr); } diff --git a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp index 129ba422..7a00a1f7 100644 --- a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp +++ b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp @@ -22,7 +22,7 @@ namespace xo { } auto - IGCObjectVisitor_DX1Collector::generation_of(const DX1Collector & self, role r, const void * addr) noexcept -> Generation + IGCObjectVisitor_DX1Collector::generation_of(const DX1Collector & self, Role r, const void * addr) noexcept -> Generation { return self.generation_of(r, addr); } diff --git a/utest/X1Collector.test.cpp b/utest/X1Collector.test.cpp index 535b64cd..bfea2ebe 100644 --- a/utest/X1Collector.test.cpp +++ b/utest/X1Collector.test.cpp @@ -47,7 +47,7 @@ namespace ut { using xo::mm::DArena; using xo::mm::ArenaConfig; using xo::mm::Generation; - using xo::mm::role; + using xo::mm::Role; using xo::mm::padding; using xo::facet::obj; using xo::facet::with_facet; @@ -145,7 +145,7 @@ namespace ut { REQUIRE(roots->store()->reserved() >= cfg.object_roots_z_); REQUIRE(roots->store()->reserved() < cfg.object_roots_z_ + roots->store()->page_z_); - const DArena * from_0 = gc.get_space(role::from_space(), Generation{0}); + const DArena * from_0 = gc.get_space(Role::from_space(), Generation{0}); REQUIRE(from_0 != nullptr); REQUIRE(from_0->reserved() >= tc.tenured_z_); @@ -153,29 +153,29 @@ namespace ut { REQUIRE(from_0->reserved() % from_0->page_z_ == 0); REQUIRE(from_0->allocated() == 0); - const DArena * from_1 = gc.get_space(role::from_space(), Generation{1}); + const DArena * from_1 = gc.get_space(Role::from_space(), Generation{1}); REQUIRE(from_1 != nullptr); REQUIRE(from_1->reserved() == from_0->reserved()); REQUIRE(from_1->allocated() == 0); - to_0 = gc.get_space(role::to_space(), Generation{0}); + to_0 = gc.get_space(Role::to_space(), Generation{0}); REQUIRE(to_0 != nullptr); REQUIRE(to_0->reserved() == from_0->reserved()); REQUIRE(to_0->allocated() == 0); - const DArena * to_1 = gc.get_space(role::to_space(), Generation{1}); + const DArena * to_1 = gc.get_space(Role::to_space(), Generation{1}); REQUIRE(to_1 != nullptr); REQUIRE(to_1->reserved() == to_0->reserved()); REQUIRE(to_1->allocated() == 0); - const DArena * from_2 = gc.get_space(role::from_space(), Generation{2}); + const DArena * from_2 = gc.get_space(Role::from_space(), Generation{2}); REQUIRE(from_2 == nullptr); - const DArena * to_2 = gc.get_space(role::to_space(), Generation{2}); + const DArena * to_2 = gc.get_space(Role::to_space(), Generation{2}); REQUIRE(to_2 == nullptr); @@ -224,7 +224,7 @@ namespace ut { { REQUIRE(x0_o.iface() != nullptr); REQUIRE(x0_o.data() != nullptr); - REQUIRE(gc.contains(role::to_space(), x0_o.data())); + REQUIRE(gc.contains(Role::to_space(), x0_o.data())); /* check alloc info for newly-allocated object */ AllocInfo info = gc.alloc_info((std::byte *)x0_o.data()); @@ -238,7 +238,7 @@ namespace ut { { REQUIRE(n1_o.iface() != nullptr); REQUIRE(n1_o.data() != nullptr); - REQUIRE(gc.contains(role::to_space(), n1_o.data())); + REQUIRE(gc.contains(Role::to_space(), n1_o.data())); /* check alloc info for newly-allocated object */ AllocInfo info = gc.alloc_info((std::byte *)n1_o.data()); @@ -252,7 +252,7 @@ namespace ut { { REQUIRE(l0_o.iface() != nullptr); REQUIRE(l0_o.data() != nullptr); - REQUIRE(gc.contains(role::to_space(), l0_o.data())); + REQUIRE(gc.contains(Role::to_space(), l0_o.data())); /* check alloc info for newly-allocated object */ AllocInfo info = gc.alloc_info((std::byte *)l0_o.data()); @@ -271,16 +271,16 @@ namespace ut { log && log(xtag("l0_o.data()->head_.data()", l0_o.data()->head_.data())); log && log(xtag("x0_o.data()", x0_o.data())); - REQUIRE(!gc.contains(role::from_space(), x0_o.data())); - REQUIRE(gc.contains(role::to_space(), x0_o.data())); + REQUIRE(!gc.contains(Role::from_space(), x0_o.data())); + REQUIRE(gc.contains(Role::to_space(), x0_o.data())); REQUIRE(x0_o.data()->value() == 3.1415927); - REQUIRE(!gc.contains(role::from_space(), n1_o.data())); - REQUIRE(gc.contains(role::to_space(), n1_o.data())); + REQUIRE(!gc.contains(Role::from_space(), n1_o.data())); + REQUIRE(gc.contains(Role::to_space(), n1_o.data())); REQUIRE(n1_o.data()->value() == 42); - REQUIRE(!gc.contains(role::from_space(), l0_o.data())); - REQUIRE(gc.contains(role::to_space(), l0_o.data())); + REQUIRE(!gc.contains(Role::from_space(), l0_o.data())); + REQUIRE(gc.contains(Role::to_space(), l0_o.data())); REQUIRE(l0_o.data()->is_empty() == false); REQUIRE((void*)l0_o.data()->head_.data() == (void*)x0_o.data()); From a67d8b962a411883993b1930873c77c759306c80 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 7 Apr 2026 11:44:02 -0400 Subject: [PATCH 128/174] xo-gc: scaffold utest for GCObjectStore --- include/xo/gc/X1CollectorConfig.hpp | 1 - utest/CMakeLists.txt | 1 + utest/GCObjectStore.test.cpp | 94 +++++++++++++++++++++++++++++ utest/Object2.test.cpp | 2 +- utest/X1Collector.test.cpp | 10 +-- utest/gc_utest_main.cpp | 30 ++++++++- 6 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 utest/GCObjectStore.test.cpp diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp index 5bbc377a..4b889efa 100644 --- a/include/xo/gc/X1CollectorConfig.hpp +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -8,7 +8,6 @@ #include "GCObjectStoreConfig.hpp" #include "MutationLogConfig.hpp" #include "object_age.hpp" -//#include "Generation.hpp" #include #include #include diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 0f3c9676..5ffcac82 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -7,6 +7,7 @@ set(UTEST_SRCS Collector.test.cpp X1Collector.test.cpp DX1CollectorIterator.test.cpp + GCObjectStore.test.cpp Object2.test.cpp random_allocs.cpp ) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp new file mode 100644 index 00000000..399a7aef --- /dev/null +++ b/utest/GCObjectStore.test.cpp @@ -0,0 +1,94 @@ +/** @file GCObjectStore.test.cpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#include +#include +#include +#include +#include +#include + +namespace ut { + using xo::scm::DList; + using xo::scm::DInteger; + using xo::mm::GCObjectStoreConfig; + using xo::mm::GCObjectStore; + using xo::mm::AGCObject; + using xo::mm::ArenaConfig; + using xo::facet::typeseq; + using xo::facet::impl_for; + using xo::scope; + 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) + : n_gen_{n_gen}, + n_survive_{n_survive}, + gc_size_{gc_z}, + object_type_z_{type_z} + {} + + /** number of generations in gco store **/ + uint32_t n_gen_ = 0; + /** object promotes on surviving this many gc cycles **/ + uint32_t n_survive_ = 0; + /** size of each generation's half-space, in bytes **/ + size_t gc_size_ = 0; + /** Storage for object type array, in bytes. + * (need to allow 1 pointer per type) + **/ + uint32_t object_type_z_ = 0; + + }; + + static std::vector s_testcase_v = { + /** n_gen, n_survive, gc_size, object_type_z **/ + Testcase(2, 4, 16 * 1024, 8 * 128), + }; + } + + TEST_CASE("GCObjectStore-1", "[GCObjectStore]") + { + constexpr bool c_debug_flag = false; + scope log(XO_DEBUG(c_debug_flag)); + + 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]; + + /** config for each half-space **/ + ArenaConfig arena_config + = (ArenaConfig() + .with_name("arena-name-not-used") + .with_size(tc.gc_size_) + .with_store_header_flag(true)); + + GCObjectStoreConfig gcos_config(arena_config, + tc.n_gen_, + tc.n_survive_, + tc.object_type_z_, + c_debug_flag); + + // object type storage will be empty unless we install a type. + GCObjectStore gcos(gcos_config); + + REQUIRE(gcos.is_type_installed(typeseq::id()) == false); + + //REQUIRE(nullptr == gcos.lookup_type(typeseq::id())); + +#ifdef NOT_YET + + +// // Usual path would be via ACollector interface; that's inconvenient here +// +#endif + } + } + +} /*namespace ut*/ + +/* end GCObjectStore.test.cpp */ diff --git a/utest/Object2.test.cpp b/utest/Object2.test.cpp index fa0baecb..4bda353f 100644 --- a/utest/Object2.test.cpp +++ b/utest/Object2.test.cpp @@ -63,7 +63,7 @@ namespace ut { TEST_CASE("printable1", "[pp][x1][list]") { - constexpr bool c_debug_flag = true; + constexpr bool c_debug_flag = false; scope log(XO_DEBUG(c_debug_flag)); bool ok = SetupObject2::register_facets(); diff --git a/utest/X1Collector.test.cpp b/utest/X1Collector.test.cpp index bfea2ebe..04e5c453 100644 --- a/utest/X1Collector.test.cpp +++ b/utest/X1Collector.test.cpp @@ -11,13 +11,13 @@ #include "DArray.hpp" #include -//#include "number/IGCObject_DFloat.hpp" -#include "number/IGCObject_DInteger.hpp" -#include "list/IGCObject_DList.hpp" +#include +#include +//#include "list/IGCObject_DList.hpp" -#include -#include #include +//#include +#include #include #include diff --git a/utest/gc_utest_main.cpp b/utest/gc_utest_main.cpp index 8c426003..cb1a31f3 100644 --- a/utest/gc_utest_main.cpp +++ b/utest/gc_utest_main.cpp @@ -1,6 +1,32 @@ /* file gc_utest_main.cpp */ -#define CATCH_CONFIG_MAIN -#include "catch2/catch.hpp" +#include +#include + +#define CATCH_CONFIG_RUNNER +#include + +using xo::S_gc_tag; +using xo::InitSubsys; +using xo::InitEvidence; + +// ensure xo-gc properly initialized when Subsystem::initialize_all() runs +static InitEvidence s_init = (InitSubsys::require()); + +int +main(int argc, char* argv[]) +{ + using xo::Subsystem; + + // Your custom initialization code here + Subsystem::initialize_all(); + + // Run Catch2's test session + int result = Catch::Session().run(argc, argv); + + // cleanup here, if any + + return result; +} /* end gc_utest_main.cpp */ From 1387673f154e293f0916ac2a42a217d0101dc2f7 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 7 Apr 2026 21:16:55 -0400 Subject: [PATCH 129/174] xo-gc: retire Collector.forward_inplace + mock x-gc utest W/ MockCollector supporting AGCObjectVisitor facet --- include/xo/gc/DX1Collector.hpp | 9 ++- .../xo/gc/detail/ICollector_DX1Collector.hpp | 6 -- src/gc/DX1Collector.cpp | 31 ++------- src/gc/facet/ICollector_DX1Collector.cpp | 5 -- utest/CMakeLists.txt | 12 ++++ utest/DMockCollector.cpp | 37 ++++++++++ utest/DMockCollector.hpp | 40 +++++++++++ utest/IGCObjectVisitor_DMockCollector.cpp | 44 ++++++++++++ utest/IGCObjectVisitor_DMockCollector.hpp | 68 +++++++++++++++++++ utest/MockCollector.hpp | 13 ++++ utest/gc_utest_main.cpp | 10 +-- .../idl/IGCObjectVisitor_DMockCollector.json5 | 27 ++++++++ utest/init_gc_utest.cpp | 48 +++++++++++++ utest/init_gc_utest.hpp | 34 ++++++++++ 14 files changed, 342 insertions(+), 42 deletions(-) create mode 100644 utest/DMockCollector.cpp create mode 100644 utest/DMockCollector.hpp create mode 100644 utest/IGCObjectVisitor_DMockCollector.cpp create mode 100644 utest/IGCObjectVisitor_DMockCollector.hpp create mode 100644 utest/MockCollector.hpp create mode 100644 utest/idl/IGCObjectVisitor_DMockCollector.json5 create mode 100644 utest/init_gc_utest.cpp create mode 100644 utest/init_gc_utest.hpp diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index aaa2b083..5eafa802 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -274,13 +274,18 @@ namespace xo { /** Execute gc immediately, for all generations < @p upto **/ void execute_gc(Generation upto) noexcept; +#ifdef OBSOLETE // replaced by visit_child() /** Evacuate object at @p *lhs_data to to-space. * Replace original with forwarding pointer to new location **/ void forward_inplace(AGCObject * lhs_iface, void ** lhs_data); +#endif - /** Supports the GCObjectVisitor facet. - * Synonym for forward_inplace + /** Supports GCObjectVisitor facet. + * During gc phase: + * 1. evacuate object at @p *lhs_data to to-space. + * 2. replace @p *lhs_data with forwarding pointer + * to new location. **/ void visit_child(AGCObject * lhs_iface, void ** lhs_data); diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index c81513f0..d9b43a5c 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -111,12 +111,6 @@ Require: gc not in progress **/ Source must be owned by this collector. Increments object age **/ static void * alloc_copy(DX1Collector & self, std::byte * src); - /** evacuate @p *lhs, that refers to state with interface @p lhs_iface, -to collector @p d's to-space. Replace *lhs_data with forwarding pointer - -Require: gc in progress - **/ - static void forward_inplace(DX1Collector & self, AGCObject * lhs_iface, void ** lhs_data); ///@} }; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 28636368..00bd9fb4 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -584,32 +584,13 @@ namespace xo { } } - void - DX1Collector::forward_inplace(AGCObject * lhs_iface, - void ** lhs_data) - { - // TODO: streamline once GCObject refactored so that - // forward_children takes GCObjectVisitor instead of Collector - // argument. - - Generation upto = runstate_.gc_upto(); - - if (runstate_.is_running()) { - // called during collection phase - gco_store_.forward_inplace_aux(this->ref(), lhs_iface, lhs_data, upto); - } else if (runstate_.is_verify()) { - // called during verify_ok - this->_verify_aux(lhs_iface, *lhs_data); - } else { - // should be unreachable - assert(false); - } - } - void DX1Collector::visit_child(AGCObject * lhs_iface, void ** lhs_data) { + // MAYBE: adapter distinct from DX1Collector that supports GCObjectVisitor facet, + // calls DX1Collector::_verify_aux() + if (runstate_.is_running()) { Generation upto = runstate_.gc_upto(); @@ -661,17 +642,17 @@ namespace xo { auto DX1Collector::super_alloc(typeseq t, size_type z) noexcept -> value_type { - return with_facet::mkobj(new_space()).super_alloc(t, z); + return with_facet::mkobj(this->new_space()).super_alloc(t, z); } auto DX1Collector::sub_alloc(size_type z, bool complete) noexcept -> value_type { - return with_facet::mkobj(new_space()).sub_alloc(z, complete); + return with_facet::mkobj(this->new_space()).sub_alloc(z, complete); } auto DX1Collector::alloc_copy(value_type src) noexcept -> value_type { - return with_facet::mkobj(new_space()).alloc_copy(src); + return with_facet::mkobj(this->new_space()).alloc_copy(src); } bool diff --git a/src/gc/facet/ICollector_DX1Collector.cpp b/src/gc/facet/ICollector_DX1Collector.cpp index a656ca6b..a20a9744 100644 --- a/src/gc/facet/ICollector_DX1Collector.cpp +++ b/src/gc/facet/ICollector_DX1Collector.cpp @@ -99,11 +99,6 @@ namespace xo { { return self.alloc_copy(src); } - auto - ICollector_DX1Collector::forward_inplace(DX1Collector & self, AGCObject * lhs_iface, void ** lhs_data) -> void - { - self.forward_inplace(lhs_iface, lhs_data); - } } /*namespace mm*/ } /*namespace xo*/ diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 5ffcac82..599e1ea1 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -9,9 +9,21 @@ set(UTEST_SRCS DX1CollectorIterator.test.cpp GCObjectStore.test.cpp Object2.test.cpp + + DMockCollector.cpp + IGCObjectVisitor_DMockCollector.cpp + + init_gc_utest.cpp random_allocs.cpp ) +# mock collector for unit test +xo_add_genfacetimpl( + TARGET xo-gc-facetimpl-gcobjectvisitor-mockcollector + FACET_PKG xo_alloc2 + INPUT idl/IGCObjectVisitor_DMockCollector.json5 +) + if (ENABLE_TESTING) xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS}) xo_headeronly_dependency(${UTEST_EXE} randomgen) diff --git a/utest/DMockCollector.cpp b/utest/DMockCollector.cpp new file mode 100644 index 00000000..69f90508 --- /dev/null +++ b/utest/DMockCollector.cpp @@ -0,0 +1,37 @@ +/** @file DMockCollector.cpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#include "MockCollector.hpp" + +namespace xo { + namespace mm { + + Generation + DMockCollector::generation_of(Role r, const void * addr) const noexcept + { + return p_gco_store_->generation_of(r, addr); + } + + AllocInfo + DMockCollector::alloc_info(void * mem) const noexcept + { + return p_gco_store_->alloc_info((std::byte *)mem); + } + + void + DMockCollector::visit_child(AGCObject * lhs_iface, void ** lhs_data) + { + p_gco_store_->forward_inplace_aux(this->ref(), lhs_iface, lhs_data, upto_); + } + + std::byte * + DMockCollector::alloc_copy(void * src) noexcept { + return p_gco_store_->new_space()->alloc_copy((std::byte *)src); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DMockCollector.cpp */ diff --git a/utest/DMockCollector.hpp b/utest/DMockCollector.hpp new file mode 100644 index 00000000..ca1c0dfb --- /dev/null +++ b/utest/DMockCollector.hpp @@ -0,0 +1,40 @@ +/** @file DMockCollector.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include +#include + +namespace xo { + namespace mm { + + /** @brief Mock Collector + * + * Intended to help unit test a GCObjectSotre instance. + * Mock a Collector in collection phase for generations 0 <= g < @ref upto_. + **/ + class DMockCollector { + public: + explicit DMockCollector(GCObjectStore * gcos, Generation upto) : p_gco_store_{gcos}, upto_{upto} {} + + template + obj ref() { return obj(this); } + + Generation generation_of(Role r, const void * addr) const noexcept; + AllocInfo alloc_info(void * mem) const noexcept; + + void visit_child(AGCObject * lhs_iface, void ** lhs_data); + std::byte * alloc_copy(void * src) noexcept; + + private: + GCObjectStore * p_gco_store_ = nullptr; + Generation upto_; + }; + + } /*namespace mm*/ +} /*namespaace xo*/ + +/* end DMockCollector.hpp */ diff --git a/utest/IGCObjectVisitor_DMockCollector.cpp b/utest/IGCObjectVisitor_DMockCollector.cpp new file mode 100644 index 00000000..f4472874 --- /dev/null +++ b/utest/IGCObjectVisitor_DMockCollector.cpp @@ -0,0 +1,44 @@ +/** @file IGCObjectVisitor_DMockCollector.cpp + * + * Generated automagically from ingredients: + * 1. code generator: + * [xo-facet/codegen/genfacet] + * arguments: + * --input [idl/IGCObjectVisitor_DMockCollector.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [iface_facet_any.hpp.j2] + * 3. idl for facet methods + * [idl/IGCObjectVisitor_DMockCollector.json5] +**/ + +#include "./IGCObjectVisitor_DMockCollector.hpp" + +namespace xo { + namespace mm { + auto + IGCObjectVisitor_DMockCollector::alloc_info(const DMockCollector & self, void * addr) -> AllocInfo + { + return self.alloc_info(addr); + } + + auto + IGCObjectVisitor_DMockCollector::generation_of(const DMockCollector & self, Role r, const void * addr) noexcept -> Generation + { + return self.generation_of(r, addr); + } + + auto + IGCObjectVisitor_DMockCollector::alloc_copy(DMockCollector & self, std::byte * src) -> void * + { + return self.alloc_copy(src); + } + auto + IGCObjectVisitor_DMockCollector::visit_child(DMockCollector & self, AGCObject * iface, void ** pp_data) noexcept -> void + { + self.visit_child(iface, pp_data); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IGCObjectVisitor_DMockCollector.cpp */ diff --git a/utest/IGCObjectVisitor_DMockCollector.hpp b/utest/IGCObjectVisitor_DMockCollector.hpp new file mode 100644 index 00000000..ac9db2bb --- /dev/null +++ b/utest/IGCObjectVisitor_DMockCollector.hpp @@ -0,0 +1,68 @@ +/** @file IGCObjectVisitor_DMockCollector.hpp + * + * Generated automagically from ingredients: + * 1. code generator: + * [xo-facet/codegen/genfacet] + * arguments: + * --input [idl/IGCObjectVisitor_DMockCollector.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [iface_facet_repr.hpp.j2] + * 3. idl for facet methods + * [idl/IGCObjectVisitor_DMockCollector.json5] + **/ + +#pragma once + +#include "GCObjectVisitor.hpp" +#include "DMockCollector.hpp" + +namespace xo { namespace mm { class IGCObjectVisitor_DMockCollector; } } + +namespace xo { + namespace facet { + template <> + struct FacetImplementation + { + using ImplType = xo::mm::IGCObjectVisitor_Xfer + ; + }; + } +} + +namespace xo { + namespace mm { + /** @class IGCObjectVisitor_DMockCollector + **/ + class IGCObjectVisitor_DMockCollector { + public: + /** @defgroup mm-gcobjectvisitor-dmockcollector-type-traits **/ + ///@{ + using Copaque = xo::mm::AGCObjectVisitor::Copaque; + using Opaque = xo::mm::AGCObjectVisitor::Opaque; + ///@} + /** @defgroup mm-gcobjectvisitor-dmockcollector-methods **/ + ///@{ + // const methods + /** allocation metadata for gc-aware data at address @p gco. +@p gco must be the result of a call to collector's alloc() function **/ + static AllocInfo alloc_info(const DMockCollector & self, void * addr); + /** generation to which pointer @p addr belongs, given role @p r; +sentinel if @p addr is not owned by collector **/ + static Generation generation_of(const DMockCollector & self, Role r, const void * addr) noexcept; + + // non-const methods + /** 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); + /** visit child of a gc-aware object. May update child in-place! **/ + static void visit_child(DMockCollector & self, AGCObject * iface, void ** pp_data) noexcept; + ///@} + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end */ \ No newline at end of file diff --git a/utest/MockCollector.hpp b/utest/MockCollector.hpp new file mode 100644 index 00000000..4e3f3d1b --- /dev/null +++ b/utest/MockCollector.hpp @@ -0,0 +1,13 @@ +/** @file MockCollector.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include "DMockCollector.hpp" +#include "IGCObjectVisitor_DMockCollector.hpp" +//#include "ICollector_DMockCollector.hpp" +//#include "IAllocator_DMockCollector.hpp" + +/* end MockCollector.hpp */ diff --git a/utest/gc_utest_main.cpp b/utest/gc_utest_main.cpp index cb1a31f3..5d4c4ea8 100644 --- a/utest/gc_utest_main.cpp +++ b/utest/gc_utest_main.cpp @@ -1,17 +1,19 @@ /* file gc_utest_main.cpp */ -#include +#include "init_gc_utest.hpp" #include #define CATCH_CONFIG_RUNNER #include -using xo::S_gc_tag; +using xo::S_gc_utest_tag; using xo::InitSubsys; using xo::InitEvidence; -// ensure xo-gc properly initialized when Subsystem::initialize_all() runs -static InitEvidence s_init = (InitSubsys::require()); +// ensure xo-gc/utest as mock subsystem +// properly initialized when Subsystem::initialize_all() runs +// +static InitEvidence s_init = (InitSubsys::require()); int main(int argc, char* argv[]) diff --git a/utest/idl/IGCObjectVisitor_DMockCollector.json5 b/utest/idl/IGCObjectVisitor_DMockCollector.json5 new file mode 100644 index 00000000..9d104bf7 --- /dev/null +++ b/utest/idl/IGCObjectVisitor_DMockCollector.json5 @@ -0,0 +1,27 @@ +{ + mode: "implementation", + output_cpp_dir: ".", + output_hpp_dir: "./", + output_impl_subdir: ".", + includes: [ +// "", +// "" + ], + local_types: [ +// { +// name: "typeseq", +// doc: ["identifies a c++ type"], +// definition: "xo::reflect::typeseq" +// }, + ], + namespace1: "xo", + namespace2: "mm", + facet_idl: "idl/GCObjectVisitor.json5", + brief: "provide AGCObjectVisitor interface for DMockCollector", + using_doxygen: true, + repr: "DMockCollector", + doc: [ + "Implement AGCObjectVisitor for DMockCollector.", + "Evacuate object pointer (migrate to to-space) during collection phase" + ], +} diff --git a/utest/init_gc_utest.cpp b/utest/init_gc_utest.cpp new file mode 100644 index 00000000..a88f6046 --- /dev/null +++ b/utest/init_gc_utest.cpp @@ -0,0 +1,48 @@ +/** @file init_gc_utest.cpp +* + * @author Roland Conybeare, Apr 2026 + **/ + +#include "init_gc_utest.hpp" +#include "MockCollector.hpp" +#include +#include +#include + +namespace xo { + using xo::mm::SetupGcUtest; + using xo::facet::FacetRegistry; + using xo::reflect::typeseq; + + bool + SetupGcUtest::register_facets() + { + scope log(XO_DEBUG(true)); + + FacetRegistry::register_impl(); + + log && log(xtag("DMockCollector.tseq", typeseq::id())); + + return true; + } + + void + InitSubsys::init() + { + SetupGcUtest::register_facets(); + } + + InitEvidence + InitSubsys::require() { + InitEvidence retval; + + /* recursive subsystem deps for xo-gc/utest */ + retval ^= InitSubsys::require(); + + /* xo-gc/utest/'s own initialization code */ + retval ^= Subsystem::provide("gc-utest", &init); + + return retval; + } + +} diff --git a/utest/init_gc_utest.hpp b/utest/init_gc_utest.hpp new file mode 100644 index 00000000..ccadf842 --- /dev/null +++ b/utest/init_gc_utest.hpp @@ -0,0 +1,34 @@ +/** @file init_gc_utest.hpp +* + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include + +namespace xo { + /* tag to represent xo-gc/utest/ as a mock subsystem within ordered initialization + * + * (so we can follow usual patterns for setting up facet tables) + */ + enum S_gc_utest_tag {}; + + template <> + struct InitSubsys { + static void init(); + static InitEvidence require(); + }; + + namespace mm { + + class SetupGcUtest { + public: + /** Register gc/utest (facet,impl) combinations with FacetRegistry **/ + static bool register_facets(); + }; + } /*namespace mm*/ + +} /*namespace xo*/ + +/* end init_gc_utest.hpp */ From 04131b1ece9e0f1f4646090c6c62be152c11e34e Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 9 Apr 2026 19:12:41 -0400 Subject: [PATCH 130/174] xo-gc: utest: fairly comprehensive GCObjectStore utest Generative! --- src/gc/DX1Collector.cpp | 2 +- src/gc/GCObjectStore.cpp | 24 +- utest/DX1CollectorIterator.test.cpp | 2 +- utest/GCObjectStore.test.cpp | 525 +++++++++++++++++++++++++++- utest/Object2.test.cpp | 2 +- utest/X1Collector.test.cpp | 6 +- utest/init_gc_utest.cpp | 2 +- 7 files changed, 534 insertions(+), 29 deletions(-) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 00bd9fb4..1faebdc1 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 f7ae5497..7f21f5eb 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 66a9e417..4d3ca6c2 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 399a7aef..dd11503d 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 4bda353f..5c259fb3 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 04e5c453..3bd4d4ee 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 a88f6046..c708c561 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(); From 18b9899f30d4b66a9217b200bef0c79d33eb2688 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 9 Apr 2026 19:30:58 -0400 Subject: [PATCH 131/174] + xo-gc: utest: + README.md --- utest/README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 utest/README.md diff --git a/utest/README.md b/utest/README.md new file mode 100644 index 00000000..4d6477db --- /dev/null +++ b/utest/README.md @@ -0,0 +1,35 @@ +Unit test here relies on generated facet support. + +Also rehearsing subsystem setup that we use in other xo subsystems +to perform facet registration. +(This is for form's sake. Could just as well invoke directly from gc_utest_main.cpp). + +idl/ config for obj. + + +IGCObject_DMockCollector.*pp automatically generated from idl/ + +MockCollector.hpp fop header file for DX1Collector. + +DMockCollector.*pp mock ACollector impl. Wraps a GCObjectStore pointer. + Load-bearing for GCObjectStore.test.cpp + +init_gc_utest.*pp mock subsystem setup for this directory, + as if it were an "xo-gc-utest" subsystem + +GCObjectStore.test.cpp Generative tests for GCObjectStore. + Generate non-trivial object graphs, + verify evacuation algorithm. + +---------------------------------------------------------------- +These don't use anything downstream of idl/ + +Collector.test.cpp X1 tests. Does some random allocs, + but doesn't exercise GC features + +X1Collector.test.cpp standalone X1Collector test. + does some atomic allocs + verifies + GC behavior for a few xo-object2/ types. + Verify simple tests here. + +DX1CollectorIterator.test.cpp Collector iterator test From 518a3d637a9454fef821640cef2eec81b94f5062 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 9 Apr 2026 21:30:52 -0400 Subject: [PATCH 132/174] xo-gc: utest: minor improvements for coverage --- src/gc/GCObjectStore.cpp | 4 ++++ utest/GCObjectStore.test.cpp | 27 ++++++++++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 7f21f5eb..9ff067da 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -119,6 +119,7 @@ namespace xo { || (static_cast(tseq.seqno()) > object_types_.size())) { + // LCOV_EXCL_START log.retroactively_enable("out-of-bounds", xtag("tseq", tseq), xtag("tname", TypeRegistry::id2name(tseq))); @@ -131,17 +132,20 @@ namespace xo { xtag("types.hi", object_types_.store()->hi_)); return nullptr; + // LCOV_EXCL_STOP } const ObjectTypeSlot & slot = object_types_[tseq.seqno()]; if (slot.is_null()) { + // LCOV_EXCL_START log.retroactively_enable("null-vtable", xtag("tseq", tseq), xtag("tname", TypeRegistry::id2name(tseq))); assert(false); return nullptr; + // LCOV_EXCL_STOP } return slot.iface(); diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index dd11503d..552ad28f 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -31,6 +31,7 @@ namespace ut { using xo::mm::AGCObjectVisitor; using xo::mm::Generation; using xo::mm::Role; + using xo::mm::object_age; using xo::mm::ArenaConfig; using xo::mm::AAllocator; using xo::mm::DArena; @@ -403,7 +404,7 @@ namespace ut { REQUIRE(x2_v[i].gco_._typeseq() == x2_v[i].tseq_); } - // new objects appear in to-space for generation 0 + // gcos can reveal info about allocs for (size_t i = 0, n = x1_v.size(); i < n; ++i) { const auto & x1 = x1_v.at(i); @@ -415,16 +416,22 @@ namespace ut { 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))); + // also can use header2size / header2tseq convenience functions + REQUIRE(gcos.header2size(obj_info.header()) == obj_info.size()); + REQUIRE(gcos.header2age(obj_info.header()) == object_age{0}); + REQUIRE(gcos.header2tseq(obj_info.header()) == obj_info.tseq()); + } - if (gi == 0) - REQUIRE(gcos.to_space(gi)->allocated() > 0); - else - REQUIRE(gcos.to_space(gi)->allocated() == 0); + // new objects appear in to-space for generation 0 + for (Generation gi = g0; gi < gn; ++gi) { + INFO(tostr(xtag("gi", gi))); - REQUIRE(gcos.from_space(gi)->allocated() == 0); - } + if ((gi == 0) && (x1_v.size() > 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 @@ -585,6 +592,8 @@ namespace ut { // - forward_inplace_aux() // used from DX1Collector.visit_child // - cleanup_phase() // used from DX1Collector._cleanup_phase + // - report_object_types + } } From dc31e0f772e9e2a0403f137d47686b562e7aca6f Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 10 Apr 2026 01:10:03 -0400 Subject: [PATCH 133/174] xo-interpreter2 stack: + reason arg to visit_gco_children() Helps streamline DX1Collector in xo-gc/. Want both forward and verify entry points for the same representation. --- include/xo/gc/DX1Collector.hpp | 11 ++----- .../detail/IGCObjectVisitor_DX1Collector.hpp | 2 +- src/gc/DX1Collector.cpp | 19 ++++++++---- src/gc/GCObjectStore.cpp | 8 ++--- .../facet/IGCObjectVisitor_DX1Collector.cpp | 4 +-- utest/DMockCollector.cpp | 7 +++-- utest/DMockCollector.hpp | 2 +- utest/GCObjectStore.test.cpp | 31 ++++++++++++++----- utest/IGCObjectVisitor_DMockCollector.cpp | 4 +-- utest/IGCObjectVisitor_DMockCollector.hpp | 2 +- 10 files changed, 55 insertions(+), 35 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 5eafa802..98fc254b 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -274,20 +274,13 @@ namespace xo { /** Execute gc immediately, for all generations < @p upto **/ void execute_gc(Generation upto) noexcept; -#ifdef OBSOLETE // replaced by visit_child() - /** Evacuate object at @p *lhs_data to to-space. - * Replace original with forwarding pointer to new location - **/ - void forward_inplace(AGCObject * lhs_iface, void ** lhs_data); -#endif - /** Supports GCObjectVisitor facet. - * During gc phase: + * 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(AGCObject * lhs_iface, void ** lhs_data); + void visit_child(VisitReason reason, AGCObject * lhs_iface, void ** lhs_data); // ----- allocation ----- diff --git a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp b/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp index 95534db6..5c18c8eb 100644 --- a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp +++ b/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp @@ -58,7 +58,7 @@ Source must be owned by this collector. Increments object age **/ static void * alloc_copy(DX1Collector & self, std::byte * src); /** visit child of a gc-aware object. May update child in-place! **/ - static void visit_child(DX1Collector & self, AGCObject * iface, void ** pp_data) noexcept; + static void visit_child(DX1Collector & self, VisitReason reason, AGCObject * iface, void ** pp_data) noexcept; ///@} }; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 1faebdc1..63b6ef57 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -404,7 +404,7 @@ namespace xo { // - X1Collector::forward_inplace() -> _verify_aux() // - gco.visit_gco_children(self); + gco.visit_gco_children(VisitReason::verify(), self); } @@ -585,21 +585,28 @@ namespace xo { } void - DX1Collector::visit_child(AGCObject * lhs_iface, + DX1Collector::visit_child(VisitReason reason, + AGCObject * lhs_iface, void ** lhs_data) { // MAYBE: adapter distinct from DX1Collector that supports GCObjectVisitor facet, // calls DX1Collector::_verify_aux() - if (runstate_.is_running()) { + switch (reason.code()) { + case VisitReason::code::forward: + { Generation upto = runstate_.gc_upto(); // called during collection phase - gco_store_.forward_inplace_aux(this->ref(), lhs_iface, lhs_data, upto); - } else if (runstate_.is_verify()) { + gco_store_.forward_inplace_aux + (this->ref(), lhs_iface, lhs_data, upto); + break; + } + case VisitReason::code::verify: // called during verify_ok this->_verify_aux(lhs_iface, *lhs_data); - } else { + break; + default: // should be unreachable assert(false); } diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 9ff067da..1f54a4c9 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -501,7 +501,7 @@ namespace xo { log && log("disposition: not in from-space. Don't forward, but check children"); obj gco(lhs_iface, object_data); - gco.visit_gco_children(gc); + gco.visit_gco_children(VisitReason::forward(), gc); return; } @@ -714,7 +714,7 @@ namespace xo { // Nested control reenters // X1Collector::forward_inplace() -> _verify_aux() // - gco.visit_gco_children(gc); + gco.visit_gco_children(VisitReason::forward(), gc); } else { ++(p_verify_stats->n_no_iface_); continue; @@ -783,7 +783,7 @@ namespace xo { GCMoveCheckpoint gray_lo_v = this->snap_move_checkpoint(upto); - from_src.visit_gco_children(gc); + from_src.visit_gco_children(VisitReason::forward(), gc); // For each generation g: // traverse objects newer than gray_lo_v[g], to make sure children @@ -1013,7 +1013,7 @@ namespace xo { assert(iface->_has_null_vptr() == false); - iface->visit_gco_children(src, gc); + iface->visit_gco_children(src, VisitReason::forward(), gc); gray_lo_v[g] = ((std::byte *)src) + z; diff --git a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp index 7a00a1f7..17f364ed 100644 --- a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp +++ b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp @@ -33,9 +33,9 @@ namespace xo { return self.alloc_copy(src); } auto - IGCObjectVisitor_DX1Collector::visit_child(DX1Collector & self, AGCObject * iface, void ** pp_data) noexcept -> void + IGCObjectVisitor_DX1Collector::visit_child(DX1Collector & self, VisitReason reason, AGCObject * iface, void ** pp_data) noexcept -> void { - self.visit_child(iface, pp_data); + self.visit_child(reason, iface, pp_data); } } /*namespace mm*/ diff --git a/utest/DMockCollector.cpp b/utest/DMockCollector.cpp index 69f90508..6b6e9204 100644 --- a/utest/DMockCollector.cpp +++ b/utest/DMockCollector.cpp @@ -21,9 +21,12 @@ namespace xo { } void - DMockCollector::visit_child(AGCObject * lhs_iface, void ** lhs_data) + DMockCollector::visit_child(VisitReason reason, AGCObject * lhs_iface, void ** lhs_data) { - p_gco_store_->forward_inplace_aux(this->ref(), lhs_iface, lhs_data, upto_); + (void)reason; + + p_gco_store_->forward_inplace_aux + (this->ref(), lhs_iface, lhs_data, upto_); } std::byte * diff --git a/utest/DMockCollector.hpp b/utest/DMockCollector.hpp index ca1c0dfb..3297f8fa 100644 --- a/utest/DMockCollector.hpp +++ b/utest/DMockCollector.hpp @@ -26,7 +26,7 @@ namespace xo { Generation generation_of(Role r, const void * addr) const noexcept; AllocInfo alloc_info(void * mem) const noexcept; - void visit_child(AGCObject * lhs_iface, void ** lhs_data); + void visit_child(VisitReason reason, AGCObject * lhs_iface, void ** lhs_data); std::byte * alloc_copy(void * src) noexcept; private: diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index 552ad28f..75766563 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -295,9 +295,10 @@ namespace ut { * 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)); + 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); @@ -325,7 +326,7 @@ namespace ut { } } - // verify basic arena partitioning + // verify basic arena partitioning + sizing { REQUIRE(g0 != g1); REQUIRE(gcos.new_space()); @@ -371,7 +372,7 @@ namespace ut { } } - // allocator + // allocator api auto alloc = obj(gcos.new_space()); // create object(s). @@ -420,6 +421,7 @@ namespace ut { REQUIRE(gcos.header2size(obj_info.header()) == obj_info.size()); REQUIRE(gcos.header2age(obj_info.header()) == object_age{0}); REQUIRE(gcos.header2tseq(obj_info.header()) == obj_info.tseq()); + REQUIRE(gcos.is_forwarding_header(obj_info.header()) == false); } // new objects appear in to-space for generation 0 @@ -441,6 +443,7 @@ namespace ut { for (size_t i = 0, n = x1_v.size(); i < n; ++i) { const auto & x1 = x1_v.at(i); + REQUIRE(gcos.contains(Role::from_space(), x1.gco_.data())); REQUIRE(gcos.contains_allocated(Role::from_space(), x1.gco_.data())); AllocInfo obj_info = gcos.alloc_info((std::byte *)x1.gco_.data()); REQUIRE(obj_info.size() >= x1.alloc_z_); @@ -578,7 +581,8 @@ namespace ut { // 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); + auto x1p_data + = gcos.deep_move_root(mock_gc_visitor, x1.gco_, g1); // control here under normal GC use // would represent a configuration fail @@ -590,10 +594,23 @@ namespace ut { // Things to test: // - deep_move_interior() // used from MutationLogStore // - forward_inplace_aux() // used from DX1Collector.visit_child - // - cleanup_phase() // used from DX1Collector._cleanup_phase // - report_object_types + // - report_object_ages() + bool sanitize_flag = true; + + // swaps to- and from- spaces again + // Now from-space will be empty, all live objects in to-space + + gcos.cleanup_phase(g1, sanitize_flag); + +#ifdef NOT_YET + gcos.verify_ok(xxx objectvisitor, + xxx &verify_stats); +#endif + + // - verify_ok } } diff --git a/utest/IGCObjectVisitor_DMockCollector.cpp b/utest/IGCObjectVisitor_DMockCollector.cpp index f4472874..34a0c155 100644 --- a/utest/IGCObjectVisitor_DMockCollector.cpp +++ b/utest/IGCObjectVisitor_DMockCollector.cpp @@ -33,9 +33,9 @@ namespace xo { return self.alloc_copy(src); } auto - IGCObjectVisitor_DMockCollector::visit_child(DMockCollector & self, AGCObject * iface, void ** pp_data) noexcept -> void + IGCObjectVisitor_DMockCollector::visit_child(DMockCollector & self, VisitReason reason, AGCObject * iface, void ** pp_data) noexcept -> void { - self.visit_child(iface, pp_data); + self.visit_child(reason, iface, pp_data); } } /*namespace mm*/ diff --git a/utest/IGCObjectVisitor_DMockCollector.hpp b/utest/IGCObjectVisitor_DMockCollector.hpp index ac9db2bb..359972ba 100644 --- a/utest/IGCObjectVisitor_DMockCollector.hpp +++ b/utest/IGCObjectVisitor_DMockCollector.hpp @@ -58,7 +58,7 @@ Source must be owned by this collector. Increments object age **/ static void * alloc_copy(DMockCollector & self, std::byte * src); /** visit child of a gc-aware object. May update child in-place! **/ - static void visit_child(DMockCollector & self, AGCObject * iface, void ** pp_data) noexcept; + static void visit_child(DMockCollector & self, VisitReason reason, AGCObject * iface, void ** pp_data) noexcept; ///@} }; From 53d9ab77471032a165c34e2ffa963f35c05808f0 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 10 Apr 2026 20:32:55 -0400 Subject: [PATCH 134/174] xo-gc: refactor: migrate verify impl DX1Collector -> GCObjectStore --- include/xo/gc/DX1Collector.hpp | 2 + include/xo/gc/GCObjectStore.hpp | 12 +++++ src/gc/DX1Collector.cpp | 4 +- src/gc/GCObjectStore.cpp | 77 ++++++++++++++++++++++++++------- utest/GCObjectStore.test.cpp | 77 ++++++++++++++++++++++++++++++--- 5 files changed, 149 insertions(+), 23 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 98fc254b..b0feefcf 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -358,10 +358,12 @@ namespace xo { /** cleanup after gc **/ void _cleanup_phase(Generation upto); +#ifdef OBSOLETE /** Verify that pointer {@p iface, @p data} is valid: * destination either in to-space, or somewhere outside this collector **/ void _verify_aux(AGCObject * iface, void * data); +#endif public: /** garbage collector configuration **/ diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index dbb591c3..e1acf0cb 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -55,6 +55,9 @@ namespace xo { **/ Generation generation_of(Role r, const void * addr) const noexcept; + /** return details from last error (i.e. from g0 to-space) **/ + AllocError last_error() const noexcept; + /** get allocation size from header **/ std::size_t header2size(header_type hdr) const noexcept; /** get generation counter from alloc header **/ @@ -167,6 +170,15 @@ namespace xo { void ** lhs_data, Generation upto); + /** categorize fop {@p lhs_iface, @p lhs_data} + * based on location of @p lhs_data. + * Update @p *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); + /** Cleanup at the end of a gc cycle. * Reset from-space * (current from-space is former to-space, diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 63b6ef57..a358e68d 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -604,7 +604,7 @@ namespace xo { } case VisitReason::code::verify: // called during verify_ok - this->_verify_aux(lhs_iface, *lhs_data); + gco_store_.verify_aux(lhs_iface, *lhs_data, &verify_stats_); break; default: // should be unreachable @@ -612,6 +612,7 @@ namespace xo { } } +#ifdef OBSOLETE void DX1Collector::_verify_aux(AGCObject * iface, void * data) { @@ -640,6 +641,7 @@ namespace xo { ++(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 1f54a4c9..e20f92ab 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -164,6 +164,12 @@ namespace xo { return Generation::sentinel(); } + AllocError + GCObjectStore::last_error() const noexcept + { + return this->get_space(Role::to_space(), Generation{0})->last_error(); + } + auto GCObjectStore::header2size(header_type hdr) const noexcept -> size_type { @@ -364,16 +370,35 @@ namespace xo { (void)error_mm; - std::uint64_t n_age = config_.arena_config_.header_.max_age() + 1; + std::uint32_t hard_n_age = config_.arena_config_.header_.max_age() + 1; + // maximum age of a still-existing object + std::uint32_t soft_max_age = 0; + + // first pass, establish max age + for (Generation g{0}; g < config_.n_generation_; ++g) { + const DArena * arena = this->get_space(Role::to_space(), g); + + for (AllocInfo info : *arena) { + if (info.is_forwarding_tseq()) { + assert(false); + return false; + } + + uint32_t age = info.age(); + + assert(age < hard_n_age); + + soft_max_age = std::max(soft_max_age, age); + } + } // stats, indexed by age - DArray * stats_v = DArray::empty(mm, n_age); + DArray * stats_v = DArray::empty(mm, soft_max_age + 1); if (!stats_v) return false; - // pre-populate with empty dictionaries for each age bucket - for (std::uint64_t a = 0; a < n_age; ++a) { + for (std::uint32_t a = 0; a <= soft_max_age; ++a) { DDictionary * recd = DDictionary::make(mm); if (!recd) @@ -386,14 +411,12 @@ namespace xo { stats_v->push_back(obj(recd)); } - log && log(xtag("n_age", n_age), + log && log(xtag("soft_max_age", soft_max_age), xtag("stats_v.size", stats_v->size())); + // second pass, populate // scan to-space, count objects by age - - // track largest age with at least one object - std::int64_t max_age_present = 0; - + // for (Generation g{0}; g < config_.n_generation_; ++g) { const DArena * arena = this->get_space(Role::to_space(), g); @@ -406,9 +429,6 @@ namespace xo { uint32_t age = info.age(); size_t z = info.size(); - if (static_cast(age) > max_age_present) - max_age_present = age; - auto recd = obj::from(stats_v->at(age)); assert(recd); @@ -428,9 +448,6 @@ namespace xo { } } - // trim to only report ages up to max observed - stats_v->resize(max_age_present + 1); - *p_output = obj(stats_v); return true; @@ -642,6 +659,36 @@ namespace xo { } } /*_forward_inplace_aux*/ + void + GCObjectStore::verify_aux(AGCObject * iface, + void * data, + X1VerifyStats * p_verify_stats) + { + //scope log(XO_DEBUG(config_.debug_flag_), xtag("data", data)); + + (void)iface; + + 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 + + ++(p_verify_stats->n_from_); + } else { + ++(p_verify_stats->n_ext_); + } + } else { + assert(this->contains(Role::to_space(), data)); + + ++(p_verify_stats->n_to_); + } + } + void GCObjectStore::swap_roles(Generation upto) noexcept { diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index 75766563..198c0219 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,8 @@ namespace ut { 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, uint32_t n_test_obj, uint32_t n_test_assign) : n_gen_{n_gen}, @@ -60,6 +63,8 @@ namespace ut { gc_size_{gc_z}, object_type_z_{type_z}, do_type_registration_{do_type_registration}, + report_size_{report_z}, + error_size_{error_z}, n_test_obj_{n_test_obj}, n_test_assign_{n_test_assign} {} @@ -80,20 +85,29 @@ namespace ut { * (i.e. DBoolean) **/ bool do_type_registration_ = false; + /** size for report-output arena **/ + size_t report_size_ = 0; + /** size for error-output arena **/ + size_t error_size_ = 0; /** #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; }; + constexpr uint32_t c_report_z1 = 64 * 1024; + constexpr uint32_t c_error_z1 = 16 * 1024; + static std::vector s_testcase_v = { - /** 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), + // 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), }; /** record capturing some stats for a (randomly created) gc-aware object **/ @@ -300,6 +314,22 @@ namespace ut { .with_size(tc.gc_size_ * tc.n_gen_) .with_store_header_flag(true)); + /** Arena for holding report output: + * See GCObjectStore methods .report_object_types(), .report_object_ages() + **/ + DArena report_arena + = DArena::map(ArenaConfig().with_name("report-arena") + .with_size(tc.report_size_) + .with_store_header_flag(true)); + obj report_mm(&report_arena); + + /** Arena for holding error messages **/ + DArena error_arena + = DArena::map(ArenaConfig().with_name("error-arena") + .with_size(tc.error_size_) + .with_store_header_flag(true)); + obj error_mm(&error_arena); + // object type storage will be empty unless we install a type. GCObjectStore gcos(gcos_config); @@ -611,6 +641,39 @@ namespace ut { #endif // - verify_ok + + { + obj report_gco; + bool ok = gcos.report_object_types(report_mm, error_mm, &report_gco); + + REQUIRE(ok); + REQUIRE(report_gco); + + // TODO: print report_gco, verify output + + // discard report + + report_gco.reset(); + report_mm->clear(); + } + + { + obj report_gco; + 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())); + } + + REQUIRE(ok); + REQUIRE(report_gco); + + // TODO: print report_gco, verify output + + report_gco.reset(); + report_mm->clear(); + } } } From c4eb58f6ffa4a8374f22b5897af1f9f9a9564a93 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 11 Apr 2026 16:51:52 -0400 Subject: [PATCH 135/174] 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*/ From d1e2ae38f3bd6b780664e70c53c4bc5bd31c9e5f Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 11 Apr 2026 19:02:05 -0400 Subject: [PATCH 136/174] xo-gc: utest: refactor for modularity --- utest/GCObjectStore.test.cpp | 544 +++++++++++++++++++++-------------- 1 file changed, 323 insertions(+), 221 deletions(-) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index fa66d812..d258dba2 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -389,94 +389,15 @@ namespace ut { REQUIRE(!gcos.from_space(gn)); } } - } - - TEST_CASE("GCObjectStore-1", "[GCObjectStore]") - { - constexpr bool c_debug_flag = true; - scope log0(XO_DEBUG(c_debug_flag), "GCObjectStore test"); - - std::uint64_t seed = 12168164826603821466ul; - //random_seed(&seed); - log0 && log0(xtag("seed", seed)); - - for (size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { - // Loop iterations here are independent. - // Could execute test cases in any order - - // deterministic seed choice for each testcase - // -> individual cases preserve rng behavior - // regardless of testcase order and/or subsetting - - auto rgen = xoshiro256ss(seed + i_tc); - - const Testcase & tc = s_testcase_v[i_tc]; - - scope log1(XO_DEBUG(tc.debug_flag_), "testcase loop", xtag("i_tc", i_tc)); - - INFO(tostr(xtag("i_tc", i_tc), xtag("n_tc", n_tc))); - - /** config for each half-space **/ - ArenaConfig arena_config - = (ArenaConfig() - .with_name("arena-name-not-used") - .with_size(tc.gc_size_) - .with_store_header_flag(true)); - - GCObjectStoreConfig gcos_config(arena_config, - tc.n_gen_, - tc.n_survive_, - tc.object_type_z_, - tc.debug_flag_); - - /** Parallel arena for reference - * - * We will allocate parallel object model in this arena - * for reference; then compare with GCObjectStore behavior. - * - * 1. arena2 doesn't have any generation layer cake stuff - * 2. arena2 doesn't have concept of installed types. - * It doesn't have or require any builtin ability to traverse an object model - **/ - DArena arena2 - = DArena::map(ArenaConfig().with_name("arena2-reference") - .with_size(tc.gc_size_ * tc.n_gen_) - .with_store_header_flag(true)); - - /** Arena for holding report output: - * See GCObjectStore methods .report_object_types(), .report_object_ages() - **/ - DArena report_arena - = DArena::map(ArenaConfig().with_name("report-arena") - .with_size(tc.report_size_) - .with_store_header_flag(true)); - obj report_mm(&report_arena); - - /** Arena for holding error messages **/ - DArena error_arena - = DArena::map(ArenaConfig().with_name("error-arena") - .with_size(tc.error_size_) - .with_store_header_flag(true)); - obj error_mm(&error_arena); - - X1VerifyStats verify_stats; - - // object type storage will be empty unless we install a type. - GCObjectStore gcos(gcos_config, &verify_stats); + void + gcos_verify_vacant(const Testcase & tc, + const GCObjectStore & gcos) + { Generation g0{0}; - Generation g1{1}; + //Generation g1{1}; Generation gn{tc.n_gen_}; - // scaffold mock collector doing incremental collection - DMockCollector mock_gc(&gcos, g1); - auto mock_gc_visitor = mock_gc.ref(); - - REQUIRE(gcos.is_type_installed(typeseq::id()) == false); - REQUIRE(gcos.is_type_installed(typeseq::id()) == false); - - gcos_install_test_types(tc, &gcos); - gcos_verify_arena_partitioning(tc, gcos); // verify we have non-zero space! { @@ -490,60 +411,65 @@ namespace ut { REQUIRE(gcos.from_space(gi)->reserved() >= tc.gc_size_); } } + } - // allocator api - auto alloc = obj(gcos.new_space()); - - // create object(s). - // details depend on test case - - std::vector x1_v; - std::vector x2_v; - { - switch (tc.obj_graph_type_) { - case TestGraphType::selfcycle: - selfcycle_object_graph(&x1_v, - &gcos, - &x2_v, - &arena2); - break; - case TestGraphType::random: - random_object_graph(tc.n_test_obj_, - tc.n_test_assign_, - &rgen, - &x1_v, - &gcos, - &x2_v, - &arena2); - break; - } - - //x1_v.push_back(Recd(DBoolean::box(alloc, true), - // sizeof(DBoolean), - // typeseq::id())); + void + gcos_construct_ab_object_graphs(const Testcase & tc, + GCObjectStore * p_gcos, + DArena * p_arena2, + std::vector * p_x1_v, + std::vector * p_x2_v, + xoshiro256ss * p_rgen) + { + switch (tc.obj_graph_type_) { + case TestGraphType::selfcycle: + selfcycle_object_graph(p_x1_v, + p_gcos, + p_x2_v, + p_arena2); + break; + case TestGraphType::random: + random_object_graph(tc.n_test_obj_, + tc.n_test_assign_, + p_rgen, + p_x1_v, + p_gcos, + p_x2_v, + p_arena2); + break; } - log1 && log1("verify before any gcos side effects"); + //x1_v.push_back(Recd(DBoolean::box(alloc, true), + // sizeof(DBoolean), + // typeseq::id())); + } - { - // traverses stored objects, updates counters - // in verify_stats (= gco.p_verify_stats_, via ctor) - // - gcos.verify_ok(mock_gc_visitor); + void + gcos_verify_consistency(obj mock_gc_visitor, + GCObjectStore * p_gcos, + const X1VerifyStats & verify_stats) + { + // traverses stored objects, updates counters + // in verify_stats (= gco.p_verify_stats_, via ctor) + // + p_gcos->verify_ok(mock_gc_visitor); - INFO(tostr(xtag("n_gc_root", verify_stats.n_gc_root_), - xtag("n_ext", verify_stats.n_ext_), - xtag("n_from", verify_stats.n_from_), - xtag("n_to", verify_stats.n_to_), - xtag("n_fwd", verify_stats.n_fwd_), - xtag("n_no_iface", verify_stats.n_no_iface_))); + INFO(tostr(xtag("n_gc_root", verify_stats.n_gc_root_), + xtag("n_ext", verify_stats.n_ext_), + xtag("n_from", verify_stats.n_from_), + xtag("n_to", verify_stats.n_to_), + xtag("n_fwd", verify_stats.n_fwd_), + xtag("n_no_iface", verify_stats.n_no_iface_))); - REQUIRE(verify_stats.is_ok()); - } - - // someday: print the graph. Need a cycle-detecting printer + REQUIRE(verify_stats.is_ok()); + } + void + gcos_verify_ab_equivalence(const std::vector & x1_v, + const std::vector & x2_v) + { REQUIRE(x1_v.size() == x2_v.size()); + for (size_t i = 0, n = x1_v.size(); i < n; ++i) { REQUIRE(x1_v[i].alloc_z_ == x2_v[i].alloc_z_); REQUIRE(x1_v[i].tseq_ == x2_v[i].tseq_); @@ -551,7 +477,12 @@ namespace ut { REQUIRE(x1_v[i].gco_._typeseq() == x1_v[i].tseq_); REQUIRE(x2_v[i].gco_._typeseq() == x2_v[i].tseq_); } + } + void + gcos_verify_allocinfo(const GCObjectStore & gcos, + const std::vector & x1_v) + { // gcos can reveal info about allocs for (size_t i = 0, n = x1_v.size(); i < n; ++i) { @@ -570,6 +501,15 @@ namespace ut { REQUIRE(gcos.header2tseq(obj_info.header()) == obj_info.tseq()); REQUIRE(gcos.is_forwarding_header(obj_info.header()) == false); } + } + + void + gcos_verify_gen0_only_allocated(const Testcase & tc, + const GCObjectStore & gcos, + const std::vector & x1_v) + { + Generation g0{0}; + Generation gn{tc.n_gen_}; // new objects appear in to-space for generation 0 for (Generation gi = g0; gi < gn; ++gi) { @@ -582,35 +522,128 @@ namespace ut { REQUIRE(gcos.from_space(gi)->allocated() == 0); } + } - // swap_roles [but only for generation < g1, i.e. g0 + void + gcos_verify_gen0_fromspace_only_allocated(const Testcase & tc, + const GCObjectStore & gcos, + const std::vector & x1_v) + { + for (size_t i = 0, n = x1_v.size(); i < n; ++i) { + const auto & x1 = x1_v.at(i); + + REQUIRE(gcos.contains(Role::from_space(), x1.gco_.data())); + REQUIRE(gcos.contains_allocated(Role::from_space(), x1.gco_.data())); + AllocInfo obj_info = gcos.alloc_info((std::byte *)x1.gco_.data()); + REQUIRE(obj_info.size() >= x1.alloc_z_); + + REQUIRE(obj_info.payload().first == (std::byte *)x1.gco_.data()); + REQUIRE(obj_info.tseq() == x1.tseq_.seqno()); + + Generation g0{0}; + Generation gn{tc.n_gen_}; + + for (Generation gi = g0; gi < gn; ++gi) { + INFO(tostr(xtag("gi", gi))); + + if (gi == 0) + REQUIRE(gcos.from_space(gi)->allocated() > 0); + else + REQUIRE(gcos.from_space(gi)->allocated() == 0); + + REQUIRE(gcos.to_space(gi)->allocated() == 0); + } + } + } + + void + gcos_verify_forwarding(const GCObjectStore & gcos, + const Recd & x1, + obj x1_gco) + { + REQUIRE(gcos.contains_allocated(Role::from_space(), x1_gco.data())); + AllocInfo obj_info = gcos.alloc_info((std::byte *)x1_gco.data()); + REQUIRE(obj_info.size() >= x1.alloc_z_); + + REQUIRE(obj_info.payload().first == (std::byte *)x1_gco.data()); + REQUIRE(obj_info.is_forwarding_tseq()); + } + + void + gcos_verify_forwarding_destination(const GCObjectStore & gcos, + const Recd & x1, + obj x1p_gco) + { + REQUIRE(gcos.contains_allocated(Role::to_space(), x1p_gco.data())); + AllocInfo obj1p_info = gcos.alloc_info((std::byte *)x1p_gco.data()); + REQUIRE(obj1p_info.size() >= x1.alloc_z_); + + REQUIRE(obj1p_info.payload().first == (std::byte *)x1p_gco.data()); + REQUIRE(obj1p_info.tseq() == x1.tseq_.seqno()); + + REQUIRE(x1p_gco.data() != nullptr); + REQUIRE(gcos.contains(Role::to_space(), x1p_gco.data())); + REQUIRE(gcos.contains_allocated(Role::to_space(), x1p_gco.data())); + } + + void + gcos_verify_forwarded_ab_equivalence(obj x1p_gco, + obj x2_gco) + { + // written out polymorphic comparison + + // match DBoolean.. + bool match_attempted = false; { - gcos.swap_roles(g1); + auto x1p_b = obj::from(x1p_gco); + auto x2_b = obj::from(x2_gco); - for (size_t i = 0, n = x1_v.size(); i < n; ++i) { - const auto & x1 = x1_v.at(i); + if (x1p_b && x2_b) { + match_attempted = true; - REQUIRE(gcos.contains(Role::from_space(), x1.gco_.data())); - REQUIRE(gcos.contains_allocated(Role::from_space(), x1.gco_.data())); - AllocInfo obj_info = gcos.alloc_info((std::byte *)x1.gco_.data()); - REQUIRE(obj_info.size() >= x1.alloc_z_); + REQUIRE(x1p_b->value() == x2_b->value()); + } + } - REQUIRE(obj_info.payload().first == (std::byte *)x1.gco_.data()); - REQUIRE(obj_info.tseq() == x1.tseq_.seqno()); + // match DList.. + { + auto x1p_b = obj::from(x1p_gco); + auto x2_b = obj::from(x2_gco); - for (Generation gi = g0; gi < gn; ++gi) { - INFO(tostr(xtag("gi", gi))); + if (x1p_b && x2_b) { + match_attempted = true; - if (gi == 0) - REQUIRE(gcos.from_space(gi)->allocated() > 0); - else - REQUIRE(gcos.from_space(gi)->allocated() == 0); + // TODO: we could figure out the index in {x1_v[], x2_v[]} + // of x*_b {head, rest} respectively, + // and verify they're consistent. - REQUIRE(gcos.to_space(gi)->allocated() == 0); + REQUIRE(x1p_b->head()._typeseq() == x2_b->head()._typeseq()); + REQUIRE(x1p_b->size() == x2_b->size()); + + if (x1p_b->rest()) { + REQUIRE(x2_b->rest()); + } else { + // unreachable, since using sentinel objectd for nil list + REQUIRE(x2_b->rest() == nullptr); } } } + REQUIRE(match_attempted); + } + + void + gcos_move_roots_and_verify(const Testcase & tc, + GCObjectStore * p_gcos, + obj mock_gc_visitor, + const std::vector & x1_v, + const std::vector & x2_v, + bool debug_flag) + { + scope log(XO_DEBUG(debug_flag)); + + Generation g1{1}; + // try moving everything to to-space. // For this to week we must have registered the type, // so gc knows how to traverse it @@ -619,8 +652,8 @@ namespace ut { const auto & x1 = x1_v.at(i); const auto & x2 = x2_v.at(i); - log1 && log1("moving roots"); - log1 && log1(xtag("i", i), + log && log("moving roots"); + log && log(xtag("i", i), xtag("n", n), xtag("x1.tseq_", x1.tseq_), xtag("x1.tname", TypeRegistry::id2name(x1.tseq_))); @@ -650,99 +683,40 @@ namespace ut { // it's possible that x1.gco_ is already a forwarding pointer // before we call deep_move_root(). - AGCObject * x1p_iface = gcos.lookup_type(x1.tseq_); + AGCObject * x1p_iface = p_gcos->lookup_type(x1.tseq_); REQUIRE(x1p_iface); + // snapshot root before moving obj x1_gco = x1.gco_; // modifies x1.gco_ in place - auto x1p_data = gcos.deep_move_root(mock_gc_visitor, - x1p_iface, (void **)&(x1.gco_.data_), - g1); + auto x1p_data = p_gcos->deep_move_root(mock_gc_visitor, + x1p_iface, (void **)&(x1.gco_.data_), + g1); REQUIRE(x1p_data); REQUIRE(x1p_data == x1.gco_.data_); obj x1p_gco(x1p_iface, x1p_data); - // obj (x1_gco) now forwarding pointer to x1p_gco = x1.gco_ - { - REQUIRE(gcos.contains_allocated(Role::from_space(), x1_gco.data())); - AllocInfo obj_info = gcos.alloc_info((std::byte *)x1_gco.data()); - REQUIRE(obj_info.size() >= x1.alloc_z_); - - REQUIRE(obj_info.payload().first == (std::byte *)x1_gco.data()); - REQUIRE(obj_info.is_forwarding_tseq()); - } + // obj (x1_gco) now forwarding pointer (to x1p_gco = x1.gco_) + gcos_verify_forwarding(*p_gcos, x1, x1_gco); // obj1p same contents as original obj - { - REQUIRE(gcos.contains_allocated(Role::to_space(), x1p_gco.data())); - AllocInfo obj1p_info = gcos.alloc_info((std::byte *)x1p_gco.data()); - REQUIRE(obj1p_info.size() >= x1.alloc_z_); - - REQUIRE(obj1p_info.payload().first == (std::byte *)x1p_gco.data()); - REQUIRE(obj1p_info.tseq() == x1.tseq_.seqno()); - } - - REQUIRE(x1p_gco.data() != nullptr); - REQUIRE(gcos.contains(Role::to_space(), x1p_gco.data())); - REQUIRE(gcos.contains_allocated(Role::to_space(), x1p_gco.data())); + gcos_verify_forwarding_destination(*p_gcos, x1, x1p_gco); // x1p_gco must look like x2.gco - REQUIRE(x1p_gco._typeseq() == x2.gco_._typeseq()); - // written out polymorphic comparison - { - // match DBoolean.. - bool match_attempted = false; - { - auto x1p_b = obj::from(x1p_gco); - auto x2_b = obj::from(x2.gco_); - - if (x1p_b && x2_b) { - match_attempted = true; - - REQUIRE(x1p_b->value() == x2_b->value()); - } - } - - // match DList.. - { - auto x1p_b = obj::from(x1p_gco); - auto x2_b = obj::from(x2.gco_); - - if (x1p_b && x2_b) { - match_attempted = true; - - // TODO: we could figure out the index in {x1_v[], x2_v[]} - // of x*_b {head, rest} respectively, - // and verify they're consistent. - - REQUIRE(x1p_b->head()._typeseq() == x2_b->head()._typeseq()); - REQUIRE(x1p_b->size() == x2_b->size()); - - if (x1p_b->rest()) { - REQUIRE(x2_b->rest()); - } else { - // unreachable, since using sentinel objectd for nil list - REQUIRE(x2_b->rest() == nullptr); - } - } - } - - REQUIRE(match_attempted); - } - + gcos_verify_forwarded_ab_equivalence(x1p_gco, x2.gco_); } else { // can still try to move something. // but will fail since type isn't registered auto x1p_data - = gcos.deep_move_root(mock_gc_visitor, - x1.gco_.iface(), - (void **)&(x1.gco_.data_), - g1); + = p_gcos->deep_move_root(mock_gc_visitor, + x1.gco_.iface(), + (void **)&(x1.gco_.data_), + g1); // control here under normal GC use // would represent a configuration fail @@ -750,6 +724,134 @@ namespace ut { REQUIRE(x1p_data == nullptr); } } + } + + // fixture for GCObjectStore-1 test + class GcosFixture { + public: + explicit GcosFixture(const Testcase & tc); + + GCObjectStoreConfig gcos_config_; + + /** Parallel arena for reference + * + * We will allocate parallel object model in this arena + * for reference; then compare with GCObjectStore behavior. + * + * 1. arena2 doesn't have any generation layer cake stuff + * 2. arena2 doesn't have concept of installed types. + * It doesn't have or require any builtin ability to traverse an object model + **/ + DArena arena2_; + /** Arena for holding report output: + * See GCObjectStore methods .report_object_types(), .report_object_ages() + **/ + DArena report_arena_; + /** Arena for holding error messages **/ + DArena error_arena_; + }; + + GcosFixture::GcosFixture(const Testcase & tc) + : gcos_config_{ArenaConfig() + .with_name("gcos-fixture-arena-name-notused") + .with_size(tc.gc_size_) + .with_store_header_flag(true), + tc.n_gen_, + tc.n_survive_, + tc.object_type_z_, + tc.debug_flag_}, + arena2_{DArena::map(ArenaConfig().with_name("arena2-ref") + .with_size(tc.gc_size_ * tc.n_gen_) + .with_store_header_flag(true))}, + report_arena_{DArena::map(ArenaConfig().with_name("report-arena") + .with_size(tc.report_size_) + .with_store_header_flag(true))}, + error_arena_{DArena::map(ArenaConfig().with_name("error-arena") + .with_size(tc.error_size_) + .with_store_header_flag(true))} + {} + + } + + TEST_CASE("GCObjectStore-1", "[GCObjectStore]") + { + constexpr bool c_debug_flag = true; + scope log0(XO_DEBUG(c_debug_flag), "GCObjectStore test"); + + std::uint64_t seed = 12168164826603821466ul; + //random_seed(&seed); + log0 && log0(xtag("seed", seed)); + + for (size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + // Loop iterations here are independent. + // Could execute test cases in any order + + // deterministic seed choice for each testcase + // -> individual cases preserve rng behavior + // regardless of testcase order and/or subsetting + + auto rgen = xoshiro256ss(seed + i_tc); + + const Testcase & tc = s_testcase_v[i_tc]; + + scope log1(XO_DEBUG(tc.debug_flag_), "testcase loop", xtag("i_tc", i_tc)); + + INFO(tostr(xtag("i_tc", i_tc), xtag("n_tc", n_tc))); + + GcosFixture fixture(tc); + + obj report_mm(&fixture.report_arena_); + obj error_mm(&fixture.error_arena_); + + X1VerifyStats verify_stats; + + // object type storage will be empty unless we install a type. + GCObjectStore gcos(fixture.gcos_config_, &verify_stats); + + Generation g0{0}; + Generation g1{1}; + Generation gn{tc.n_gen_}; + + // scaffold mock collector doing incremental collection + DMockCollector mock_gc(&gcos, g1); + auto mock_gc_visitor = mock_gc.ref(); + + REQUIRE(gcos.is_type_installed(typeseq::id()) == false); + REQUIRE(gcos.is_type_installed(typeseq::id()) == false); + + gcos_install_test_types(tc, &gcos); + gcos_verify_arena_partitioning(tc, gcos); + gcos_verify_vacant(tc, gcos); + + // allocator api + auto alloc = obj(gcos.new_space()); + + // create object(s). + // details depend on test case + + std::vector x1_v; + std::vector x2_v; + + gcos_construct_ab_object_graphs(tc, &gcos, &fixture.arena2_, &x1_v, &x2_v, &rgen); + + log1 && log1("verify before any gcos side effects"); + + gcos_verify_consistency(mock_gc_visitor, + &gcos, + verify_stats); + + // someday: print the graph. Need a cycle-detecting printer + + gcos_verify_ab_equivalence(x1_v, x2_v); + gcos_verify_allocinfo(gcos, x1_v); + gcos_verify_gen0_only_allocated(tc, gcos, x1_v); + + // swap_roles [but only for generation < g1, i.e. g0 + gcos.swap_roles(g1); + + gcos_verify_gen0_fromspace_only_allocated(tc, gcos, x1_v); + + gcos_move_roots_and_verify(tc, &gcos, mock_gc_visitor, x1_v, x2_v, tc.debug_flag_); // Things to test: // - deep_move_interior() // used from MutationLogStore From 9e74e35c682b02b31fab080ee8366675a4ec7287 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 14:54:38 -0400 Subject: [PATCH 137/174] xo-gc: refactor: demote GCObjectVisitor to GCObjectStore No longer needed by DX1Collector Also retires utest/MockCollector --- CMakeLists.txt | 4 +- ...CObjectVisitor_DGCObjectStoreVisitor.json5 | 28 ++++++++ include/xo/gc/DGCObjectStoreVisitor.hpp | 49 +++++++++++++ include/xo/gc/GCObjectStore.hpp | 12 ++-- include/xo/gc/GCObjectStoreVisitor.hpp | 11 +++ include/xo/gc/X1Collector.hpp | 1 - ...GCObjectVisitor_DGCObjectStoreVisitor.hpp} | 32 ++++----- src/gc/CMakeLists.txt | 4 +- .../gc/DGCObjectStoreVisitor.cpp | 21 ++++-- src/gc/DX1Collector.cpp | 40 ++++------- src/gc/GCObjectStore.cpp | 39 ++++++----- src/gc/SetupGc.cpp | 5 +- ...IGCObjectVisitor_DGCObjectStoreVisitor.cpp | 44 ++++++++++++ .../facet/IGCObjectVisitor_DX1Collector.cpp | 44 ------------ utest/CMakeLists.txt | 10 --- utest/DMockCollector.hpp | 40 ----------- utest/GCObjectStore.test.cpp | 58 ++++++++-------- utest/IGCObjectVisitor_DMockCollector.cpp | 44 ------------ utest/IGCObjectVisitor_DMockCollector.hpp | 68 ------------------- utest/MockCollector.hpp | 13 ---- utest/init_gc_utest.cpp | 6 +- 21 files changed, 242 insertions(+), 331 deletions(-) create mode 100644 idl/IGCObjectVisitor_DGCObjectStoreVisitor.json5 create mode 100644 include/xo/gc/DGCObjectStoreVisitor.hpp create mode 100644 include/xo/gc/GCObjectStoreVisitor.hpp rename include/xo/gc/detail/{IGCObjectVisitor_DX1Collector.hpp => IGCObjectVisitor_DGCObjectStoreVisitor.hpp} (53%) rename utest/DMockCollector.cpp => src/gc/DGCObjectStoreVisitor.cpp (56%) create mode 100644 src/gc/facet/IGCObjectVisitor_DGCObjectStoreVisitor.cpp delete mode 100644 src/gc/facet/IGCObjectVisitor_DX1Collector.cpp delete mode 100644 utest/DMockCollector.hpp delete mode 100644 utest/IGCObjectVisitor_DMockCollector.cpp delete mode 100644 utest/IGCObjectVisitor_DMockCollector.hpp delete mode 100644 utest/MockCollector.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index aa0df8ef..7a74f5c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,9 +27,9 @@ xo_add_genfacetimpl( # note: manual target; generated code committed to git xo_add_genfacetimpl( - TARGET xo-gc-facetimpl-gcobjectvisitor-x1collector + TARGET xo-gc-facetimpl-gcobjectvisitor-gcobjectstorevisitor FACET_PKG xo_alloc2 - INPUT idl/IGCObjectVisitor_DX1Collector.json5 + INPUT idl/IGCObjectVisitor_DGCObjectStoreVisitor.json5 ) # ---------------------------------------------------------------- diff --git a/idl/IGCObjectVisitor_DGCObjectStoreVisitor.json5 b/idl/IGCObjectVisitor_DGCObjectStoreVisitor.json5 new file mode 100644 index 00000000..d393ba47 --- /dev/null +++ b/idl/IGCObjectVisitor_DGCObjectStoreVisitor.json5 @@ -0,0 +1,28 @@ +{ + mode: "implementation", + output_cpp_dir: "src/gc/facet", + output_hpp_dir: "include/xo/gc", + 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/GCObjectVisitor.json5", + brief: "provide AGCObjectVisitor interface for DGCObjectStoreVisitor", + using_doxygen: true, + repr: "DGCObjectStoreVisitor", + doc: [ + "Implement AGCObjectVisitor for DGCObjectStoreVisitor.", + "Visit a gc-aware object. Either evacuate+forward (for gc cycle),", + "or check consistency (for verify_ok)", + ], +} diff --git a/include/xo/gc/DGCObjectStoreVisitor.hpp b/include/xo/gc/DGCObjectStoreVisitor.hpp new file mode 100644 index 00000000..7ba65dad --- /dev/null +++ b/include/xo/gc/DGCObjectStoreVisitor.hpp @@ -0,0 +1,49 @@ +/** @file DGCObjectStoreVisitor.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace xo { + namespace mm { + + class GCObjectStore; // see GCObjectStore.hpp + class AGCObject; // see AGCObject.hpp + + /** @brief visitor shim for GCObjectStore + * + * For a GC cycle, remembers which generations + * are being collected + **/ + class DGCObjectStoreVisitor { + public: + DGCObjectStoreVisitor(GCObjectStore * gcos, Generation upto); + + template + obj ref() { return obj(this); } + + Generation generation_of(Role r, const void * addr) const noexcept; + AllocInfo alloc_info(void * mem) const noexcept; + + void visit_child(VisitReason reason, AGCObject * lhs_iface, void ** lhs_data); + std::byte * alloc_copy(void * src) noexcept; + + private: + /** object storage **/ + GCObjectStore * p_gco_store_ = nullptr; + /** collecting generations up to this bound **/ + Generation upto_; + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end DGCObjectVisitor.hpp */ diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index ba925f9b..76b15977 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -134,7 +134,7 @@ namespace xo { * to call AGCObject visitor method (forward_children()) on each * object stored here. **/ - void verify_ok(obj gc) noexcept; + void verify_ok() noexcept; /** Register object type with this collector. * Provides shallow copy and pointer forwarding for instances of this @@ -156,8 +156,7 @@ namespace xo { * * Require: runstate_.is_running() **/ - void * deep_move_root(obj gc, - const AGCObject * root_iface, + void * deep_move_root(const AGCObject * root_iface, void ** root_data, Generation upto); @@ -170,15 +169,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 + void visit_child_aux(VisitReason reason, + AGCObject * lhs_iface, + void ** lhs_data, + Generation upto); /** Evacuate object at @p *lhs_data to to-space, during collection phase * acting on generations g in [0 ,.., upto). diff --git a/include/xo/gc/GCObjectStoreVisitor.hpp b/include/xo/gc/GCObjectStoreVisitor.hpp new file mode 100644 index 00000000..37fa95b4 --- /dev/null +++ b/include/xo/gc/GCObjectStoreVisitor.hpp @@ -0,0 +1,11 @@ +/** @file GCObjectStoreVisitor.hpp +* + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include "DGCObjectStoreVisitor.hpp" +#include "detail/IGCObjectVisitor_DGCObjectStoreVisitor.hpp" + +/* end GCObjectStoreVisitor.hpp */ diff --git a/include/xo/gc/X1Collector.hpp b/include/xo/gc/X1Collector.hpp index c35f5251..38999844 100644 --- a/include/xo/gc/X1Collector.hpp +++ b/include/xo/gc/X1Collector.hpp @@ -8,6 +8,5 @@ #include "DX1Collector.hpp" #include "detail/ICollector_DX1Collector.hpp" #include "detail/IAllocator_DX1Collector.hpp" -#include "detail/IGCObjectVisitor_DX1Collector.hpp" /* end X1Collector.hpp */ diff --git a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp b/include/xo/gc/detail/IGCObjectVisitor_DGCObjectStoreVisitor.hpp similarity index 53% rename from include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp rename to include/xo/gc/detail/IGCObjectVisitor_DGCObjectStoreVisitor.hpp index 5c18c8eb..de14f5b0 100644 --- a/include/xo/gc/detail/IGCObjectVisitor_DX1Collector.hpp +++ b/include/xo/gc/detail/IGCObjectVisitor_DGCObjectStoreVisitor.hpp @@ -1,64 +1,64 @@ -/** @file IGCObjectVisitor_DX1Collector.hpp +/** @file IGCObjectVisitor_DGCObjectStoreVisitor.hpp * * Generated automagically from ingredients: * 1. code generator: * [xo-facet/codegen/genfacet] * arguments: - * --input [idl/IGCObjectVisitor_DX1Collector.json5] + * --input [idl/IGCObjectVisitor_DGCObjectStoreVisitor.json5] * 2. jinja2 template for abstract facet .hpp file: * [iface_facet_repr.hpp.j2] * 3. idl for facet methods - * [idl/IGCObjectVisitor_DX1Collector.json5] + * [idl/IGCObjectVisitor_DGCObjectStoreVisitor.json5] **/ #pragma once #include "GCObjectVisitor.hpp" -#include "DX1Collector.hpp" +#include "DGCObjectStoreVisitor.hpp" -namespace xo { namespace mm { class IGCObjectVisitor_DX1Collector; } } +namespace xo { namespace mm { class IGCObjectVisitor_DGCObjectStoreVisitor; } } namespace xo { namespace facet { template <> struct FacetImplementation + xo::mm::DGCObjectStoreVisitor> { using ImplType = xo::mm::IGCObjectVisitor_Xfer - ; + ; }; } } namespace xo { namespace mm { - /** @class IGCObjectVisitor_DX1Collector + /** @class IGCObjectVisitor_DGCObjectStoreVisitor **/ - class IGCObjectVisitor_DX1Collector { + class IGCObjectVisitor_DGCObjectStoreVisitor { public: - /** @defgroup mm-gcobjectvisitor-dx1collector-type-traits **/ + /** @defgroup mm-gcobjectvisitor-dgcobjectstorevisitor-type-traits **/ ///@{ using Copaque = xo::mm::AGCObjectVisitor::Copaque; using Opaque = xo::mm::AGCObjectVisitor::Opaque; ///@} - /** @defgroup mm-gcobjectvisitor-dx1collector-methods **/ + /** @defgroup mm-gcobjectvisitor-dgcobjectstorevisitor-methods **/ ///@{ // const methods /** allocation metadata for gc-aware data at address @p gco. @p gco must be the result of a call to collector's alloc() function **/ - static AllocInfo alloc_info(const DX1Collector & self, void * addr); + static AllocInfo alloc_info(const DGCObjectStoreVisitor & self, void * addr); /** generation to which pointer @p addr belongs, given role @p r; sentinel if @p addr is not owned by collector **/ - static Generation generation_of(const DX1Collector & self, Role r, const void * addr) noexcept; + static Generation generation_of(const DGCObjectStoreVisitor & self, Role r, const void * addr) noexcept; // non-const methods /** allocate copy of source object at address @p src. Source must be owned by this collector. Increments object age **/ - static void * alloc_copy(DX1Collector & self, std::byte * src); + static void * alloc_copy(DGCObjectStoreVisitor & self, std::byte * src); /** visit child of a gc-aware object. May update child in-place! **/ - static void visit_child(DX1Collector & self, VisitReason reason, AGCObject * iface, void ** pp_data) noexcept; + static void visit_child(DGCObjectStoreVisitor & self, VisitReason reason, AGCObject * iface, void ** pp_data) noexcept; ///@} }; diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index 7cbce173..0dcfa2e3 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -12,10 +12,12 @@ set(SELF_SRCS X1CollectorConfig.cpp DX1Collector.cpp facet/ICollector_DX1Collector.cpp - facet/IGCObjectVisitor_DX1Collector.cpp DX1CollectorIterator.cpp + DGCObjectStoreVisitor.cpp + facet/IGCObjectVisitor_DGCObjectStoreVisitor.cpp + GCObjectStoreConfig.cpp GCObjectStore.cpp diff --git a/utest/DMockCollector.cpp b/src/gc/DGCObjectStoreVisitor.cpp similarity index 56% rename from utest/DMockCollector.cpp rename to src/gc/DGCObjectStoreVisitor.cpp index 833d32b2..9c2618d2 100644 --- a/utest/DMockCollector.cpp +++ b/src/gc/DGCObjectStoreVisitor.cpp @@ -1,27 +1,34 @@ -/** @file DMockCollector.cpp +/** @file DGCObjectStoreVisitor.cpp * * @author Roland Conybeare, Apr 2026 **/ -#include "MockCollector.hpp" +#include "GCObjectStoreVisitor.hpp" +#include "GCObjectStore.hpp" namespace xo { namespace mm { + DGCObjectStoreVisitor::DGCObjectStoreVisitor(GCObjectStore * gcos, + Generation upto) + : p_gco_store_{gcos}, upto_{upto} + {} + Generation - DMockCollector::generation_of(Role r, const void * addr) const noexcept + DGCObjectStoreVisitor::generation_of(Role r, const void * addr) const noexcept { return p_gco_store_->generation_of(r, addr); } AllocInfo - DMockCollector::alloc_info(void * mem) const noexcept + DGCObjectStoreVisitor::alloc_info(void * mem) const noexcept { return p_gco_store_->alloc_info((std::byte *)mem); } void - DMockCollector::visit_child(VisitReason reason, AGCObject * lhs_iface, void ** lhs_data) + DGCObjectStoreVisitor::visit_child(VisitReason reason, + AGCObject * lhs_iface, void ** lhs_data) { switch (reason.code()) { case VisitReason::code::forward: @@ -37,11 +44,11 @@ namespace xo { } std::byte * - DMockCollector::alloc_copy(void * src) noexcept { + DGCObjectStoreVisitor::alloc_copy(void * src) noexcept { return p_gco_store_->new_space()->alloc_copy((std::byte *)src); } } /*namespace mm*/ } /*namespace xo*/ -/* end DMockCollector.cpp */ +/* end DGCObjectVisitor.cpp */ diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index cf079781..d8f4021f 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -4,6 +4,7 @@ **/ #include "X1Collector.hpp" +#include "GCObjectStoreVisitor.hpp" #include #include @@ -13,7 +14,6 @@ #include #include -#include #include #include #include "object_age.hpp" @@ -383,7 +383,8 @@ namespace xo { // Add run state so DX1Collector can recognize forward_inplace() // calls made for the purpose of checking child pointers. - auto self = this->ref(); + DGCObjectStoreVisitor visitor(&gco_store_, + Generation{0} /*not used for verify*/); GCRunState saved_runstate = runstate_; { @@ -404,8 +405,7 @@ namespace xo { // - X1Collector::forward_inplace() -> _verify_aux() // - gco.visit_gco_children(VisitReason::verify(), self); - + gco.visit_gco_children(VisitReason::verify(), visitor.ref()); } X1VerifyStats post = verify_stats_; @@ -417,7 +417,7 @@ namespace xo { } // 3. scan to-space for each generation - gco_store_.verify_ok(this->ref()); + gco_store_.verify_ok(); // 4. scan mutation logs mlog_store_.verify_ok(&gco_store_, @@ -483,7 +483,7 @@ namespace xo { //auto t0 = std::chrono::steady_clock::now(); log && log("memory"); - auto visitor = [&log](const MemorySizeInfo & info) { + auto resource_visitor = [&log](const MemorySizeInfo & info) { log && log(xtag("resource", info.resource_name_), xtag("used", info.used_), xtag("alloc", info.allocated_), @@ -492,7 +492,7 @@ namespace xo { xtag("lo", info.lo_), xtag("hi", info.hi_)); }; - this->visit_pools(visitor); + this->visit_pools(resource_visitor); if (config_.sanitize_flag_) { log && log("step 0a : verify"); @@ -500,6 +500,8 @@ namespace xo { } + DGCObjectStoreVisitor gco_visitor(&gco_store_, upto); + log && log("step 0b : update run state"); this->runstate_ = GCRunState::gc_upto(upto); @@ -519,7 +521,7 @@ namespace xo { log && log("step 2b : [STUB] copy pinned"); log && log("step 3 : [STUB] forward mutation log"); - mlog_store_.forward_mutation_log(this->ref(), upto); + mlog_store_.forward_mutation_log(gco_visitor.ref(), upto); log && log("step 4a : [STUB] run destructors"); log && log("step 4b : [STUB] keep reachable weak pointers"); @@ -576,8 +578,7 @@ 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()->iface(), + void * root_to = gco_store_.deep_move_root(slot.root()->iface(), (void **)&(slot.root()->data_), upto); slot.root()->reset_opaque(root_to); @@ -594,24 +595,9 @@ namespace xo { // 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(); + Generation upto = runstate_.gc_upto(); - // called during collection phase - gco_store_.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); - } + gco_store_.visit_child_aux(reason, lhs_iface, lhs_data, upto); } auto diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index b76801d2..18188a4c 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -4,6 +4,7 @@ **/ #include "GCObjectStore.hpp" +#include "GCObjectStoreVisitor.hpp" #include "X1VerifyStats.hpp" #include @@ -475,35 +476,33 @@ namespace xo { return (g < upto); } -#ifdef NOT_YET void - GCObjectStore::visit_child(VisitReason reason, - AGCObject * lhs_iface, - void ** lhs_data) + GCObjectStore::visit_child_aux(VisitReason reason, + AGCObject * lhs_iface, + void ** lhs_data, + Generation upto) { - // 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(); + DGCObjectStoreVisitor gcos_visitor(this, upto); + auto gcos_visitor_obj + = obj(&gcos_visitor); // called during collection phase this->forward_inplace_aux - (this->ref(), lhs_iface, lhs_data, upto); + (gcos_visitor_obj, lhs_iface, lhs_data, upto); break; } case VisitReason::code::verify: // called during verify_ok - gco_store_.verify_aux(lhs_iface, *lhs_data); + this->verify_aux(lhs_iface, *lhs_data); break; default: // should be unreachable assert(false); } } -#endif void GCObjectStore::forward_inplace_aux(obj gc, @@ -768,8 +767,11 @@ namespace xo { } void - GCObjectStore::verify_ok(obj gc) noexcept + GCObjectStore::verify_ok() noexcept { + Generation unused_gen; + DGCObjectStoreVisitor visitor{this, unused_gen}; + for (Generation g(0); g < config_.n_generation_; ++g) { const DArena * space = this->get_space(Role::to_space(), g); @@ -789,7 +791,7 @@ namespace xo { // assembled fop for gc-aware object obj gco(iface, const_cast(data)); - gco.visit_gco_children(VisitReason::verify(), gc); + gco.visit_gco_children(VisitReason::verify(), visitor.ref()); } else { ++(p_verify_stats_->n_no_iface_); continue; @@ -830,8 +832,7 @@ namespace xo { } void * - GCObjectStore::deep_move_root(obj gc, - const AGCObject * root_iface, + GCObjectStore::deep_move_root(const AGCObject * root_iface, void ** root_data, Generation upto) { @@ -849,8 +850,10 @@ namespace xo { bool src_in_from_space = this->contains(Role::from_space(), *root_data); + DGCObjectStoreVisitor visitor(this, upto); + if (src_in_from_space) { - *root_data = this->_deep_move_gc_owned(gc, *root_data, upto); + *root_data = this->_deep_move_gc_owned(visitor.ref(), *root_data, upto); } else { // we aren't moving from_src, it's not gc-owned. // However we are moving all its gc-owned children @@ -860,7 +863,7 @@ namespace xo { auto root = obj(root_iface, *root_data); - root.visit_gco_children(VisitReason::forward(), gc); + root.visit_gco_children(VisitReason::forward(), visitor.ref()); // For each generation g: // traverse objects newer than gray_lo_v[g], to make sure children @@ -868,7 +871,7 @@ namespace xo { // Remember that forwarding may promote objects to older generation, // so need multiple passes // - this->_forward_children_until_fixpoint(gc, upto, gray_lo_v); + this->_forward_children_until_fixpoint(visitor.ref(), upto, gray_lo_v); // reminder: *root_data preserved diff --git a/src/gc/SetupGc.cpp b/src/gc/SetupGc.cpp index fa0ea721..dc5fad04 100644 --- a/src/gc/SetupGc.cpp +++ b/src/gc/SetupGc.cpp @@ -5,6 +5,7 @@ #include "SetupGc.hpp" #include "X1Collector.hpp" +#include "GCObjectStoreVisitor.hpp" #include #include @@ -24,9 +25,11 @@ namespace xo { FacetRegistry::register_impl(); FacetRegistry::register_impl(); - FacetRegistry::register_impl(); + + FacetRegistry::register_impl(); log && log(xtag("DX1Collector.tseq", typeseq::id())); + log && log(xtag("DGCObjectStoreVisitor.tseq", typeseq::id())); log && log(xtag("ACollector.tseq", typeseq::id())); log && log(xtag("AGCObjectVisitor.tseq", typeseq::id())); diff --git a/src/gc/facet/IGCObjectVisitor_DGCObjectStoreVisitor.cpp b/src/gc/facet/IGCObjectVisitor_DGCObjectStoreVisitor.cpp new file mode 100644 index 00000000..b0306f3a --- /dev/null +++ b/src/gc/facet/IGCObjectVisitor_DGCObjectStoreVisitor.cpp @@ -0,0 +1,44 @@ +/** @file IGCObjectVisitor_DGCObjectStoreVisitor.cpp + * + * Generated automagically from ingredients: + * 1. code generator: + * [xo-facet/codegen/genfacet] + * arguments: + * --input [idl/IGCObjectVisitor_DGCObjectStoreVisitor.json5] + * 2. jinja2 template for abstract facet .hpp file: + * [iface_facet_any.hpp.j2] + * 3. idl for facet methods + * [idl/IGCObjectVisitor_DGCObjectStoreVisitor.json5] +**/ + +#include "detail/IGCObjectVisitor_DGCObjectStoreVisitor.hpp" + +namespace xo { + namespace mm { + auto + IGCObjectVisitor_DGCObjectStoreVisitor::alloc_info(const DGCObjectStoreVisitor & self, void * addr) -> AllocInfo + { + return self.alloc_info(addr); + } + + auto + IGCObjectVisitor_DGCObjectStoreVisitor::generation_of(const DGCObjectStoreVisitor & self, Role r, const void * addr) noexcept -> Generation + { + return self.generation_of(r, addr); + } + + auto + IGCObjectVisitor_DGCObjectStoreVisitor::alloc_copy(DGCObjectStoreVisitor & self, std::byte * src) -> void * + { + return self.alloc_copy(src); + } + auto + IGCObjectVisitor_DGCObjectStoreVisitor::visit_child(DGCObjectStoreVisitor & self, VisitReason reason, AGCObject * iface, void ** pp_data) noexcept -> void + { + self.visit_child(reason, iface, pp_data); + } + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end IGCObjectVisitor_DGCObjectStoreVisitor.cpp */ diff --git a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp b/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp deleted file mode 100644 index 17f364ed..00000000 --- a/src/gc/facet/IGCObjectVisitor_DX1Collector.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/** @file IGCObjectVisitor_DX1Collector.cpp - * - * Generated automagically from ingredients: - * 1. code generator: - * [xo-facet/codegen/genfacet] - * arguments: - * --input [idl/IGCObjectVisitor_DX1Collector.json5] - * 2. jinja2 template for abstract facet .hpp file: - * [iface_facet_any.hpp.j2] - * 3. idl for facet methods - * [idl/IGCObjectVisitor_DX1Collector.json5] -**/ - -#include "detail/IGCObjectVisitor_DX1Collector.hpp" - -namespace xo { - namespace mm { - auto - IGCObjectVisitor_DX1Collector::alloc_info(const DX1Collector & self, void * addr) -> AllocInfo - { - return self.alloc_info(addr); - } - - auto - IGCObjectVisitor_DX1Collector::generation_of(const DX1Collector & self, Role r, const void * addr) noexcept -> Generation - { - return self.generation_of(r, addr); - } - - auto - IGCObjectVisitor_DX1Collector::alloc_copy(DX1Collector & self, std::byte * src) -> void * - { - return self.alloc_copy(src); - } - auto - IGCObjectVisitor_DX1Collector::visit_child(DX1Collector & self, VisitReason reason, AGCObject * iface, void ** pp_data) noexcept -> void - { - self.visit_child(reason, iface, pp_data); - } - - } /*namespace mm*/ -} /*namespace xo*/ - -/* end IGCObjectVisitor_DX1Collector.cpp */ diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 599e1ea1..8f9b9503 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -10,20 +10,10 @@ set(UTEST_SRCS GCObjectStore.test.cpp Object2.test.cpp - DMockCollector.cpp - IGCObjectVisitor_DMockCollector.cpp - init_gc_utest.cpp random_allocs.cpp ) -# mock collector for unit test -xo_add_genfacetimpl( - TARGET xo-gc-facetimpl-gcobjectvisitor-mockcollector - FACET_PKG xo_alloc2 - INPUT idl/IGCObjectVisitor_DMockCollector.json5 -) - if (ENABLE_TESTING) xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS}) xo_headeronly_dependency(${UTEST_EXE} randomgen) diff --git a/utest/DMockCollector.hpp b/utest/DMockCollector.hpp deleted file mode 100644 index 3297f8fa..00000000 --- a/utest/DMockCollector.hpp +++ /dev/null @@ -1,40 +0,0 @@ -/** @file DMockCollector.hpp - * - * @author Roland Conybeare, Apr 2026 - **/ - -#pragma once - -#include -#include - -namespace xo { - namespace mm { - - /** @brief Mock Collector - * - * Intended to help unit test a GCObjectSotre instance. - * Mock a Collector in collection phase for generations 0 <= g < @ref upto_. - **/ - class DMockCollector { - public: - explicit DMockCollector(GCObjectStore * gcos, Generation upto) : p_gco_store_{gcos}, upto_{upto} {} - - template - obj ref() { return obj(this); } - - Generation generation_of(Role r, const void * addr) const noexcept; - AllocInfo alloc_info(void * mem) const noexcept; - - void visit_child(VisitReason reason, AGCObject * lhs_iface, void ** lhs_data); - std::byte * alloc_copy(void * src) noexcept; - - private: - GCObjectStore * p_gco_store_ = nullptr; - Generation upto_; - }; - - } /*namespace mm*/ -} /*namespaace xo*/ - -/* end DMockCollector.hpp */ diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index d258dba2..8e1ae05c 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -5,8 +5,6 @@ #include #include -#include "MockCollector.hpp" - #include #include #include @@ -27,7 +25,6 @@ namespace ut { 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::X1VerifyStats; @@ -395,7 +392,6 @@ namespace ut { const GCObjectStore & gcos) { Generation g0{0}; - //Generation g1{1}; Generation gn{tc.n_gen_}; @@ -413,6 +409,13 @@ namespace ut { } } + /** Generate two copies of a random object graph for test case @p tc. + * Store first graph in @p *p_x1_v, allocating + * entirely from @p p_gcos new-space. + * Store second graph in @p *p_x2_v, allocating + * entirely from @p p_arena2. + * Use random number generator @p_rgen + **/ void gcos_construct_ab_object_graphs(const Testcase & tc, GCObjectStore * p_gcos, @@ -444,24 +447,26 @@ namespace ut { // typeseq::id())); } + /** Invoke built-in consistency verification for @p *p_gcos. + **/ void - gcos_verify_consistency(obj mock_gc_visitor, - GCObjectStore * p_gcos, - const X1VerifyStats & verify_stats) + gcos_verify_consistency(GCObjectStore * p_gcos) { // traverses stored objects, updates counters // in verify_stats (= gco.p_verify_stats_, via ctor) // - p_gcos->verify_ok(mock_gc_visitor); + p_gcos->verify_ok(); - 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_))); + X1VerifyStats * verify_stats = p_gcos->verify_stats(); - REQUIRE(verify_stats.is_ok()); + 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()); } void @@ -635,7 +640,7 @@ namespace ut { void gcos_move_roots_and_verify(const Testcase & tc, GCObjectStore * p_gcos, - obj mock_gc_visitor, + Generation upto, const std::vector & x1_v, const std::vector & x2_v, bool debug_flag) @@ -690,9 +695,9 @@ namespace ut { obj x1_gco = x1.gco_; // modifies x1.gco_ in place - auto x1p_data = p_gcos->deep_move_root(mock_gc_visitor, - x1p_iface, (void **)&(x1.gco_.data_), - g1); + auto x1p_data + = p_gcos->deep_move_root(x1p_iface, (void **)&(x1.gco_.data_), upto); + REQUIRE(x1p_data); REQUIRE(x1p_data == x1.gco_.data_); @@ -713,8 +718,7 @@ namespace ut { // but will fail since type isn't registered auto x1p_data - = p_gcos->deep_move_root(mock_gc_visitor, - x1.gco_.iface(), + = p_gcos->deep_move_root(x1.gco_.iface(), (void **)&(x1.gco_.data_), g1); @@ -812,10 +816,6 @@ namespace ut { Generation g1{1}; Generation gn{tc.n_gen_}; - // scaffold mock collector doing incremental collection - DMockCollector mock_gc(&gcos, g1); - auto mock_gc_visitor = mock_gc.ref(); - REQUIRE(gcos.is_type_installed(typeseq::id()) == false); REQUIRE(gcos.is_type_installed(typeseq::id()) == false); @@ -836,9 +836,7 @@ namespace ut { log1 && log1("verify before any gcos side effects"); - gcos_verify_consistency(mock_gc_visitor, - &gcos, - verify_stats); + gcos_verify_consistency(&gcos); // someday: print the graph. Need a cycle-detecting printer @@ -851,7 +849,7 @@ namespace ut { gcos_verify_gen0_fromspace_only_allocated(tc, gcos, x1_v); - gcos_move_roots_and_verify(tc, &gcos, mock_gc_visitor, x1_v, x2_v, tc.debug_flag_); + gcos_move_roots_and_verify(tc, &gcos, g1, x1_v, x2_v, tc.debug_flag_); // Things to test: // - deep_move_interior() // used from MutationLogStore @@ -870,7 +868,7 @@ namespace ut { // traverses stored objects, updates counters // in verify_stats (= gco.p_verify_stats_, via ctor) // - gcos.verify_ok(mock_gc_visitor); + gcos.verify_ok(); INFO(tostr(xtag("n_gc_root", verify_stats.n_gc_root_), xtag("n_ext", verify_stats.n_ext_), diff --git a/utest/IGCObjectVisitor_DMockCollector.cpp b/utest/IGCObjectVisitor_DMockCollector.cpp deleted file mode 100644 index 34a0c155..00000000 --- a/utest/IGCObjectVisitor_DMockCollector.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/** @file IGCObjectVisitor_DMockCollector.cpp - * - * Generated automagically from ingredients: - * 1. code generator: - * [xo-facet/codegen/genfacet] - * arguments: - * --input [idl/IGCObjectVisitor_DMockCollector.json5] - * 2. jinja2 template for abstract facet .hpp file: - * [iface_facet_any.hpp.j2] - * 3. idl for facet methods - * [idl/IGCObjectVisitor_DMockCollector.json5] -**/ - -#include "./IGCObjectVisitor_DMockCollector.hpp" - -namespace xo { - namespace mm { - auto - IGCObjectVisitor_DMockCollector::alloc_info(const DMockCollector & self, void * addr) -> AllocInfo - { - return self.alloc_info(addr); - } - - auto - IGCObjectVisitor_DMockCollector::generation_of(const DMockCollector & self, Role r, const void * addr) noexcept -> Generation - { - return self.generation_of(r, addr); - } - - auto - IGCObjectVisitor_DMockCollector::alloc_copy(DMockCollector & self, std::byte * src) -> void * - { - return self.alloc_copy(src); - } - auto - IGCObjectVisitor_DMockCollector::visit_child(DMockCollector & self, VisitReason reason, AGCObject * iface, void ** pp_data) noexcept -> void - { - self.visit_child(reason, iface, pp_data); - } - - } /*namespace mm*/ -} /*namespace xo*/ - -/* end IGCObjectVisitor_DMockCollector.cpp */ diff --git a/utest/IGCObjectVisitor_DMockCollector.hpp b/utest/IGCObjectVisitor_DMockCollector.hpp deleted file mode 100644 index 359972ba..00000000 --- a/utest/IGCObjectVisitor_DMockCollector.hpp +++ /dev/null @@ -1,68 +0,0 @@ -/** @file IGCObjectVisitor_DMockCollector.hpp - * - * Generated automagically from ingredients: - * 1. code generator: - * [xo-facet/codegen/genfacet] - * arguments: - * --input [idl/IGCObjectVisitor_DMockCollector.json5] - * 2. jinja2 template for abstract facet .hpp file: - * [iface_facet_repr.hpp.j2] - * 3. idl for facet methods - * [idl/IGCObjectVisitor_DMockCollector.json5] - **/ - -#pragma once - -#include "GCObjectVisitor.hpp" -#include "DMockCollector.hpp" - -namespace xo { namespace mm { class IGCObjectVisitor_DMockCollector; } } - -namespace xo { - namespace facet { - template <> - struct FacetImplementation - { - using ImplType = xo::mm::IGCObjectVisitor_Xfer - ; - }; - } -} - -namespace xo { - namespace mm { - /** @class IGCObjectVisitor_DMockCollector - **/ - class IGCObjectVisitor_DMockCollector { - public: - /** @defgroup mm-gcobjectvisitor-dmockcollector-type-traits **/ - ///@{ - using Copaque = xo::mm::AGCObjectVisitor::Copaque; - using Opaque = xo::mm::AGCObjectVisitor::Opaque; - ///@} - /** @defgroup mm-gcobjectvisitor-dmockcollector-methods **/ - ///@{ - // const methods - /** allocation metadata for gc-aware data at address @p gco. -@p gco must be the result of a call to collector's alloc() function **/ - static AllocInfo alloc_info(const DMockCollector & self, void * addr); - /** generation to which pointer @p addr belongs, given role @p r; -sentinel if @p addr is not owned by collector **/ - static Generation generation_of(const DMockCollector & self, Role r, const void * addr) noexcept; - - // non-const methods - /** 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); - /** visit child of a gc-aware object. May update child in-place! **/ - static void visit_child(DMockCollector & self, VisitReason reason, AGCObject * iface, void ** pp_data) noexcept; - ///@} - }; - - } /*namespace mm*/ -} /*namespace xo*/ - -/* end */ \ No newline at end of file diff --git a/utest/MockCollector.hpp b/utest/MockCollector.hpp deleted file mode 100644 index 4e3f3d1b..00000000 --- a/utest/MockCollector.hpp +++ /dev/null @@ -1,13 +0,0 @@ -/** @file MockCollector.hpp - * - * @author Roland Conybeare, Apr 2026 - **/ - -#pragma once - -#include "DMockCollector.hpp" -#include "IGCObjectVisitor_DMockCollector.hpp" -//#include "ICollector_DMockCollector.hpp" -//#include "IAllocator_DMockCollector.hpp" - -/* end MockCollector.hpp */ diff --git a/utest/init_gc_utest.cpp b/utest/init_gc_utest.cpp index c708c561..5f1958cf 100644 --- a/utest/init_gc_utest.cpp +++ b/utest/init_gc_utest.cpp @@ -4,7 +4,7 @@ **/ #include "init_gc_utest.hpp" -#include "MockCollector.hpp" +//#include "MockCollector.hpp" #include #include #include @@ -19,9 +19,9 @@ namespace xo { { scope log(XO_DEBUG(false)); - FacetRegistry::register_impl(); + //FacetRegistry::register_impl(); - log && log(xtag("DMockCollector.tseq", typeseq::id())); + //log && log(xtag("DMockCollector.tseq", typeseq::id())); return true; } From 134f5614a35d72b6201a81f687e24b8b75cc13cc Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 15:00:48 -0400 Subject: [PATCH 138/174] xo-gc: streamline: drop .visit_child methods no longer needed, thanks to DGCObjectStoreVisitor --- include/xo/gc/DX1Collector.hpp | 8 ----- include/xo/gc/GCObjectStore.hpp | 53 +++++++++++++------------------- src/gc/DGCObjectStoreVisitor.cpp | 4 +-- src/gc/DX1Collector.cpp | 13 -------- src/gc/GCObjectStore.cpp | 38 +++-------------------- 5 files changed, 29 insertions(+), 87 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index b0feefcf..a582ed87 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -274,14 +274,6 @@ namespace xo { /** Execute gc immediately, for all generations < @p upto **/ void execute_gc(Generation upto) noexcept; - /** Supports 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); - // ----- allocation ----- /** simple allocation. allocate @p z bytes of memory diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 76b15977..5fdddeb3 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -169,37 +169,6 @@ namespace xo { void * from_src, Generation upto); - /** 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_aux(VisitReason reason, - AGCObject * lhs_iface, - void ** lhs_data, - Generation upto); - - /** 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 - * forward_children() - * - * Replace original with forwarding pointer to new location - **/ - void forward_inplace_aux(obj gc, - AGCObject * lhs_iface, - void ** lhs_data, - Generation upto); - - /** categorize fop {@p lhs_iface, @p lhs_data} - * based on location of @p lhs_data. - * 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); - /** Cleanup at the end of a gc cycle. * Reset from-space * (current from-space is former to-space, @@ -252,6 +221,28 @@ namespace xo { AGCObject * iface, void * from_src); + /** 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 + * forward_children() + * + * Replace original with forwarding pointer to new location + **/ + void _forward_inplace_aux(obj gc, + AGCObject * lhs_iface, + void ** lhs_data, + Generation upto); + + /** categorize fop {@p lhs_iface, @p lhs_data} + * based on location of @p lhs_data. + * 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); + + friend class DGCObjectStoreVisitor; + private: /** configuration for gc-aware object store **/ GCObjectStoreConfig config_; diff --git a/src/gc/DGCObjectStoreVisitor.cpp b/src/gc/DGCObjectStoreVisitor.cpp index 9c2618d2..713bf912 100644 --- a/src/gc/DGCObjectStoreVisitor.cpp +++ b/src/gc/DGCObjectStoreVisitor.cpp @@ -32,11 +32,11 @@ namespace xo { { switch (reason.code()) { case VisitReason::code::forward: - p_gco_store_->forward_inplace_aux + 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); + p_gco_store_->_verify_aux(lhs_iface, *lhs_data); break; default: assert(false); diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index d8f4021f..6232bb30 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -587,19 +587,6 @@ namespace xo { } } - void - DX1Collector::visit_child(VisitReason reason, - AGCObject * lhs_iface, - void ** lhs_data) - { - // MAYBE: adapter distinct from DX1Collector that supports GCObjectVisitor facet, - // calls DX1Collector::_verify_aux() - - Generation upto = runstate_.gc_upto(); - - gco_store_.visit_child_aux(reason, lhs_iface, lhs_data, upto); - } - auto DX1Collector::alloc(typeseq t, size_type z) noexcept -> value_type { diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 18188a4c..971d053f 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -477,38 +477,10 @@ namespace xo { } void - GCObjectStore::visit_child_aux(VisitReason reason, - AGCObject * lhs_iface, - void ** lhs_data, - Generation upto) - { - switch (reason.code()) { - case VisitReason::code::forward: - { - DGCObjectStoreVisitor gcos_visitor(this, upto); - auto gcos_visitor_obj - = obj(&gcos_visitor); - - // called during collection phase - this->forward_inplace_aux - (gcos_visitor_obj, lhs_iface, lhs_data, upto); - break; - } - case VisitReason::code::verify: - // called during verify_ok - this->verify_aux(lhs_iface, *lhs_data); - break; - default: - // should be unreachable - assert(false); - } - } - - void - GCObjectStore::forward_inplace_aux(obj gc, - AGCObject * lhs_iface, - void ** lhs_data, - Generation upto) + GCObjectStore::_forward_inplace_aux(obj gc, + AGCObject * lhs_iface, + void ** lhs_data, + Generation upto) { // upto == runstate_.gc_upto() @@ -693,7 +665,7 @@ namespace xo { } /*_forward_inplace_aux*/ void - GCObjectStore::verify_aux(AGCObject * iface, + GCObjectStore::_verify_aux(AGCObject * iface, void * data) { scope log(XO_DEBUG(config_.debug_flag_)); From 2b9bde89b507c075bb0390c9afed18ca86284be6 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 15:03:05 -0400 Subject: [PATCH 139/174] xo-gc: tidy: reorder method defns --- src/gc/GCObjectStore.cpp | 62 ++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 971d053f..b956d366 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -695,37 +695,6 @@ namespace xo { } } - void - GCObjectStore::swap_roles(Generation upto) noexcept - { - 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)); - - std::swap(space_[Role::to_space()][g], space_[Role::from_space()][g]); - } - } - - void - GCObjectStore::cleanup_phase(Generation upto, - bool sanitize_flag) - { - scope log(XO_DEBUG(config_.debug_flag_), xtag("upto", upto)); - - // everything live has been copied out of from-space - // -> now set to empty - // - for (Generation g = Generation{0}; g < upto; ++g) { - if (sanitize_flag) { - space_[Role::from_space()][g]->scrub(); - } - - space_[Role::from_space()][g]->clear(); - } - } - auto GCObjectStore::snap_move_checkpoint(Generation upto) -> GCMoveCheckpoint { @@ -803,6 +772,37 @@ namespace xo { return true; } + void + GCObjectStore::swap_roles(Generation upto) noexcept + { + 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)); + + std::swap(space_[Role::to_space()][g], space_[Role::from_space()][g]); + } + } + + void + GCObjectStore::cleanup_phase(Generation upto, + bool sanitize_flag) + { + scope log(XO_DEBUG(config_.debug_flag_), xtag("upto", upto)); + + // everything live has been copied out of from-space + // -> now set to empty + // + for (Generation g = Generation{0}; g < upto; ++g) { + if (sanitize_flag) { + space_[Role::from_space()][g]->scrub(); + } + + space_[Role::from_space()][g]->clear(); + } + } + void * GCObjectStore::deep_move_root(const AGCObject * root_iface, void ** root_data, From b1155655d5f396af0a4f151bf6298004aac98efb Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 15:05:39 -0400 Subject: [PATCH 140/174] xo-gc: tidy: SetupGc.*pp -> setup_gc.*pp --- include/xo/gc/SetupGc.hpp | 20 ------------------- src/gc/CMakeLists.txt | 2 +- src/gc/SetupGc.cpp | 42 --------------------------------------- src/gc/init_gc.cpp | 2 +- 4 files changed, 2 insertions(+), 64 deletions(-) delete mode 100644 include/xo/gc/SetupGc.hpp delete mode 100644 src/gc/SetupGc.cpp diff --git a/include/xo/gc/SetupGc.hpp b/include/xo/gc/SetupGc.hpp deleted file mode 100644 index bf5c5c6d..00000000 --- a/include/xo/gc/SetupGc.hpp +++ /dev/null @@ -1,20 +0,0 @@ -/** @file SetupGc.hpp - * - * @author Roland Conybeare, Mar 2026 - **/ - -#pragma once - -namespace xo { - namespace mm { - - class SetupGc { - public: - /** Register gc (facet,impl) combinations with FacetRegistry **/ - static bool register_facets(); - }; - - } /*namespace mm*/ -} /*namespace xo*/ - -/* end SetupGc.hpp */ diff --git a/src/gc/CMakeLists.txt b/src/gc/CMakeLists.txt index 0dcfa2e3..5fa098f5 100644 --- a/src/gc/CMakeLists.txt +++ b/src/gc/CMakeLists.txt @@ -4,7 +4,7 @@ set(SELF_LIB xo_gc) set(SELF_SRCS init_gc.cpp - SetupGc.cpp + setup_gc.cpp IAllocator_DX1Collector.cpp IAllocIterator_DX1CollectorIterator.cpp diff --git a/src/gc/SetupGc.cpp b/src/gc/SetupGc.cpp deleted file mode 100644 index dc5fad04..00000000 --- a/src/gc/SetupGc.cpp +++ /dev/null @@ -1,42 +0,0 @@ -/** @file SetupGc.cpp - * - * @author Roland Conybeare, Mar 2026 - **/ - -#include "SetupGc.hpp" -#include "X1Collector.hpp" -#include "GCObjectStoreVisitor.hpp" -#include -#include - -namespace xo { - using xo::mm::AAllocator; - using xo::mm::ACollector; - using xo::mm::DX1Collector; - using xo::facet::FacetRegistry; - using xo::reflect::typeseq; - - namespace mm { - - bool - SetupGc::register_facets() - { - scope log(XO_DEBUG(true)); - - FacetRegistry::register_impl(); - FacetRegistry::register_impl(); - - FacetRegistry::register_impl(); - - log && log(xtag("DX1Collector.tseq", typeseq::id())); - log && log(xtag("DGCObjectStoreVisitor.tseq", typeseq::id())); - - log && log(xtag("ACollector.tseq", typeseq::id())); - log && log(xtag("AGCObjectVisitor.tseq", typeseq::id())); - - return true; - } - } -} /*namespace xo*/ - -/* end SetupGc.cpp */ diff --git a/src/gc/init_gc.cpp b/src/gc/init_gc.cpp index 40dd700a..66002396 100644 --- a/src/gc/init_gc.cpp +++ b/src/gc/init_gc.cpp @@ -4,7 +4,7 @@ **/ #include "init_gc.hpp" -#include "SetupGc.hpp" +#include "setup_gc.hpp" #include #include From d0fea5fcf1a69777ec4c2c72ae223b92b7471c45 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 15:06:01 -0400 Subject: [PATCH 141/174] xo-gc: missed files in prev commit --- include/xo/gc/setup_gc.hpp | 20 ++++++++++++++++++ src/gc/setup_gc.cpp | 42 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 include/xo/gc/setup_gc.hpp create mode 100644 src/gc/setup_gc.cpp diff --git a/include/xo/gc/setup_gc.hpp b/include/xo/gc/setup_gc.hpp new file mode 100644 index 00000000..bf5c5c6d --- /dev/null +++ b/include/xo/gc/setup_gc.hpp @@ -0,0 +1,20 @@ +/** @file SetupGc.hpp + * + * @author Roland Conybeare, Mar 2026 + **/ + +#pragma once + +namespace xo { + namespace mm { + + class SetupGc { + public: + /** Register gc (facet,impl) combinations with FacetRegistry **/ + static bool register_facets(); + }; + + } /*namespace mm*/ +} /*namespace xo*/ + +/* end SetupGc.hpp */ diff --git a/src/gc/setup_gc.cpp b/src/gc/setup_gc.cpp new file mode 100644 index 00000000..23beed55 --- /dev/null +++ b/src/gc/setup_gc.cpp @@ -0,0 +1,42 @@ +/** @file setup_gc.cpp + * + * @author Roland Conybeare, Mar 2026 + **/ + +#include "setup_gc.hpp" +#include "X1Collector.hpp" +#include "GCObjectStoreVisitor.hpp" +#include +#include + +namespace xo { + using xo::mm::AAllocator; + using xo::mm::ACollector; + using xo::mm::DX1Collector; + using xo::facet::FacetRegistry; + using xo::reflect::typeseq; + + namespace mm { + + bool + SetupGc::register_facets() + { + scope log(XO_DEBUG(true)); + + FacetRegistry::register_impl(); + FacetRegistry::register_impl(); + + FacetRegistry::register_impl(); + + log && log(xtag("DX1Collector.tseq", typeseq::id())); + log && log(xtag("DGCObjectStoreVisitor.tseq", typeseq::id())); + + log && log(xtag("ACollector.tseq", typeseq::id())); + log && log(xtag("AGCObjectVisitor.tseq", typeseq::id())); + + return true; + } + } +} /*namespace xo*/ + +/* end setup_gc.cpp */ From 46ce36d0f5752b53081451f191ffb13d92cec451 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 15:38:42 -0400 Subject: [PATCH 142/174] xo-gc: doc: + comment on GCObjectVisitor entry points --- .../xo/gc/detail/IGCObjectVisitor_DGCObjectStoreVisitor.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/include/xo/gc/detail/IGCObjectVisitor_DGCObjectStoreVisitor.hpp b/include/xo/gc/detail/IGCObjectVisitor_DGCObjectStoreVisitor.hpp index de14f5b0..a7a987f7 100644 --- a/include/xo/gc/detail/IGCObjectVisitor_DGCObjectStoreVisitor.hpp +++ b/include/xo/gc/detail/IGCObjectVisitor_DGCObjectStoreVisitor.hpp @@ -46,10 +46,12 @@ namespace xo { ///@{ // const methods /** allocation metadata for gc-aware data at address @p gco. -@p gco must be the result of a call to collector's alloc() function **/ +@p gco must be the result of a call to collector's alloc() function +note: load-bearing for xo-gc/MutationLogStore **/ static AllocInfo alloc_info(const DGCObjectStoreVisitor & self, void * addr); /** generation to which pointer @p addr belongs, given role @p r; -sentinel if @p addr is not owned by collector **/ +sentinel if @p addr is not owned by collector. +note: load-bearing for xo-gc/MutationLogStore **/ static Generation generation_of(const DGCObjectStoreVisitor & self, Role r, const void * addr) noexcept; // non-const methods From 8c884224238aaf90791fe4ae54dd1dc826b981c3 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 15:42:49 -0400 Subject: [PATCH 143/174] xo-gc: tidy: + Generation methods .g0(), .g1() Use in GCObjectStore.test.cpp --- utest/GCObjectStore.test.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index 8e1ae05c..a6ab049c 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -812,10 +812,6 @@ namespace ut { // object type storage will be empty unless we install a type. GCObjectStore gcos(fixture.gcos_config_, &verify_stats); - Generation g0{0}; - Generation g1{1}; - Generation gn{tc.n_gen_}; - REQUIRE(gcos.is_type_installed(typeseq::id()) == false); REQUIRE(gcos.is_type_installed(typeseq::id()) == false); @@ -823,9 +819,6 @@ namespace ut { gcos_verify_arena_partitioning(tc, gcos); gcos_verify_vacant(tc, gcos); - // allocator api - auto alloc = obj(gcos.new_space()); - // create object(s). // details depend on test case @@ -845,11 +838,11 @@ namespace ut { gcos_verify_gen0_only_allocated(tc, gcos, x1_v); // swap_roles [but only for generation < g1, i.e. g0 - gcos.swap_roles(g1); + gcos.swap_roles(Generation::g1()); gcos_verify_gen0_fromspace_only_allocated(tc, gcos, x1_v); - gcos_move_roots_and_verify(tc, &gcos, g1, x1_v, x2_v, tc.debug_flag_); + gcos_move_roots_and_verify(tc, &gcos, Generation::g1(), x1_v, x2_v, tc.debug_flag_); // Things to test: // - deep_move_interior() // used from MutationLogStore @@ -861,7 +854,7 @@ namespace ut { // 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(Generation::g1(), sanitize_flag); } { From d78bdf2e96213e7ea28dd47de5c6a8b8facea57e Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 15:45:12 -0400 Subject: [PATCH 144/174] xo-gc: utest: move X1VerifyStats into GcosFixture --- utest/GCObjectStore.test.cpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index a6ab049c..f3542ae2 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -753,6 +753,9 @@ namespace ut { DArena report_arena_; /** Arena for holding error messages **/ DArena error_arena_; + + /** statistics collected by GCObjectStore.verify_ok() **/ + X1VerifyStats verify_stats_; }; GcosFixture::GcosFixture(const Testcase & tc) @@ -807,10 +810,8 @@ namespace ut { obj report_mm(&fixture.report_arena_); obj error_mm(&fixture.error_arena_); - X1VerifyStats verify_stats; - // object type storage will be empty unless we install a type. - GCObjectStore gcos(fixture.gcos_config_, &verify_stats); + GCObjectStore gcos(fixture.gcos_config_, &fixture.verify_stats_); REQUIRE(gcos.is_type_installed(typeseq::id()) == false); REQUIRE(gcos.is_type_installed(typeseq::id()) == false); @@ -863,14 +864,14 @@ namespace ut { // gcos.verify_ok(); - INFO(tostr(xtag("n_gc_root", verify_stats.n_gc_root_), - xtag("n_ext", verify_stats.n_ext_), - xtag("n_from", verify_stats.n_from_), - xtag("n_to", verify_stats.n_to_), - xtag("n_fwd", verify_stats.n_fwd_), - xtag("n_no_iface", verify_stats.n_no_iface_))); + INFO(tostr(xtag("n_gc_root", 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_), + xtag("n_fwd", fixture.verify_stats_.n_fwd_), + xtag("n_no_iface", fixture.verify_stats_.n_no_iface_))); - REQUIRE(verify_stats.is_ok()); + REQUIRE(fixture.verify_stats_.is_ok()); } { From 880087d0bec67f89d7d7bedf8ea9e2ccaabf4be2 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 15:53:20 -0400 Subject: [PATCH 145/174] xo-gc: utest: move GCObjectStore into GcosFixture --- utest/GCObjectStore.test.cpp | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index f3542ae2..e1db7339 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -735,6 +735,9 @@ namespace ut { public: explicit GcosFixture(const Testcase & tc); + auto report_mm() { return obj(&report_arena_); } + auto error_mm() { return obj(&error_arena_); } + GCObjectStoreConfig gcos_config_; /** Parallel arena for reference @@ -756,6 +759,9 @@ namespace ut { /** statistics collected by GCObjectStore.verify_ok() **/ X1VerifyStats verify_stats_; + + /** the thing we're exercising using this fixture **/ + GCObjectStore gcos_; }; GcosFixture::GcosFixture(const Testcase & tc) @@ -775,7 +781,8 @@ namespace ut { .with_store_header_flag(true))}, error_arena_{DArena::map(ArenaConfig().with_name("error-arena") .with_size(tc.error_size_) - .with_store_header_flag(true))} + .with_store_header_flag(true))}, + gcos_{gcos_config_, &verify_stats_} {} } @@ -807,11 +814,7 @@ namespace ut { GcosFixture fixture(tc); - obj report_mm(&fixture.report_arena_); - obj error_mm(&fixture.error_arena_); - - // object type storage will be empty unless we install a type. - GCObjectStore gcos(fixture.gcos_config_, &fixture.verify_stats_); + GCObjectStore & gcos = fixture.gcos_; REQUIRE(gcos.is_type_installed(typeseq::id()) == false); REQUIRE(gcos.is_type_installed(typeseq::id()) == false); @@ -876,7 +879,7 @@ namespace ut { { obj report_gco; - bool ok = gcos.report_object_types(report_mm, error_mm, &report_gco); + bool ok = gcos.report_object_types(fixture.report_mm(), fixture.error_mm(), &report_gco); REQUIRE(ok); REQUIRE(report_gco); @@ -886,16 +889,16 @@ namespace ut { // discard report report_gco.reset(); - report_mm->clear(); + fixture.report_mm()->clear(); } { obj report_gco; - bool ok = gcos.report_object_ages(report_mm, error_mm, &report_gco); + bool ok = gcos.report_object_ages(fixture.report_mm(), fixture.error_mm(), &report_gco); if (!ok) { log1.retroactively_enable(); - log1 && log1(xtag("error", report_mm.last_error())); + log1 && log1(xtag("error", fixture.report_mm().last_error())); } REQUIRE(ok); @@ -906,7 +909,7 @@ namespace ut { // discard report report_gco.reset(); - report_mm->clear(); + fixture.report_mm()->clear(); } } /* loop over test cases */ } /* TEST_CASE(GCObjectStore-1) */ From b44e4c7fe70f7a3111557035875ad453cb5a3eb9 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 16:15:28 -0400 Subject: [PATCH 146/174] xo-gc: utest: scaffold for multiple gc loops --- utest/GCObjectStore.test.cpp | 81 ++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index e1db7339..20bee849 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -65,8 +65,10 @@ namespace ut { 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_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}, @@ -76,8 +78,10 @@ namespace ut { 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_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} {} @@ -103,11 +107,15 @@ namespace ut { 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; - + /** first loop: #of cells in random object graph **/ + uint32_t n_i0_test_obj_ = 0; + /** first loop: #of random assignments to attempt (these may create cycles, for example) **/ + uint32_t n_i0_test_assign_ = 0; + /** 2nd+later loop: #of cells in random object graph **/ + uint32_t n_i1_test_obj_ = 0; + /** 2nd+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_ = false; }; @@ -117,20 +125,29 @@ namespace ut { constexpr uint32_t c_report_z1 = 64 * 1024; constexpr uint32_t c_error_z1 = 16 * 1024; +# define T true +# define F false + static std::vector s_testcase_v = { // 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, 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), + /** n_gen, n_survive, gc_size, object_type_z, do_type_registration, + * report_z, error_z, n_i0_obj, n_i0_test_assign, debug_flag + **/ + Testcase(2, 4, 16 * 1024, 8 * 128, F, c_report_z1, c_error_z1, c_random, 0, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_selfcycle, 1, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 2, 13, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 2, 25, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 5, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 4, 2, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 50, 25, 0, 0, F), + }; +# undef T +# undef F + /** record capturing some stats for a (randomly created) gc-aware object **/ struct Recd { Recd() = default; @@ -420,6 +437,7 @@ namespace ut { gcos_construct_ab_object_graphs(const Testcase & tc, GCObjectStore * p_gcos, DArena * p_arena2, + uint32_t loop_index, std::vector * p_x1_v, std::vector * p_x2_v, xoshiro256ss * p_rgen) @@ -432,13 +450,22 @@ namespace ut { p_arena2); break; case TestGraphType::random: - random_object_graph(tc.n_test_obj_, - tc.n_test_assign_, - p_rgen, - p_x1_v, - p_gcos, - p_x2_v, - p_arena2); + { + uint32_t n_test_obj = ((loop_index == 0) + ? tc.n_i0_test_obj_ + : tc.n_i1_test_obj_); + uint32_t n_test_assign = ((loop_index == 0) + ? tc.n_i0_test_assign_ + : tc.n_i1_test_assign_); + + random_object_graph(n_test_obj, + n_test_assign, + p_rgen, + p_x1_v, + p_gcos, + p_x2_v, + p_arena2); + } break; } @@ -829,7 +856,9 @@ namespace ut { std::vector x1_v; std::vector x2_v; - gcos_construct_ab_object_graphs(tc, &gcos, &fixture.arena2_, &x1_v, &x2_v, &rgen); + uint32_t loop_index = 0; + + gcos_construct_ab_object_graphs(tc, &gcos, &fixture.arena2_, loop_index, &x1_v, &x2_v, &rgen); log1 && log1("verify before any gcos side effects"); From 7c150b7a92170a1ae67ec57c77b63b9fe66ba587 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 19:42:02 -0400 Subject: [PATCH 147/174] xo-gc: utest: expand to multi-cycle utests --- include/xo/gc/GCObjectStore.hpp | 5 + include/xo/gc/GCObjectStoreConfig.hpp | 5 +- include/xo/gc/X1VerifyStats.hpp | 6 +- src/gc/DGCObjectStoreVisitor.cpp | 4 +- src/gc/GCObjectStore.cpp | 77 +++++- utest/GCObjectStore.test.cpp | 334 +++++++++++++++++--------- 6 files changed, 299 insertions(+), 132 deletions(-) diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index 5fdddeb3..c318fcb2 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -169,6 +169,11 @@ namespace xo { void * from_src, Generation upto); + /** allocate copy of @p src on behalf of a collection cycle. + * Entry point for DGCObjectStoreVisitor::alloc_copy() + **/ + std::byte * alloc_copy(void * src) noexcept; + /** Cleanup at the end of a gc cycle. * Reset from-space * (current from-space is former to-space, diff --git a/include/xo/gc/GCObjectStoreConfig.hpp b/include/xo/gc/GCObjectStoreConfig.hpp index 9185721f..f991218a 100644 --- a/include/xo/gc/GCObjectStoreConfig.hpp +++ b/include/xo/gc/GCObjectStoreConfig.hpp @@ -25,10 +25,11 @@ namespace xo { * @p age collections. Equals the number of times object * has been promoted. * - * Must be consistent + * Must be consistent with promotion_threshold(g) **/ Generation age2gen(object_age age) const noexcept { - return Generation(age % n_survive_threshold_); + return Generation(std::min(age / n_survive_threshold_, + n_generation_ - 1)); } /** age threshold for promotion to generation @p g **/ diff --git a/include/xo/gc/X1VerifyStats.hpp b/include/xo/gc/X1VerifyStats.hpp index 0e2fa783..e3912e49 100644 --- a/include/xo/gc/X1VerifyStats.hpp +++ b/include/xo/gc/X1VerifyStats.hpp @@ -16,7 +16,7 @@ namespace xo { class X1VerifyStats { public: bool is_ok() const noexcept { - return (n_from_ == 0) && (n_fwd_ == 0) && (n_no_iface_ == 0); + return (n_from_ == 0) && (n_fwd_ == 0) && (n_age_bad_ == 0) && (n_no_iface_ == 0); } void clear() { *this = X1VerifyStats(); } @@ -30,6 +30,10 @@ namespace xo { std::uint32_t n_to_ = 0; /** counts forwarding object encountered in to-space scan. Fatal if non-zero **/ std::uint32_t n_fwd_ = 0; + /** counts objects in expected generation for age **/ + std::uint32_t n_age_ok_ = 0; + /** counts objects in wrong generation for age **/ + std::uint32_t n_age_bad_ = 0; /** counts missing GCObject interface. Fatal if non-zero **/ std::uint32_t n_no_iface_ = 0; /** live mlog entry refers to to-space, as expected **/ diff --git a/src/gc/DGCObjectStoreVisitor.cpp b/src/gc/DGCObjectStoreVisitor.cpp index 713bf912..104f3216 100644 --- a/src/gc/DGCObjectStoreVisitor.cpp +++ b/src/gc/DGCObjectStoreVisitor.cpp @@ -45,7 +45,9 @@ namespace xo { std::byte * DGCObjectStoreVisitor::alloc_copy(void * src) noexcept { - return p_gco_store_->new_space()->alloc_copy((std::byte *)src); + // check whether we're promoting src. + + return p_gco_store_->alloc_copy((std::byte *)src); } } /*namespace mm*/ diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index b956d366..896f02e4 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -375,7 +375,7 @@ namespace xo { std::uint32_t hard_n_age = config_.arena_config_.header_.max_age() + 1; // maximum age of a still-existing object - std::uint32_t soft_max_age = 0; + std::uint32_t soft_max_age = 0; // first pass, establish max age for (Generation g{0}; g < config_.n_generation_; ++g) { @@ -665,8 +665,7 @@ namespace xo { } /*_forward_inplace_aux*/ void - GCObjectStore::_verify_aux(AGCObject * iface, - void * data) + GCObjectStore::_verify_aux(AGCObject * iface, void * data) { scope log(XO_DEBUG(config_.debug_flag_)); @@ -692,6 +691,13 @@ namespace xo { assert(this->contains(Role::to_space(), data)); ++(p_verify_stats_->n_to_); + + AllocInfo info = this->alloc_info((std::byte *)data); + + if (config_.age2gen(object_age(info.age())) == g1) + ++(p_verify_stats_->n_age_ok_); + else + ++(p_verify_stats_->n_age_bad_); } } @@ -700,7 +706,9 @@ namespace xo { { GCMoveCheckpoint gray_lo_v; - for (uint32_t g = 0; g < upto; ++g) { + // If we're collecting gen0, still need to allow for objects getting promoted + + for (uint32_t g = 0; g < std::min(upto + 1, config_.n_generation_); ++g) { gray_lo_v[g] = this->to_space(Generation{g})->free_; } @@ -710,7 +718,7 @@ namespace xo { void GCObjectStore::verify_ok() noexcept { - Generation unused_gen; + Generation unused_gen; DGCObjectStoreVisitor visitor{this, unused_gen}; for (Generation g(0); g < config_.n_generation_; ++g) { @@ -733,6 +741,8 @@ namespace xo { obj gco(iface, const_cast(data)); gco.visit_gco_children(VisitReason::verify(), visitor.ref()); + + // nested control reenters at GCObjectStore::_verify_aux() } else { ++(p_verify_stats_->n_no_iface_); continue; @@ -848,7 +858,7 @@ namespace xo { // reminder: *root_data preserved } - + return *root_data; } @@ -870,6 +880,35 @@ namespace xo { return this->_deep_move_gc_owned(gc, from_src, upto); } + std::byte * + GCObjectStore::alloc_copy(void * src) noexcept + { + scope log(XO_DEBUG(true)); + + AllocInfo src_info = this->alloc_info((std::byte *)src); + uint32_t age1p = std::min(src_info.age() + 1, + c_max_object_age); + /** g_copy will be one of {g, g+1} where g + * is the generation of src + **/ + Generation g_copy = config_.age2gen(object_age(age1p)); + DArena * dest_arena = this->to_space(g_copy); + + log && log(xtag("age1p", age1p), xtag("g_copy", g_copy)); + + // 1. we could have used + // dest_arena->alloc_copy((std::byte *)src); + // here. + // 2. but then dest_arena will have to recompute object header. + // 3. instead prefer the header info we already have at hand. + + return dest_arena->_alloc(src_info.size(), + DArena::alloc_mode::standard, + typeseq(src_info.tseq()), + age1p, + __PRETTY_FUNCTION__); + } + void * GCObjectStore::_deep_move_gc_owned(obj gc, void * from_src, @@ -896,8 +935,21 @@ namespace xo { /* here: object at from_src not already forwarded */ if (!this->_check_move_policy(hdr, from_src, upto)) { - /* object at from_src is in generation that is not being collected */ - log && log("disposition: not moving from_src"); + if (log) { + object_age age = this->header2age(hdr); + Generation expect_g = config_.age2gen(age); + Generation actual_g = this->generation_of(Role::to_space(), from_src); + Generation actual_g2 = this->generation_of(Role::from_space(), from_src); + + /* object at from_src is in generation that is not being collected. + * g = intended generation + */ + log("disposition: not moving from_src", + xtag("age", age), + xtag("expect-g", expect_g), + xtag("actual-g", actual_g), + xtag("actual-g2", actual_g2)); + } return from_src; } @@ -1043,7 +1095,14 @@ namespace xo { do { fixup_work = 0; - for (Generation g = Generation{0}; g < upto; ++g) { + // reminder: oldest collected generation is upto-1. + // Objects that were in generation upto-1 may have promoted + // to generation upto < config_.n_generation_ + // + Generation g_ub(std::min(upto + 1, config_.n_generation_)); + + for (Generation g = Generation{0}; g < g_ub; ++g) { + /** object index for this pass **/ size_t i_obj = 0; diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index 20bee849..f0232686 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ namespace ut { using xo::mm::DArena; using xo::mm::AllocInfo; using xo::mm::c_max_generation; + using xo::print_backtrace_dwarf; using xo::facet::obj; using xo::facet::TypeRegistry; using xo::facet::typeseq; @@ -65,6 +67,7 @@ namespace ut { size_t report_z, size_t error_z, 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, @@ -78,6 +81,7 @@ namespace ut { report_size_{report_z}, error_size_{error_z}, 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}, @@ -107,6 +111,8 @@ namespace ut { size_t error_size_ = 0; /** 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 **/ uint32_t n_i0_test_obj_ = 0; /** first loop: #of random assignments to attempt (these may create cycles, for example) **/ @@ -115,7 +121,7 @@ namespace ut { uint32_t n_i1_test_obj_ = 0; /** 2nd+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_ = false; }; @@ -126,23 +132,38 @@ namespace ut { constexpr uint32_t c_error_z1 = 16 * 1024; # define T true -# define F false +# define F false static std::vector s_testcase_v = { // 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_i0_obj, n_i0_test_assign, debug_flag + * report_z, error_z, + * n_gc_loop, + * n_i0_obj, n_i0_test_assign, + * n_i1_obj, n_i1_test_assign, + * debug_flag + * n_i1_obj + * n_i0_test_assign | + * n_i0_obj | | + * n_gc_loop | | | + * v v v v **/ - Testcase(2, 4, 16 * 1024, 8 * 128, F, c_report_z1, c_error_z1, c_random, 0, 0, 0, 0, F), - Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_selfcycle, 1, 0, 0, 0, F), - Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 0, 0, 0, F), - Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 2, 13, 0, 0, F), - Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 2, 25, 0, 0, F), - Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 5, 0, 0, 0, F), - Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 4, 2, 0, 0, F), - Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 50, 25, 0, 0, F), - + Testcase(2, 4, 16 * 1024, 8 * 128, F, c_report_z1, c_error_z1, c_random, 1, 0, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_selfcycle, 1, 1, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_selfcycle, 3, 1, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_selfcycle, 4, 1, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 1, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 2, 1, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 4, 1, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 8, 1, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 2, 13, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 2, 25, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 5, 0, 0, 0, F), + //Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 5, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 4, 2, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 50, 25, 0, 0, F), + //Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 50, 25, 0, 0, F), }; # undef T @@ -206,7 +227,7 @@ namespace ut { * (*p_v2)[i] allocated entirely from @p p_arena2 **/ void - random_object_graph(uint32_t n_obj, + random_object_graph(uint32_t n_new_obj, uint32_t n_assign, xoshiro256ss * p_rgen, std::vector * p_v, @@ -216,10 +237,10 @@ namespace ut { { scope log(XO_DEBUG(true)); - if (n_obj == 0) + if (n_new_obj == 0 && n_assign == 0) return; - for (uint32_t i_obj = 0; i_obj < n_obj; ++i_obj) { + 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; // randomly-constructed node in object graph @@ -298,7 +319,7 @@ namespace ut { for (uint32_t j = 0; j < n_assign; ++j) { // choose an object at random - uint32_t lhs_ix = (*p_rgen)() % n_obj; + uint32_t lhs_ix = (*p_rgen)() % p_v->size(); assert(lhs_ix < p_v->size()); @@ -315,19 +336,19 @@ namespace ut { if (sample < 50) { // modify head. skip usual gc write-barrier stuff - uint32_t rhs_ix = (*p_rgen)() % n_obj; + uint32_t rhs_ix = (*p_rgen)() % p_v->size(); auto rhs1 = (*p_v)[rhs_ix].gco_; auto rhs2 = (*p_v2)[rhs_ix].gco_; if (log) { log("replacing edge in random object graph"); - log(xtag("n-obj", n_obj)); + log(xtag("n-obj", p_v->size())); 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; @@ -350,7 +371,7 @@ namespace ut { { // 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())); @@ -432,6 +453,8 @@ namespace ut { * Store second graph in @p *p_x2_v, allocating * entirely from @p p_arena2. * Use random number generator @p_rgen + * + * @p loop_index counts iteration with one gc-like phase. **/ void gcos_construct_ab_object_graphs(const Testcase & tc, @@ -444,10 +467,12 @@ namespace ut { { switch (tc.obj_graph_type_) { case TestGraphType::selfcycle: - selfcycle_object_graph(p_x1_v, - p_gcos, - p_x2_v, - p_arena2); + if (loop_index == 0) { + selfcycle_object_graph(p_x1_v, + p_gcos, + p_x2_v, + p_arena2); + } break; case TestGraphType::random: { @@ -491,6 +516,8 @@ namespace ut { xtag("n_from", verify_stats->n_from_), xtag("n_to", verify_stats->n_to_), xtag("n_fwd", verify_stats->n_fwd_), + xtag("n_age_ok", verify_stats->n_age_ok_), + xtag("n_age_bad", verify_stats->n_age_bad_), xtag("n_no_iface", verify_stats->n_no_iface_))); REQUIRE(verify_stats->is_ok()); @@ -511,8 +538,13 @@ namespace ut { } } + /** verify reasonable alloc info values. + * object store has been subject to @p loop_index + * collection cycles + **/ void gcos_verify_allocinfo(const GCObjectStore & gcos, + uint32_t loop_index, const std::vector & x1_v) { // gcos can reveal info about allocs @@ -529,7 +561,7 @@ namespace ut { // also can use header2size / header2tseq convenience functions REQUIRE(gcos.header2size(obj_info.header()) == obj_info.size()); - REQUIRE(gcos.header2age(obj_info.header()) == object_age{0}); + 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); } @@ -538,19 +570,22 @@ namespace ut { void gcos_verify_gen0_only_allocated(const Testcase & tc, const GCObjectStore & gcos, + uint32_t loop_index, const std::vector & x1_v) { Generation g0{0}; Generation gn{tc.n_gen_}; - // new objects appear in to-space for generation 0 + // new objects appear in to-space for generation 0. for (Generation gi = g0; gi < gn; ++gi) { INFO(tostr(xtag("gi", gi))); - if ((gi == 0) && (x1_v.size() > 0)) - REQUIRE(gcos.to_space(gi)->allocated() > 0); - else - REQUIRE(gcos.to_space(gi)->allocated() == 0); + if (loop_index == 0) { + if ((gi == 0) && (x1_v.size() > 0)) + REQUIRE(gcos.to_space(gi)->allocated() > 0); + else + REQUIRE(gcos.to_space(gi)->allocated() == 0); + } REQUIRE(gcos.from_space(gi)->allocated() == 0); } @@ -559,46 +594,97 @@ namespace ut { void gcos_verify_gen0_fromspace_only_allocated(const Testcase & tc, const GCObjectStore & gcos, + uint32_t loop_index, + Generation upto, const std::vector & x1_v) { + Generation g0{0}; + Generation gn{tc.n_gen_}; + + for (Generation gi = g0; gi < gn; ++gi) { + if (gi < upto) { + // we're collecting generation gi. + // Before we begin, to-space had better be empty + // (everthing in gi is in from-space) + + REQUIRE(gcos.to_space(gi)->allocated() == 0); + } else { + // we're not collecting generation gi. + // from-space must be empty. + // May have content in to-space + + REQUIRE(gcos.from_space(gi)->allocated() == 0); + } + } + for (size_t i = 0, n = x1_v.size(); i < n; ++i) { const auto & x1 = x1_v.at(i); - REQUIRE(gcos.contains(Role::from_space(), x1.gco_.data())); - REQUIRE(gcos.contains_allocated(Role::from_space(), x1.gco_.data())); + // 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 (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); + + 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()); - - Generation g0{0}; - Generation gn{tc.n_gen_}; - - for (Generation gi = g0; gi < gn; ++gi) { - INFO(tostr(xtag("gi", gi))); - - if (gi == 0) - REQUIRE(gcos.from_space(gi)->allocated() > 0); - else - REQUIRE(gcos.from_space(gi)->allocated() == 0); - - REQUIRE(gcos.to_space(gi)->allocated() == 0); - } } } void gcos_verify_forwarding(const GCObjectStore & gcos, + Generation upto, const Recd & x1, obj x1_gco) { - REQUIRE(gcos.contains_allocated(Role::from_space(), x1_gco.data())); + 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()); - REQUIRE(obj_info.size() >= x1.alloc_z_); + 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.is_forwarding_tseq()); + + 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()); + + REQUIRE(g >= upto); + } + +// if (!obj_info.is_forwarding_tseq()) +// print_backtrace_dwarf(true /*demangle*/); + +// REQUIRE(obj_info.is_forwarding_tseq()); } void @@ -731,9 +817,9 @@ namespace ut { obj x1p_gco(x1p_iface, x1p_data); // obj (x1_gco) now forwarding pointer (to x1p_gco = x1.gco_) - gcos_verify_forwarding(*p_gcos, x1, x1_gco); + gcos_verify_forwarding(*p_gcos, upto, x1, x1_gco); - // obj1p same contents as original obj + // obj1p in to-space, same contents as original obj gcos_verify_forwarding_destination(*p_gcos, x1, x1p_gco); // x1p_gco must look like x2.gco @@ -811,7 +897,7 @@ namespace ut { .with_store_header_flag(true))}, gcos_{gcos_config_, &verify_stats_} {} - + } TEST_CASE("GCObjectStore-1", "[GCObjectStore]") @@ -830,7 +916,7 @@ namespace ut { // 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]; @@ -856,89 +942,99 @@ namespace ut { std::vector x1_v; std::vector x2_v; - uint32_t loop_index = 0; + 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)); - gcos_construct_ab_object_graphs(tc, &gcos, &fixture.arena2_, loop_index, &x1_v, &x2_v, &rgen); + // construct, extend, and/or modify object graphs in {x1_v, x2_v} - log1 && log1("verify before any gcos side effects"); + gcos_construct_ab_object_graphs(tc, &gcos, &fixture.arena2_, loop_index, &x1_v, &x2_v, &rgen); - gcos_verify_consistency(&gcos); + log1 && log1("verify before any gcos side effects"); - // someday: print the graph. Need a cycle-detecting printer + gcos_verify_consistency(&gcos); - gcos_verify_ab_equivalence(x1_v, x2_v); - gcos_verify_allocinfo(gcos, x1_v); - gcos_verify_gen0_only_allocated(tc, gcos, x1_v); + // someday: print the graph. Need a cycle-detecting printer - // swap_roles [but only for generation < g1, i.e. g0 - gcos.swap_roles(Generation::g1()); + gcos_verify_ab_equivalence(x1_v, x2_v); + gcos_verify_allocinfo(gcos, loop_index, x1_v); + gcos_verify_gen0_only_allocated(tc, gcos, loop_index, x1_v); - gcos_verify_gen0_fromspace_only_allocated(tc, gcos, x1_v); + // swap_roles [but only for generation < g1, i.e. g0 + gcos.swap_roles(Generation::g1()); - gcos_move_roots_and_verify(tc, &gcos, Generation::g1(), x1_v, x2_v, tc.debug_flag_); + gcos_verify_gen0_fromspace_only_allocated(tc, gcos, loop_index, Generation::g1(), x1_v); - // Things to test: - // - deep_move_interior() // used from MutationLogStore - // - forward_inplace_aux() // used from DX1Collector.visit_child + gcos_move_roots_and_verify(tc, &gcos, Generation::g1(), x1_v, x2_v, tc.debug_flag_); - { - bool sanitize_flag = true; + // Things to test: + // - deep_move_interior() // used from MutationLogStore + // - forward_inplace_aux() // used from DX1Collector.visit_child - // swaps to- and from- spaces again - // Now from-space will be empty, all live objects in to-space + { + bool sanitize_flag = true; - gcos.cleanup_phase(Generation::g1(), sanitize_flag); - } + // swaps to- and from- spaces again + // Now from-space will be empty, all live objects in to-space - { - // traverses stored objects, updates counters - // in verify_stats (= gco.p_verify_stats_, via ctor) - // - gcos.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_), - xtag("n_fwd", fixture.verify_stats_.n_fwd_), - xtag("n_no_iface", fixture.verify_stats_.n_no_iface_))); - - REQUIRE(fixture.verify_stats_.is_ok()); - } - - { - obj report_gco; - bool ok = gcos.report_object_types(fixture.report_mm(), fixture.error_mm(), &report_gco); - - REQUIRE(ok); - REQUIRE(report_gco); - - // TODO: print report_gco, verify output - - // discard report - - report_gco.reset(); - fixture.report_mm()->clear(); - } - - { - obj report_gco; - bool ok = gcos.report_object_ages(fixture.report_mm(), fixture.error_mm(), &report_gco); - - if (!ok) { - log1.retroactively_enable(); - log1 && log1(xtag("error", fixture.report_mm().last_error())); + gcos.cleanup_phase(Generation::g1(), sanitize_flag); } - REQUIRE(ok); - REQUIRE(report_gco); + { + fixture.verify_stats_.clear(); - // TODO: print report_gco, verify output + // traverses stored objects, updates counters + // in verify_stats (= gco.p_verify_stats_, via ctor) + // + gcos.verify_ok(); - // discard report - - report_gco.reset(); - fixture.report_mm()->clear(); + 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()); + } + + // report stats by type + { + obj report_gco; + bool ok = gcos.report_object_types(fixture.report_mm(), fixture.error_mm(), &report_gco); + + REQUIRE(ok); + REQUIRE(report_gco); + + // TODO: print report_gco, verify output + + // discard report + + report_gco.reset(); + fixture.report_mm()->clear(); + } + + // report stats by age + { + obj report_gco; + bool ok = gcos.report_object_ages(fixture.report_mm(), fixture.error_mm(), &report_gco); + + if (!ok) { + log1.retroactively_enable(); + log1 && log1(xtag("error", fixture.report_mm().last_error())); + } + + REQUIRE(ok); + REQUIRE(report_gco); + + // TODO: print report_gco, verify output + + // discard report + + report_gco.reset(); + fixture.report_mm()->clear(); + } } } /* loop over test cases */ } /* TEST_CASE(GCObjectStore-1) */ From b593e43375a5a193a016c977ab30a08f5942e1f3 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 19:54:49 -0400 Subject: [PATCH 148/174] xo-gc: utest: verify no alloc errors --- utest/GCObjectStore.test.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index f0232686..f068de8d 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -949,6 +949,9 @@ namespace ut { gcos_construct_ab_object_graphs(tc, &gcos, &fixture.arena2_, loop_index, &x1_v, &x2_v, &rgen); + // no allocation errors + REQUIRE(gcos.last_error().error_ == xo::mm::error::ok); + log1 && log1("verify before any gcos side effects"); gcos_verify_consistency(&gcos); From fade487b1ec1e4713652266d7f182849ecd23173 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 12 Apr 2026 21:06:50 -0400 Subject: [PATCH 149/174] xo-gc: utest: + MutationLogStore.test.cpp --- utest/CMakeLists.txt | 1 + utest/MutationLogStore.test.cpp | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 utest/MutationLogStore.test.cpp diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 8f9b9503..45709220 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -7,6 +7,7 @@ set(UTEST_SRCS Collector.test.cpp X1Collector.test.cpp DX1CollectorIterator.test.cpp + MutationLogStore.test.cpp GCObjectStore.test.cpp Object2.test.cpp diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp new file mode 100644 index 00000000..fda7eb0a --- /dev/null +++ b/utest/MutationLogStore.test.cpp @@ -0,0 +1,53 @@ +/** @file MutationLogStore.test.cpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#include +#include +#include +#include + +namespace ut { + using xo::rng::random_seed; + using xo::xtag; + using xo::scope; + + namespace { + + struct Testcase { + }; + + static std::vector s_testcase_v = { + + }; + + class MlsFixture { + }; + } + + TEST_CASE("MutationLogStore-1", "[MutationLogStore]") + { + constexpr bool c_debug_flag = true; + scope log0(XO_DEBUG(c_debug_flag), "MutationLogStore test"); + + std::uint64_t seed = 7988747704879432247ul; + //random_seed(&seed); + log0 && log0(xtag("seed", seed)); + + for (size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + 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))); + + MlsFixture fixture(tc); + } + + } +} /*namespace ut*/ + +/* end MutationLogStore.test.cpp */ From 7c6e56c321167d4011843b66b23a2974d9b3927b Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 16 Apr 2026 20:01:55 -0400 Subject: [PATCH 150/174] xo-gc: utest: moving test util code to shared GcosTestUtil.*pp --- utest/CMakeLists.txt | 2 + utest/GCObjectStore.test.cpp | 309 ++------------------------------ utest/MutationLogStore.test.cpp | 104 ++++++++++- 3 files changed, 119 insertions(+), 296 deletions(-) diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 45709220..e2c623be 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -11,6 +11,8 @@ set(UTEST_SRCS GCObjectStore.test.cpp Object2.test.cpp + GcosTestutil.cpp + init_gc_utest.cpp random_allocs.cpp ) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index f068de8d..876c5d27 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -3,6 +3,7 @@ * @author Roland Conybeare, Apr 2026 **/ +#include "GcosTestutil.hpp" #include #include #include @@ -53,13 +54,6 @@ 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, @@ -169,284 +163,11 @@ namespace ut { # undef T # undef F - /** 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 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 - * 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_new_obj, - uint32_t n_assign, - xoshiro256ss * p_rgen, - std::vector * p_v, - GCObjectStore * p_gcos, - std::vector * p_v2, - DArena * p_arena2) - { - scope log(XO_DEBUG(true)); - - if (n_new_obj == 0 && n_assign == 0) - return; - - 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; - // 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 lhs_ix = (*p_rgen)() % p_v->size(); - - assert(lhs_ix < p_v->size()); - - // is it a list cell? - auto xj1 = obj::from((*p_v)[lhs_ix].gco_); - auto xj2 = obj::from((*p_v2)[lhs_ix].gco_); - - if (xj1) { - assert(xj2); - - // flip a coin -- try modifying one of {car, cdr} - uint32_t sample = (*p_rgen)() % 100; - - if (sample < 50) { - // modify head. skip usual gc write-barrier stuff - - uint32_t rhs_ix = (*p_rgen)() % p_v->size(); - - auto rhs1 = (*p_v)[rhs_ix].gco_; - auto rhs2 = (*p_v2)[rhs_ix].gco_; - - if (log) { - log("replacing edge in random object graph"); - log(xtag("n-obj", p_v->size())); - 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)); - } - } - - void - gcos_verify_vacant(const Testcase & tc, - const GCObjectStore & gcos) - { - Generation g0{0}; - Generation gn{tc.n_gen_}; - - - // 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_); - } - } - } - /** Generate two copies of a random object graph for test case @p tc. * Store first graph in @p *p_x1_v, allocating * entirely from @p p_gcos new-space. @@ -468,10 +189,10 @@ namespace ut { switch (tc.obj_graph_type_) { case TestGraphType::selfcycle: if (loop_index == 0) { - selfcycle_object_graph(p_x1_v, - p_gcos, - p_x2_v, - p_arena2); + GcosTestutil::selfcycle_object_graph(p_x1_v, + p_gcos, + p_x2_v, + p_arena2); } break; case TestGraphType::random: @@ -483,13 +204,13 @@ namespace ut { ? tc.n_i0_test_assign_ : tc.n_i1_test_assign_); - random_object_graph(n_test_obj, - n_test_assign, - p_rgen, - p_x1_v, - p_gcos, - p_x2_v, - p_arena2); + GcosTestutil::random_object_graph(n_test_obj, + n_test_assign, + p_rgen, + p_x1_v, + p_gcos, + p_x2_v, + p_arena2); } break; } @@ -932,9 +653,9 @@ namespace ut { REQUIRE(gcos.is_type_installed(typeseq::id()) == false); REQUIRE(gcos.is_type_installed(typeseq::id()) == false); - gcos_install_test_types(tc, &gcos); - gcos_verify_arena_partitioning(tc, gcos); - gcos_verify_vacant(tc, gcos); + 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); // create object(s). // details depend on test case diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index fda7eb0a..3f0f7228 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -3,27 +3,113 @@ * @author Roland Conybeare, Apr 2026 **/ +#include #include +#include #include +#include #include #include namespace ut { + using xo::mm::MutationLogStore; + using xo::mm::MutationLogConfig; + using xo::mm::GCObjectStore; + using xo::mm::GCObjectStoreConfig; + using xo::mm::DArena; + using xo::mm::ArenaConfig; + using xo::mm::X1VerifyStats; + using xo::rng::xoshiro256ss; using xo::rng::random_seed; 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} {} + + /** number of generations in gco store **/ + uint32_t n_gen_ = 0; + /** object prommotes on surviving this many gc cycles **/ + uint32_t n_survive_ = 0; + /** size of each generation's half-space in bytes **/ + size_t gc_size_ = 0; + /** storage for object type in bytes **/ + uint32_t object_type_z_ = 0; + /** if true register types for gc-aware types used in unit test **/ + bool do_type_registration_ = false; + /** storage for mutation log (mult by 3 x n_gen_) **/ + uint32_t mutation_log_z_ = 0; + /** true if enabling mutation-log feature + * (load-bearing for incremental gc) + **/ + bool mlog_enabled_flag_ = false; + /** 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 **/ + uint32_t n_i0_test_obj_ = 0; + /** first loop: #of random assignments to attempt **/ + uint32_t n_i0_test_assign_ = 0; + /** 2nd+later loop: #of cells in random object graph **/ + uint32_t n_i1_test_obj_ = 0; + /** 2nd+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_; }; + 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; + +# define T true +# define F false + static std::vector s_testcase_v = { - }; +# undef T +# undef F + class MlsFixture { + public: + explicit MlsFixture(const Testcase &); + + GCObjectStoreConfig gcos_config_; + MutationLogConfig mls_config_; + + /** statistics called by GCObjectStore.verify_ok() **/ + X1VerifyStats verify_stats_; + GCObjectStore gcos_; + MutationLogStore mls_; }; + + MlsFixture::MlsFixture(const Testcase & tc) + : gcos_config_{(ArenaConfig() + .with_name("mlog-fixture-arena-name-notused") + .with_size(tc.gc_size_) + .with_store_header_flag(true)), + tc.n_gen_, + tc.n_survive_, + tc.object_type_z_, + tc.debug_flag_}, + mls_config_{tc.n_gen_, + tc.mutation_log_z_, + tc.mlog_enabled_flag_, + tc.debug_flag_}, + gcos_{gcos_config_, &verify_stats_}, + mls_{mls_config_, &gcos_} + {} } TEST_CASE("MutationLogStore-1", "[MutationLogStore]") @@ -40,11 +126,25 @@ namespace ut { const Testcase & tc = s_testcase_v[i_tc]; - scope log1(XO_DEBUG(tc.debug_flag_), "testcase loop", xtag("i_tc", 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))); MlsFixture fixture(tc); + + // TODO: + // 1. move GCObjectStore.test.cpp + // shared code to separate .*pp files + // - gcos_testutil.*pp + // + // 2. add mutation log tests. Entry points + // - init_mlogs() + // - verify_ok() + // - assign_member() + // - swap_roles() + // - forward_mutation_log() } } From cd29e9b853b9243e77852b6d025534e0a6838ec3 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 16 Apr 2026 20:57:17 -0400 Subject: [PATCH 151/174] xo-gc: utest: + missed GcosTestUtil.*pp --- utest/GcosTestutil.cpp | 356 +++++++++++++++++++++++++++++++++++++++++ utest/GcosTestutil.hpp | 89 +++++++++++ 2 files changed, 445 insertions(+) create mode 100644 utest/GcosTestutil.cpp create mode 100644 utest/GcosTestutil.hpp diff --git a/utest/GcosTestutil.cpp b/utest/GcosTestutil.cpp new file mode 100644 index 00000000..222b4871 --- /dev/null +++ b/utest/GcosTestutil.cpp @@ -0,0 +1,356 @@ +/** @file GcosTestutil.cpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#include "GcosTestutil.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ut { + using xo::scm::ListOps; + using xo::scm::DList; + using xo::scm::DBoolean; + using xo::mm::GCObjectStore; + using xo::mm::AGCObject; + using xo::mm::AAllocator; + using xo::mm::DArena; + using xo::mm::Role; + using xo::mm::Generation; + using xo::mm::c_max_generation; + using xo::rng::xoshiro256ss; + using xo::facet::TypeRegistry; + using xo::facet::obj; + using xo::facet::typeseq; + using xo::facet::impl_for; + using xo::scope; + using xo::xtag; + + /** Create two isomorphic object graphs. + * Each graph comprises a single DList cell + * that points to itself + **/ + void + GcosTestutil::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 + * 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 + GcosTestutil::random_object_graph(uint32_t n_new_obj, + uint32_t n_assign, + xoshiro256ss * p_rgen, + std::vector * p_v, + GCObjectStore * p_gcos, + std::vector * p_v2, + DArena * p_arena2) + { + scope log(XO_DEBUG(true)); + + if (n_new_obj == 0 && n_assign == 0) + return; + + 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; + // 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 lhs_ix = (*p_rgen)() % p_v->size(); + + assert(lhs_ix < p_v->size()); + + // is it a list cell? + auto xj1 = obj::from((*p_v)[lhs_ix].gco_); + auto xj2 = obj::from((*p_v2)[lhs_ix].gco_); + + if (xj1) { + assert(xj2); + + // flip a coin -- try modifying one of {car, cdr} + uint32_t sample = (*p_rgen)() % 100; + + if (sample < 50) { + // modify head. skip usual gc write-barrier stuff + + uint32_t rhs_ix = (*p_rgen)() % p_v->size(); + + auto rhs1 = (*p_v)[rhs_ix].gco_; + auto rhs2 = (*p_v2)[rhs_ix].gco_; + + if (log) { + log("replacing edge in random object graph"); + log(xtag("n-obj", p_v->size())); + 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*/ + + void + GcosTestutil::gcos_install_test_types(bool do_type_registration, + GCObjectStore * p_gcos) + { + // verify that GCOS recongnizes as registered, + // the types we intend using for unit test + + if (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 + GcosTestutil::gcos_verify_arena_partitioning(uint32_t n_gen, + size_t gc_size, + const GCObjectStore & gcos) + { + Generation g0{0}; + Generation g1{1}; + Generation gn{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() >= gc_size); + REQUIRE(gcos.from_space(g0)); + + for (Generation gi = g1; gi < 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)); + } + } + + void + GcosTestutil::gcos_verify_vacant(uint32_t n_gen, + size_t gc_size, + const GCObjectStore & gcos) + { + Generation g0{0}; + Generation gn{n_gen}; + + // 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() >= gc_size); + + REQUIRE(gcos.from_space(gi)->allocated() == 0); + REQUIRE(gcos.from_space(gi)->reserved() >= gc_size); + } + } + } + + /** Generate two copies of a random object graph for test case @p tc. + * Store first graph in @p *p_x1_v, allocating + * entirely from @p p_gcos new-space. + * Store second graph in @p *p_x2_v, allocating + * entirely from @p p_arena2. + * Use random number generator @p_rgen + * + * @p loop_index counts iteration with one gc-like phase. + **/ + void + GcosTestutil::gcos_construct_ab_object_graphs(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, + GCObjectStore * p_gcos, + DArena * p_arena2, + uint32_t loop_index, + std::vector * p_x1_v, + 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; + + 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, + p_rgen, + p_x1_v, + p_gcos, + p_x2_v, + p_arena2); + } + break; + } + + //x1_v.push_back(Recd(DBoolean::box(alloc, true), + // sizeof(DBoolean), + // typeseq::id())); + } +} /*namespace ut*/ + +/* end GcosTestutil.cpp */ diff --git a/utest/GcosTestutil.hpp b/utest/GcosTestutil.hpp new file mode 100644 index 00000000..f5e06533 --- /dev/null +++ b/utest/GcosTestutil.hpp @@ -0,0 +1,89 @@ +/** @file GcosTestutil.hpp + * + * @author Roland Conybeare, Apr 2026 + **/ + +#pragma once + +#include +#include +#include +#include + +namespace ut { + using xo::facet::obj; + + enum class TestGraphType { + /* list cell pointing to itself */ + selfcycle, + /* random object graph */ + random, + }; + + /** record capturing some stats for a (randomly created) gc-aware object **/ + struct Recd { + using AGCObject = xo::mm::AGCObject; + using typeseq = xo::reflect::typeseq; + + 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_; + }; + + struct GcosTestutil { + using GCObjectStore = xo::mm::GCObjectStore; + using DArena = xo::mm::DArena; + using xoshiro256ss = xo::rng::xoshiro256ss; + + static void + selfcycle_object_graph(std::vector * p_v1, + GCObjectStore * p_gcos, + std::vector * p_v2, + DArena * arena2); + + static void + random_object_graph(uint32_t n_new_obj, + uint32_t n_assign, + xoshiro256ss * p_rgen, + std::vector * p_v, + GCObjectStore * p_gcos, + std::vector * p_v2, + DArena * p_arena2); + + static void + gcos_install_test_types(bool do_type_registration, + GCObjectStore * p_gcos); + + static void + gcos_verify_arena_partitioning(uint32_t n_gen, + size_t gc_size, + const GCObjectStore & gcos); + + static void + gcos_verify_vacant(uint32_t n_gen, + size_t gc_size, + const GCObjectStore & gcos); + + static void + gcos_construct_ab_object_graphs(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, + GCObjectStore * p_gcos, + DArena * p_arena2, + uint32_t loop_index, + std::vector * p_x1_v, + std::vector * p_x2_v, + xoshiro256ss * p_rgen); + }; +} /*namespace ut*/ + +/* end GcosTestutil.hpp */ From d8b8e9415c2116e0d58276eed85ce82c0c2b9490 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 17 Apr 2026 07:58:22 -0400 Subject: [PATCH 152/174] xo-gc: utest: promote aux helpers to GcosTestutil.*pp --- utest/GCObjectStore.test.cpp | 167 ++++------------------------------- utest/GcosTestutil.cpp | 150 ++++++++++++++++++++++++------- utest/GcosTestutil.hpp | 24 +++++ 3 files changed, 159 insertions(+), 182 deletions(-) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index 876c5d27..c0442eef 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -154,10 +154,8 @@ namespace ut { Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 2, 13, 0, 0, F), Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 2, 25, 0, 0, F), Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 5, 0, 0, 0, F), - //Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 5, 0, 0, 0, F), Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 4, 2, 0, 0, F), Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 50, 25, 0, 0, F), - //Testcase(2, 4, 16 * 1024, 8 * 128, T, c_report_z1, c_error_z1, c_random, 1, 50, 25, 0, 0, F), }; # undef T @@ -168,150 +166,6 @@ namespace ut { namespace { // aux functions specific to GCObjectStore-1 unit test below - /** Generate two copies of a random object graph for test case @p tc. - * Store first graph in @p *p_x1_v, allocating - * entirely from @p p_gcos new-space. - * Store second graph in @p *p_x2_v, allocating - * entirely from @p p_arena2. - * Use random number generator @p_rgen - * - * @p loop_index counts iteration with one gc-like phase. - **/ - void - gcos_construct_ab_object_graphs(const Testcase & tc, - GCObjectStore * p_gcos, - DArena * p_arena2, - uint32_t loop_index, - std::vector * p_x1_v, - std::vector * p_x2_v, - xoshiro256ss * p_rgen) - { - switch (tc.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) - ? tc.n_i0_test_obj_ - : tc.n_i1_test_obj_); - uint32_t n_test_assign = ((loop_index == 0) - ? tc.n_i0_test_assign_ - : tc.n_i1_test_assign_); - - GcosTestutil::random_object_graph(n_test_obj, - n_test_assign, - p_rgen, - p_x1_v, - p_gcos, - p_x2_v, - p_arena2); - } - break; - } - - //x1_v.push_back(Recd(DBoolean::box(alloc, true), - // sizeof(DBoolean), - // typeseq::id())); - } - - /** Invoke built-in consistency verification for @p *p_gcos. - **/ - void - gcos_verify_consistency(GCObjectStore * p_gcos) - { - // traverses stored objects, updates counters - // in verify_stats (= gco.p_verify_stats_, via ctor) - // - p_gcos->verify_ok(); - - X1VerifyStats * verify_stats = p_gcos->verify_stats(); - - 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_age_ok", verify_stats->n_age_ok_), - xtag("n_age_bad", verify_stats->n_age_bad_), - xtag("n_no_iface", verify_stats->n_no_iface_))); - - REQUIRE(verify_stats->is_ok()); - } - - void - gcos_verify_ab_equivalence(const std::vector & x1_v, - const std::vector & x2_v) - { - REQUIRE(x1_v.size() == x2_v.size()); - - for (size_t i = 0, n = x1_v.size(); i < n; ++i) { - REQUIRE(x1_v[i].alloc_z_ == x2_v[i].alloc_z_); - REQUIRE(x1_v[i].tseq_ == x2_v[i].tseq_); - - REQUIRE(x1_v[i].gco_._typeseq() == x1_v[i].tseq_); - REQUIRE(x2_v[i].gco_._typeseq() == x2_v[i].tseq_); - } - } - - /** verify reasonable alloc info values. - * object store has been subject to @p loop_index - * collection cycles - **/ - void - gcos_verify_allocinfo(const GCObjectStore & gcos, - uint32_t loop_index, - const std::vector & x1_v) - { - // gcos can reveal info about allocs - 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()); - - // 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); - } - } - - void - gcos_verify_gen0_only_allocated(const Testcase & tc, - const GCObjectStore & gcos, - uint32_t loop_index, - const std::vector & x1_v) - { - Generation g0{0}; - Generation gn{tc.n_gen_}; - - // new objects appear in to-space for generation 0. - for (Generation gi = g0; gi < gn; ++gi) { - INFO(tostr(xtag("gi", gi))); - - if (loop_index == 0) { - if ((gi == 0) && (x1_v.size() > 0)) - REQUIRE(gcos.to_space(gi)->allocated() > 0); - else - REQUIRE(gcos.to_space(gi)->allocated() == 0); - } - - REQUIRE(gcos.from_space(gi)->allocated() == 0); - } - } - void gcos_verify_gen0_fromspace_only_allocated(const Testcase & tc, const GCObjectStore & gcos, @@ -392,7 +246,7 @@ namespace ut { REQUIRE(obj_info.payload().first == (std::byte *)x1_gco.data()); if (obj_info.is_forwarding_tseq()) { - /* object was forwarded, so got collected */ + /* object was forwarded, so got collected */ REQUIRE(obj_info.is_forwarding_tseq()); } else { /* not forwarded is ok iff in generation g >= upto */ @@ -668,20 +522,29 @@ namespace ut { // construct, extend, and/or modify object graphs in {x1_v, x2_v} - gcos_construct_ab_object_graphs(tc, &gcos, &fixture.arena2_, loop_index, &x1_v, &x2_v, &rgen); + GcosTestutil::gcos_construct_ab_object_graphs(tc.obj_graph_type_, + tc.n_i0_test_obj_, + tc.n_i0_test_assign_, + tc.n_i1_test_obj_, + tc.n_i1_test_assign_, + &gcos, + &fixture.arena2_, + loop_index, + &x1_v, &x2_v, + &rgen); // no allocation errors REQUIRE(gcos.last_error().error_ == xo::mm::error::ok); log1 && log1("verify before any gcos side effects"); - gcos_verify_consistency(&gcos); + GcosTestutil::gcos_verify_consistency(&gcos); // someday: print the graph. Need a cycle-detecting printer - gcos_verify_ab_equivalence(x1_v, x2_v); - gcos_verify_allocinfo(gcos, loop_index, x1_v); - gcos_verify_gen0_only_allocated(tc, gcos, loop_index, x1_v); + 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 [but only for generation < g1, i.e. g0 gcos.swap_roles(Generation::g1()); diff --git a/utest/GcosTestutil.cpp b/utest/GcosTestutil.cpp index 222b4871..59741a8f 100644 --- a/utest/GcosTestutil.cpp +++ b/utest/GcosTestutil.cpp @@ -4,6 +4,7 @@ **/ #include "GcosTestutil.hpp" +#include #include #include #include @@ -18,13 +19,16 @@ namespace ut { using xo::scm::ListOps; using xo::scm::DList; using xo::scm::DBoolean; + using xo::mm::X1VerifyStats; using xo::mm::GCObjectStore; using xo::mm::AGCObject; using xo::mm::AAllocator; using xo::mm::DArena; + using xo::mm::AllocInfo; using xo::mm::Role; using xo::mm::Generation; using xo::mm::c_max_generation; + using xo::mm::object_age; using xo::rng::xoshiro256ss; using xo::facet::TypeRegistry; using xo::facet::obj; @@ -316,41 +320,127 @@ namespace ut { std::vector * p_x1_v, 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; + { + 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); + 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, - p_rgen, - p_x1_v, - p_gcos, - p_x2_v, - p_arena2); - } - break; + GcosTestutil::random_object_graph(n_test_obj, + n_test_assign, + p_rgen, + p_x1_v, + p_gcos, + p_x2_v, + p_arena2); + } + break; + } + + //x1_v.push_back(Recd(DBoolean::box(alloc, true), + // sizeof(DBoolean), + // typeseq::id())); + } + + void + GcosTestutil::gcos_verify_consistency(GCObjectStore * p_gcos) + { + // traverses stored objects, updates counters + // in verify_stats (= gco.p_verify_stats_, via ctor) + // + p_gcos->verify_ok(); + + X1VerifyStats * verify_stats = p_gcos->verify_stats(); + + 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_age_ok", verify_stats->n_age_ok_), + xtag("n_age_bad", verify_stats->n_age_bad_), + xtag("n_no_iface", verify_stats->n_no_iface_))); + + REQUIRE(verify_stats->is_ok()); + } + + void + GcosTestutil::gcos_verify_ab_equivalence(const std::vector & x1_v, + const std::vector & x2_v) + { + REQUIRE(x1_v.size() == x2_v.size()); + + for (size_t i = 0, n = x1_v.size(); i < n; ++i) { + REQUIRE(x1_v[i].alloc_z_ == x2_v[i].alloc_z_); + REQUIRE(x1_v[i].tseq_ == x2_v[i].tseq_); + + REQUIRE(x1_v[i].gco_._typeseq() == x1_v[i].tseq_); + REQUIRE(x2_v[i].gco_._typeseq() == x2_v[i].tseq_); + } + } + + void + GcosTestutil::gcos_verify_allocinfo(const GCObjectStore & gcos, + uint32_t loop_index, + const std::vector & x1_v) + { + // gcos can reveal info about allocs + 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()); + + // 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); + } + } + + void + GcosTestutil::gcos_verify_gen0_only_allocated(uint32_t n_gen, + const GCObjectStore & gcos, + uint32_t loop_index, + const std::vector & x1_v) + { + Generation g0{0}; + Generation gn{n_gen}; + + // new objects appear in to-space for generation 0. + for (Generation gi = g0; gi < gn; ++gi) { + INFO(tostr(xtag("gi", gi))); + + if (loop_index == 0) { + if ((gi == 0) && (x1_v.size() > 0)) + REQUIRE(gcos.to_space(gi)->allocated() > 0); + else + REQUIRE(gcos.to_space(gi)->allocated() == 0); } - //x1_v.push_back(Recd(DBoolean::box(alloc, true), - // sizeof(DBoolean), - // typeseq::id())); + REQUIRE(gcos.from_space(gi)->allocated() == 0); } + } + } /*namespace ut*/ /* end GcosTestutil.cpp */ diff --git a/utest/GcosTestutil.hpp b/utest/GcosTestutil.hpp index f5e06533..b0b3e0b9 100644 --- a/utest/GcosTestutil.hpp +++ b/utest/GcosTestutil.hpp @@ -83,6 +83,30 @@ namespace ut { std::vector * p_x1_v, std::vector * p_x2_v, xoshiro256ss * p_rgen); + + /** Invoke built-in consistency verification for @p *p_gcos. + **/ + static void + gcos_verify_consistency(GCObjectStore * p_gcos); + + static void + gcos_verify_ab_equivalence(const std::vector & x1_v, + const std::vector & x2_v); + + /** verify reasonable alloc info values. + * object store has been subject to @p loop_index + * collection cycles + **/ + static void + gcos_verify_allocinfo(const GCObjectStore & gcos, + uint32_t loop_index, + const std::vector & x1_v); + + static void + gcos_verify_gen0_only_allocated(uint32_t n_gen, + const GCObjectStore & gcos, + uint32_t loop_index, + const std::vector & x1_v); }; } /*namespace ut*/ From 3f3e77bf24f9b1fc3735cb1e7b7e3de0d10c1cdd Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 18 Apr 2026 00:18:54 -0400 Subject: [PATCH 153/174] xo-gc: utest: move gcos_verify_gen0_fromspace_only_allocation -> *pp --- utest/GCObjectStore.test.cpp | 66 ++---------------------------------- utest/GcosTestutil.cpp | 63 ++++++++++++++++++++++++++++++++++ utest/GcosTestutil.hpp | 9 +++++ 3 files changed, 74 insertions(+), 64 deletions(-) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index c0442eef..2ddac4cd 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -166,69 +166,6 @@ namespace ut { namespace { // aux functions specific to GCObjectStore-1 unit test below - void - gcos_verify_gen0_fromspace_only_allocated(const Testcase & tc, - const GCObjectStore & gcos, - uint32_t loop_index, - Generation upto, - const std::vector & x1_v) - { - Generation g0{0}; - Generation gn{tc.n_gen_}; - - for (Generation gi = g0; gi < gn; ++gi) { - if (gi < upto) { - // we're collecting generation gi. - // Before we begin, to-space had better be empty - // (everthing in gi is in from-space) - - REQUIRE(gcos.to_space(gi)->allocated() == 0); - } else { - // we're not collecting generation gi. - // from-space must be empty. - // May have content in to-space - - REQUIRE(gcos.from_space(gi)->allocated() == 0); - } - } - - for (size_t i = 0, n = x1_v.size(); i < n; ++i) { - const auto & x1 = x1_v.at(i); - - // 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 (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); - - 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()); - } - } - void gcos_verify_forwarding(const GCObjectStore & gcos, Generation upto, @@ -549,7 +486,8 @@ namespace ut { // swap_roles [but only for generation < g1, i.e. g0 gcos.swap_roles(Generation::g1()); - gcos_verify_gen0_fromspace_only_allocated(tc, gcos, loop_index, Generation::g1(), x1_v); + GcosTestutil::gcos_verify_gen0_fromspace_only_allocated(tc.n_gen_, gcos, loop_index, + Generation::g1(), x1_v); gcos_move_roots_and_verify(tc, &gcos, Generation::g1(), x1_v, x2_v, tc.debug_flag_); diff --git a/utest/GcosTestutil.cpp b/utest/GcosTestutil.cpp index 59741a8f..cf5a795b 100644 --- a/utest/GcosTestutil.cpp +++ b/utest/GcosTestutil.cpp @@ -441,6 +441,69 @@ namespace ut { } } + void + GcosTestutil::gcos_verify_gen0_fromspace_only_allocated(uint32_t n_gen, + const GCObjectStore & gcos, + uint32_t loop_index, + Generation upto, + const std::vector & x1_v) + { + Generation g0{0}; + Generation gn{n_gen}; + + for (Generation gi = g0; gi < gn; ++gi) { + if (gi < upto) { + // we're collecting generation gi. + // Before we begin, to-space had better be empty + // (everthing in gi is in from-space) + + REQUIRE(gcos.to_space(gi)->allocated() == 0); + } else { + // we're not collecting generation gi. + // from-space must be empty. + // May have content in to-space + + REQUIRE(gcos.from_space(gi)->allocated() == 0); + } + } + + for (size_t i = 0, n = x1_v.size(); i < n; ++i) { + const auto & x1 = x1_v.at(i); + + // 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 (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); + + 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()); + } + } + } /*namespace ut*/ /* end GcosTestutil.cpp */ diff --git a/utest/GcosTestutil.hpp b/utest/GcosTestutil.hpp index b0b3e0b9..46bc475a 100644 --- a/utest/GcosTestutil.hpp +++ b/utest/GcosTestutil.hpp @@ -7,10 +7,12 @@ #include #include +#include #include #include namespace ut { + using xo::mm::Generation; using xo::facet::obj; enum class TestGraphType { @@ -107,6 +109,13 @@ namespace ut { const GCObjectStore & gcos, uint32_t loop_index, const std::vector & x1_v); + + static void + gcos_verify_gen0_fromspace_only_allocated(uint32_t n_gen, + const GCObjectStore & gcos, + uint32_t loop_index, + Generation upto, + const std::vector & x1_v); }; } /*namespace ut*/ From f042850cec8f5fa0d66d8335c9fdcf56221c643b Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 18 Apr 2026 00:26:51 -0400 Subject: [PATCH 154/174] xo-gc: utest: move remaining aux fns to GcosTestutil.*pp --- utest/GCObjectStore.test.cpp | 193 +---------------------------------- utest/GcosTestutil.cpp | 189 ++++++++++++++++++++++++++++++++++ utest/GcosTestutil.hpp | 23 +++++ 3 files changed, 215 insertions(+), 190 deletions(-) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index 2ddac4cd..7c29dc85 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -166,195 +166,6 @@ namespace ut { namespace { // aux functions specific to GCObjectStore-1 unit test below - void - gcos_verify_forwarding(const GCObjectStore & gcos, - Generation upto, - 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()); - - 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()); - - 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()); - - REQUIRE(g >= upto); - } - -// if (!obj_info.is_forwarding_tseq()) -// print_backtrace_dwarf(true /*demangle*/); - -// REQUIRE(obj_info.is_forwarding_tseq()); - } - - void - gcos_verify_forwarding_destination(const GCObjectStore & gcos, - const Recd & x1, - obj x1p_gco) - { - REQUIRE(gcos.contains_allocated(Role::to_space(), x1p_gco.data())); - AllocInfo obj1p_info = gcos.alloc_info((std::byte *)x1p_gco.data()); - REQUIRE(obj1p_info.size() >= x1.alloc_z_); - - REQUIRE(obj1p_info.payload().first == (std::byte *)x1p_gco.data()); - REQUIRE(obj1p_info.tseq() == x1.tseq_.seqno()); - - REQUIRE(x1p_gco.data() != nullptr); - REQUIRE(gcos.contains(Role::to_space(), x1p_gco.data())); - REQUIRE(gcos.contains_allocated(Role::to_space(), x1p_gco.data())); - } - - void - gcos_verify_forwarded_ab_equivalence(obj x1p_gco, - obj x2_gco) - { - // written out polymorphic comparison - - // match DBoolean.. - bool match_attempted = false; - { - 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); - } - - void - gcos_move_roots_and_verify(const Testcase & tc, - GCObjectStore * p_gcos, - Generation upto, - const std::vector & x1_v, - const std::vector & x2_v, - bool debug_flag) - { - scope log(XO_DEBUG(debug_flag)); - - Generation g1{1}; - - // try moving everything to to-space. - // For this to week we must have registered the type, - // so gc knows how to traverse it - // - 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); - - log && log("moving roots"); - log && log(xtag("i", i), - xtag("n", n), - xtag("x1.tseq_", x1.tseq_), - xtag("x1.tname", TypeRegistry::id2name(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 = p_gcos->lookup_type(x1.tseq_); - REQUIRE(x1p_iface); - - // snapshot root before moving - obj x1_gco = x1.gco_; - - // modifies x1.gco_ in place - auto x1p_data - = p_gcos->deep_move_root(x1p_iface, (void **)&(x1.gco_.data_), upto); - - REQUIRE(x1p_data); - REQUIRE(x1p_data == x1.gco_.data_); - - obj x1p_gco(x1p_iface, x1p_data); - - // obj (x1_gco) now forwarding pointer (to x1p_gco = x1.gco_) - gcos_verify_forwarding(*p_gcos, upto, x1, x1_gco); - - // obj1p in to-space, same contents as original obj - gcos_verify_forwarding_destination(*p_gcos, x1, x1p_gco); - - // x1p_gco must look like x2.gco - REQUIRE(x1p_gco._typeseq() == x2.gco_._typeseq()); - - gcos_verify_forwarded_ab_equivalence(x1p_gco, x2.gco_); - } else { - // can still try to move something. - // but will fail since type isn't registered - - auto x1p_data - = p_gcos->deep_move_root(x1.gco_.iface(), - (void **)&(x1.gco_.data_), - g1); - - // control here under normal GC use - // would represent a configuration fail - - REQUIRE(x1p_data == nullptr); - } - } - } - // fixture for GCObjectStore-1 test class GcosFixture { public: @@ -489,7 +300,9 @@ namespace ut { GcosTestutil::gcos_verify_gen0_fromspace_only_allocated(tc.n_gen_, gcos, loop_index, Generation::g1(), x1_v); - gcos_move_roots_and_verify(tc, &gcos, Generation::g1(), x1_v, x2_v, tc.debug_flag_); + GcosTestutil::gcos_move_roots_and_verify(tc.do_type_registration_, + &gcos, + Generation::g1(), x1_v, x2_v, tc.debug_flag_); // Things to test: // - deep_move_interior() // used from MutationLogStore diff --git a/utest/GcosTestutil.cpp b/utest/GcosTestutil.cpp index cf5a795b..be2ad829 100644 --- a/utest/GcosTestutil.cpp +++ b/utest/GcosTestutil.cpp @@ -504,6 +504,195 @@ namespace ut { } } + void + GcosTestutil::gcos_move_roots_and_verify(bool do_type_registration, + GCObjectStore * p_gcos, + Generation upto, + const std::vector & x1_v, + const std::vector & x2_v, + bool debug_flag) + { + scope log(XO_DEBUG(debug_flag)); + + Generation g1{1}; + + // try moving everything to to-space. + // For this to week we must have registered the type, + // so gc knows how to traverse it + // + 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); + + log && log("moving roots"); + log && log(xtag("i", i), + xtag("n", n), + xtag("x1.tseq_", x1.tseq_), + xtag("x1.tname", TypeRegistry::id2name(x1.tseq_))); + + if (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 = p_gcos->lookup_type(x1.tseq_); + REQUIRE(x1p_iface); + + // snapshot root before moving + obj x1_gco = x1.gco_; + + // modifies x1.gco_ in place + auto x1p_data + = p_gcos->deep_move_root(x1p_iface, (void **)&(x1.gco_.data_), upto); + + REQUIRE(x1p_data); + REQUIRE(x1p_data == x1.gco_.data_); + + obj x1p_gco(x1p_iface, x1p_data); + + // obj (x1_gco) now forwarding pointer (to x1p_gco = x1.gco_) + GcosTestutil::gcos_verify_forwarding(*p_gcos, upto, x1, x1_gco); + + // obj1p in to-space, same contents as original obj + gcos_verify_forwarding_destination(*p_gcos, x1, x1p_gco); + + // x1p_gco must look like x2.gco + REQUIRE(x1p_gco._typeseq() == x2.gco_._typeseq()); + + gcos_verify_forwarded_ab_equivalence(x1p_gco, x2.gco_); + } else { + // can still try to move something. + // but will fail since type isn't registered + + auto x1p_data + = p_gcos->deep_move_root(x1.gco_.iface(), + (void **)&(x1.gco_.data_), + g1); + + // control here under normal GC use + // would represent a configuration fail + + REQUIRE(x1p_data == nullptr); + } + } + } + + void + GcosTestutil::gcos_verify_forwarding(const GCObjectStore & gcos, + Generation upto, + 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()); + + 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()); + + 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()); + + REQUIRE(g >= upto); + } + + // if (!obj_info.is_forwarding_tseq()) + // print_backtrace_dwarf(true /*demangle*/); + + // REQUIRE(obj_info.is_forwarding_tseq()); + } + + void + GcosTestutil::gcos_verify_forwarding_destination(const GCObjectStore & gcos, + const Recd & x1, + obj x1p_gco) + { + REQUIRE(gcos.contains_allocated(Role::to_space(), x1p_gco.data())); + AllocInfo obj1p_info = gcos.alloc_info((std::byte *)x1p_gco.data()); + REQUIRE(obj1p_info.size() >= x1.alloc_z_); + + REQUIRE(obj1p_info.payload().first == (std::byte *)x1p_gco.data()); + REQUIRE(obj1p_info.tseq() == x1.tseq_.seqno()); + + REQUIRE(x1p_gco.data() != nullptr); + REQUIRE(gcos.contains(Role::to_space(), x1p_gco.data())); + REQUIRE(gcos.contains_allocated(Role::to_space(), x1p_gco.data())); + } + + void + GcosTestutil::gcos_verify_forwarded_ab_equivalence(obj x1p_gco, + obj x2_gco) + { + // 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); + } + } /*namespace ut*/ /* end GcosTestutil.cpp */ diff --git a/utest/GcosTestutil.hpp b/utest/GcosTestutil.hpp index 46bc475a..66bc39e0 100644 --- a/utest/GcosTestutil.hpp +++ b/utest/GcosTestutil.hpp @@ -41,6 +41,7 @@ namespace ut { struct GcosTestutil { using GCObjectStore = xo::mm::GCObjectStore; + using AGCObject = xo::mm::AGCObject; using DArena = xo::mm::DArena; using xoshiro256ss = xo::rng::xoshiro256ss; @@ -116,6 +117,28 @@ namespace ut { uint32_t loop_index, Generation upto, const std::vector & x1_v); + static void + gcos_move_roots_and_verify(bool do_type_registration, + GCObjectStore * p_gcos, + Generation upto, + const std::vector & x1_v, + const std::vector & x2_v, + bool debug_flag); + + static void + gcos_verify_forwarding(const GCObjectStore & gcos, + Generation upto, + const Recd & x1, + obj x1_gco); + + static void + gcos_verify_forwarding_destination(const GCObjectStore & gcos, + const Recd & x1, + obj x1p_gco); + + static void + gcos_verify_forwarded_ab_equivalence(obj x1p_gco, + obj x2_gco); }; } /*namespace ut*/ From 39b1ad1b21a7350c65768099a8018892acd8b300 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 19 Apr 2026 15:13:13 -0400 Subject: [PATCH 155/174] xo-gc: utest: mutation log tests --- include/xo/gc/DGCObjectStoreVisitor.hpp | 4 +- include/xo/gc/GCObjectStore.hpp | 12 + include/xo/gc/MutationLogEntry.hpp | 4 +- include/xo/gc/MutationLogStore.hpp | 15 +- src/gc/DX1Collector.cpp | 3 +- src/gc/GCObjectStore.cpp | 43 ++- src/gc/MutationLogStore.cpp | 9 +- utest/CMakeLists.txt | 11 + utest/DMockCollector.cpp | 106 +++++++ utest/DMockCollector.hpp | 65 +++++ utest/GCObjectStore.test.cpp | 14 +- utest/GcosTestutil.cpp | 305 +++++++++++++++------ utest/GcosTestutil.hpp | 42 ++- utest/ICollector_DMockCollector.cpp | 106 +++++++ utest/MlsTestutil.cpp | 110 ++++++++ utest/MlsTestutil.hpp | 27 ++ utest/MockCollector.hpp | 11 + utest/MutationLogStore.test.cpp | 258 +++++++++++++++-- utest/detail/ICollector_DMockCollector.hpp | 120 ++++++++ utest/idl/ICollector_DMockCollector.json5 | 24 ++ utest/init_gc_utest.cpp | 4 +- 21 files changed, 1156 insertions(+), 137 deletions(-) create mode 100644 utest/DMockCollector.cpp create mode 100644 utest/DMockCollector.hpp create mode 100644 utest/ICollector_DMockCollector.cpp create mode 100644 utest/MlsTestutil.cpp create mode 100644 utest/MlsTestutil.hpp create mode 100644 utest/MockCollector.hpp create mode 100644 utest/detail/ICollector_DMockCollector.hpp create mode 100644 utest/idl/ICollector_DMockCollector.json5 diff --git a/include/xo/gc/DGCObjectStoreVisitor.hpp b/include/xo/gc/DGCObjectStoreVisitor.hpp index 7ba65dad..4112ed40 100644 --- a/include/xo/gc/DGCObjectStoreVisitor.hpp +++ b/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/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index c318fcb2..f816e05b 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/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/include/xo/gc/MutationLogEntry.hpp b/include/xo/gc/MutationLogEntry.hpp index c76427a3..5d016730 100644 --- a/include/xo/gc/MutationLogEntry.hpp +++ b/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/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index 78042fac..fbe6a8b5 100644 --- a/include/xo/gc/MutationLogStore.hpp +++ b/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/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 6232bb30..239f999e 100644 --- a/src/gc/DX1Collector.cpp +++ b/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/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 896f02e4..f8e8c479 100644 --- a/src/gc/GCObjectStore.cpp +++ b/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/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 4970aa1c..2a9d6ac3 100644 --- a/src/gc/MutationLogStore.cpp +++ b/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/utest/CMakeLists.txt b/utest/CMakeLists.txt index e2c623be..4f555807 100644 --- a/utest/CMakeLists.txt +++ b/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/utest/DMockCollector.cpp b/utest/DMockCollector.cpp new file mode 100644 index 00000000..eb180685 --- /dev/null +++ b/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/utest/DMockCollector.hpp b/utest/DMockCollector.hpp new file mode 100644 index 00000000..69e9a223 --- /dev/null +++ b/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/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index 7c29dc85..19a6f92f 100644 --- a/utest/GCObjectStore.test.cpp +++ b/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/utest/GcosTestutil.cpp b/utest/GcosTestutil.cpp index be2ad829..61739dd7 100644 --- a/utest/GcosTestutil.cpp +++ b/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/utest/GcosTestutil.hpp b/utest/GcosTestutil.hpp index 66bc39e0..93e2c5bc 100644 --- a/utest/GcosTestutil.hpp +++ b/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/utest/ICollector_DMockCollector.cpp b/utest/ICollector_DMockCollector.cpp new file mode 100644 index 00000000..bfb6b3a2 --- /dev/null +++ b/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/utest/MlsTestutil.cpp b/utest/MlsTestutil.cpp new file mode 100644 index 00000000..125fcdf1 --- /dev/null +++ b/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/utest/MlsTestutil.hpp b/utest/MlsTestutil.hpp new file mode 100644 index 00000000..ae8c5e20 --- /dev/null +++ b/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/utest/MockCollector.hpp b/utest/MockCollector.hpp new file mode 100644 index 00000000..adc12f78 --- /dev/null +++ b/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/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index 3f0f7228..c2c03c85 100644 --- a/utest/MutationLogStore.test.cpp +++ b/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/utest/detail/ICollector_DMockCollector.hpp b/utest/detail/ICollector_DMockCollector.hpp new file mode 100644 index 00000000..3607c2bd --- /dev/null +++ b/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/utest/idl/ICollector_DMockCollector.json5 b/utest/idl/ICollector_DMockCollector.json5 new file mode 100644 index 00000000..d0461090 --- /dev/null +++ b/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/utest/init_gc_utest.cpp b/utest/init_gc_utest.cpp index 5f1958cf..f92c1f4b 100644 --- a/utest/init_gc_utest.cpp +++ b/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())); From 362dcf73fce59db036013a73f4effd7f97b24784 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 19 Apr 2026 19:57:49 -0400 Subject: [PATCH 156/174] xo-gc: bugfix + mutation log test passes --- include/xo/gc/MutationLogStore.hpp | 1 + src/gc/MutationLogStore.cpp | 20 ++- utest/GCObjectStore.test.cpp | 2 +- utest/GcosTestutil.cpp | 258 +++++++++++++++++++---------- utest/GcosTestutil.hpp | 53 +++++- utest/MutationLogStore.test.cpp | 115 +++++++++---- 6 files changed, 321 insertions(+), 128 deletions(-) diff --git a/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index fbe6a8b5..4a39e6c6 100644 --- a/include/xo/gc/MutationLogStore.hpp +++ b/include/xo/gc/MutationLogStore.hpp @@ -35,6 +35,7 @@ namespace xo { **/ void init_mlogs(std::size_t page_z); + const MutationLogConfig & config() const noexcept { return config_; } 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 **/ diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 2a9d6ac3..9b141381 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -29,10 +29,10 @@ namespace xo { for (std::uint32_t mlog_role = 0; mlog_role < c_n_role + 1; ++mlog_role) { this->mlog_storage_[mlog_role][igen] - = _make_mlog(igen, - label_v[mlog_role], - config_.mutation_log_z_, - page_z); + = this->_make_mlog(igen, + label_v[mlog_role], + config_.mutation_log_z_, + page_z); this->mlog_[mlog_role][igen] = &(mlog_storage_[mlog_role][igen]); @@ -152,6 +152,8 @@ namespace xo { *p_lhs = rhs; if (!config_.enabled_flag_) { + log && log(xtag("msg", "noop b/c incremental gc disabled")); + // only need to log mutations when incremental gc is enabled return; } @@ -163,6 +165,8 @@ namespace xo { Generation src_g = gco_store->generation_of(Role::to_space(), p_lhs); if (src_g.is_sentinel()) { + log && log(xtag("msg", "noop because src not gc-owned")); + // only need mlog entries for gc-owned pointers. // In this case pointer does not originate in gc-owned space return; @@ -171,11 +175,15 @@ namespace xo { Generation dest_g = gco_store->generation_of(Role::to_space(), rhs.data()); if (dest_g.is_sentinel()) { + log && log(xtag("msg", "noop because dest not gc-owned")); + // similarly, don't need mlog entry to non-gc-owned destination return; } if (src_g < dest_g) { + log && log(xtag("msg", "noop because src gen younger than dest gen")); + // young-to-old pointers don't need to be remembered, // since a GC cycle that collects an (old) generation is guarnatted // to also collect all younger generations. @@ -194,6 +202,8 @@ namespace xo { assert(src_hdr && dest_hdr); if (gco_store->header2age(*src_hdr) <= gco_store->header2age(*dest_hdr)) { + log && log(xtag("msg", "noop because src age no older than dest age")); + // source and destination have the same age; // therefore are always collected on the same set of GC cycles // -> no need to remember separately. @@ -258,7 +268,7 @@ namespace xo { // - to_mlog, triage_mlog are empty for (Generation child_gen{0}; - child_gen + 2 < config_.n_generation_; + child_gen + 1 < config_.n_generation_; ++child_gen) { MutationLog * from_mlog = this->mlog_[Role::from_space()][child_gen]; diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index 19a6f92f..6c7ed42b 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -275,7 +275,7 @@ namespace ut { // construct, extend, and/or modify object graphs in {x1_v, x2_v} - GcosTestutil::gcos_construct_ab_object_graphs(nullptr /*cmd_seq*/, + GcosTestutil::gcos_construct_ab_object_graphs(TestSequence{} /*test_seq*/, tc.obj_graph_type_, tc.n_i0_test_obj_, tc.n_i0_test_assign_, diff --git a/utest/GcosTestutil.cpp b/utest/GcosTestutil.cpp index 61739dd7..bfece569 100644 --- a/utest/GcosTestutil.cpp +++ b/utest/GcosTestutil.cpp @@ -24,6 +24,7 @@ namespace ut { using xo::mm::ACollector; using xo::mm::DMockCollector; using xo::mm::X1VerifyStats; + using xo::mm::MutationLog; using xo::mm::GCObjectStore; using xo::mm::AGCObject; using xo::mm::AAllocator; @@ -316,7 +317,7 @@ namespace ut { * @p loop_index counts iteration with one gc-like phase. **/ void - GcosTestutil::gcos_construct_ab_object_graphs(Step * cmd_seq, + GcosTestutil::gcos_construct_ab_object_graphs(TestSequence test_seq, TestGraphType obj_graph_type, uint32_t n_i0_test_obj, uint32_t n_i0_test_assign, @@ -331,104 +332,185 @@ namespace ut { std::vector * p_x2_v, xoshiro256ss * p_rgen) { - if (cmd_seq && (loop_index == 0)) { - // do scripted sequence only + /** TestSequence memory layout + * + * test_seq + * | + * v + * TestSequence + * +-----------+ cmd_seq_[] (shared, sentinel-terminated) + * | cmd_seq_ |---> +-------+-------+-------+-------+-------+-------+ + * +-----------+ | step0 | step1 | step2 | step3 | step4 | SENTL | + * | phases_ |-\ +-------+-------+-------+-------+-------+-------+ + * +-----------+ | ix: 0 1 2 3 4 5 + * | + * | phases_[] (sentinel-terminated) + * \-> +-----------+-----------+ + * | Phase 0 | SENTINEL | + * | lo_ix_=0 | lo_ix_=-1 | + * | hi_ix_=5 | hi_ix_=-1 | + * | mlog_new_ | mlog_new_ | + * +-----------+-----------+ + * + * Phase.lo_ix_ / hi_ix_ index into test_seq->cmd_seq_[]. + * Phase 0 executes cmd_seq_[lo_ix_ .. hi_ix_), i.e. steps 0..4. + * Sentinel phase has lo_ix_ == -1. + * + * Each Step has {cmd_, arg0_ix_, arg1_ix_}. + * arg0_ix_ and arg1_ix_ index into x1_v[] / x2_v[], + * referring to objects created by earlier steps. + * + * Example (seq1): + * step0: {make_bool, 0, 0} -> x1_v[0] = #f + * step1: {make_bool, 1, 0} -> x1_v[1] = #t + * step2: {make_nil, 0, 0} -> x1_v[2] = () + * step3: {make_cons, 0, 2} -> x1_v[3] = cons(x1_v[0], x1_v[2]) = (#f) + * step4: {assign_head, 3, 1} -> set-car!(x1_v[3], x1_v[1]) => (#t) + **/ - auto alloc = obj(p_gcos->new_space()); - auto alloc2 = obj(p_arena2); - DMockCollector mock(p_mls, p_gcos); - auto mockgc = obj(&mock); + if (!test_seq.is_sentinel()) { + // Explicit command sequence. + // Each command creates a new node or modifies an existing one - while (cmd_seq->is_command()) { - bool is_alloc = false; - obj xi; - obj xi2; - uint64_t alloc_z = 0; - typeseq tseq; + // 1. Sequence of commands for this call. + // Will be phases[loop_index] if well-defined. + // 2. Expected effect on mutation log + // + Phase * phase_expect = nullptr; + { + Phase * p_phase = test_seq.phases_; - 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); + if (test_seq.phases_) { + for (uint32_t i = 0; i < loop_index; ++i) { + if (!p_phase->is_sentinel()) + ++p_phase; + else + p_phase = nullptr; } - 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)); + phase_expect = p_phase; + } + + Step * cmd_seq = test_seq.cmd_seq_; + + if (phase_expect && cmd_seq) { + // Do scripted sequence only. + // For this phases that is + // cmd_seq[ix] + // for + // phase_expect->lo_ix_ <= ix < phase_expect->hi_ix_ + + auto alloc = obj(p_gcos->new_space()); + auto alloc2 = obj(p_arena2); + DMockCollector mock(p_mls, p_gcos); + auto mockgc = obj(&mock); + + for (int32_t ix = phase_expect->lo_ix_, hi = phase_expect->hi_ix_; ix < hi; ++ix) { + const Step & cmd = cmd_seq[ix]; + + bool is_alloc = false; + obj xi; + obj xi2; + uint64_t alloc_z = 0; + typeseq tseq; + + switch (cmd.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.arg0_ix_).gco_; + auto r1 = obj::from(p_x1_v->at(cmd.arg1_ix_).gco_); + auto h2 = p_x2_v->at(cmd.arg0_ix_).gco_; + auto r2 = obj::from(p_x2_v->at(cmd.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.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.arg0_ix_).gco_); + auto rhs1 = p_x1_v->at(cmd.arg1_ix_).gco_; + + auto lhs2 = obj::from(p_x2_v->at(cmd.arg0_ix_).gco_); + auto rhs2 = p_x2_v->at(cmd.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; + // check expected results + + for (Generation gi{0}; gi + 1 < Generation(p_mls->config().n_generation_); ++gi) { + MutationLog * mlog = p_mls->get_mlog(Role::to_space(), gi); + + REQUIRE(mlog); + REQUIRE(mlog->size() == phase_expect->mlog_new_z_[gi]); + } } } else { switch (obj_graph_type) { + case TestGraphType::fixed: + assert(false); // unreachable + break; + case TestGraphType::selfcycle: if (loop_index == 0) { GcosTestutil::selfcycle_object_graph(p_x1_v, diff --git a/utest/GcosTestutil.hpp b/utest/GcosTestutil.hpp index 93e2c5bc..05eba321 100644 --- a/utest/GcosTestutil.hpp +++ b/utest/GcosTestutil.hpp @@ -11,6 +11,7 @@ #include #include #include +#include namespace ut { using xo::mm::Generation; @@ -46,6 +47,54 @@ namespace ut { uint32_t arg1_ix_; }; + /** a phase comprises: + * 1. start with {gcos,mls} in known + valid state. + * 2. perform a sequence of commands + * (in general a mix of allocs and mutations) + * command sequence in @ref cmd_seq_, null-terminated + * 3. verify mlog state after sequence + * 4. run instrumented collection phase + * 4a. swap roles (i.e. from- and to- spaces) + * 4b. move roots, see gcos_move_roots_and_verify() + * 4c. update mutation log, see forward_mutation_log() + * 4d. cleanup (reset from- spaces) + * 5. re-verify {gcos,mls} in valid state + **/ + struct Phase { + bool is_sentinel() const noexcept { return lo_ix_ == -1; } + + /** Command sequence for this phase. + * See TestSequence.cmd_seq_ + * Phase comprises commands cmd_seq_[ix] for lo_ix <= ix < hi_ix + **/ + int32_t lo_ix_ = -1; + int32_t hi_ix_ = -1; + /** expected number of new entries in + * to-space mutation log after executing @ref cmd_seq_ + **/ + std::array mlog_new_z_; + }; + + struct TestSequence { + bool is_sentinel() const noexcept { return cmd_seq_ == nullptr; } + + /** shared null-terminated command sequence. + * references are taken relative to cmd_seq_[0]. + * A step + * {make_cons, x, y} -> make a cons cell. + * head from value ~ cmd_seq_[x] + * rest from value ~ cmd_seq_[y] + * + **/ + Step * cmd_seq_ = nullptr; + + /** array of phases. + * One gc cycle per phase. + * Sentinel phase has {lo_ix_ = -1, hi_ix_ = -1}; + **/ + Phase * phases_ = nullptr; + }; + enum class TestGraphType { /* spelled out sequence of Steps */ fixed, @@ -110,9 +159,11 @@ namespace ut { const GCObjectStore & gcos); /** sequence of steps. if non-null, ends with step s: s.cmd_ == Step::Cmd::Sentinel + * + * @p p_cmd_seq pointer to null-terminated array of Step[] arrays **/ static void - gcos_construct_ab_object_graphs(Step * cmd_seq, + gcos_construct_ab_object_graphs(TestSequence test_seq, TestGraphType obj_graph_type, uint32_t n_i0_test_obj, uint32_t n_i0_test_assign, diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index c2c03c85..bdeb79b8 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -40,7 +40,7 @@ namespace ut { size_t gc_z, uint32_t type_z, bool do_type_registration, - Step * cmd_seq, + TestSequence test_seq, uint32_t mlog_z, bool mlog_enabled_flag, TestGraphType obj_graph_type, @@ -57,7 +57,7 @@ namespace ut { do_type_registration_{do_type_registration}, mutation_log_z_{mlog_z}, mlog_enabled_flag_{mlog_enabled_flag}, - cmd_seq_{cmd_seq}, + test_seq_{test_seq}, obj_graph_type_{obj_graph_type}, n_gc_loop_{n_gc_loop}, n_i0_test_obj_{n_i0_test_obj}, @@ -83,8 +83,8 @@ namespace ut { * (load-bearing for incremental gc) **/ bool mlog_enabled_flag_ = false; - /** first loop: explicit cell alloc/assign **/ - Step * cmd_seq_ = nullptr; + /** if non-null; run contents of cmd_seq_[i] on loop #i **/ + TestSequence test_seq_; /** object graph type **/ TestGraphType obj_graph_type_ = TestGraphType::random; /** #of gc-like "move all the roots" phases to perform **/ @@ -107,47 +107,96 @@ namespace ut { using Cmd = Step::Cmd; - static Step seq0[] = { + static Step step_0[] = { {Cmd::make_bool, 0, 0}, // #f - {Cmd::make_nil, 0, 0}, // #nil + {Cmd::make_nil, 0, 0}, // #nil {Cmd::make_cons, 0, 1}, // cons(#f,#nil) - {Cmd::sentinel, 0, 0}, + {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}, + static Phase phase_0[] = { + // + // lo hi mlog_new_z_[] + // v v v + { 0, 3, {0} }, + { -1, -1, {0} }, }; + static TestSequence seq_0 { step_0, phase_0 }; + + // seq1: side effect on head of cons cell. + // But no mlog entry b/c all object ages are equal + // -> no x-age pointers + // + static Step step_1[] = { + {Cmd::make_bool, 0, 0}, // [0]: #f + {Cmd::make_bool, 1, 0}, // [1]: #t + {Cmd::make_nil, 0, 0}, // [2]: #nil + {Cmd::make_cons, 0, 2}, // [3]: cons(#f,#nil) + {Cmd::assign_head, 3, 1}, // set-car(cons(#f,#nil),#t) + {Cmd::sentinel, 0, 0}, + }; + + static Phase phase_1[] = { + // + // lo hi mlog_new_z_[] + // v v v + { 0, 5, {0} }, + { -1, -1, {0} }, + }; + + static TestSequence seq_1 { step_1, phase_1 }; + + static Step step_2[] = { + // ----- phase 0 ----- + {Cmd::make_bool, 0, 0}, // [0]: #f + {Cmd::make_bool, 1, 0}, // [1]: #t + {Cmd::make_nil, 0, 0}, // [2]: #nil + {Cmd::make_cons, 0, 2}, // [3]: cons(#f,#nil) + // ----- phase 1 ----- + {Cmd::make_bool, 1, 0}, // [4]: #t + {Cmd::assign_head, 3, 4}, // set-car(cons(#f,#nil),#t) + {Cmd::sentinel, 0, 0}, + }; + + static Phase phase_2[] = { + // + // lo hi mlog_new_z_[] + // v v v + { 0, 4, {0} }, // phase 0 + { 4, 6, {1} }, // phase 1. set-car makes 1x xgen ptr from g1->g0 + { -1, -1, {0} }, + }; + + static TestSequence seq_2 { step_2, phase_2 }; + +# define seq_nil TestSequence{} # 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 + * 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), + Testcase(2, 4, 16 * 1024, 8 * 128, F, seq_nil, 0, F, c_random, 1, 0, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_nil, 0, F, c_selfcycle, 1, 1, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_0, 0, F, c_fixed, 1, 0, 0, 0, 0, F), + Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_1, 0, F, c_fixed, 1, 0, 0, 0, 0, F), + Testcase(2, 1, 16 * 1024, 8 * 128, T, seq_2, 128, T, c_fixed, 2, 0, 0, 0, 0, T), }; # undef T @@ -284,7 +333,7 @@ namespace ut { 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_, + GcosTestutil::gcos_construct_ab_object_graphs(tc.test_seq_, tc.obj_graph_type_, tc.n_i0_test_obj_, tc.n_i0_test_assign_, From 4ad5498d72b42c96687a3e8f6d7e3934fc71454a Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 19 Apr 2026 19:59:08 -0400 Subject: [PATCH 157/174] xo-gc: utest: tidy sanitize flag --- utest/MutationLogStore.test.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index bdeb79b8..d1721844 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -67,6 +67,8 @@ namespace ut { debug_flag_{debug_flag} {} + bool sanitize_flag() const noexcept { return true; } + /** number of generations in gco store **/ uint32_t n_gen_ = 0; /** object prommotes on surviving this many gc cycles **/ @@ -389,11 +391,7 @@ namespace ut { // // reset (+ perhaps clean) from-space - { - // TODO: consider moving sanitize_flag to Testcase - bool sanitize_flag = true; - gcos.cleanup_phase(gk, sanitize_flag); - } + gcos.cleanup_phase(gk, tc.sanitize_flag()); // scan {gcos, mls} to collect counters in *gcos.verify_stats() { From 3b989bb89efd51281943ff4c58362781f3b0a2f8 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 19 Apr 2026 20:44:31 -0400 Subject: [PATCH 158/174] xo-gc: utest: + 2nd gc cycle for mlog test --- utest/MutationLogStore.test.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index d1721844..f5dfd07b 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -15,6 +15,7 @@ #include #include #include +#include // for ::getpagesize() namespace ut { using xo::scm::DList; @@ -100,7 +101,7 @@ namespace ut { /** 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_; + bool debug_flag_ = false; }; constexpr TestGraphType c_selfcycle = TestGraphType::selfcycle; @@ -158,6 +159,8 @@ namespace ut { // ----- phase 1 ----- {Cmd::make_bool, 1, 0}, // [4]: #t {Cmd::assign_head, 3, 4}, // set-car(cons(#f,#nil),#t) + // ----- phase 2 ----- + // ----- end ----- {Cmd::sentinel, 0, 0}, }; @@ -167,6 +170,7 @@ namespace ut { // v v v { 0, 4, {0} }, // phase 0 { 4, 6, {1} }, // phase 1. set-car makes 1x xgen ptr from g1->g0 + { 6, 6, {0} }, // phase 2. now both {src,dest} are in g1 { -1, -1, {0} }, }; @@ -198,7 +202,7 @@ namespace ut { Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_nil, 0, F, c_selfcycle, 1, 1, 0, 0, 0, F), Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_0, 0, F, c_fixed, 1, 0, 0, 0, 0, F), Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_1, 0, F, c_fixed, 1, 0, 0, 0, 0, F), - Testcase(2, 1, 16 * 1024, 8 * 128, T, seq_2, 128, T, c_fixed, 2, 0, 0, 0, 0, T), + Testcase(2, 1, 16 * 1024, 8 * 128, T, seq_2, 128, T, c_fixed, 3, 0, 0, 0, 0, T), }; # undef T From daf7d027bed415ed26db120cdc22ad57614098d3 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 19 Apr 2026 21:20:38 -0400 Subject: [PATCH 159/174] xo-gc: bugfixes + seq3 mlog utest --- src/gc/MutationLogStore.cpp | 5 +++-- utest/MutationLogStore.test.cpp | 39 ++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 9b141381..e92818c3 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -391,8 +391,9 @@ namespace xo { from_entry.parent()); if (parent_gen_to.is_sentinel()) { - void * parent_fr = *from_entry.p_data(); + // parent is not in to-space + void * parent_fr = from_entry.parent(); AllocInfo parent_info = gc.alloc_info((std::byte *)parent_fr); if (parent_info.is_forwarding_tseq()) { @@ -408,7 +409,7 @@ namespace xo { parent_to); parent_info = gc.alloc_info((std::byte *)parent_to); - assert(!parent_gen_to.sentinel()); + assert(!parent_gen_to.is_sentinel()); // Since parent already forwarded, we don't have to preserve child // or update parent object. diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index f5dfd07b..84ce34eb 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -127,6 +127,8 @@ namespace ut { static TestSequence seq_0 { step_0, phase_0 }; + // ---------------------------------------------------------------- + // seq1: side effect on head of cons cell. // But no mlog entry b/c all object ages are equal // -> no x-age pointers @@ -150,6 +152,8 @@ namespace ut { static TestSequence seq_1 { step_1, phase_1 }; + // ---------------------------------------------------------------- + static Step step_2[] = { // ----- phase 0 ----- {Cmd::make_bool, 0, 0}, // [0]: #f @@ -176,6 +180,38 @@ namespace ut { static TestSequence seq_2 { step_2, phase_2 }; + // ---------------------------------------------------------------- + + static Step step_3[] = { + // ----- phase 0 ----- + {Cmd::make_bool, 0, 0}, // [0]: #f + {Cmd::make_bool, 1, 0}, // [1]: #t + {Cmd::make_nil, 0, 0}, // [2]: #nil + {Cmd::make_cons, 0, 2}, // [3]: cons(#f,#nil) + // ----- phase 1 ----- + {Cmd::make_bool, 1, 0}, // [4]: #t + {Cmd::assign_head, 3, 4}, // set-car(cons(#f,#nil),#t) + // ----- phase 2 ----- + // ----- phase 3 ----- + // ----- end ----- + {Cmd::sentinel, 0, 0}, + }; + + static Phase phase_3[] = { + // + // lo hi mlog_new_z_[] + // v v v + { 0, 4, {0} }, // phase 0 + { 4, 6, {1} }, // phase 1. set-car makes 1x xage ptr + { 6, 6, {1} }, // phase 2. now src in g1, dest in g0 + { 6, 6, {0} }, // phase 3. now dest in g1 + { -1, -1, {0} }, + }; + + static TestSequence seq_3 { step_3, phase_3 }; + + // ---------------------------------------------------------------- + # define seq_nil TestSequence{} # define nil nullptr # define T true @@ -202,7 +238,8 @@ namespace ut { Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_nil, 0, F, c_selfcycle, 1, 1, 0, 0, 0, F), Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_0, 0, F, c_fixed, 1, 0, 0, 0, 0, F), Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_1, 0, F, c_fixed, 1, 0, 0, 0, 0, F), - Testcase(2, 1, 16 * 1024, 8 * 128, T, seq_2, 128, T, c_fixed, 3, 0, 0, 0, 0, T), + 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, T), }; # undef T From f79c8a9c736875abbb1ecb8903e04f27f60e6839 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 24 Apr 2026 21:24:02 -0400 Subject: [PATCH 160/174] xo-gc: refactor for MutationLogStore bugs [TESTFAIL] --- include/xo/gc/MutationLogEntry.hpp | 10 +++++ include/xo/gc/MutationLogStore.hpp | 7 +-- src/gc/MutationLogEntry.cpp | 68 ++++++++++++++++++++++++++++++ src/gc/MutationLogStore.cpp | 33 +++++++++------ utest/MutationLogStore.test.cpp | 55 ++++++++++++++++++++++-- 5 files changed, 154 insertions(+), 19 deletions(-) diff --git a/include/xo/gc/MutationLogEntry.hpp b/include/xo/gc/MutationLogEntry.hpp index 5d016730..5141ba3d 100644 --- a/include/xo/gc/MutationLogEntry.hpp +++ b/include/xo/gc/MutationLogEntry.hpp @@ -9,6 +9,7 @@ namespace xo { namespace mm { + class GCObjectStore; // see xo-gc/ GCObjectStore.hpp /** @brief Track a cross-generational pointer * @@ -40,6 +41,15 @@ namespace xo { /** true iff child pointer has been altered since this mlog entry created **/ bool is_superseded() const noexcept { return *p_data_ != snap_.data(); } + /** Refresh snapshot when *p_data_ does not match snap_.data_ + * Get updated facet information from destination alloc header. + * It's possible that *p_data_ no longer points to gc-owned space + * + * @return true if snapshot updated. false if this entry should be discarded + **/ + bool refresh_snapshot(Generation parent_gen, + GCObjectStore * gcos) noexcept; + private: /** address of object containing logged mutation **/ void * parent_ = nullptr; diff --git a/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index 4a39e6c6..b1b2787e 100644 --- a/include/xo/gc/MutationLogStore.hpp +++ b/include/xo/gc/MutationLogStore.hpp @@ -139,18 +139,19 @@ namespace xo { MutationLogStatistics _preserve_child_of_live_parent(obj gc, Generation upto, Generation parent_gen, - const MutationLogEntry & from_entry, + MutationLogEntry & from_entry, MutationLog * keep_mlog); +#ifdef OBSOLETE /** On behalf of collector @p gc: * * helper function to decide whether to keep a mutation log entry - * @return true iff mlog entry appended to @p keep_mlog **/ - bool _check_keep_mutation_aux(const MutationLogEntry & from_entry, + void _check_keep_mutation_aux(MutationLogEntry & from_entry, Generation parent_gen_to, void * child_to, MutationLog * keep_mlog); +#endif public: diff --git a/src/gc/MutationLogEntry.cpp b/src/gc/MutationLogEntry.cpp index 6e3a0edc..56c34ad9 100644 --- a/src/gc/MutationLogEntry.cpp +++ b/src/gc/MutationLogEntry.cpp @@ -4,8 +4,11 @@ **/ #include "MutationLogEntry.hpp" +#include namespace xo { + using xo::reflect::typeseq; + namespace mm { MutationLogEntry::MutationLogEntry(void * parent, @@ -16,6 +19,71 @@ namespace xo { snap_{snap} {} + bool + MutationLogEntry::refresh_snapshot(Generation parent_gen, + GCObjectStore * gcos) noexcept + { + void * child_data = *p_data_; + // note: never the same child_info as computed at the top of + // MutationLogEntry._preserve_child_of_live_parent() + // Child pointer was either forwarding pointer or moved. + // In either case must pickup info for new location. + // + AllocInfo child_info = gcos->alloc_info((std::byte*)child_data); + + if (child_info.is_forwarding_tseq()) { + // code salvaged from MutationLogStore._check_keep_mutation_aux() + // as reminder. But if caller is gc will have to know this anyway, + // so it can move child + + // if (info.is_forwarding_tseq()) { + // child_data = *(void **)child_data; + // info = gcos->alloc_info((std::byte *)child_data); + //} + + assert(false); // for now assuming caller forward child + } + + Generation child_gen_to + = gcos->generation_of(Role::to_space(), child_data); + + if (child_gen_to.is_sentinel()) { + // child no longer points to gc-owned space. + // 1. may not have an alloc header (could be a static global for example), + // so AllocInfo not available + // 2. doesn't need a mutation log entry since this gc can't move destination + + snap_.data_ = nullptr; // hygiene + + return false; + } + + const GCObjectStoreConfig & config = gcos->config(); + + bool need_mlog_entry + = ((child_gen_to + 1 < config.n_generation_) + && (config.promotion_threshold(parent_gen) + > config.promotion_threshold(child_gen_to))); + + if (need_mlog_entry) { + AGCObject * iface = gcos->lookup_type(typeseq(child_info.tseq())); + + if (iface) { + this->snap_ = obj(iface, child_data); + + // snapshot updated, keep mlog entry + return true; + } else { + assert(false); + + return false; + } + } else { + // retire this entry. + return false; + } + } + } /*namespace mm*/ } /*namespace xo*/ diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index e92818c3..de5d0e61 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -412,7 +412,8 @@ namespace xo { assert(!parent_gen_to.is_sentinel()); // Since parent already forwarded, we don't have to preserve child - // or update parent object. + // or update parent object. GC already guaranteed to have visited + // parent's child pointers // // Do need to replace mlog entry to reflect new parent location. @@ -421,14 +422,19 @@ namespace xo { - (std::byte *)from_entry.parent()); void ** p_data_to = (void **)((std::byte *)(parent_to) + offset); - void * child_to = *p_data_to; + //void * child_to = *p_data_to; MutationLogEntry to_entry(parent_to, p_data_to, from_entry.snap()); + if (to_entry.refresh_snapshot(parent_gen_to, gco_store_)) + keep_mlog->push_back(to_entry); + +#ifdef OBSOLETE this->_check_keep_mutation_aux(to_entry, parent_gen_to, child_to, keep_mlog); +#endif } else { @@ -459,7 +465,7 @@ namespace xo { MutationLogStore::_preserve_child_of_live_parent(obj gc, Generation upto, Generation parent_gen, - const MutationLogEntry & from_entry, + MutationLogEntry & from_entry, MutationLog * keep_mlog) { void * child_fr = *from_entry.p_data(); @@ -487,29 +493,31 @@ namespace xo { // TODO: make this a method on AllocInfo child_to = *(void **)child_fr; - // assigning through address of P->C pointer - // also makes mlog entry current - } else { // [MLOG2] ++counters.n_rescue_; child_to = gco_store_->deep_move_interior(gc, child_fr, upto); + } - // update child pointer in parent object - *from_entry.p_data() = child_to; + // update child pointer in parent object. + // either forwarded or moved + *from_entry.p_data() = child_to; + + // TODO: pass statistics object + if (from_entry.refresh_snapshot(parent_gen, gco_store_)) { + keep_mlog->push_back(from_entry); } // child_to generation in {gen, gen+1} - this->_check_keep_mutation_aux(from_entry, parent_gen, child_to, keep_mlog); - return counters; } - bool - MutationLogStore::_check_keep_mutation_aux(const MutationLogEntry & from_entry, +#ifdef OBSOLETE + void + MutationLogStore::_check_keep_mutation_aux(MutationLogEntry & from_entry, Generation parent_gen_to, void * child_to, MutationLog * keep_mlog) @@ -539,6 +547,7 @@ namespace xo { return false; } } +#endif } /*namespace mm*/ diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index 84ce34eb..6bf6fb9e 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -212,6 +212,51 @@ namespace ut { // ---------------------------------------------------------------- + static Step step_4[] = { + // ----- phase 0 ----- + {Cmd::make_bool, 0, 0}, // [0]: #f + {Cmd::make_bool, 1, 0}, // [1]: #t + {Cmd::make_nil, 0, 0}, // [2]: #nil + {Cmd::make_cons, 0, 2}, // [3]: cons(#f,#nil) + + // 1st gc + + // ----- phase 1 ----- + {Cmd::make_bool, 1, 0}, // [4]: #t + {Cmd::assign_head, 3, 4}, // set-car([3],#t) + + // 2nd gc. [0]..[3] promote to g1 + // [4] in g0 so [3]->[4] requires mlog entry + + // ----- phase 2 ----- + {Cmd::make_bool, 0, 0}, // [5]: #f + {Cmd::assign_head, 3, 5}, // set-car([3],#f) + + // 3rd gc. [4] promotes to g1, + // [5] in g0 so [3]->[5] requires mlog entry + + // ----- phase 3 ----- + // ----- phase 4 ----- + // ----- end ----- + {Cmd::sentinel, 0, 0}, + }; + + static Phase phase_4[] = { + // + // lo hi mlog_new_z_[] + // v v v + { 0, 4, {0} }, // phase 0 gc + { 4, 6, {1} }, // phase 1 gc. set-car makes 1x xage ptr + { 6, 8, {2} }, // phase 2 gc. now src in g1, dest [4] in g0 + { 8, 8, {1} }, // phase 3 gc. new dest [5] in g0 + { 8, 8, {0} }, // phase 4 gc. now dest in g1 + { -1, -1, {0} }, + }; + + static TestSequence seq_4 { step_4, phase_4 }; + + // ---------------------------------------------------------------- + # define seq_nil TestSequence{} # define nil nullptr # define T true @@ -239,7 +284,8 @@ namespace ut { Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_0, 0, F, c_fixed, 1, 0, 0, 0, 0, F), Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_1, 0, F, c_fixed, 1, 0, 0, 0, 0, F), 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, T), + 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, T), }; # undef T @@ -376,6 +422,8 @@ namespace ut { 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)); + INFO(xtag("loop_index", loop_index)); + GcosTestutil::gcos_construct_ab_object_graphs(tc.test_seq_, tc.obj_graph_type_, tc.n_i0_test_obj_, @@ -442,9 +490,8 @@ namespace ut { REQUIRE(gcos.verify_stats()->is_ok()); } - } - } - + } /*one gc cycle per loop*/ + } /*testcase loop*/ } } /*namespace ut*/ From 866ab0503dad1279ef754360d45cd2828a645dec Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 25 Apr 2026 15:50:08 -0400 Subject: [PATCH 161/174] xo-gc: bugfixes. xo-gc utests pass. --- include/xo/gc/MutationLogEntry.hpp | 19 ++- include/xo/gc/MutationLogStore.hpp | 2 + src/gc/MutationLogEntry.cpp | 133 +++++++++++++++++---- src/gc/MutationLogStore.cpp | 185 +++++++++++++---------------- utest/MutationLogStore.test.cpp | 2 +- 5 files changed, 214 insertions(+), 127 deletions(-) diff --git a/include/xo/gc/MutationLogEntry.hpp b/include/xo/gc/MutationLogEntry.hpp index 5141ba3d..9702834c 100644 --- a/include/xo/gc/MutationLogEntry.hpp +++ b/include/xo/gc/MutationLogEntry.hpp @@ -10,6 +10,7 @@ namespace xo { namespace mm { class GCObjectStore; // see xo-gc/ GCObjectStore.hpp + class MutationLogStatistics; // see xo-gc/ MutationLogStatistics.hpp /** @brief Track a cross-generational pointer * @@ -38,9 +39,25 @@ namespace xo { /** 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 **/ + /** true iff child pointer has been altered since this mlog entry created + * WARNING: extra care needed around forwarding pointers + **/ bool is_superseded() const noexcept { return *p_data_ != snap_.data(); } + /** Update parent and snapshot if either has been forwarded. + * Do not try to correct *p_data_: it might now point outside + * gc-owned space, in which case need not be intelligible + **/ + bool check_forward_inplace(GCObjectStore * gcos, + MutationLogStatistics * p_counters) noexcept; + + /** update @ref parent_, @ref p_data_ iff parent has been forwarded + * @return true iff encountered forwarded parent + * Require: parent must be gc-owned, since we rely on AllocInfo existing + **/ + bool check_forward_parent_inplace(GCObjectStore * gcos, + MutationLogStatistics * p_counters) noexcept; + /** Refresh snapshot when *p_data_ does not match snap_.data_ * Get updated facet information from destination alloc header. * It's possible that *p_data_ no longer points to gc-owned space diff --git a/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index b1b2787e..018b2869 100644 --- a/include/xo/gc/MutationLogStore.hpp +++ b/include/xo/gc/MutationLogStore.hpp @@ -135,6 +135,8 @@ namespace xo { * with parent P in generation @p parent_gen: * ensure child C is evacuated, and append @p from_entry to * @p keep_mlog. + * + * Require: child is gc-owned **/ MutationLogStatistics _preserve_child_of_live_parent(obj gc, Generation upto, diff --git a/src/gc/MutationLogEntry.cpp b/src/gc/MutationLogEntry.cpp index 56c34ad9..39bf802d 100644 --- a/src/gc/MutationLogEntry.cpp +++ b/src/gc/MutationLogEntry.cpp @@ -4,9 +4,11 @@ **/ #include "MutationLogEntry.hpp" -#include +#include "GCObjectStore.hpp" +#include "MutationLogStatistics.hpp" namespace xo { + using xo::mm::MutationLogStatistics; using xo::reflect::typeseq; namespace mm { @@ -19,39 +21,110 @@ namespace xo { snap_{snap} {} + bool + MutationLogEntry::check_forward_inplace(GCObjectStore * gcos, + MutationLogStatistics * p_counters) noexcept + { + /** Several cases here based on state of {mlog entry, parent}. + * + * 1. gc already updated P->P', C->C', + * still have snap->P, snap->C + * update P'->C', snap->P', snap->C' + * 2. gc already updated P->C to P->C'. + * still have snap->C. mlog entry is current. + * update snap->C to snap->C' + * 3. gc has not updated P->C, + * but it has (independently) evacuated C to C'. + * still have snap->C, P->C. mlog entry is current. + * update P->C' and snap->C' + * 4. gc has not moved P or C. mlog entry is up to date. + * no update. P,C may yet be rescued, or may be garbage. + * 5. P->D, D not in {C,C'} + * mlog entry has been superseded. + * Be careful of confusing this case with 1..4 + **/ + + // if either of {parent, snapshot} has been forwarded, + // update with destination. + // + // Don't do this with child, since child might now point + // outside gc-owned space + + bool upd_flag = this->check_forward_parent_inplace(gcos, p_counters); + + AllocInfo snap_info = gcos->alloc_info((std::byte *)snap_.data_); + + if (snap_info.is_forwarding_tseq()) { + void * snap_from = snap_.data_; + void * snap_to = *(void **)snap_.data_; + + if (snap_from == *p_data_) { + // parent still refers to forwarding pointer, needs fix + *p_data_ = snap_to; + // also fix snapshot + this->snap_.reset_opaque(snap_to); + + upd_flag = true; + } else if (snap_to == *p_data_) { + // parent updated, but snapshot stale + this->snap_.reset_opaque(snap_to); + + upd_flag = true; + } else { + // superseded mlog entry + } + } + + return upd_flag; + } + + bool + MutationLogEntry::check_forward_parent_inplace(GCObjectStore * gcos, + MutationLogStatistics * p_counters) noexcept + { + AllocInfo parent_info = gcos->alloc_info((std::byte *)parent_); + + if (parent_info.is_forwarding_tseq()) { + void * parent_to = *(void **)parent_; + + std::size_t offset + = (std::byte *)p_data_ - (std::byte *)parent_; + + void ** p_data_to = (void **)((std::byte *)parent_to + offset); + + this->parent_ = parent_to; + this->p_data_ = p_data_to; + + ++(p_counters->n_live_parent_); + + return true; + } else { + return false; + } + } + bool MutationLogEntry::refresh_snapshot(Generation parent_gen, GCObjectStore * gcos) noexcept { + scope log(XO_DEBUG(gcos->config().debug_flag_)); + void * child_data = *p_data_; - // note: never the same child_info as computed at the top of - // MutationLogEntry._preserve_child_of_live_parent() - // Child pointer was either forwarding pointer or moved. - // In either case must pickup info for new location. - // - AllocInfo child_info = gcos->alloc_info((std::byte*)child_data); - - if (child_info.is_forwarding_tseq()) { - // code salvaged from MutationLogStore._check_keep_mutation_aux() - // as reminder. But if caller is gc will have to know this anyway, - // so it can move child - - // if (info.is_forwarding_tseq()) { - // child_data = *(void **)child_data; - // info = gcos->alloc_info((std::byte *)child_data); - //} - - assert(false); // for now assuming caller forward child - } Generation child_gen_to = gcos->generation_of(Role::to_space(), child_data); if (child_gen_to.is_sentinel()) { - // child no longer points to gc-owned space. - // 1. may not have an alloc header (could be a static global for example), - // so AllocInfo not available - // 2. doesn't need a mutation log entry since this gc can't move destination + log && log("child not in to-space"); + + // unreachable, since: + // 1. not in from-space, if it were forwarded, + // caller would already have forwarded this mlog entry. + // 2. caller already confirmed mlog entry current. + // 3. would not create mlog entry for non-gc-owned child + // in any case, would be legal to discard mlog entry here. + + assert(false); snap_.data_ = nullptr; // hygiene @@ -66,6 +139,12 @@ namespace xo { > config.promotion_threshold(child_gen_to))); if (need_mlog_entry) { + AllocInfo child_info = gcos->alloc_info((std::byte*)child_data); + + assert(!child_info.is_forwarding_tseq()); + + log && log("need mlog entry", xtag("tseq", child_info.tseq())); + AGCObject * iface = gcos->lookup_type(typeseq(child_info.tseq())); if (iface) { @@ -74,11 +153,17 @@ namespace xo { // snapshot updated, keep mlog entry return true; } else { + log && log("facet install error!"); + + // facet install error + assert(false); return false; } } else { + log && log("retire mlog entry"); + // retire this entry. return false; } diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index de5d0e61..e57f40d4 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -258,12 +258,21 @@ namespace xo { MutationLogStore::forward_mutation_log(obj gc, Generation upto) { + scope log0(XO_DEBUG(config_.debug_flag_)); + /** non-zero if at least one object was rescued (from any generation) * by mutation log scan **/ std::size_t work = 0; + /** count outer loop iterations */ + std::size_t i_fixpoint_loop = 0; + do { + scope log1(XO_DEBUG(log0), "fixpoint", xtag("i", i_fixpoint_loop)); + + work = 0; + // on 1st iteration, for all generations: // - to_mlog, triage_mlog are empty @@ -271,7 +280,10 @@ namespace xo { child_gen + 1 < config_.n_generation_; ++child_gen) { - MutationLog * from_mlog = this->mlog_[Role::from_space()][child_gen]; + scope log2(XO_DEBUG(log1), xtag("gen", child_gen)); + + MutationLog * from_mlog + = this->mlog_[Role::from_space()][child_gen]; if (!from_mlog->empty()) { MutationLog * to_mlog = this->mlog_[Role::to_space()][child_gen]; @@ -294,10 +306,14 @@ namespace xo { work += stats.n_rescue_; } } + + ++i_fixpoint_loop; } while (work > 0); // here: reached fixpoints, any remaining triaged mlogs can be discarded for (Generation child_gen{0}; child_gen + 2 < config_.n_generation_; ++child_gen) { + log0 && log0("dismiss unelected triage", xtag("gen", child_gen)); + MutationLog * triage_mlog = this->mlog_[c_n_role][child_gen]; triage_mlog->clear(); @@ -372,89 +388,85 @@ namespace xo { for (MutationLogEntry & from_entry : *from_mlog) { if (log) { - log(xtag("i_from", i_from)); + log(xtag("i_from", i_from), + xtag("parent", from_entry.parent()), + xtag("snap", from_entry.snap().data())); } - if (from_entry.is_superseded()) { - // there must be a second mlog entry that refers to - // the new child. Rely on that second entry, - // skipping this one. - - // [MLOG0] obsolete mutation -> skip - ++counters.n_stale_; - continue; - } - - /* here: mlog current */ + // fixup gc-owned forwarded pointers in from_entry. + // May update from_entry.p_data_, but not *(from_entry.p_data_), since: + // 1. *p_data_ may not be gc-owned + // 2. want to preserve ability to superseded mlog entry + // + // load-bearing at least for [MLOG3] + // + from_entry.check_forward_inplace(gco_store_, &counters); + // Two possibiities for parent in to-space: + // 1. belongs to generation not subject to collection this cycle. + // 2. from_entry updated above for new location + // Generation parent_gen_to = gc.generation_of(Role::to_space(), from_entry.parent()); if (parent_gen_to.is_sentinel()) { - // parent is not in to-space + // parent is not in to-space. + // Only gc-owned parents eligible for mlog entries. + // Therefore parent must be in from-space + // Since not rescued, it may be garbage. - void * parent_fr = from_entry.parent(); - AllocInfo parent_info = gc.alloc_info((std::byte *)parent_fr); + log && log("parent not in to-space -> must be in from-space"); - if (parent_info.is_forwarding_tseq()) { - /* [MLOG3] */ + Generation parent_gen_from = gc.generation_of(Role::from_space(), + from_entry.parent()); + assert(!parent_gen_from.is_sentinel()); - ++counters.n_live_parent_; - - // new parent location in to-space - // TODO: method on AllocInfo to streamline this - void * parent_to = *(void **)parent_fr; - - parent_gen_to = gc.generation_of(Role::to_space(), - parent_to); - parent_info = gc.alloc_info((std::byte *)parent_to); - - assert(!parent_gen_to.is_sentinel()); - - // Since parent already forwarded, we don't have to preserve child - // or update parent object. GC already guaranteed to have visited - // parent's child pointers - // - // Do need to replace mlog entry to reflect new parent location. - - std::size_t offset - = ((std::byte *)from_entry.p_data() - - (std::byte *)from_entry.parent()); - - void ** p_data_to = (void **)((std::byte *)(parent_to) + offset); - //void * child_to = *p_data_to; - - MutationLogEntry to_entry(parent_to, p_data_to, from_entry.snap()); - - if (to_entry.refresh_snapshot(parent_gen_to, gco_store_)) - keep_mlog->push_back(to_entry); - -#ifdef OBSOLETE - this->_check_keep_mutation_aux(to_entry, - parent_gen_to, - child_to, - keep_mlog); -#endif + if (from_entry.is_superseded()) { + log && log("entry superseded -> discard"); + // parent mutated again after from_entry. + // If new child needs rescue, that will rely on mlog + // entry for that second mutation + ++counters.n_stale_; } else { - ++counters.n_triage_; + log && log("entry current -> triage"); - // parent hasn't been collected and may be garbage. - // However this is only provisional, since - // parent could turn out to be reachable via some other mutation. + // although parent appears to be garbage, + // it may get rescued via some other mlog entry. + // Keep mlog entry while considering other mutations. + + ++counters.n_triage_; triage_mlog->push_back(from_entry); } } else { - /* [MLOG1, MLOG2] */ + // parent in to-space: p_data_ is valid, can check superseded - counters - += this->_preserve_child_of_live_parent(gc, - upto, - parent_gen_to, - from_entry, - keep_mlog); + log && log("parent in to-space"); + + if (from_entry.is_superseded()) { + log && log("entry superseded -> discard"); + // there must be a second mlog entry that refers to + // the new child. Rely on that second entry, + // skipping this one. + + // [MLOG0] obsolete mutation -> skip + ++counters.n_stale_; + } else { + log && log("entry current -> preserve child"); + + /* [MLOG1, MLOG2] */ + + log && log(xtag("case", "MLOG1|MLOG2")); + + counters + += this->_preserve_child_of_live_parent(gc, + upto, + parent_gen_to, + from_entry, + keep_mlog); + } } } @@ -468,6 +480,8 @@ namespace xo { MutationLogEntry & from_entry, MutationLog * keep_mlog) { + scope log(XO_DEBUG(config_.debug_flag_)); + void * child_fr = *from_entry.p_data(); AllocInfo child_info = gc.alloc_info((std::byte *)(child_fr)); @@ -489,6 +503,8 @@ namespace xo { if (child_info.is_forwarding_tseq()) { // [MLOG1] + log && log(xtag("case", "MLOG1"), xtag("msg", "child forwarded")); + // child already forwarded. // TODO: make this a method on AllocInfo child_to = *(void **)child_fr; @@ -496,6 +512,8 @@ namespace xo { } else { // [MLOG2] + log && log(xtag("case", "MLOG2"), xtag("msg", "rescue child")); + ++counters.n_rescue_; child_to = gco_store_->deep_move_interior(gc, child_fr, upto); @@ -503,7 +521,7 @@ namespace xo { // update child pointer in parent object. // either forwarded or moved - *from_entry.p_data() = child_to; + *(from_entry.p_data()) = child_to; // TODO: pass statistics object if (from_entry.refresh_snapshot(parent_gen, gco_store_)) { @@ -515,41 +533,6 @@ namespace xo { return counters; } -#ifdef OBSOLETE - void - MutationLogStore::_check_keep_mutation_aux(MutationLogEntry & from_entry, - Generation parent_gen_to, - void * child_to, - MutationLog * keep_mlog) - { - Generation child_gen_to - = gco_store_->generation_of(Role::to_space(), child_to); - - bool need_mlog_entry - = ((child_gen_to + 1 < config_.n_generation_) - && (gco_store_->config().promotion_threshold(parent_gen_to) - > gco_store_->config().promotion_threshold(child_gen_to))); - - if (need_mlog_entry) { - // 1. P->C pointer is still cross-age (xage), and - // 2. this matters; in future P will promote before C - // - // Need to keep entry because parent will be eligible for promotion - // before child - - keep_mlog->push_back(from_entry); - - return true; - } else { - // child now in final generation, - // no longer need to track incoming mutations. - - return false; - } - } -#endif - - } /*namespace mm*/ } /*namespace xo*/ diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index 6bf6fb9e..c9f5a3de 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -285,7 +285,7 @@ namespace ut { Testcase(2, 4, 16 * 1024, 8 * 128, T, seq_1, 0, F, c_fixed, 1, 0, 0, 0, 0, F), 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, T), + Testcase(2, 2, 16 * 1024, 8 * 128, T, seq_4, 128, T, c_fixed, 4, 0, 0, 0, 0, F), }; # undef T From 6de2e4e5199d5b1f5f14c13a9f40c402ad12b477 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 25 Apr 2026 19:30:17 -0400 Subject: [PATCH 162/174] xo-gc: utest w/ DInteger --- utest/GcosTestutil.cpp | 34 +++++++++++++++++++- utest/GcosTestutil.hpp | 10 +++++- utest/MutationLogStore.test.cpp | 55 ++++++++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 3 deletions(-) diff --git a/utest/GcosTestutil.cpp b/utest/GcosTestutil.cpp index bfece569..6ba1b5d4 100644 --- a/utest/GcosTestutil.cpp +++ b/utest/GcosTestutil.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ namespace ut { using xo::scm::ListOps; using xo::scm::DList; using xo::scm::DBoolean; + using xo::scm::DInteger; using xo::mm::ACollector; using xo::mm::DMockCollector; using xo::mm::X1VerifyStats; @@ -236,6 +238,10 @@ namespace ut { 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())); + } { REQUIRE(p_gcos->install_type(impl_for())); REQUIRE(p_gcos->is_type_installed(typeseq::id())); @@ -465,6 +471,19 @@ namespace ut { xi2 = DBoolean::box(alloc2, value); } break; + case Step::Cmd::make_int: + { + int value = cmd.arg0_ix_; + + is_alloc = true; + + xi = DInteger::box(alloc, value); + alloc_z = sizeof(DInteger); + tseq = typeseq::id(); + + xi2 = DInteger::box(alloc2, value); + } + break; case Step::Cmd::assign_head: { is_alloc = false; @@ -860,8 +879,9 @@ namespace ut { { // written out polymorphic comparison - // match DBoolean.. bool match_attempted = false; + + // match DBoolean.. { auto x1p_b = obj::from(x1p_gco); auto x2_b = obj::from(x2_gco); @@ -873,6 +893,18 @@ namespace ut { } } + // match DInteger.. + { + 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); diff --git a/utest/GcosTestutil.hpp b/utest/GcosTestutil.hpp index 05eba321..a570e85a 100644 --- a/utest/GcosTestutil.hpp +++ b/utest/GcosTestutil.hpp @@ -29,6 +29,8 @@ namespace ut { make_cons, /** allocate a boolean **/ make_bool, + /** allocate an integer **/ + make_int, /** modify the head of a list x1_v[arg0_ix_]; replace with x1_v[arg1_ix_] **/ assign_head, @@ -41,7 +43,13 @@ namespace ut { bool is_command() const { return cmd_ != Cmd::sentinel; } Cmd cmd_; - /** arg0 object index (index into x1_v[]) **/ + /** arg0 object index (index into x1_v[]) + * + * when cmd_ is make_bool: + * 0 -> false, 1 -> true + * when cmd_ is make_int: + * value of integer + **/ uint32_t arg0_ix_; /** arg1 object index (index into x1_v[]) **/ uint32_t arg1_ix_; diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index c9f5a3de..2538aa3d 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -7,6 +7,7 @@ #include "MlsTestutil.hpp" #include #include +#include #include #include #include @@ -20,6 +21,7 @@ namespace ut { using xo::scm::DList; using xo::scm::DBoolean; + using xo::scm::DInteger; using xo::mm::MutationLogStore; using xo::mm::MutationLogConfig; using xo::mm::GCObjectStore; @@ -257,6 +259,49 @@ namespace ut { // ---------------------------------------------------------------- + static Step step_5[] = { + // ----- phase 0 ----- + {Cmd::make_int, 99, 0}, // [0]: 99 + {Cmd::make_nil, 0, 0}, // [1]: #nil + {Cmd::make_cons, 0, 1}, // [2]: cons([0],[1]) -> cons(99,#nil) + + // 1st gc + + // ----- phase 1 ----- + + {Cmd::make_int, 15, 0}, // [3]: 15 + {Cmd::assign_head, 2, 3}, // set-car([2],[3]) -> set-car([2],15) + + // 2nd gc. [1]..[2] promote to g1 + // [3] in g0 so [2]->[3] requires mlog entry + + // ----- phase 2 ----- + {Cmd::make_int, 24, 0}, // [4]: 33 + {Cmd::assign_head, 2, 4}, // set-car([2],[4]) -> set-car([2],33) + + // ----- phase 3 ----- + // ----- phase 4 ----- + // ----- end ----- + {Cmd::sentinel, 0, 0}, + }; + + static Phase phase_5[] = { + // + // lo hi mlog_new_z_[] + // v v v + { 0, 3, {0} }, // phase 0 gc + { 3, 5, {1} }, // phase 1 gc. set-car makes 1x xage ptr + { 5, 7, {2} }, // phase 2 gc. now src in g1, dest [3] in g0 + { 7, 7, {1} }, // phase 3 gc. new dest [4] in g0 + { 7, 7, {0} }, // phase 4 gc. now dest in g1 + { -1, -1, {0} }, + }; + + static TestSequence seq_5 { step_5, phase_5 }; + + + // ---------------------------------------------------------------- + # define seq_nil TestSequence{} # define nil nullptr # define T true @@ -286,6 +331,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, F), }; # undef T @@ -403,12 +449,19 @@ namespace ut { { REQUIRE(gcos.is_type_installed(typeseq::id()) == false); 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); + + if (tc.do_type_registration_) { + REQUIRE(gcos.is_type_installed(typeseq::id()) == true); + REQUIRE(gcos.is_type_installed(typeseq::id()) == true); + REQUIRE(gcos.is_type_installed(typeseq::id()) == true); + } + 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 **/ From e6540bd5fe17976837d599b6478351220a20fd64 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 25 Apr 2026 20:06:15 -0400 Subject: [PATCH 163/174] xo-gc: utest: support assign_root This is so we can generate garbage --- utest/GcosTestutil.cpp | 21 +++++++++++++++++++++ utest/GcosTestutil.hpp | 14 +++++++++++--- utest/MutationLogStore.test.cpp | 19 ++++++++++++++----- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/utest/GcosTestutil.cpp b/utest/GcosTestutil.cpp index 6ba1b5d4..27e09b96 100644 --- a/utest/GcosTestutil.cpp +++ b/utest/GcosTestutil.cpp @@ -484,6 +484,27 @@ namespace ut { xi2 = DInteger::box(alloc2, value); } break; + case Step::Cmd::assign_root: + { + is_alloc = false; + + auto z1 = p_x1_v->at(cmd.arg1_ix_).alloc_z_; + auto tseq1 = p_x1_v->at(cmd.arg1_ix_).tseq_; + auto rhs1 = p_x1_v->at(cmd.arg1_ix_).gco_; + + auto z2 = p_x2_v->at(cmd.arg1_ix_).alloc_z_; + auto tseq2 = p_x2_v->at(cmd.arg1_ix_).tseq_; + auto rhs2 = p_x2_v->at(cmd.arg1_ix_).gco_; + + p_x1_v->at(cmd.arg0_ix_).alloc_z_ = z1; + p_x1_v->at(cmd.arg0_ix_).tseq_ = tseq1; + p_x1_v->at(cmd.arg0_ix_).gco_ = rhs1; + + p_x2_v->at(cmd.arg0_ix_).alloc_z_ = z2; + p_x2_v->at(cmd.arg0_ix_).tseq_ = tseq2; + p_x2_v->at(cmd.arg0_ix_).gco_ = rhs2; + } + break; case Step::Cmd::assign_head: { is_alloc = false; diff --git a/utest/GcosTestutil.hpp b/utest/GcosTestutil.hpp index a570e85a..a3ffdf24 100644 --- a/utest/GcosTestutil.hpp +++ b/utest/GcosTestutil.hpp @@ -31,6 +31,8 @@ namespace ut { make_bool, /** allocate an integer **/ make_int, + /** assign a top-level value from one slot to another **/ + assign_root, /** modify the head of a list x1_v[arg0_ix_]; replace with x1_v[arg1_ix_] **/ assign_head, @@ -49,10 +51,16 @@ namespace ut { * 0 -> false, 1 -> true * when cmd_ is make_int: * value of integer + * when cmd_ is assign_root: + * index of lhs value to replace **/ - uint32_t arg0_ix_; - /** arg1 object index (index into x1_v[]) **/ - uint32_t arg1_ix_; + uint32_t arg0_ix_ = 0; + /** arg1 object index (index into x1_v[]) + * + * when cmd_ is assign_root: + * index of rhs value to assign + **/ + uint32_t arg1_ix_ = 0; }; /** a phase comprises: diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index 2538aa3d..3546ba44 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -265,22 +265,31 @@ namespace ut { {Cmd::make_nil, 0, 0}, // [1]: #nil {Cmd::make_cons, 0, 1}, // [2]: cons([0],[1]) -> cons(99,#nil) - // 1st gc + // phase 0 gc (1st gc) // ----- phase 1 ----- {Cmd::make_int, 15, 0}, // [3]: 15 {Cmd::assign_head, 2, 3}, // set-car([2],[3]) -> set-car([2],15) - // 2nd gc. [1]..[2] promote to g1 + // phase 1 gc (2nd gc) + // [1]..[2] promote to g1 // [3] in g0 so [2]->[3] requires mlog entry // ----- phase 2 ----- {Cmd::make_int, 24, 0}, // [4]: 33 {Cmd::assign_head, 2, 4}, // set-car([2],[4]) -> set-car([2],33) + // phase 2 gc (3rd gc) + // ----- phase 3 ----- + + {Cmd::assign_root, 2, 0}, // [2] = [0] = 99 + + // o.g. [2] now garbage + // ----- phase 4 ----- + // ----- end ----- {Cmd::sentinel, 0, 0}, }; @@ -292,8 +301,8 @@ namespace ut { { 0, 3, {0} }, // phase 0 gc { 3, 5, {1} }, // phase 1 gc. set-car makes 1x xage ptr { 5, 7, {2} }, // phase 2 gc. now src in g1, dest [3] in g0 - { 7, 7, {1} }, // phase 3 gc. new dest [4] in g0 - { 7, 7, {0} }, // phase 4 gc. now dest in g1 + { 7, 8, {1} }, // phase 3 gc. new dest [4] in g0 + { 8, 8, {0} }, // phase 4 gc. now dest [4] in g1 { -1, -1, {0} }, }; @@ -331,7 +340,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, F), + Testcase(2, 2, 16 * 1024, 8 * 128, T, seq_5, 128, T, c_fixed, 4, 0, 0, 0, 0, T), }; # undef T From bf7aee92036ec22ccf344f7dcec9bf003050ad8d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 25 Apr 2026 20:32:29 -0400 Subject: [PATCH 164/174] xo-gc: minor debug output --- src/gc/GCObjectStore.cpp | 3 ++- utest/MutationLogStore.test.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index f8e8c479..830fdb43 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -794,7 +794,8 @@ namespace xo { GCObjectStore::install_type(const AGCObject & meta) noexcept { scope log(XO_DEBUG(config_.debug_flag_), - xtag("tseq", meta._typeseq())); + xtag("tseq", meta._typeseq()), + xtag("tname", TypeRegistry::id2name(meta._typeseq()))); typeseq tseq = meta._typeseq(); diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index 3546ba44..40e5f25e 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -286,7 +286,8 @@ namespace ut { {Cmd::assign_root, 2, 0}, // [2] = [0] = 99 - // o.g. [2] now garbage + // phase 3 gc (4th gc) + // o.g. o.g. [2] would be garbage, if we collected g1 // ----- phase 4 ----- From a845bc17f3533c49d5a9d04f77e1e0b89f4f1f76 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Tue, 28 Apr 2026 23:17:00 -0400 Subject: [PATCH 165/174] xo-object2: obj argument to DArray::push_back() --- include/xo/gc/X1CollectorConfig.hpp | 12 ++ src/gc/GCObjectStore.cpp | 17 ++- src/gc/X1CollectorConfig.cpp | 16 +++ utest/Collector.test.cpp | 173 +++++++++++++++++++++++++++- utest/init_gc_utest.cpp | 5 +- 5 files changed, 210 insertions(+), 13 deletions(-) diff --git a/include/xo/gc/X1CollectorConfig.hpp b/include/xo/gc/X1CollectorConfig.hpp index 4b889efa..a8681c1d 100644 --- a/include/xo/gc/X1CollectorConfig.hpp +++ b/include/xo/gc/X1CollectorConfig.hpp @@ -24,8 +24,20 @@ namespace xo { **/ X1CollectorConfig with_name(std::string name); + /** copy of this config, + * but with @ref n_generation_ set to @p n_gen + **/ + X1CollectorConfig with_n_gen(uint32_t n_gen); + + /** copy of this config, + * but with @ref n_survive_ set to @p n_survive + **/ + X1CollectorConfig with_n_survive(uint32_t n_survive); + /** copy of this config, * but with @c arena_config_.size_ set to @p gen_z + * + * TODO: rename to with_halfspace_size() **/ X1CollectorConfig with_size(std::size_t gen_z); diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 830fdb43..0dc5ed38 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -305,7 +305,7 @@ namespace xo { // could use c++ vector in scratch space instead of running on // boxed types. // - DArray * stats_v = DArray::empty(mm, object_types_.size()); + DArray * stats_v = DArray::_empty(mm, object_types_.size()); if (!stats_v) return false; @@ -346,7 +346,8 @@ namespace xo { recd->upsert_cstr(mm, "n-live", DInteger::box(mm, 0)); recd->upsert_cstr(mm, "bytes", DInteger::box(mm, 0)); - stats_v->assign_at(tseq.seqno(), obj(recd)); + stats_v->assign_at(mm.try_to_facet(), + tseq.seqno(), obj(recd)); } } @@ -385,13 +386,15 @@ namespace xo { stats_v->resize(max_tseq + 1); - DArray * final_stats_v = DArray::empty(mm, n_tseq_present); + DArray * final_stats_v = DArray::_empty(mm, n_tseq_present); for (std::size_t i = 0, n = stats_v->size(); i < n; ++i) { auto recd = stats_v->at(i); if (recd) { - bool ok = final_stats_v->push_back(recd); + obj gc = mm.try_to_facet(); + + bool ok = final_stats_v->push_back(gc, recd); assert(ok); } } @@ -433,7 +436,7 @@ namespace xo { } // stats, indexed by age - DArray * stats_v = DArray::empty(mm, soft_max_age + 1); + DArray * stats_v = DArray::_empty(mm, soft_max_age + 1); if (!stats_v) return false; @@ -448,7 +451,9 @@ namespace xo { recd->upsert_cstr(mm, "n-live", DInteger::box(mm, 0)); recd->upsert_cstr(mm, "bytes", DInteger::box(mm, 0)); - stats_v->push_back(obj(recd)); + obj gc = mm.try_to_facet(); + + stats_v->push_back(gc, obj(recd)); } log && log(xtag("soft_max_age", soft_max_age), diff --git a/src/gc/X1CollectorConfig.cpp b/src/gc/X1CollectorConfig.cpp index 812e276f..c7781fbb 100644 --- a/src/gc/X1CollectorConfig.cpp +++ b/src/gc/X1CollectorConfig.cpp @@ -16,6 +16,22 @@ namespace xo { return copy; } + X1CollectorConfig + X1CollectorConfig::with_n_gen(std::uint32_t n_gen) + { + X1CollectorConfig copy = *this; + copy.n_generation_ = n_gen; + return copy; + } + + X1CollectorConfig + X1CollectorConfig::with_n_survive(std::uint32_t n_survive) + { + X1CollectorConfig copy = *this; + copy.n_survive_threshold_ = n_survive; + return copy; + } + X1CollectorConfig X1CollectorConfig::with_size(std::size_t gen_z) { diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp index 225a5ab6..94baeb58 100644 --- a/utest/Collector.test.cpp +++ b/utest/Collector.test.cpp @@ -7,12 +7,15 @@ * see xo-object2/utest **/ -#include -#include "Collector.hpp" +//#include #include "random_allocs.hpp" -#include "detail/ICollector_DX1Collector.hpp" -#include "detail/IAllocator_DX1Collector.hpp" -//#include "gc/DX1Collector.hpp" +#include +#include +#include +#include +//#include "detail/ICollector_DX1Collector.hpp" +//#include "detail/IAllocator_DX1Collector.hpp" +#include #include #include #include @@ -21,10 +24,15 @@ #include namespace xo { + using xo::scm::DList; + using xo::scm::DArray; + using xo::scm::DInteger; using xo::mm::AAllocator; using xo::mm::ACollector; + using xo::mm::AGCObject; using xo::mm::X1CollectorConfig; using xo::mm::DX1Collector; + using xo::mm::Role; using xo::mm::ArenaConfig; using xo::mm::AllocHeaderConfig; using xo::mm::Generation; @@ -263,8 +271,163 @@ namespace xo { auto rng = rng::xoshiro256ss(seed); + // these are not gc-aware objects. + // just testing ability to work as a low-level allocator REQUIRE(utest::AllocUtil::random_allocs(25, false, &rng, x1alloc)); } + + namespace { + class Testcase { + public: + Testcase(uint32_t ng, uint32_t ns, size_t gcz, uint32_t otz, bool dbg_flag) + : n_gen_{ng}, + n_survive_{ns}, + gc_halfspace_z_{gcz}, + object_type_z_{otz}, + debug_flag_{dbg_flag} + {} + + public: + /** number of generations in gco store **/ + uint32_t n_gen_ = 0; + /** promote to next gen on surviving this number of gc cycles **/ + uint32_t n_survive_ = 0; + /** size of each generations' half-space, in bytes **/ + size_t gc_halfspace_z_ = 0; + /** storage for object type array, in bytes + * one 8-byte facet pointer per type + **/ + uint32_t object_type_z_; +#ifdef NOT_YET + /** size for error output arena **/ + size_t error_size_ = 0; +#endif + /** true to enable debug output for this test case **/ + bool debug_flag_ = false; + }; + + class X1Fixture { + public: + explicit X1Fixture(uint32_t i_tc, const Testcase & tc); + +#ifdef NOT_IN_USE + DArena error_arena_; +#endif + DX1Collector gc_; + }; + + X1Fixture::X1Fixture(uint32_t i_tc, const Testcase & tc) + : gc_(X1CollectorConfig() + .with_name("collector-x1-gc-" + std::to_string(i_tc)) + .with_n_gen(tc.n_gen_) + .with_n_survive(tc.n_survive_) + .with_size(tc.gc_halfspace_z_) + .with_debug_flag(tc.debug_flag_)) + {} + +# define nil nullptr +# define T true +# define F false + + static std::vector s_testcase_v = { + /** + * debug_flag + * object_type_z | + * gc_halfspace_z | | + * n_survive | | | + * n_gen | | | | + * v v v v v + **/ + Testcase(1, 2, 16 * 1024, 128, F), + }; + +# undef T +# undef F +# undef nil + } /*namespace*/ + + // full collector test. + TEST_CASE("collector-x1-gc", "[alloc2][gc]") + { + scope log(XO_DEBUG(true), + "DX1Collector gc test"); + + //std::uint64_t seed = 7988747704879432247ul; + //random_seed(&seed); + + for (size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + // 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))); + + X1Fixture fixture(i_tc, tc); + + auto & x1 = fixture.gc_; + + REQUIRE(x1.verify_ok()); + + auto mm = x1.ref(); + auto gc = mm.to_facet(); + Generation g1{1}; + { + auto roots = DArray::_empty(mm, 1)->ref(); + REQUIRE(mm->contains_allocated(Role::to_space(), roots.data())); + + gc.add_gc_root(&roots); + { + auto x1 = DInteger::box(mm, 42); + auto x1_gco = obj(x1); + auto l1 = DList::cons(mm, x1, DList::_nil()); + +#ifdef NOT_YET + REQUIRE(roots->push_back(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]); + + gc->request_gc(g1); // 1st GC + + // x1 target got moved, og locn now relabeled from-space + REQUIRE(mm->contains(Role::from_space(), x1.data())); + REQUIRE(!mm->contains_allocated(Role::from_space(), x1.data())); + // 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->contains_allocated(Role::to_space(), roots.data())); + } + } + +#ifdef NOT_YET + /* typed collector i/face */ + auto x1gc = with_facet::mkobj(&x1state); + /* typed allocator i/face */ + auto x1alloc = with_facet::mkobj(&x1state); + + REQUIRE(x1gc.iface()); + REQUIRE(x1gc.data()); + + REQUIRE(x1alloc.iface()); + REQUIRE(x1alloc.data()); + + rng::Seed seed; + log && log("ratio: seed=", seed); + + auto rng = rng::xoshiro256ss(seed); + + // these are not gc-aware objects. + // just testing ability to work as a low-level allocator + REQUIRE(utest::AllocUtil::random_allocs(25, false, &rng, x1alloc)); +#endif + } } } diff --git a/utest/init_gc_utest.cpp b/utest/init_gc_utest.cpp index f92c1f4b..6aa39cc8 100644 --- a/utest/init_gc_utest.cpp +++ b/utest/init_gc_utest.cpp @@ -12,7 +12,7 @@ namespace xo { using xo::mm::SetupGcUtest; using xo::facet::FacetRegistry; - using xo::reflect::typeseq; + //using xo::reflect::typeseq; bool SetupGcUtest::register_facets() @@ -33,7 +33,8 @@ namespace xo { } InitEvidence - InitSubsys::require() { + InitSubsys::require() + { InitEvidence retval; /* recursive subsystem deps for xo-gc/utest */ From 3d6ad3709b28f28b8aed66162c450a7a97311aee Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 1 May 2026 19:54:26 -0400 Subject: [PATCH 166/174] refactor focusing on xo-alloc2/ xo-gc/ write-barrier ability to inform allocator of gco->gco mutation, via AAllocator i/face. --- include/xo/gc/DX1Collector.hpp | 8 ++++++++ include/xo/gc/detail/IAllocator_DX1Collector.hpp | 9 +++++++++ src/gc/DX1Collector.cpp | 15 ++++++++++++++- src/gc/GCObjectStore.cpp | 10 +++++----- src/gc/IAllocator_DX1Collector.cpp | 11 +++++++++++ utest/Collector.test.cpp | 2 -- utest/GcosTestutil.cpp | 4 ++-- 7 files changed, 49 insertions(+), 10 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index a582ed87..84e04c5c 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -334,6 +334,14 @@ namespace xo { /** discard all allocated memory **/ void clear() noexcept; + /** perform fop assignment of (rhs_iface,rhs_data) + * to (lhs_iface,lhs_data) within allocation @parent + * + create mlog entry if necessary. + **/ + void barrier_assign_aux(void * parent, + AGCObject * lhs_iface, void ** lhs_data, + AGCObject * rhs_iface, void * rhs_data); + private: /** aux init function: initialize @ref roots_ arena **/ void _init_gc_roots(const X1CollectorConfig & cfg, std::size_t page_z); diff --git a/include/xo/gc/detail/IAllocator_DX1Collector.hpp b/include/xo/gc/detail/IAllocator_DX1Collector.hpp index ca529990..f61d9fa9 100644 --- a/include/xo/gc/detail/IAllocator_DX1Collector.hpp +++ b/include/xo/gc/detail/IAllocator_DX1Collector.hpp @@ -71,6 +71,15 @@ namespace xo { /** reset to empty state; clears all generations **/ static void clear(DX1Collector & d); + /** assignment of fop (rhs_iface,rhs_data) to (lhs_iface, lhs_data) + * with write barrier (creating mlog entry if creating older->younger pointer + **/ + static void barrier_assign_aux(DX1Collector & d, + void * parent, + AGCObject * lhs_iface, + void ** lhs_data, + AGCObject * rhs_iface, + void * rhs_data); /** invoke destructor **/ static void destruct_data(DX1Collector & d); }; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 239f999e..af7adff9 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -14,7 +14,7 @@ #include #include -#include +//#include #include #include "object_age.hpp" #include @@ -691,6 +691,19 @@ namespace xo { } } } + + void + DX1Collector::barrier_assign_aux(void * parent, + AGCObject * lhs_iface, void ** lhs_data, + AGCObject * rhs_iface, void * rhs_data) + { + scope log(XO_DEBUG(config_.debug_flag_), + xtag("parent", parent), + xtag("lhs.iface", lhs_iface), xtag("&lhs.data", lhs_data), + xtag("rhs.iface", rhs_iface), xtag("rhs.data", rhs_data)); + + // see .assign_member() + } } /*namespace mm*/ } /*namespace xo*/ diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 0dc5ed38..3be25127 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -346,7 +346,7 @@ namespace xo { recd->upsert_cstr(mm, "n-live", DInteger::box(mm, 0)); recd->upsert_cstr(mm, "bytes", DInteger::box(mm, 0)); - stats_v->assign_at(mm.try_to_facet(), + stats_v->assign_at(mm, //mm.try_to_facet(), tseq.seqno(), obj(recd)); } } @@ -392,9 +392,9 @@ namespace xo { auto recd = stats_v->at(i); if (recd) { - obj gc = mm.try_to_facet(); + //obj mm = mm.try_to_facet(); - bool ok = final_stats_v->push_back(gc, recd); + bool ok = final_stats_v->push_back(mm, recd); assert(ok); } } @@ -451,9 +451,9 @@ namespace xo { recd->upsert_cstr(mm, "n-live", DInteger::box(mm, 0)); recd->upsert_cstr(mm, "bytes", DInteger::box(mm, 0)); - obj gc = mm.try_to_facet(); + //obj gc = mm.try_to_facet(); - stats_v->push_back(gc, obj(recd)); + stats_v->push_back(mm, obj(recd)); } log && log(xtag("soft_max_age", soft_max_age), diff --git a/src/gc/IAllocator_DX1Collector.cpp b/src/gc/IAllocator_DX1Collector.cpp index c0c93d6c..cf307e0f 100644 --- a/src/gc/IAllocator_DX1Collector.cpp +++ b/src/gc/IAllocator_DX1Collector.cpp @@ -133,6 +133,17 @@ namespace xo { d.clear(); } + void + IAllocator_DX1Collector::barrier_assign_aux(DX1Collector & d, + void * parent, + AGCObject * lhs_iface, + void ** lhs_data, + AGCObject * rhs_iface, + void * rhs_data) + { + d.barrier_assign_aux(parent, lhs_iface, lhs_data, rhs_iface, rhs_data); + } + void IAllocator_DX1Collector::destruct_data(DX1Collector & d) { diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp index 94baeb58..117f4ebb 100644 --- a/utest/Collector.test.cpp +++ b/utest/Collector.test.cpp @@ -13,8 +13,6 @@ #include #include #include -//#include "detail/ICollector_DX1Collector.hpp" -//#include "detail/IAllocator_DX1Collector.hpp" #include #include #include diff --git a/utest/GcosTestutil.cpp b/utest/GcosTestutil.cpp index 27e09b96..220be254 100644 --- a/utest/GcosTestutil.cpp +++ b/utest/GcosTestutil.cpp @@ -524,14 +524,14 @@ namespace ut { assert(p_mls); assert(mockgc); - lhs1->assign_head(mockgc, rhs1); + lhs1->assign_head_gc(mockgc, rhs1); // alloc2 is ord arena -> no mlog } break; } if (is_alloc) { - p_x1_v->push_back(Recd(xi, alloc_z, tseq)); + p_x1_v->push_back(Recd(xi, alloc_z, tseq)); p_x2_v->push_back(Recd(xi2, alloc_z, tseq)); } } From 8336f0a47ac1bb84c52a89af5665edab5beaff51 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 2 May 2026 13:49:29 -0400 Subject: [PATCH 167/174] xo-gc stack: refactor + streamline. Retiring unused Collector typealiases. Fix #include topology. Fix/improve write barrier setup. --- include/xo/gc/DX1Collector.hpp | 4 +- include/xo/gc/MutationLogStore.hpp | 22 ++++++---- .../xo/gc/detail/ICollector_DX1Collector.hpp | 1 + src/gc/DX1Collector.cpp | 30 +++++--------- src/gc/MutationLogStore.cpp | 40 +++++++++++++------ utest/DMockCollector.cpp | 7 +++- 6 files changed, 63 insertions(+), 41 deletions(-) diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 84e04c5c..e8b930fe 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -307,7 +307,9 @@ namespace xo { // ----- mutation ----- - /** Modify a gc-owned pointer @p *p_lhs, within allocation @p parent, + /** DEPRECATED. Prefer .barrier_assign_aux(). + * + * Modify a gc-owned pointer @p *p_lhs, within allocation @p parent, * to point to @p rhs. * * Motivation: need special handling for cross-generational pointers with diff --git a/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index 018b2869..cc389555 100644 --- a/include/xo/gc/MutationLogStore.hpp +++ b/include/xo/gc/MutationLogStore.hpp @@ -55,14 +55,22 @@ namespace xo { void verify_ok() noexcept; /** on behalf of gc-aware object store @p gc, - * change the value of a child pointer at @p p_lhs - * with parent object @p parent. p_lhs and parent must belong - * to the same allocation. + * change the value of a child pointer {@p lhs_iface, @p *lhs_data} + * with parent object @p parent, to point to {@p rhs_iface, @p rhs_data} + * p_lhs and parent must belong to the same allocation. + * + * @p lhs_iface can be nullptr, if parent holds ordinary pointer + * instead of fop (i.e. DRepr* instead of obj). + * + * @p rhs_iface must be non-null, it's load-bearing for mlog entry + * snapshot member. **/ - void assign_member(GCObjectStore * gc, - void * parent, - obj * p_lhs, - obj rhs); + void assign_member_aux(GCObjectStore * gc, + void * parent, + AGCObject * lhs_iface, + void ** lhs_data, + AGCObject * rhs_iface, + void * rhs_data); /** swap {to, from} roles **/ diff --git a/include/xo/gc/detail/ICollector_DX1Collector.hpp b/include/xo/gc/detail/ICollector_DX1Collector.hpp index d9b43a5c..285db9ff 100644 --- a/include/xo/gc/detail/ICollector_DX1Collector.hpp +++ b/include/xo/gc/detail/ICollector_DX1Collector.hpp @@ -104,6 +104,7 @@ Return false if installation fails (e.g. memory exhausted) **/ **/ static void request_gc(DX1Collector & self, Generation upto); /** Assign pointer @p p_lhs to destination @p rhs, within parent allocation @p parent +DEPRECATED. Only used in MockCollector for gc unit test Require: gc not in progress **/ static void assign_member(DX1Collector & self, void * parent, obj * p_lhs, obj & rhs); diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index af7adff9..076f3480 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -620,24 +620,11 @@ namespace xo { void DX1Collector::assign_member(void * parent, obj * p_lhs, obj rhs) { - scope log(XO_DEBUG(config_.debug_flag_), - xtag("parent", parent), xtag("lhs", p_lhs), xtag("rhs", rhs.data())); - - // ++ stats.n_mutation_; - - if (runstate_.is_running()) { - *p_lhs = rhs; - - // for removal of all doubt: - // don't log mutations during GC cycle. - // That said: should not be happening! - assert(false); - - return; - } else { - mlog_store_.assign_member(&gco_store_, parent, p_lhs, rhs); - - } + this->barrier_assign_aux(parent, + p_lhs->iface(), + p_lhs->opaque_data_addr(), + rhs.iface(), + rhs.opaque_data()); } /*assign_member*/ DX1CollectorIterator @@ -702,7 +689,12 @@ namespace xo { xtag("lhs.iface", lhs_iface), xtag("&lhs.data", lhs_data), xtag("rhs.iface", rhs_iface), xtag("rhs.data", rhs_data)); - // see .assign_member() + mlog_store_.assign_member_aux(&gco_store_, + parent, + lhs_iface, + lhs_data, + rhs_iface, + rhs_data); } } /*namespace mm*/ } /*namespace xo*/ diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index e57f40d4..82a7d782 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -139,17 +139,31 @@ namespace xo { } /*verify_ok*/ void - MutationLogStore::assign_member(GCObjectStore * gco_store, - void * parent, - obj * p_lhs, - obj rhs) + MutationLogStore::assign_member_aux(GCObjectStore * gco_store, + void * parent, + AGCObject * lhs_iface, + void ** lhs_addr, + AGCObject * rhs_iface, + void * rhs_data) { scope log(XO_DEBUG(config_.debug_flag_), - xtag("parent", parent), xtag("lhs", p_lhs), xtag("rhs", rhs.data())); + xtag("parent", parent), + xtag("lhs.iface", lhs_iface), + xtag("&lhs.data", lhs_addr), + xtag("rhs.iface", rhs_iface), + xtag("rhs.data", rhs_data)); // ++ stats.n_mutation_; - *p_lhs = rhs; + assert(parent); + assert(lhs_addr); + assert(rhs_iface); + assert(rhs_data); + + if (lhs_iface) + *lhs_iface = *rhs_iface; + + *lhs_addr = rhs_data; if (!config_.enabled_flag_) { log && log(xtag("msg", "noop b/c incremental gc disabled")); @@ -162,7 +176,7 @@ namespace xo { // 1. generation of lhs // 2. generation of rhs - Generation src_g = gco_store->generation_of(Role::to_space(), p_lhs); + Generation src_g = gco_store->generation_of(Role::to_space(), lhs_addr); if (src_g.is_sentinel()) { log && log(xtag("msg", "noop because src not gc-owned")); @@ -172,7 +186,7 @@ namespace xo { return; } - Generation dest_g = gco_store->generation_of(Role::to_space(), rhs.data()); + Generation dest_g = gco_store->generation_of(Role::to_space(), rhs_data); if (dest_g.is_sentinel()) { log && log(xtag("msg", "noop because dest not gc-owned")); @@ -197,7 +211,7 @@ namespace xo { const DArena * arena = gco_store->get_space(Role::to_space(), src_g); const DArena::header_type * src_hdr = arena->obj2hdr(parent); - const DArena::header_type * dest_hdr = arena->obj2hdr(rhs.data()); + const DArena::header_type * dest_hdr = arena->obj2hdr(rhs_data); assert(src_hdr && dest_hdr); @@ -222,16 +236,16 @@ namespace xo { // control here: we have an older->younger pointer, need to log it - void ** lhs_addr = reinterpret_cast(&(p_lhs->data_)); + obj snap(rhs_iface, rhs_data); - this->_append_mutation(dest_g, parent, lhs_addr, rhs); + this->_append_mutation(dest_g, parent, lhs_addr, snap); } void MutationLogStore::_append_mutation(Generation dest_g, void * parent, void ** addr, - obj rhs) + obj snap) { // mlog keyed by generation in which pointer _destination_ resides: // collection that moves destination generation around needs to also @@ -239,7 +253,7 @@ namespace xo { // MutationLog * mlog = this->mlog_[Role::to_space()][dest_g]; - mlog->push_back(MutationLogEntry(parent, addr, rhs)); + mlog->push_back(MutationLogEntry(parent, addr, snap)); } void diff --git a/utest/DMockCollector.cpp b/utest/DMockCollector.cpp index eb180685..a4573f2d 100644 --- a/utest/DMockCollector.cpp +++ b/utest/DMockCollector.cpp @@ -91,7 +91,12 @@ namespace xo { void DMockCollector::assign_member(void * parent, obj * p_lhs, obj & rhs) { - mls_->assign_member(gcos_, parent, p_lhs, rhs); + mls_->assign_member_aux(gcos_, + parent, + p_lhs->iface(), + p_lhs->opaque_data_addr(), + rhs.iface(), + rhs.opaque_data()); } void * From 1a88975567858ca4d633028efaa1e6164854136d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 2 May 2026 13:58:22 -0400 Subject: [PATCH 168/174] tidy: drop stale ACollector comments --- src/gc/GCObjectStore.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index 3be25127..fb29873e 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -346,8 +346,8 @@ namespace xo { recd->upsert_cstr(mm, "n-live", DInteger::box(mm, 0)); recd->upsert_cstr(mm, "bytes", DInteger::box(mm, 0)); - stats_v->assign_at(mm, //mm.try_to_facet(), - tseq.seqno(), obj(recd)); + stats_v->assign_at(mm, tseq.seqno(), + obj(recd)); } } @@ -392,8 +392,6 @@ namespace xo { auto recd = stats_v->at(i); if (recd) { - //obj mm = mm.try_to_facet(); - bool ok = final_stats_v->push_back(mm, recd); assert(ok); } @@ -451,8 +449,6 @@ namespace xo { recd->upsert_cstr(mm, "n-live", DInteger::box(mm, 0)); recd->upsert_cstr(mm, "bytes", DInteger::box(mm, 0)); - //obj gc = mm.try_to_facet(); - stats_v->push_back(mm, obj(recd)); } From 50d87d33717bab3eebc9bc76d85a4de18aba1d35 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 7 May 2026 23:44:32 -0400 Subject: [PATCH 169/174] xo-gc stack: fix mutation setup + xo-reader2 utest --- include/xo/gc/DX1Collector.hpp | 23 ++++++++ src/gc/DX1Collector.cpp | 9 ++- src/gc/GCObjectStore.cpp | 11 +++- src/gc/MutationLogStore.cpp | 19 ++++++- utest/CMakeLists.txt | 1 + utest/Collector.test.cpp | 86 ++++++++++++++++++++++++---- utest/GCObjectConversion.test.cpp | 95 +++++++++++++++++++++++++++++++ utest/MutationLogStore.test.cpp | 2 +- utest/X1Collector.test.cpp | 1 - 9 files changed, 224 insertions(+), 23 deletions(-) create mode 100644 utest/GCObjectConversion.test.cpp diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index e8b930fe..349fa51c 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -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_; }; diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 076f3480..7ea97982 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -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]; diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index fb29873e..c8eac5d6 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -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()), diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 82a7d782..0719423a 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -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). 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)); diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 4f555807..401371f6 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -9,6 +9,7 @@ set(UTEST_SRCS DX1CollectorIterator.test.cpp MutationLogStore.test.cpp GCObjectStore.test.cpp + GCObjectConversion.test.cpp Object2.test.cpp DMockCollector.cpp diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp index 117f4ebb..ceb277b4 100644 --- a/utest/Collector.test.cpp +++ b/utest/Collector.test.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -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(&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(); auto gc = mm.to_facet(); - Generation g1{1}; + + REQUIRE(mm._typeseq() == typeseq::id()); + REQUIRE(gc._typeseq() == typeseq::id()); + + 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(); REQUIRE(mm->contains_allocated(Role::to_space(), roots.data())); @@ -383,12 +414,32 @@ namespace xo { auto x1_gco = obj(x1); auto l1 = DList::cons(mm, x1, DList::_nil()); -#ifdef NOT_YET - REQUIRE(roots->push_back(l1)); + REQUIRE(l1._typeseq() == typeseq::id()); + 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()); + + // 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)); + { + 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())); + } } diff --git a/utest/GCObjectConversion.test.cpp b/utest/GCObjectConversion.test.cpp new file mode 100644 index 00000000..1b094070 --- /dev/null +++ b/utest/GCObjectConversion.test.cpp @@ -0,0 +1,95 @@ +/** @file GCObjectConversion.test.cpp + * + * @author Roland Conybeare, May 2026 + **/ + +#include "GCObjectConversion.hpp" +#include +#include +#include +#include +#include +#include + +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(&arena); + auto v1 = DArray::empty(mm, 3); + + REQUIRE(v1); + REQUIRE(v1->size() == 0); + + { + obj v1_seq + = GCObjectConversion>::from_gco(mm /*not used*/, v1); + + REQUIRE(v1_seq); + REQUIRE(v1_seq == v1); + REQUIRE(v1_seq->size() == 0); + } + + { + obj l1_seq + = GCObjectConversion>::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(&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>::from_gco(mm /*not used*/, l1); + + REQUIRE(l1_seq); + } + + { + obj v1_seq + = GCObjectConversion>::from_gco(mm /*not used*/, l1); + + REQUIRE(!v1_seq); + } + } + + } /*namespace ut*/ +} /*namespace xo*/ + +/* end GCObjectConversion.cpp */ diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index 40e5f25e..f37ea67b 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -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 diff --git a/utest/X1Collector.test.cpp b/utest/X1Collector.test.cpp index 3bd4d4ee..421c4639 100644 --- a/utest/X1Collector.test.cpp +++ b/utest/X1Collector.test.cpp @@ -13,7 +13,6 @@ #include #include #include -//#include "list/IGCObject_DList.hpp" #include //#include From d0496a32450b69ab3c5622c742e0e4384210c4a0 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Fri, 8 May 2026 08:19:31 -0400 Subject: [PATCH 170/174] xo-alloc2: tidy for ++ utest coverage --- src/gc/DX1Collector.cpp | 3 ++- utest/Collector.test.cpp | 2 ++ utest/DX1CollectorIterator.test.cpp | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 7ea97982..3eec1a71 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -607,7 +607,8 @@ namespace xo { auto DX1Collector::alloc_copy(value_type src) noexcept -> value_type { - return with_facet::mkobj(this->new_space()).alloc_copy(src); + return this->new_space()->alloc_copy(src); + //return with_facet::mkobj(this->new_space()).alloc_copy(src); } bool diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp index ceb277b4..9141b576 100644 --- a/utest/Collector.test.cpp +++ b/utest/Collector.test.cpp @@ -29,6 +29,7 @@ namespace xo { using xo::mm::CollectorTypeRegistry; using xo::mm::AAllocator; using xo::mm::ACollector; + using xo::mm::ICollector_Any; using xo::mm::AGCObject; using xo::mm::X1CollectorConfig; using xo::mm::DX1Collector; @@ -59,6 +60,7 @@ namespace xo { REQUIRE(!gc1); REQUIRE(gc1.iface() != nullptr); REQUIRE(gc1.data() == nullptr); + REQUIRE(gc1._typeseq() == typeseq::id()); } TEST_CASE("DX1Collector-1", "[alloc2][gc][DX1Collector]") diff --git a/utest/DX1CollectorIterator.test.cpp b/utest/DX1CollectorIterator.test.cpp index 4d3ca6c2..e61c6a99 100644 --- a/utest/DX1CollectorIterator.test.cpp +++ b/utest/DX1CollectorIterator.test.cpp @@ -179,6 +179,8 @@ namespace xo { obj ix = range.begin(); obj end_ix = range.end(); + REQUIRE(ix._typeseq() == typeseq::id()); + REQUIRE(end_ix._typeseq() == typeseq::id()); REQUIRE(ix.iface()); REQUIRE(ix.data()); REQUIRE(end_ix.iface()); From 08df9a56355c35351de615046c6bad4c793e08b9 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 10 May 2026 18:19:41 -0400 Subject: [PATCH 171/174] xo-alloc2 xo-gc: assorted utest-guided cleanup ++ coverage --- include/xo/gc/X1CollectorIterator.hpp | 11 +++++++ src/gc/setup_gc.cpp | 3 ++ utest/Collector.test.cpp | 44 +++++++++++++++++++-------- utest/DX1CollectorIterator.test.cpp | 4 +-- utest/random_allocs.cpp | 7 +++-- utest/random_allocs.hpp | 4 +++ 6 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 include/xo/gc/X1CollectorIterator.hpp diff --git a/include/xo/gc/X1CollectorIterator.hpp b/include/xo/gc/X1CollectorIterator.hpp new file mode 100644 index 00000000..07a97dfe --- /dev/null +++ b/include/xo/gc/X1CollectorIterator.hpp @@ -0,0 +1,11 @@ +/** @file X1CollectorIterator.hpp + * + * @author Roland Conybeare, May 2026 + **/ + +#pragma once + +#include "DX1CollectorIterator.hpp" +#include "detail/IAllocIterator_DX1CollectorIterator.hpp" + +/* end X1CollectorIterator.hpp */ diff --git a/src/gc/setup_gc.cpp b/src/gc/setup_gc.cpp index 23beed55..7927390b 100644 --- a/src/gc/setup_gc.cpp +++ b/src/gc/setup_gc.cpp @@ -5,6 +5,7 @@ #include "setup_gc.hpp" #include "X1Collector.hpp" +#include "X1CollectorIterator.hpp" #include "GCObjectStoreVisitor.hpp" #include #include @@ -25,10 +26,12 @@ namespace xo { FacetRegistry::register_impl(); FacetRegistry::register_impl(); + FacetRegistry::register_impl(); FacetRegistry::register_impl(); log && log(xtag("DX1Collector.tseq", typeseq::id())); + log && log(xtag("DX1CollectorIterator.tseq", typeseq::id())); log && log(xtag("DGCObjectStoreVisitor.tseq", typeseq::id())); log && log(xtag("ACollector.tseq", typeseq::id())); diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp index 9141b576..eff2eda1 100644 --- a/utest/Collector.test.cpp +++ b/utest/Collector.test.cpp @@ -39,6 +39,7 @@ namespace xo { using xo::mm::AllocHeader; using xo::mm::Generation; using xo::mm::c_max_generation; + using xo::facet::DVariantPlaceholder; using xo::facet::with_facet; using xo::reflect::typeseq; using xo::scope; @@ -60,7 +61,7 @@ namespace xo { REQUIRE(!gc1); REQUIRE(gc1.iface() != nullptr); REQUIRE(gc1.data() == nullptr); - REQUIRE(gc1._typeseq() == typeseq::id()); + REQUIRE(gc1._typeseq() == typeseq::id()); } TEST_CASE("DX1Collector-1", "[alloc2][gc][DX1Collector]") @@ -187,6 +188,15 @@ namespace xo { { scope log(XO_DEBUG(false), "DX1Collector alloc test"); + constexpr uint32_t c_n_alloc = 25; + constexpr uint32_t c_reserved_z = 4*1024*1024; + constexpr uint32_t c_max_alloc = c_reserved_z / c_n_alloc; + // allowance for per-alloc overhead + constexpr uint32_t c_max_alloc_payload = c_max_alloc - 32; + + static_assert(c_max_alloc > 0); + static_assert(c_max_alloc_payload < c_max_alloc); + ArenaConfig arena_cfg = { .name_ = "_test_unused", .size_ = 4*1024*1024, .store_header_flag_ = true, @@ -226,22 +236,31 @@ namespace xo { auto rng = rng::xoshiro256ss(seed); bool catch_flag = false; - REQUIRE(utest::AllocUtil::random_allocs(25, catch_flag, &rng, x1alloc)); + REQUIRE(utest::AllocUtil::random_allocs(c_n_alloc, c_max_alloc_payload, + catch_flag, &rng, x1alloc)); } TEST_CASE("collector-x1-alloc2", "[alloc2][gc]") { - scope log(XO_DEBUG(false), + constexpr bool c_debug_flag = true; + scope log(XO_DEBUG(c_debug_flag), "DX1Collector alloc test2"); - ArenaConfig arena_cfg = { .name_ = "_test_unused", - .size_ = 4*1024*1024, - .store_header_flag_ = true, - .header_ = AllocHeaderConfig(8 /*guard_z*/, - 0xfd /*guard-byte*/, - 0 /*tseq-bits*/, - 0 /*age-bits*/, - 16 /*size-bits*/), + constexpr uint32_t c_n_alloc = 25; + constexpr uint32_t c_reserved_z = 4*1024*1024; + constexpr uint32_t c_max_alloc = c_reserved_z / c_n_alloc; + // allowance for per-alloc overhead + constexpr uint32_t c_max_alloc_payload = c_max_alloc - 32; + + ArenaConfig arena_cfg = { + .name_ = "_test_unused", + .size_ = c_reserved_z, + .store_header_flag_ = true, + .header_ = AllocHeaderConfig(8 /*guard_z*/, + 0xfd /*guard-byte*/, + 0 /*tseq-bits*/, + 0 /*age-bits*/, + 16 /*size-bits*/), }; /* collector with one generation collapses to a non-generational copying collector */ @@ -277,7 +296,8 @@ namespace xo { // these are not gc-aware objects. // just testing ability to work as a low-level allocator - REQUIRE(utest::AllocUtil::random_allocs(25, false, &rng, x1alloc)); + REQUIRE(utest::AllocUtil::random_allocs(c_n_alloc, c_max_alloc_payload, + c_debug_flag, &rng, x1alloc)); } namespace { diff --git a/utest/DX1CollectorIterator.test.cpp b/utest/DX1CollectorIterator.test.cpp index e61c6a99..53d11ce0 100644 --- a/utest/DX1CollectorIterator.test.cpp +++ b/utest/DX1CollectorIterator.test.cpp @@ -179,8 +179,8 @@ namespace xo { obj ix = range.begin(); obj end_ix = range.end(); - REQUIRE(ix._typeseq() == typeseq::id()); - REQUIRE(end_ix._typeseq() == typeseq::id()); + REQUIRE(ix._typeseq() == typeseq::id()); + REQUIRE(end_ix._typeseq() == typeseq::id()); REQUIRE(ix.iface()); REQUIRE(ix.data()); REQUIRE(end_ix.iface()); diff --git a/utest/random_allocs.cpp b/utest/random_allocs.cpp index 2325140d..ff0e663e 100644 --- a/utest/random_allocs.cpp +++ b/utest/random_allocs.cpp @@ -39,6 +39,7 @@ namespace utest { bool AllocUtil::random_allocs(uint32_t n_alloc, + uint32_t max_alloc_z, bool catch_flag, xoshiro256ss * p_rgen, obj mm) @@ -64,6 +65,8 @@ namespace utest { double si = ngen(*p_rgen); double zi = ::pow(2.0, si); std::size_t z = ::ceil(zi); + if (z > max_alloc_z) + z = max_alloc_z; bool ok_flag = true; @@ -132,7 +135,7 @@ namespace utest { for (const byte * p = info.guard_lo().first; p != info.guard_lo().second; ++p) { - REQUIRE_ORFAIL(ok_flag, catch_flag, (char)*p == info.guard_byte()); + REQUIRE_ORFAIL(ok_flag, catch_flag, (uint8_t)*p == info.guard_byte()); } REQUIRE_ORFAIL(ok_flag, catch_flag, @@ -146,7 +149,7 @@ namespace utest { for (const byte * p = info.guard_hi().first; p != info.guard_hi().second; ++p) { - REQUIRE_ORFAIL(ok_flag, catch_flag, (char)*p == info.guard_byte()); + REQUIRE_ORFAIL(ok_flag, catch_flag, (uint8_t)*p == info.guard_byte()); } diff --git a/utest/random_allocs.hpp b/utest/random_allocs.hpp index ab0afc4e..6428a3f0 100644 --- a/utest/random_allocs.hpp +++ b/utest/random_allocs.hpp @@ -34,8 +34,12 @@ namespace utest { /** generate a random sequence of allocations. * verify allocator behavior + * + * Will not alloc more than n_alloc * max_alloc_z bytes + * (not counting allocator overhead) **/ static bool random_allocs(std::uint32_t n_alloc, + std::uint32_t max_alloc_z, bool catch_flag, xo::rng::xoshiro256ss * p_rgen, xo::facet::obj alloc); From e1e48612afd6c2e789dcd2bb8df035192c16cf20 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Mon, 11 May 2026 09:27:24 -0400 Subject: [PATCH 172/174] xo-gc stack: coverage improvement + related tidying --- .../xo/gc/detail/IAllocator_DX1Collector.hpp | 2 + src/gc/IAllocator_DX1Collector.cpp | 2 + utest/CMakeLists.txt | 1 + utest/Collector.test.cpp | 22 ++++ utest/MockCollector.test.cpp | 119 ++++++++++++++++++ utest/X1Collector.test.cpp | 12 +- 6 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 utest/MockCollector.test.cpp diff --git a/include/xo/gc/detail/IAllocator_DX1Collector.hpp b/include/xo/gc/detail/IAllocator_DX1Collector.hpp index f61d9fa9..9fd90bf9 100644 --- a/include/xo/gc/detail/IAllocator_DX1Collector.hpp +++ b/include/xo/gc/detail/IAllocator_DX1Collector.hpp @@ -80,8 +80,10 @@ namespace xo { void ** lhs_data, AGCObject * rhs_iface, void * rhs_data); +#ifdef OBSOLETE /** invoke destructor **/ static void destruct_data(DX1Collector & d); +#endif }; } /*namespace mm*/ diff --git a/src/gc/IAllocator_DX1Collector.cpp b/src/gc/IAllocator_DX1Collector.cpp index cf307e0f..7cc6add5 100644 --- a/src/gc/IAllocator_DX1Collector.cpp +++ b/src/gc/IAllocator_DX1Collector.cpp @@ -144,11 +144,13 @@ namespace xo { d.barrier_assign_aux(parent, lhs_iface, lhs_data, rhs_iface, rhs_data); } +#ifdef OBSOLETE void IAllocator_DX1Collector::destruct_data(DX1Collector & d) { d.~DX1Collector(); } +#endif } /*namespace mm*/ } /*namespace xo*/ diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 401371f6..f226762e 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -14,6 +14,7 @@ set(UTEST_SRCS DMockCollector.cpp ICollector_DMockCollector.cpp + MockCollector.test.cpp MlsTestutil.cpp GcosTestutil.cpp diff --git a/utest/Collector.test.cpp b/utest/Collector.test.cpp index eff2eda1..26d1416e 100644 --- a/utest/Collector.test.cpp +++ b/utest/Collector.test.cpp @@ -58,6 +58,7 @@ namespace xo { /* empty variant collector */ obj gc1; + REQUIRE(!gc1._has_null_vptr()); REQUIRE(!gc1); REQUIRE(gc1.iface() != nullptr); REQUIRE(gc1.data() == nullptr); @@ -150,6 +151,12 @@ namespace xo { /* typed collector -- repr known at compile time */ obj x1(&gc); + REQUIRE(!x1._has_null_vptr()); + REQUIRE(x1.iface()); + REQUIRE(x1.data()); + + x1._drop(); + REQUIRE(x1.iface()); REQUIRE(x1.data()); } @@ -182,6 +189,11 @@ namespace xo { REQUIRE(x1.iface()); REQUIRE(x1.data()); + + x1._drop(); + + REQUIRE(x1.iface()); + REQUIRE(x1.data()); } TEST_CASE("collector-x1-alloc", "[alloc2][gc]") @@ -238,6 +250,11 @@ namespace xo { bool catch_flag = false; REQUIRE(utest::AllocUtil::random_allocs(c_n_alloc, c_max_alloc_payload, catch_flag, &rng, x1alloc)); + + x1gc._drop(); + + REQUIRE(x1gc.iface()); + REQUIRE(x1gc.data()); } TEST_CASE("collector-x1-alloc2", "[alloc2][gc]") @@ -298,6 +315,11 @@ namespace xo { // just testing ability to work as a low-level allocator REQUIRE(utest::AllocUtil::random_allocs(c_n_alloc, c_max_alloc_payload, c_debug_flag, &rng, x1alloc)); + + x1gc._drop(); + + REQUIRE(x1gc.iface()); + REQUIRE(x1gc.data()); } namespace { diff --git a/utest/MockCollector.test.cpp b/utest/MockCollector.test.cpp new file mode 100644 index 00000000..9092d2b1 --- /dev/null +++ b/utest/MockCollector.test.cpp @@ -0,0 +1,119 @@ +/** @file MockCollector.test.cpp + * + * @author Roland Conybeare, May 2026 + **/ + +#include "MockCollector.hpp" +#include +#include +#include +#include +#include + +namespace ut { + using xo::scm::DInteger; + using xo::mm::ACollector; + using xo::mm::DMockCollector; + using xo::mm::MutationLogConfig; + using xo::mm::MutationLogStore; + using xo::mm::GCObjectStoreConfig; + using xo::mm::GCObjectStore; + using xo::mm::AGCObject; + using xo::mm::Generation; + using xo::mm::Role; + using xo::mm::X1VerifyStats; + using xo::mm::AAllocator; + using xo::mm::ArenaConfig; + using xo::mm::DArena; + using xo::facet::obj; + + // Gilding the lily here. + // The only reason to 'test' MockCollector is to suppress + // false positives on coverage reports. + // Don't care about MockCollector itself, but it also shows up + // in ICollector_Xfer, at least on the osx/clang toolchain + + TEST_CASE("MockCollector-1", "[MockCollector]") + { + // need to create a {gcos, mls} pair + + constexpr uint32_t c_space_z = 64*1024; + constexpr uint32_t c_n_gen = 1; + constexpr uint32_t c_n_survive = 0; + X1VerifyStats verify_stats; + GCObjectStoreConfig gcos_config{ArenaConfig() + .with_name("gcos-arena-name-notused") + .with_size(c_space_z) + .with_store_header_flag(true), + c_n_gen, + c_n_survive, + 64*1024 /*object_type_z*/, + false /*debug_flag*/}; + DArena report_arena{ArenaConfig().with_name("report-arena") + .with_size(64*1024) + .with_store_header_flag(true)}; + DArena error_arena{ArenaConfig().with_name("error-arena") + .with_size(64*1024) + .with_store_header_flag(true)}; + MutationLogConfig mls_config{c_n_gen, + 1024 /*mlog_z*/, + false /*mlog_enabled_flag*/, + false /*debug_flag*/}; + + GCObjectStore gcos{gcos_config, &verify_stats}; + MutationLogStore mls{mls_config, &gcos}; + DMockCollector mock(&mls, &gcos); + + auto mockgc = obj(&mock); + auto report_mm = obj(&report_arena); + auto error_mm = obj(&error_arena); + + REQUIRE(mockgc.reserved(Generation::g0(), Role::to_space()) == c_space_z); + + { + int dummy; + + REQUIRE(mockgc.locate_address(&dummy) == -1); + REQUIRE(mockgc.contains(Role::to_space(), &dummy) == false); + } + + { + obj rpt; + { + // stub + REQUIRE(mockgc.report_statistics(report_mm, error_mm, &rpt) == false); + REQUIRE(report_mm.allocated() == 0); + } + { + mockgc.report_object_types(report_mm, error_mm, &rpt); + REQUIRE(rpt); + rpt.reset(); + report_mm.clear(); + } + { + mockgc.report_object_ages(report_mm, error_mm, &rpt); + REQUIRE(rpt); + rpt.reset(); + report_mm.clear(); + } + } + + { + auto g0_from = gcos.from_space(Generation::g0()); + REQUIRE(g0_from); + auto g0_from_mm = obj(g0_from); + + // note: we don't need object types in gcos for this test, + // since we're not traversing the object graph + + auto x = DInteger::box(g0_from_mm, 42); + REQUIRE(x); + + auto x_copy = mockgc.alloc_copy((std::byte*)x.data()); + REQUIRE(x_copy); + } + } + +} /*namespace ut*/ + +/* end MockCollector.test.cpp */ diff --git a/utest/X1Collector.test.cpp b/utest/X1Collector.test.cpp index 421c4639..00999be6 100644 --- a/utest/X1Collector.test.cpp +++ b/utest/X1Collector.test.cpp @@ -119,7 +119,8 @@ namespace ut { DX1Collector gc(cfg); - CollectorTypeRegistry::instance().install_types(obj(&gc)); + CollectorTypeRegistry::instance() + .install_types(obj(&gc)); DArena * to_0 = nullptr; @@ -219,6 +220,9 @@ namespace ut { + sizeof(AllocHeader) + sizeof(DInteger) + sizeof(AllocHeader) + sizeof(DList))); + auto to0_commit_z = to_0->committed(); + auto to0_reserved_z = to_0->reserved(); + { { REQUIRE(x0_o.iface() != nullptr); @@ -285,6 +289,12 @@ namespace ut { REQUIRE((void*)l0_o.data()->head_.data() == (void*)x0_o.data()); REQUIRE((void*)l0_o.data()->rest_ == (void*)DList::_nil()); + Generation g0 = Generation::g0(); + REQUIRE(c_o.committed(g0, Role::to_space()) == to0_commit_z); + REQUIRE(c_o.reserved(g0, Role::to_space()) == to0_reserved_z); + REQUIRE(c_o.locate_address(x0_o.data()) >= 0); + REQUIRE(c_o.contains(Role::to_space(), x0_o.data())); + } catch (std::exception & ex) { std::cerr << "caught exception: " << ex.what() << std::endl; REQUIRE(false); From f1f951e1d24e3c36ac5c7b4b45977562f48c3380 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Wed, 13 May 2026 08:14:58 -0400 Subject: [PATCH 173/174] xo-gc: address some coverage loose ends --- utest/GCObjectStore.test.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/utest/GCObjectStore.test.cpp b/utest/GCObjectStore.test.cpp index 6c7ed42b..3c82f922 100644 --- a/utest/GCObjectStore.test.cpp +++ b/utest/GCObjectStore.test.cpp @@ -5,6 +5,7 @@ #include "GcosTestutil.hpp" #include +#include #include #include #include @@ -32,6 +33,8 @@ namespace ut { using xo::mm::X1VerifyStats; using xo::mm::AGCObject; using xo::mm::AGCObjectVisitor; + using xo::mm::AGCObjectVisitor; + using xo::mm::DGCObjectStoreVisitor; using xo::mm::Generation; using xo::mm::Role; using xo::mm::object_age; @@ -381,6 +384,18 @@ namespace ut { report_gco.reset(); fixture.report_mm()->clear(); } + + // operate visitor (loose ends revealed by coverage). + // mostly tested by moving objects + { + DGCObjectStoreVisitor visitor(&gcos, Generation::g0()); + auto visitor_fop = obj(&visitor); + + REQUIRE(!visitor_fop.iface()->_has_null_vptr()); + REQUIRE(visitor_fop._typeseq() == typeseq::id()); + + visitor_fop._drop(); + } } } /* loop over test cases */ } /* TEST_CASE(GCObjectStore-1) */ From b1898230a7bb805ac2afe1f3a910300b664da958 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sun, 17 May 2026 12:30:09 -0400 Subject: [PATCH 174/174] xo-gc stack: many small utest improvements. --- include/xo/gc/DX1Collector.hpp | 3 +- include/xo/gc/GCObjectStore.hpp | 3 + include/xo/gc/MutationLogStore.hpp | 3 + include/xo/gc/ObjectTypeSlot.hpp | 4 +- src/gc/DX1Collector.cpp | 14 +- src/gc/GCObjectStore.cpp | 15 ++ src/gc/MutationLogStore.cpp | 14 ++ utest/CMakeLists.txt | 1 + utest/DX1CollectorIterator.test.cpp | 21 +++ utest/MutationLogStore.test.cpp | 29 ++++ utest/ObjectAge.test.cpp | 34 ++++ utest/X1Collector.test.cpp | 261 ++++++++++++++++++++++++---- 12 files changed, 358 insertions(+), 44 deletions(-) create mode 100644 utest/ObjectAge.test.cpp diff --git a/include/xo/gc/DX1Collector.hpp b/include/xo/gc/DX1Collector.hpp index 349fa51c..a6eb86ad 100644 --- a/include/xo/gc/DX1Collector.hpp +++ b/include/xo/gc/DX1Collector.hpp @@ -47,6 +47,7 @@ namespace xo { Generation gc_upto() const { return gc_upto_; } + bool is_idle() const { return mode_ == Mode::idle; } bool is_running() const { return mode_ == Mode::gc; } bool is_verify() const { return mode_ == Mode::verify; } @@ -62,7 +63,7 @@ namespace xo { struct GCStatistics { public: GCStatistics() = default; - explicit GCStatistics(uint32_t n_gc) : n_gc_{n_gc} {}; + //explicit GCStatistics(uint32_t n_gc) : n_gc_{n_gc} {}; uint32_t n_gc() const noexcept { return n_gc_; } diff --git a/include/xo/gc/GCObjectStore.hpp b/include/xo/gc/GCObjectStore.hpp index f816e05b..e13773ae 100644 --- a/include/xo/gc/GCObjectStore.hpp +++ b/include/xo/gc/GCObjectStore.hpp @@ -195,6 +195,9 @@ namespace xo { void cleanup_phase(Generation upto, bool sanitize_flag); + /** Revert to empty state **/ + void clear(); + private: /** configure @ref object_types_, using @p page_z **/ diff --git a/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index cc389555..a62d522d 100644 --- a/include/xo/gc/MutationLogStore.hpp +++ b/include/xo/gc/MutationLogStore.hpp @@ -84,6 +84,9 @@ namespace xo { void forward_mutation_log(obj gc, Generation upto); + /** Reset mutation log store to empty state **/ + void clear(); + private: /** aux init function: create mutation log **/ MutationLog _make_mlog(uint32_t igen, char tag_char, diff --git a/include/xo/gc/ObjectTypeSlot.hpp b/include/xo/gc/ObjectTypeSlot.hpp index 214d31c2..c2b7162a 100644 --- a/include/xo/gc/ObjectTypeSlot.hpp +++ b/include/xo/gc/ObjectTypeSlot.hpp @@ -20,9 +20,7 @@ namespace xo { **/ struct ObjectTypeSlot { ObjectTypeSlot() {} - explicit ObjectTypeSlot(AGCObject * iface) { - this->store_iface(iface); - } + //explicit ObjectTypeSlot(AGCObject * iface) { this->store_iface(iface); } /** true iff this slot is empty **/ bool is_null() const noexcept { diff --git a/src/gc/DX1Collector.cpp b/src/gc/DX1Collector.cpp index 3eec1a71..643187ed 100644 --- a/src/gc/DX1Collector.cpp +++ b/src/gc/DX1Collector.cpp @@ -322,7 +322,7 @@ namespace xo { obj error_mm, obj * p_output) const noexcept { - return gco_store_.report_object_types(mm, error_mm, p_output); + return gco_store_.report_object_ages(mm, error_mm, p_output); } size_type @@ -672,15 +672,9 @@ namespace xo { void DX1Collector::clear() noexcept { - for (Role ri : Role::all()) { - for (Generation gj{0}; gj < config_.n_generation_; ++gj) { - DArena * arena = this->get_space(ri, gj); - - assert(arena); - - arena->clear(); - } - } + mlog_store_.clear(); + gco_store_.clear(); + root_set_.clear(); } void diff --git a/src/gc/GCObjectStore.cpp b/src/gc/GCObjectStore.cpp index c8eac5d6..b6f0b6e8 100644 --- a/src/gc/GCObjectStore.cpp +++ b/src/gc/GCObjectStore.cpp @@ -1182,6 +1182,21 @@ namespace xo { } while (fixup_work > 0); } /*_forward_children_until_fixpoint*/ + void + GCObjectStore::clear() + { + object_types_.clear(); + + for (Role ri : Role::all()) { + for (Generation gj{0}; gj < config_.n_generation_; ++gj) { + DArena * arena = this->get_space(ri, gj); + + assert(arena); + + arena->clear(); + } + } + } } /*namespace mm*/ } /*namespace xo*/ diff --git a/src/gc/MutationLogStore.cpp b/src/gc/MutationLogStore.cpp index 0719423a..7c1a8d78 100644 --- a/src/gc/MutationLogStore.cpp +++ b/src/gc/MutationLogStore.cpp @@ -560,6 +560,20 @@ namespace xo { return counters; } + void + MutationLogStore::clear() + { + // parallels .init_mlogs(), see also + + for (uint32_t igen = 0, ngen = config_.n_generation_; igen + 1 < ngen; ++igen) { + if (igen + 1 < c_max_generation) { + for (std::uint32_t mlog_role = 0; mlog_role < c_n_role + 1; ++mlog_role) { + this->mlog_storage_[mlog_role][igen].clear(); + } + } + } + } + } /*namespace mm*/ } /*namespace xo*/ diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index f226762e..dee2b67f 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -11,6 +11,7 @@ set(UTEST_SRCS GCObjectStore.test.cpp GCObjectConversion.test.cpp Object2.test.cpp + ObjectAge.test.cpp DMockCollector.cpp ICollector_DMockCollector.cpp diff --git a/utest/DX1CollectorIterator.test.cpp b/utest/DX1CollectorIterator.test.cpp index 53d11ce0..a9df7eb0 100644 --- a/utest/DX1CollectorIterator.test.cpp +++ b/utest/DX1CollectorIterator.test.cpp @@ -25,6 +25,7 @@ namespace xo { using xo::mm::DArena; using xo::mm::DArenaIterator; using xo::mm::X1CollectorConfig; + using xo::mm::Generation; using xo::mm::ArenaConfig; using xo::mm::AllocHeaderConfig; using xo::mm::cmpresult; @@ -34,11 +35,16 @@ namespace xo { using std::byte; namespace ut { + TEST_CASE("DX1CollectorIterator-0", "[alloc2][gc][DX1Collector]") + { + } + TEST_CASE("IAllocIterator_Xfer_DX1CollectorIterator", "[alloc2]") { /* verify IAllocIterator_Xfer is constructible + satisfies concept checks */ IAllocIterator_Xfer xfer; REQUIRE(IAllocIterator_Xfer::_valid); + } TEST_CASE("DX1CollectorIterator-1", "[alloc2][gc][DX1Collector]") @@ -128,10 +134,25 @@ namespace xo { auto ix = gc.begin(); auto end_ix = gc.end(); + REQUIRE(ix != DX1CollectorIterator::invalid()); + REQUIRE(end_ix != DX1CollectorIterator::invalid()); + REQUIRE(ix.is_valid()); REQUIRE(end_ix.is_valid()); REQUIRE(ix != end_ix); + REQUIRE(ix.gen_ix() == Generation::g0()); + REQUIRE(ix.gen_ix() < gc.config_.n_generation_); + REQUIRE(ix.gen_hi() == gc.config_.n_generation_); + REQUIRE(ix.arena_ix() == gc.to_space(Generation::g0())->begin()); + REQUIRE(ix.arena_hi() == gc.to_space(Generation::g0())->end()); + + { + // we have one alloc, so can visit it + auto info = *ix; + REQUIRE(info.is_valid()); + } + /* verify obj 'fat pointer' packaging */ auto ix_vt = with_facet::mkobj(&ix); auto end_ix_vt = with_facet::mkobj(&end_ix); diff --git a/utest/MutationLogStore.test.cpp b/utest/MutationLogStore.test.cpp index f37ea67b..956282db 100644 --- a/utest/MutationLogStore.test.cpp +++ b/utest/MutationLogStore.test.cpp @@ -24,9 +24,13 @@ namespace ut { using xo::scm::DInteger; using xo::mm::MutationLogStore; using xo::mm::MutationLogConfig; + using xo::mm::MutationLog; + using xo::mm::MutationLogEntry; using xo::mm::GCObjectStore; using xo::mm::GCObjectStoreConfig; using xo::mm::DGCObjectStoreVisitor; + using xo::mm::Role; + using xo::mm::Generation; using xo::mm::DArena; using xo::mm::ArenaConfig; using xo::mm::X1VerifyStats; @@ -434,6 +438,24 @@ namespace ut { // fixture.mls_.init_mlogs(getpagesize()); + { + MutationLog * to_0_mlog + = fixture.mls_.get_mlog(Role::to_space(), + Generation::g0()); + MutationLog * from_0_mlog + = fixture.mls_.get_mlog(Role::from_space(), + Generation::g0()); + MutationLog * triage_0_mlog + = fixture.mls_.triage_mlog(Generation::g0()); + + REQUIRE(to_0_mlog); + REQUIRE(from_0_mlog); + REQUIRE(triage_0_mlog); + REQUIRE(to_0_mlog != from_0_mlog); + REQUIRE(to_0_mlog != triage_0_mlog); + REQUIRE(from_0_mlog != triage_0_mlog); + } + { // updates counters in fixture.verify_stats_ fixture.gcos_.verify_ok(); @@ -454,6 +476,13 @@ namespace ut { GCObjectStore & gcos = fixture.gcos_; MutationLogStore & mls = fixture.mls_; + { + MutationLogEntry mentry; + + REQUIRE(mentry.parent() == nullptr); + REQUIRE(mentry.p_data() == nullptr); + } + { // gcos setup. parallels GCObjectStore.test.cpp { diff --git a/utest/ObjectAge.test.cpp b/utest/ObjectAge.test.cpp new file mode 100644 index 00000000..a9772121 --- /dev/null +++ b/utest/ObjectAge.test.cpp @@ -0,0 +1,34 @@ +/** @file ObjectAge.test.cpp + * + * @author Roland Conybeare, May 2026 + **/ + +#include "object_age.hpp" +#include + +namespace xo { + using xo::mm::object_age; + + namespace ut { + + TEST_CASE("ObjectAge-1", "[ObjectAge]") + { + REQUIRE(object_age{0} != object_age{1}); + REQUIRE(object_age{0} < object_age{1}); + REQUIRE(object_age{1} > object_age{0}); + + { + bool x = (object_age{0} > object_age{1}); + REQUIRE(x == false); + } + + { + bool x = (object_age{1} < object_age{0}); + REQUIRE(x == false); + } + } + + } /*namespace ut*/ +} /*namespace xo*/ + +/* end ObjectAge.test.cpp */ diff --git a/utest/X1Collector.test.cpp b/utest/X1Collector.test.cpp index 00999be6..481d1195 100644 --- a/utest/X1Collector.test.cpp +++ b/utest/X1Collector.test.cpp @@ -15,6 +15,7 @@ #include #include +#include //#include #include @@ -43,9 +44,14 @@ namespace ut { using xo::mm::AGCObject; using xo::mm::X1CollectorConfig; using xo::mm::DX1Collector; + using xo::mm::GCRoot; + using xo::mm::GCObjectStore; + using xo::mm::GCStatistics; using xo::mm::DArena; using xo::mm::ArenaConfig; using xo::mm::Generation; + using xo::mm::c_n_role; + using xo::mm::object_age; using xo::mm::Role; using xo::mm::padding; using xo::facet::obj; @@ -59,17 +65,14 @@ namespace ut { namespace { struct testcase_x1 { - testcase_x1(std::size_t nz, - std::size_t tz, + testcase_x1(std::size_t gz, std::size_t n_gct, std::size_t t_gct) - : nursery_z_{nz}, - tenured_z_{tz}, + : generation_z_{gz}, incr_gc_threshold_{n_gct}, full_gc_threshold_{t_gct} {} - std::size_t nursery_z_; - std::size_t tenured_z_; + std::size_t generation_z_; std::size_t incr_gc_threshold_; std::size_t full_gc_threshold_; }; @@ -79,13 +82,50 @@ namespace ut { // n_gct: nursery gc threshold // t_gct: tenured gc threshold // - // nz tz n_gct t_gct - testcase_x1(4096, 8192, 1024, 1024) + // gz n_gct t_gct + testcase_x1(8192, 1024, 1024) }; } static InitEvidence s_init = (InitSubsys::require()); + TEST_CASE("x1-config", "[gc][x1]") + { + // real purpose: ensure s_init survives static linking + REQUIRE(s_init.evidence()); + + Subsystem::initialize_all(); + + constexpr bool c_debug_flag = false; + scope log(XO_DEBUG(c_debug_flag), "x1-config test"); + + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + scope log(XO_DEBUG(false), xtag("i_tc", i_tc)); + + const testcase_x1 & tc = s_testcase_v[i_tc]; + + X1CollectorConfig cfg{ .name_ = "x1_test", + .arena_config_ = ArenaConfig{ + .size_ = tc.generation_z_, + .store_header_flag_ = true}, + .object_types_z_ = 16384, + .gc_trigger_v_{{ + tc.incr_gc_threshold_, + tc.full_gc_threshold_}}, + .debug_flag_ = c_debug_flag, + }; + + REQUIRE(cfg.n_generation_ == 2); + REQUIRE(cfg.n_survive_threshold_ == 2); + REQUIRE(cfg.age2gen(object_age{0}) == Generation::g0()); + REQUIRE(cfg.age2gen(object_age{1}) == Generation::g0()); + REQUIRE(cfg.age2gen(object_age{2}) == Generation::g1()); + REQUIRE(cfg.age2gen(object_age{99}) == Generation::g1()); + + REQUIRE(cfg.promotion_threshold(Generation::g1()) == cfg.n_survive_threshold_); + } + } + TEST_CASE("x1", "[gc][x1]") { // real purpose: ensure s_init survives static linking @@ -108,17 +148,44 @@ namespace ut { X1CollectorConfig cfg{ .name_ = "x1_test", .arena_config_ = ArenaConfig{ - .size_ = tc.tenured_z_, + .size_ = tc.generation_z_, .store_header_flag_ = true}, .object_types_z_ = 16384, .gc_trigger_v_{{ tc.incr_gc_threshold_, tc.full_gc_threshold_}}, - .debug_flag_ = c_debug_flag, - }; + .sanitize_flag_ = true, + .debug_flag_ = c_debug_flag }; DX1Collector gc(cfg); + { + // X1Collector never uses the null ctor here. + // but it relies on DArenaVector, + // which requires it for DArenaVector::resize() + // + GCRoot null_root; + + REQUIRE(null_root.root() == nullptr); + } + + // secondary allocator for reporting + DArena report_arena(ArenaConfig() + .with_name("x1_test_report_arena") + .with_size(64 * 1024)); + auto report_mm = obj(&report_arena); + + DArena error_arena(ArenaConfig() + .with_name("x1_test_error_arena") + .with_size(16 * 1024)); + auto error_mm = obj(&error_arena); + + const GCObjectStore * p_gco = nullptr; + { + const DX1Collector & c_gc = gc; + p_gco = &(c_gc.gco_store()); + } + CollectorTypeRegistry::instance() .install_types(obj(&gc)); @@ -126,7 +193,9 @@ namespace ut { /* verify configuration */ { - REQUIRE(cfg.n_generation_ == 2); + REQUIRE(gc.config().arena_config_.size_ == tc.generation_z_); + REQUIRE(gc.config().arena_config_.store_header_flag_ == true); + REQUIRE(gc.config().n_generation_ == 2); } /* verify initial collector state */ @@ -148,8 +217,8 @@ namespace ut { const DArena * from_0 = gc.get_space(Role::from_space(), Generation{0}); REQUIRE(from_0 != nullptr); - REQUIRE(from_0->reserved() >= tc.tenured_z_); - REQUIRE(from_0->reserved() < tc.tenured_z_ + from_0->page_z_); + REQUIRE(from_0->reserved() >= tc.generation_z_); + REQUIRE(from_0->reserved() < tc.generation_z_ + from_0->page_z_); REQUIRE(from_0->reserved() % from_0->page_z_ == 0); REQUIRE(from_0->allocated() == 0); @@ -203,18 +272,30 @@ namespace ut { ok = c_o.is_type_installed(typeseq::id()); REQUIRE(ok); + REQUIRE(gc_o.name() == cfg.name_); + // nothing committed yet (?) + REQUIRE(gc_o.size() == cfg.object_types_z_); + // no-op + REQUIRE(gc_o.expand(0)); + REQUIRE(gc_o.size() == cfg.object_types_z_); + + // x0_o will be added as gc root. x0_o_orig will not auto x0_o = DFloat::box(gc_o, 3.1415927); + auto x0_o_orig = x0_o; c_o.add_gc_root(&x0_o); REQUIRE(to_0->allocated() == sizeof(AllocHeader) + sizeof(DFloat)); + // n1_o will be added as gc root. n1_o_orig will not auto n1_o = DInteger::box(gc_o, 42); + auto n1_o_orig = n1_o; c_o.add_gc_root(&n1_o); + REQUIRE(to_0->allocated() == (sizeof(AllocHeader) + sizeof(DFloat) + sizeof(AllocHeader) + sizeof(DInteger))); - //DList * l0 = DList::list(gc_o, x0_o); - //auto l0_o = with_facet::mkobj(l0); + // l0_o will be added as gc root. l0_o_orig will not auto l0_o = ListOps::list(gc_o, x0_o); + auto l0_o_orig = l0_o; c_o.add_gc_root(&l0_o); REQUIRE(to_0->allocated() == (sizeof(AllocHeader) + sizeof(DFloat) + sizeof(AllocHeader) + sizeof(DInteger) @@ -231,11 +312,23 @@ namespace ut { /* check alloc info for newly-allocated object */ AllocInfo info = gc.alloc_info((std::byte *)x0_o.data()); + auto float_tseq = typeseq::id(); + auto x0_alloc_z = gc.header2size(info.header()); REQUIRE(info.age() == 0); - REQUIRE(info.tseq() == typeseq::id().seqno()); + REQUIRE(info.tseq() == float_tseq.seqno()); REQUIRE(info.size() >= sizeof(DFloat)); REQUIRE(info.size() < sizeof(DFloat) + padding::c_alloc_alignment); + + REQUIRE(sizeof(DFloat) <= x0_alloc_z); + REQUIRE(x0_alloc_z <= sizeof(DFloat) + sizeof(AllocHeader)); + REQUIRE(0 == gc.header2age(info.header())); + + REQUIRE(float_tseq.seqno() == gc.header2tseq(info.header())); + REQUIRE(false == gc.is_forwarding_header(info.header())); + + REQUIRE(gc.lookup_type(float_tseq)); + REQUIRE(gc.lookup_type(float_tseq)->_typeseq() == float_tseq); } { @@ -245,11 +338,23 @@ namespace ut { /* check alloc info for newly-allocated object */ AllocInfo info = gc.alloc_info((std::byte *)n1_o.data()); + auto integer_tseq = typeseq::id(); + auto n1_alloc_z = gc.header2size(info.header()); REQUIRE(info.age() == 0); - REQUIRE(info.tseq() == typeseq::id().seqno()); + REQUIRE(info.tseq() == integer_tseq.seqno()); REQUIRE(info.size() >= sizeof(DInteger)); REQUIRE(info.size() < sizeof(DInteger) + padding::c_alloc_alignment); + + REQUIRE(sizeof(DInteger) <= n1_alloc_z); + REQUIRE(n1_alloc_z <= sizeof(DInteger) + sizeof(AllocHeader)); + REQUIRE(0 == gc.header2age(info.header())); + + REQUIRE(integer_tseq.seqno() == gc.header2tseq(info.header())); + REQUIRE(false == gc.is_forwarding_header(info.header())); + + REQUIRE(gc.lookup_type(integer_tseq)); + REQUIRE(gc.lookup_type(integer_tseq)->_typeseq() == integer_tseq); } { @@ -259,14 +364,32 @@ namespace ut { /* check alloc info for newly-allocated object */ AllocInfo info = gc.alloc_info((std::byte *)l0_o.data()); + auto list_tseq = typeseq::id(); + auto l0_alloc_z = gc.header2size(info.header()); REQUIRE(info.age() == 0); - REQUIRE(info.tseq() == typeseq::id().seqno()); + REQUIRE(info.tseq() == list_tseq.seqno()); REQUIRE(info.size() >= sizeof(DList)); REQUIRE(info.size() < sizeof(DList) + padding::c_alloc_alignment); + + REQUIRE(sizeof(DList) <= l0_alloc_z); + REQUIRE(l0_alloc_z <= sizeof(DList) + sizeof(AllocHeader)); + REQUIRE(0 == gc.header2age(info.header())); + + REQUIRE(list_tseq.seqno() == gc.header2tseq(info.header())); + REQUIRE(false == gc.is_forwarding_header(info.header())); + + REQUIRE(gc.lookup_type(list_tseq)); + REQUIRE(gc.lookup_type(list_tseq)->_typeseq() == list_tseq); } } + { + GCStatistics stats = gc.gc_stats(); + + REQUIRE(stats.n_gc() == 0); + } + /* no GC roots, so GC is trivial */ c_o.request_gc(Generation{1}); @@ -274,20 +397,54 @@ namespace ut { log && log(xtag("l0_o.data()->head_.data()", l0_o.data()->head_.data())); log && log(xtag("x0_o.data()", x0_o.data())); - REQUIRE(!gc.contains(Role::from_space(), x0_o.data())); - REQUIRE(gc.contains(Role::to_space(), x0_o.data())); - REQUIRE(x0_o.data()->value() == 3.1415927); + // gcobjectstore is stable + REQUIRE(&gc.gco_store() == p_gco); - REQUIRE(!gc.contains(Role::from_space(), n1_o.data())); - REQUIRE(gc.contains(Role::to_space(), n1_o.data())); - REQUIRE(n1_o.data()->value() == 42); + REQUIRE(gc.runstate().gc_upto() == Generation::sentinel()); + REQUIRE(gc.runstate().is_idle()); + REQUIRE(!gc.runstate().is_running()); + REQUIRE(!gc.runstate().is_verify()); - REQUIRE(!gc.contains(Role::from_space(), l0_o.data())); - REQUIRE(gc.contains(Role::to_space(), l0_o.data())); - REQUIRE(l0_o.data()->is_empty() == false); + { + REQUIRE(!gc.contains(Role::from_space(), x0_o.data())); + REQUIRE(gc.contains(Role::to_space(), x0_o.data())); + REQUIRE(x0_o.data()->value() == 3.1415927); - REQUIRE((void*)l0_o.data()->head_.data() == (void*)x0_o.data()); - REQUIRE((void*)l0_o.data()->rest_ == (void*)DList::_nil()); + // former location of x0_o now in from-space + REQUIRE(gc.contains(Role::from_space(), x0_o_orig.data())); + REQUIRE(gc.locate_address(x0_o_orig.data()) == -1); + } + + { + REQUIRE(!gc.contains(Role::from_space(), n1_o.data())); + REQUIRE(gc.contains(Role::to_space(), n1_o.data())); + REQUIRE(n1_o.data()->value() == 42); + + REQUIRE(gc.contains(Role::from_space(), n1_o_orig.data())); + REQUIRE(gc.locate_address(n1_o_orig.data()) == -1); + } + + { + REQUIRE(!gc.contains(Role::from_space(), l0_o.data())); + REQUIRE(gc.contains(Role::to_space(), l0_o.data())); + REQUIRE(l0_o.data()->is_empty() == false); + + REQUIRE(gc.contains(Role::from_space(), l0_o_orig.data())); + REQUIRE(gc.locate_address(l0_o_orig.data()) == -1); + + REQUIRE((void*)l0_o.data()->head_.data() == (void*)x0_o.data()); + REQUIRE((void*)l0_o.data()->rest_ == (void*)DList::_nil()); + } + + { + // verify a non-gc-owned address + int x = 999; + + REQUIRE(-1 == gc.locate_address(&x)); + } + + REQUIRE(gc.size_total() == gc.committed()); + REQUIRE(gc.mutation_log_entries() == 0); Generation g0 = Generation::g0(); REQUIRE(c_o.committed(g0, Role::to_space()) == to0_commit_z); @@ -295,6 +452,50 @@ namespace ut { REQUIRE(c_o.locate_address(x0_o.data()) >= 0); REQUIRE(c_o.contains(Role::to_space(), x0_o.data())); + GCStatistics stats = gc.gc_stats(); + REQUIRE(stats.n_gc() == 1); + + { + obj report; + bool ok = c_o.report_statistics(report_mm, error_mm, &report); + REQUIRE(ok); + REQUIRE(report); + + // TODO: print report, verify output + + report_mm.clear(); + error_mm.clear(); + } + + { + obj report; + bool ok = c_o.report_object_ages(report_mm, error_mm, &report); + REQUIRE(ok); + REQUIRE(report); + + // TODO: print report, verify output + + report_mm.clear(); + error_mm.clear(); + } + + { + obj report; + bool ok = c_o.report_object_types(report_mm, error_mm, &report); + REQUIRE(ok); + REQUIRE(report); + + // TODO: print report, verify output + + report_mm.clear(); + error_mm.clear(); + } + + gc_o.clear(); + { + REQUIRE(gc_o.allocated() == 0); + } + } catch (std::exception & ex) { std::cerr << "caught exception: " << ex.what() << std::endl; REQUIRE(false);