/* @file GC.test.cpp * * author: Roland Conybeare, Aug 2025 */ #include "xo/alloc/GC.hpp" #include "xo/object/List.hpp" #include "xo/object/Integer.hpp" #include "xo/randomgen/random_seed.hpp" #include "xo/randomgen/xoshiro256.hpp" #include "xo/indentlog/scope.hpp" #include "xo/indentlog/print/tag.hpp" #include #include namespace xo { using xo::obj::List; using xo::obj::Integer; using xo::gc::GC; using xo::gc::generation_result; using xo::gc::generation; using xo::rng::Seed; using xo::rng::xoshiro256ss; namespace ut { // Also see GC unit tests in xo-alloc/utest #ifdef NOT_YET namespace { struct testcase_mlog { testcase_mlog(std::size_t nz, std::size_t tz) : nursery_z_{nz}, tenured_z_{tz} {} std::size_t nursery_z_; std::size_t tenured_z_; }; } #endif TEST_CASE("gc-mlog-1", "[alloc][gc][gc_mutation]") { up gc = GC::make( { .initial_nursery_z_ = 2024, .initial_tenured_z_ = 4048, .incr_gc_threshold_ = 512, .full_gc_threshold_ = 1024, .debug_flag_ = false }); gc->disable_gc(); REQUIRE(gc->native_gc_statistics().n_mutation_ == 0); REQUIRE(gc->native_gc_statistics().n_logged_mutation_ == 0); REQUIRE(gc.get()); /* use gc for all Object allocs */ Object::mm = gc.get(); gp l = List::list(Integer::make(gc.get(), 1)); gc->add_gc_root(reinterpret_cast(l.ptr_address())); gp l2 = List::list(Integer::make(gc.get(), 10)); gc->add_gc_root(reinterpret_cast(l2.ptr_address())); { REQUIRE(l->size() == 1); REQUIRE(l2->size() == 1); REQUIRE(gc->tospace_generation_of(l.ptr()) == generation_result::nursery); REQUIRE(gc->tospace_generation_of(l->head().ptr()) == generation_result::nursery); REQUIRE(gc->is_before_checkpoint(l.ptr()) == false); REQUIRE(gc->is_before_checkpoint(l->head().ptr()) == false); REQUIRE(gc->tospace_generation_of(l2.ptr()) == generation_result::nursery); REQUIRE(gc->tospace_generation_of(l2->head().ptr()) == generation_result::nursery); REQUIRE(gc->is_before_checkpoint(l2.ptr()) == false); REQUIRE(gc->is_before_checkpoint(l2->head().ptr()) == false); REQUIRE(gc->mlog_size() == 0); } // mutation, but not {xgen, xckp} since parent,child both in N0 l->assign_head(Integer::make(gc.get(), 2)); { REQUIRE(gc->native_gc_statistics().n_mutation_ == 1); REQUIRE(gc->native_gc_statistics().n_logged_mutation_ == 0); REQUIRE(gc->native_gc_statistics().n_xgen_mutation_ == 0); REQUIRE(gc->native_gc_statistics().n_xckp_mutation_ == 0); REQUIRE(gc->mlog_size() == 0); REQUIRE(gc->is_gc_enabled() == false); } gc->request_gc(generation::nursery); gc->enable_gc_once(); { REQUIRE(gc->is_before_checkpoint(l.ptr()) == true); REQUIRE(gc->is_before_checkpoint(l->head().ptr()) == true); REQUIRE(gc->tospace_generation_of(l.ptr()) == generation_result::nursery); REQUIRE(l->size() == 1); REQUIRE(Integer::from(l->head()).ptr()); REQUIRE(Integer::from(l->head())->value() == 2); } // mutation, xckp since parent in N1, child in N0 l->assign_head(Integer::make(gc.get(), 3)); { REQUIRE(Integer::from(l->head())->value() == 3); REQUIRE(gc->tospace_generation_of(l->head().ptr()) == generation_result::nursery); REQUIRE(gc->is_before_checkpoint(l->head().ptr()) == false); REQUIRE(gc->native_gc_statistics().n_mutation_ == 2); REQUIRE(gc->native_gc_statistics().n_logged_mutation_ == 1); REQUIRE(gc->native_gc_statistics().n_xgen_mutation_ == 0); REQUIRE(gc->native_gc_statistics().n_xckp_mutation_ == 1); REQUIRE(gc->mlog_size() == 1); } // gc promotes parent, still need mutation log for xgen ptr gc->request_gc(generation::nursery); gc->enable_gc_once(); { REQUIRE(l->size() == 1); REQUIRE(Integer::from(l->head()).ptr()); REQUIRE(Integer::from(l->head())->value() == 3); REQUIRE(gc->tospace_generation_of(l.ptr()) == generation_result::tenured); REQUIRE(gc->tospace_generation_of(l->head().ptr()) == generation_result::nursery); REQUIRE(gc->is_before_checkpoint(l->head().ptr())); REQUIRE(gc->native_gc_statistics().n_mutation_ == 2); REQUIRE(gc->native_gc_statistics().n_logged_mutation_ == 1); // counters recorded when mutation created. // not modified by gc REQUIRE(gc->native_gc_statistics().n_xgen_mutation_ == 0); REQUIRE(gc->native_gc_statistics().n_xckp_mutation_ == 1); REQUIRE(gc->mlog_size() == 1); } // gc promotes child, no longer need mutation log entry gc->request_gc(generation::nursery); gc->enable_gc_once(); { REQUIRE(l->size() == 1); REQUIRE(Integer::from(l->head()).ptr()); REQUIRE(Integer::from(l->head())->value() == 3); REQUIRE(gc->tospace_generation_of(l.ptr()) == generation_result::tenured); REQUIRE(gc->tospace_generation_of(l->head().ptr()) == generation_result::tenured); REQUIRE(gc->mlog_size() == 0); } } namespace { enum class object_type { nil, integer, cons, }; struct ObjectModel { /* 1:1 with address */ std::size_t index_; /* nil|integer|cons */ object_type type_; /* value for model of Integer::value_, if type_ is object_type::integer */ std::size_t int_value_; /* index# for model of List::head_, if type_ is object_type::list */ std::size_t head_ix_; /* index# for model of List::rest_, if type_ is object_type::list */ std::size_t rest_ix_; }; struct ObjectGraphModel { /** * @param from_graph * @param from_ix * @param to_graph * @param to_ix * @param p_visited_set **/ static bool verify_equal_aux(ObjectGraphModel & from_graph, std::size_t from_ix, ObjectGraphModel & to_graph, std::size_t to_ix, std::unordered_set * p_visited_set); /** compare models for structural equivalence; will be comparing before/after a garbage collection cycle * @param from_graph model before GC * @param to_graph model after GC * @return true iff models are equivalent **/ static bool verify_equal_models(ObjectGraphModel & from_model, ObjectGraphModel & to_model); /* build model for object graph from a vector of object pointers */ void from_root_vector(const std::vector> & object_v); /* include everything reachable from @p x in this object model */ std::size_t traverse_from_object(gp x); /* one node per xo::Object instance. */ std::vector nodes_; /* map from root index to node index number */ std::vector roots_; /* map from (original) address to index number */ std::unordered_map addr2node_map_; }; bool ObjectGraphModel::verify_equal_aux(ObjectGraphModel & from_graph, std::size_t from_ix, ObjectGraphModel & to_graph, std::size_t to_ix, std::unordered_set * p_visited_set) { if (p_visited_set->contains(from_ix)) return true; const ObjectModel & from = from_graph.nodes_.at(from_ix); const ObjectModel & to = from_graph.nodes_.at(to_ix); REQUIRE(from.type_ == to.type_); p_visited_set->insert(from.index_); if (from.type_ == object_type::nil) return true; if (from.type_ == object_type::integer) { REQUIRE(from.int_value_ == to.int_value_); return true; } if (from.type_ == object_type::cons) { return (verify_equal_aux(from_graph, from.head_ix_, to_graph, to.head_ix_, p_visited_set) && verify_equal_aux(from_graph, from.rest_ix_, to_graph, to.rest_ix_, p_visited_set)); } return false; } bool ObjectGraphModel::verify_equal_models(ObjectGraphModel & from_model, ObjectGraphModel & to_model) { REQUIRE(from_model.roots_.size() == to_model.roots_.size()); REQUIRE(from_model.nodes_.size() == to_model.nodes_.size()); std::unordered_set visited_set; for (std::size_t i = 0, n = from_model.roots_.size(); i < n; ++i) { INFO(tostr(xtag("i", i), xtag("n", n))); REQUIRE(verify_equal_aux(from_model, from_model.roots_.at(i), to_model, to_model.roots_.at(i), &visited_set)); } return true; } std::size_t ObjectGraphModel::traverse_from_object(gp x) { std::uintptr_t x_addr = reinterpret_cast(x.ptr()); auto addr2node_ix = addr2node_map_.find(x_addr); if (addr2node_ix != addr2node_map_.end()) { /* already imported (or import on call stack) */ return addr2node_ix->second; } else { ObjectModel new_model; auto x_int = Integer::from(x); auto x_list = List::from(x); std::size_t new_index = this->nodes_.size(); { if (x_int.is_null() && x_list.is_null()) throw std::runtime_error(tostr("expecting object graph containing int|cons|nil only", xtag("x", x))); if (!x_int.is_null()) { new_model.index_ = new_index; new_model.type_ = object_type::integer; new_model.int_value_ = x_int->value(); new_model.head_ix_ = 0; new_model.rest_ix_ = 0; } if (!x_list.is_null()) { if (x_list->is_nil()) { new_model.index_ = 0; new_model.type_ = object_type::nil; new_model.int_value_ = 0; new_model.head_ix_ = 0; new_model.rest_ix_ = 0; } else { new_model.index_ = new_index; new_model.type_ = object_type::cons; new_model.int_value_ = 0; /* fill below */ new_model.head_ix_ = 0; new_model.rest_ix_ = 0; } } } this->nodes_.push_back(new_model); this->addr2node_map_[x_addr] = new_index; if (!x_list.is_null() && !(x_list->is_nil())) { ObjectModel & model = this->nodes_.at(new_index); model.head_ix_ = traverse_from_object(x_list->head()); model.rest_ix_ = traverse_from_object(x_list->rest()); } return new_index; } } void ObjectGraphModel::from_root_vector(const std::vector> & root_v) { assert(nodes_.empty()); assert(addr2node_map_.empty()); /* sentinel = List::nil */ { ObjectModel sentinel; sentinel.index_ = 0; sentinel.type_ = object_type::nil; sentinel.int_value_ = 0; sentinel.head_ix_ = 0; sentinel.rest_ix_ = 0; this->nodes_.push_back(sentinel); } /* it's possible that object_v is complete. * seed model by importing all the nodes in object_v[] */ for (gp x : root_v) this->roots_.push_back(traverse_from_object(x)); } /** Generate some random data + mutations to verify GC behavior * * To setup for first GC: * RandomMutationModel model(m, n, r, k); * model.generate_seed_values(); * model.generate_random_roots(gc, &rgen); * model.generate_random_mutations(&rgen); * * To prepare for next GC * model.rejuvenate_seed_values(); * model.alter_random_roots(&rgen); * model.generate_random_mutations(&rgen); **/ struct RandomMutationModel { RandomMutationModel(std::size_t m, std::size_t n, std::size_t r, std::size_t rr, std::size_t k) : m_{m}, n_{n}, r_{r}, rr_{rr}, k_{k} {} void generate_seed_values(GC * gc); void generate_random_roots(GC * gc, xoshiro256ss * p_rgen); void generate_random_mutations(xoshiro256ss * p_rgen); void rejuvenate_seed_values(GC * gc); void alter_random_roots(xoshiro256ss * p_rgen); /* create m random list cells */ size_t m_ = 0; /** create n random integers, starting with value @ref start_ **/ size_t start_ = 0; size_t n_ = 0; /* #of roots */ size_t r_ = 0; size_t rr_ = 0; /* #of random mutations */ size_t k_ = 0; /* w1[] contains some random list cells */ std::vector> w1_; /* w2[] has all of w1[], also contains some integers */ std::vector> w2_; /* create some random roots. always pick at least one list cell */ std::vector> root_v_; }; void RandomMutationModel::generate_seed_values(GC * gc) { w1_.clear(); w2_.clear(); { for (size_t i = 0; i < m_; ++i) { w1_.push_back(List::cons(List::nil, List::nil)); } REQUIRE(w1_.size() == m_); } { std::copy(w1_.begin(), w1_.end(), std::back_inserter(w2_)); for (size_t j = 0; j < n_; ++j) { w2_.push_back(Integer::make(gc, (this->start_)++)); } REQUIRE(w2_.size() == m_ + n_); } } void RandomMutationModel::generate_random_roots(GC * gc, xoshiro256ss * p_rgen) { if (n_ > 0) { std::size_t w1_ix = (*p_rgen)() % n_; if (r_ > 0) root_v_.push_back(w2_.at(w1_ix)); } for (std::size_t i = 1; i < r_; ++i) { std::size_t w2_ix = (*p_rgen)() % (m_ + n_); root_v_.push_back(w2_.at(w2_ix)); } REQUIRE(root_v_.size() == r_); for (auto & root : root_v_) gc->add_gc_root(root.ptr_address()); } void RandomMutationModel::generate_random_mutations(xoshiro256ss * p_rgen) { for (std::size_t i = 0; i < k_; ++i) { /* pick a root list cell at random */ gp l1 = w1_.at((*p_rgen)() % w1_.size()); REQUIRE(l1.ptr()); if ((*p_rgen)() % 2 == 0) { /* pick another root list cell at random, and link it to l1 */ gp l2 = w1_.at((*p_rgen)() % w1_.size()); REQUIRE(l2.ptr()); l1->assign_rest(l2); } else { /* pick a value at random (could be list or integer), * assign to head */ gp x2 = w2_.at((*p_rgen)() % w2_.size()); REQUIRE(x2.ptr()); l1->assign_head(x2); } } } void RandomMutationModel::rejuvenate_seed_values(GC * gc) { for (std::size_t i = 0; i < w1_.size(); ++i) { INFO(xtag("i", i)); if (w1_.at(i)->_is_forwarded()) { /* w[i] survived GC */ w1_[i] = dynamic_cast(w1_[i]->_destination()); } else { /* w[i] is garbage, replace */ w1_[i] = List::cons(List::nil, List::nil); } REQUIRE(w1_[i].ptr()); } for (std::size_t j = 0; j < w2_.size(); ++j) { INFO(xtag("j", j)); if (w2_.at(j)->_is_forwarded()) { /* w2[i] survived GC */ w2_[j] = w2_[j]->_destination(); REQUIRE(w2_[j].ptr()); } else { /* w2[j] is garbage, replace */ w2_[j] = Integer::make(gc, (this->start_)++); REQUIRE(w2_[j].ptr()); } } } void RandomMutationModel::alter_random_roots(xoshiro256ss * p_rgen) { /* replace a root value rr times */ for (std::size_t i = 0; i < rr_; ++i) { /* choose new root value at random */ gp new_root; { std::size_t j = (*p_rgen)() % (w1_.size() + w2_.size()); if (j < w1_.size()) new_root = w1_.at(j); else new_root = w2_.at(j - w1_.size()); } /* choose a root to replace at random */ std::size_t j = (*p_rgen)() % root_v_.size(); root_v_[j] = new_root; } } struct testcase_stresstest { testcase_stresstest(std::size_t nz, std::size_t tz, std::size_t ngct, std::size_t tgct, std::size_t m, std::size_t n, std::size_t r, std::size_t rr, std::size_t k, bool gc_stats_flag, bool debug_flag) : nursery_z_{nz}, tenured_z_{tz}, nursery_gc_threshold_{ngct}, tenured_gc_threshold_{tgct}, m_{m}, n_{n}, r_{r}, rr_{rr}, k_{k}, gc_stats_flag_{gc_stats_flag}, debug_flag_{debug_flag} {} std::size_t nursery_z_; std::size_t tenured_z_; std::size_t nursery_gc_threshold_; std::size_t tenured_gc_threshold_; /* #of random list cells to create */ std::size_t m_; /* #of random integers to create */ std::size_t n_; /* #of gc roots to create */ std::size_t r_; /* #of gc roots to replace between cycles */ std::size_t rr_; /* #of random mutations */ std::size_t k_; bool gc_stats_flag_ = false; bool debug_flag_ = false; }; std::vector s_testcase_v = { // segfault with /* nz: nursery size * tz: tenured size * ngct: nursery gc threshold * tgct: tenured gc threshold * m: #of random list cells to create * n: #of random integers to create * r: #of gc roots to create * rr: #of gc roots to replace between iterations * k: #of random mutations to apply * * nz tz ngct tgct m n r rr k stats, debug */ testcase_stresstest(1024, 2048, 256, 1024, 2, 0, 0, 0, 0, false, false), testcase_stresstest(1024, 2048, 256, 1024, 2, 1, 5, 0, 0, false, false), testcase_stresstest(1024, 2048, 256, 1024, 5, 2, 5, 2, 10, false, false), testcase_stresstest(1024, 2048, 256, 1024, 10, 10, 5, 2, 10, false, false) }; } /*namespace*/ TEST_CASE("gc-stresstest", "[alloc][gc][gc_mutation]") { for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { const testcase_stresstest & tc = s_testcase_v[i_tc]; scope log(XO_DEBUG(tc.gc_stats_flag_)); up gc = GC::make( { .initial_nursery_z_ = tc.nursery_z_, .initial_tenured_z_ = tc.tenured_z_, .incr_gc_threshold_ = tc.nursery_gc_threshold_, .full_gc_threshold_ = tc.tenured_gc_threshold_, .stats_flag_ = tc.gc_stats_flag_, .debug_flag_ = tc.debug_flag_ }); gc->disable_gc(); REQUIRE(gc->native_gc_statistics().n_mutation_ == 0); REQUIRE(gc->native_gc_statistics().n_logged_mutation_ == 0); REQUIRE(gc.get()); /* use gc for all Object allocs */ Object::mm = gc.get(); // Plan: // - create vector of m cons cells w1[]. // - prepend w1[] to a vector of n integers; call this w2[]. // - create vector root_v[] of r gc roots. Assign each root_v[j] to some random w2[i] // - make some random mutations. // - traverse root_v[] to construct model from_model for reachable objects // - run gc // - traverse root_v[] again, to construct to_model for eachable objects // - verify from_model ~=~ to_model uint64_t seed = 8365237040761243362UL; //Seed seed; // to seed from /dev/random //std::cerr << "seed=" << seed << std::endl; auto rgen = xoshiro256ss(seed); REQUIRE(tc.m_ > 0); //REQUIRE(tc.n_ > 0); //REQUIRE(tc.r_ > 0); // data_model: generate some random data, to exercise GC RandomMutationModel data_model(tc.m_, tc.n_, tc.r_, tc.rr_, tc.k_); for (std::size_t cycle = 0; cycle < 3; ++cycle) { INFO(xtag("cycle", cycle)); if (cycle == 0) { data_model.generate_seed_values(gc.get()); data_model.generate_random_roots(gc.get(), &rgen); } else { /* figure out values in {data_model_.w1_, data_model_.w2_} that * survived GC; keep these. Discard the remainder. * don't want these as roots, because that would alter the behavior of GC. * * (For example want to verify behavior of GC w.r.t. cells that are alive only * because of a mutation) */ data_model.rejuvenate_seed_values(gc.get()); data_model.alter_random_roots(&rgen); } data_model.generate_random_mutations(&rgen); log && log(xtag("cycle", cycle), xtag("stats.before", gc->get_gc_statistics())); /* make model for contents of w2[] - baseline for post-GC comparison */ ObjectGraphModel from_model; from_model.from_root_vector(data_model.root_v_); gc->request_gc(generation::nursery); gc->enable_gc_once(); /* collector cycle changed object addresses. * build a new object model, and verify consiste1ncy with from_model */ ObjectGraphModel to_model; to_model.from_root_vector(data_model.root_v_); REQUIRE(ObjectGraphModel::verify_equal_models(from_model, to_model)); log && log(xtag("cycle", cycle), xtag("stats.after", gc->get_gc_statistics())); } } } } /*namespace ut*/ } /*namespace xo*/ /* end GC.test.cpp */