From 0ad59b7c9f4d03d4fb7f0fdc4a6c40cd3e217d80 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 23 Aug 2025 10:47:52 -0400 Subject: [PATCH] xo-imgui: ex2: animate GC copy step --- xo-alloc/include/xo/alloc/GC.hpp | 12 +- xo-alloc/include/xo/alloc/generation.hpp | 10 ++ xo-alloc/src/alloc/GC.cpp | 13 +- xo-imgui/example/ex2/imgui_ex2.cpp | 194 +++++++++++++++++++---- 4 files changed, 190 insertions(+), 39 deletions(-) diff --git a/xo-alloc/include/xo/alloc/GC.hpp b/xo-alloc/include/xo/alloc/GC.hpp index 7c4f42c6..8ddb3e23 100644 --- a/xo-alloc/include/xo/alloc/GC.hpp +++ b/xo-alloc/include/xo/alloc/GC.hpp @@ -154,6 +154,8 @@ namespace xo { /** true iff GC permitted in current state **/ bool is_gc_enabled() const { return gc_enabled_ == 0; } + /** true iff GC has been requested **/ + bool is_gc_pending() const { return incr_gc_pending_ || full_gc_pending_; } /** true during (and only during) a GC cycle **/ bool gc_in_progress() const { return runstate_.in_progress(); } /** @return reserved size of Nursery to-space **/ @@ -223,10 +225,14 @@ namespace xo { * * GC is enabled when number of calls to @ref enable_gc is at least as large * as number of calls to @ref disable_gc. + * + * @return true iff GC performed **/ - void enable_gc(); - /** same as @c this->enable_gc() followed by @c this->disable_gc() **/ - void enable_gc_once(); + bool enable_gc(); + /** same as @c this->enable_gc() followed by @c this->disable_gc() + * @return true iff GC performed + **/ + bool enable_gc_once(); // inherited from IAlloc.. diff --git a/xo-alloc/include/xo/alloc/generation.hpp b/xo-alloc/include/xo/alloc/generation.hpp index 2ed89276..0acd943a 100644 --- a/xo-alloc/include/xo/alloc/generation.hpp +++ b/xo-alloc/include/xo/alloc/generation.hpp @@ -7,6 +7,7 @@ #include #include +#include namespace xo { namespace gc { @@ -31,6 +32,15 @@ namespace xo { not_found }; + inline generation valid_genresult2gen(generation_result x) { + assert(x != generation_result::not_found); + + if (x == generation_result::nursery) + return generation::nursery; + else + return generation::tenured; + } + } /*namespace gc*/ } /*namespace xo*/ diff --git a/xo-alloc/src/alloc/GC.cpp b/xo-alloc/src/alloc/GC.cpp index e6fb188f..200ac218 100644 --- a/xo-alloc/src/alloc/GC.cpp +++ b/xo-alloc/src/alloc/GC.cpp @@ -1233,21 +1233,26 @@ namespace xo { --gc_enabled_; } - void + bool GC::enable_gc() { ++gc_enabled_; if (gc_enabled_ == 0) { /* unblock gc */ - if (incr_gc_pending_) + if (incr_gc_pending_) { this->request_gc(full_gc_pending_ ? generation::tenured : generation::nursery); + return true; + } } + + return false; } - void + bool GC::enable_gc_once() { - this->enable_gc(); + bool retval = this->enable_gc(); this->disable_gc(); + return retval; } } /*namespace gc*/ diff --git a/xo-imgui/example/ex2/imgui_ex2.cpp b/xo-imgui/example/ex2/imgui_ex2.cpp index b8456718..73338946 100644 --- a/xo-imgui/example/ex2/imgui_ex2.cpp +++ b/xo-imgui/example/ex2/imgui_ex2.cpp @@ -78,9 +78,11 @@ struct ImRect { */ struct GcGenerationDescription { GcGenerationDescription() = default; - GcGenerationDescription(const char * mnemonic, std::size_t before_ckp, std::size_t after_ckp, + GcGenerationDescription(const char * mnemonic, std::size_t tospace_scale, + std::size_t before_ckp, std::size_t after_ckp, std::size_t reserved, std::size_t committed, std::size_t gc_threshold) - : mnemonic_{mnemonic}, before_checkpoint_{before_ckp}, after_checkpoint_{after_ckp}, + : mnemonic_{mnemonic}, tospace_scale_{tospace_scale}, + before_checkpoint_{before_ckp}, after_checkpoint_{after_ckp}, reserved_{reserved}, committed_{committed}, gc_threshold_{gc_threshold} {} @@ -88,6 +90,13 @@ struct GcGenerationDescription { std::size_t scale() const { return std::max(committed_, gc_threshold_); } const char * mnemonic_ = nullptr; + + /** size of to-space in bytes represented on screen. + * (note however when we animate GC, space roles have already reversed, + * so then this will refer to old to-space = new from-space) + **/ + std::size_t tospace_scale_ = 0; + std::size_t before_checkpoint_ = 0; std::size_t after_checkpoint_ = 0; std::size_t reserved_ = 0; @@ -161,6 +170,22 @@ using xo::obj::Integer; using xo::rng::xoshiro256ss; using xo::rng::Seed; +/** details of a single copy event performed by GC **/ +struct GcCopyDetail { + GcCopyDetail(std::size_t z, generation src, std::size_t src_offset, std::size_t src_space_z) + : z_{z}, src_gen_{src}, src_offset_{src_offset}, src_space_z_{src_space_z} + {} + + /** object size in bytes **/ + std::size_t z_ = 0; + /** source location **/ + generation src_gen_; + /** offset from start of allocator **/ + std::size_t src_offset_ = 0; + /** size of source space. could store this separately **/ + std::size_t src_space_z_ = 0; +}; + struct AppState { using GC = xo::gc::GC; @@ -180,6 +205,8 @@ public: std::vector> gc_root_v_{100}; Seed seed_; xoshiro256ss rng_{seed_}; + /** remember details for each object copied by GC, so we can animate **/ + std::vector copy_detail_v_; }; AppState::AppState() @@ -225,6 +252,7 @@ GcStateDescription AppState::snapshot_gc_state() const { return GcStateDescription(GcGenerationDescription ("N", + this->nursery_tospace_scale(), gc_->nursery_before_checkpoint(), gc_->nursery_after_checkpoint(), gc_->nursery_to_reserved(), @@ -232,6 +260,7 @@ AppState::snapshot_gc_state() const { gc_->nursery_before_checkpoint() + gc_->config().incr_gc_threshold_), GcGenerationDescription ("T", + this->tenured_tospace_scale(), gc_->tenured_before_checkpoint(), gc_->tenured_after_checkpoint(), gc_->tenured_to_reserved(), @@ -874,7 +903,8 @@ draw_gc_state(const AppState & app_state, const GcStateDescription & gcstate, const ImRect & canvas_rect, ImDrawList * draw_list, - ImRect * p_nursery_alloc_rect) + ImRect * p_nursery_alloc_rect, + ImRect * p_tenured_alloc_rect) { float lm = 50; float rm = 70; @@ -892,7 +922,7 @@ draw_gc_state(const AppState & app_state, canvas_rect.y_lo() + tm), ImVec2(canvas_rect.x_hi() - rm, canvas_rect.y_lo() + tm + h + est_chart_text_height)); - /* rectangle representing allocated nursery range*/ + /* rectangle representing allocated nursery range */ ImRect N_alloc_rect; float N_x1 = 0.0; @@ -907,7 +937,7 @@ draw_gc_state(const AppState & app_state, *p_nursery_alloc_rect = N_alloc_rect; /* N0_to_size..N_to_scale: in bytes */ - std::size_t N_to_scale = app_state.nursery_tospace_scale(); + std::size_t N_to_scale = gcstate.gen_state_v_[gen2int(generation::nursery)].tospace_scale_; /* display_w .. N0_h : viewportcoords */ std::size_t display_w = canvas_rect.width() - lm - rm; @@ -919,7 +949,7 @@ draw_gc_state(const AppState & app_state, // now turn to Tenured space - std::size_t T_to_scale = app_state.tenured_tospace_scale(); + std::size_t T_to_scale = gcstate.gen_state_v_[gen2int(generation::tenured)].tospace_scale_; std::size_t T1_h = h; /* want to put to-scale image of nursery next to to-scale image of tenured; @@ -959,6 +989,8 @@ draw_gc_state(const AppState & app_state, nullptr); } + /* rectangle representing allocated tenured range */ + ImRect T_alloc_rect; std::size_t h_y0 = t_y1 + est_chart_text_height; draw_tenured(gcstate, @@ -967,9 +999,12 @@ draw_gc_state(const AppState & app_state, ImVec2(x0 + (adj_display_w * (T_to_scale/TplusN_to_scale)) - TplusN_spacer, h_y0)), draw_list, - nullptr, + &T_alloc_rect, nullptr); + if (p_tenured_alloc_rect) + *p_tenured_alloc_rect = T_alloc_rect; + /* just incremental (nursery) collections */ draw_gc_history(gcstate, generation::nursery, @@ -1013,21 +1048,84 @@ struct AnimateGcCopyCb : public xo::gc::GcCopyCallback { DrawState * p_draw_state_ = nullptr; }; +enum class draw_state_type { + alloc, + animate_gc +}; + struct DrawState { up make_gc_copy_animation(AppState * app_state) { return std::make_unique(app_state, this); } + draw_state_type state_type_ = draw_state_type::alloc; + + /** when animating copy step, display objects from AppState::copy_detail_v_[i] + * where i < .animate_copy_hi_ / 100 * AppState::copy_detail_v_.size() + **/ + float animate_copy_hi_pct_ = 0; + ImDrawList * gcw_draw_list_ = nullptr; - /* draw area */ + /** draw area **/ ImVec2 gcw_canvas_p0_; ImVec2 gcw_canvas_p1_; /** rect displaying allocated nursery space **/ ImRect gcw_nursery_alloc_rect_; + /** rect displaying allocated tenured space **/ + ImRect gcw_tenured_alloc_rect_; }; +ImRect map_src_alloc_to_screen(const GcCopyDetail & copy_detail, + const ImRect & space_rect) +{ + // TODO: methods on copy_detail / and/or ImPoint + + auto [x_coord_lo, x_coord_hi] = space_rect.x_span(); + + double w0 = copy_detail.src_offset_ / static_cast(copy_detail.src_space_z_); + float src0_x = ((1.0 - w0) * x_coord_lo) + (w0 * x_coord_hi); + + double w1 = ((copy_detail.src_offset_ + copy_detail.z_) + / static_cast(copy_detail.src_space_z_)); + float src1_x = ((1.0 - w1) * x_coord_lo) + (w1 * x_coord_hi); + + return space_rect.with_x_span(src0_x, src1_x); +} + +void animate_gc_copy(const AppState & app_state, + const DrawState & draw_state, + ImDrawList * draw_list) +{ + const ImRect & nursery_rect = draw_state.gcw_nursery_alloc_rect_; + const ImRect & tenured_rect = draw_state.gcw_tenured_alloc_rect_; + + //auto [x_coord_lo, x_coord_hi] = nursery_rect.x_span(); + + std::size_t i_copy = 0; + for (const auto & copy_detail : app_state.copy_detail_v_) { + ImRect src_rect; + + if (copy_detail.src_gen_ == generation::nursery) { + src_rect = map_src_alloc_to_screen(copy_detail, nursery_rect); + } else { + src_rect = map_src_alloc_to_screen(copy_detail, tenured_rect); + } + + float hi_copy = 0.01 * draw_state.animate_copy_hi_pct_ * app_state.copy_detail_v_.size(); + float wt = i_copy / hi_copy; + ImU32 color = IM_COL32(64, 255, static_cast(64 + (128 * wt)), 255); + + draw_list->AddRectFilled(src_rect.top_left(), + src_rect.bottom_right(), + color); + + if (++i_copy >= hi_copy) + break; + } +} + void AnimateGcCopyCb::notify_gc_copy(std::size_t z, const void * src_addr, @@ -1038,6 +1136,7 @@ AnimateGcCopyCb::notify_gc_copy(std::size_t z, using xo::scope; using xo::xtag; using xo::gc::generation_result; + using xo::gc::generation; using xo::gc::role; scope log(XO_DEBUG(false), @@ -1047,10 +1146,6 @@ AnimateGcCopyCb::notify_gc_copy(std::size_t z, xtag("src_gen", src_gen), xtag("dest_gen", dest_gen)); - const ImRect & nursery_rect = p_draw_state_->gcw_nursery_alloc_rect_; - - auto [x_coord_lo, x_coord_hi] = nursery_rect.x_span(); - auto [gen, offset, alloc] = p_app_state_->gc_->fromspace_location_of(src_addr); if (gen == generation_result::not_found) { @@ -1061,17 +1156,9 @@ AnimateGcCopyCb::notify_gc_copy(std::size_t z, assert(false); } - double w0 = offset / static_cast(alloc); - float src0_x = ((1.0 - w0) * x_coord_lo) + (w0 * x_coord_hi); + generation valid_gen = xo::gc::valid_genresult2gen(gen); - double w1 = (offset + z) / static_cast(alloc); - float src1_x = ((1.0 - w1) * x_coord_lo) + (w1 * x_coord_hi); - - ImRect src_rect = nursery_rect.with_x_span(src0_x, src1_x); - - p_draw_state_->gcw_draw_list_->AddRectFilled(src_rect.top_left(), - src_rect.bottom_right(), - IM_COL32(255, 128, 128, 255)); + p_app_state_->copy_detail_v_.push_back(GcCopyDetail(z, valid_gen, offset, alloc)); } int main(int, char **) @@ -1230,6 +1317,7 @@ int main(int, char **) AppState app_state; DrawState draw_state; + GcStateDescription gcstate = app_state.snapshot_gc_state(); app_state.gc_->add_gc_copy_callback(draw_state.make_gc_copy_animation(&app_state)); @@ -1237,10 +1325,41 @@ int main(int, char **) bool done = false; while (!done) { - /** generate random alloc **/ - app_state.generate_random_mutation(); + /** on each draw cycle, app state falls into categories: + * 1. allocation + * multiple draw cycles because many allocations per gc. + * 2. garbage collection + * multiple draw cycles to animate copying process + * Settle conflict between {GC, imgui} as to who drives event loop, + * in favor of imgui; achieve this by copying what GC did, + * so that we can animate it over multiple draw cycles + **/ - GcStateDescription gcstate = app_state.snapshot_gc_state(); + switch (draw_state.state_type_) { + case draw_state_type::alloc: + { + /** generate random alloc **/ + app_state.generate_random_mutation(); + + gcstate = app_state.snapshot_gc_state(); + + /* GC may run here, in which case control reenters via AnimateGcCopyCb; + * that callback captures copy details (per object!) in AppState + */ + if (app_state.gc_->enable_gc_once()) + draw_state.state_type_ = draw_state_type::animate_gc; + + break; + } + case draw_state_type::animate_gc: + { + /* don't update gcstate while animating, + * that would use post-GC space sizing + */ + + break; + } + } /** poll + handle events */ SDL_Event event; @@ -1310,8 +1429,10 @@ int main(int, char **) gcstate.gc_allocated_, gcstate.gc_available_); //ImGui::NewLine(); - ImGui::Text("promoted [%lu]", - gcstate.total_promoted_); + ImGui::Text("promoted [%lu] copy animation [%lu / %lu]", + gcstate.total_promoted_, + static_cast(draw_state.animate_copy_hi_pct_ * app_state.copy_detail_v_.size() / 100), + app_state.copy_detail_v_.size()); ImGui::Text("mutation [%lu] mlog [%lu]", gcstate.total_n_mutation_, @@ -1335,12 +1456,21 @@ int main(int, char **) gcstate, ImRect(canvas_p0, canvas_p1), draw_list, - &draw_state.gcw_nursery_alloc_rect_); + &draw_state.gcw_nursery_alloc_rect_, + &draw_state.gcw_tenured_alloc_rect_); - /* GC may run here, in which case control reenters via AnimateGcCopyCb; - * callback will rely on loop assignments to draw_area members. - */ - app_state.gc_->enable_gc_once(); + if (draw_state.state_type_ == draw_state_type::animate_gc) { + draw_state.animate_copy_hi_pct_ += 0.25; + animate_gc_copy(app_state, + draw_state, + draw_list); + + if (draw_state.animate_copy_hi_pct_ >= 100) { + draw_state.state_type_ = draw_state_type::alloc; + draw_state.animate_copy_hi_pct_ = 0; + app_state.copy_detail_v_.clear(); + } + } ImGui::End(); }