diff --git a/include/xo/gc/MutationLogStore.hpp b/include/xo/gc/MutationLogStore.hpp index fbe6a8b..4a39e6c 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 2a9d6ac..9b14138 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 19a6f92..6c7ed42 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 61739dd..bfece56 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 93e2c5b..05eba32 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 c2c03c8..bdeb79b 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_,