diff --git a/include/xo/alloc/ArenaAlloc.hpp b/include/xo/alloc/ArenaAlloc.hpp index 64546cf3..66dd6a70 100644 --- a/include/xo/alloc/ArenaAlloc.hpp +++ b/include/xo/alloc/ArenaAlloc.hpp @@ -36,9 +36,24 @@ namespace xo { std::size_t z, bool debug_flag); + /** size of virtual address range reserved for this allocator **/ + std::size_t reserved() const { return this->size(); } + + std::size_t page_size() const { return page_z_; } std::byte * free_ptr() const { return free_ptr_; } void set_free_ptr(std::byte * x); + /** if address @p x is allocated from this arena, + * return true along with offset relative to base address @ref lo_ + * otherwise return false with 0 + **/ + std::pair location_of(const void * x) const; + + /** allocated span **/ + std::pair allocated_span() const { + return std::make_pair(lo_, free_ptr_); + } + /** Reset to empty state **/ void reset(std::size_t /*z_ignored*/) { this->clear(); } diff --git a/include/xo/alloc/GC.hpp b/include/xo/alloc/GC.hpp index a653b54b..ab58b902 100644 --- a/include/xo/alloc/GC.hpp +++ b/include/xo/alloc/GC.hpp @@ -50,6 +50,8 @@ namespace xo { bool allow_incremental_gc_ = true; /** true to report statistics **/ bool stats_flag_ = false; + /** remember basic gc statistics for this many GC's; separately for incremental + full GCs **/ + std::size_t stats_history_z_ = 256; /** true to enable debug logging **/ bool debug_flag_ = false; }; @@ -147,17 +149,24 @@ namespace xo { const GCRunstate & runstate() const { return runstate_; } const GcStatistics & native_gc_statistics() const { return gc_statistics_; } GcStatisticsExt get_gc_statistics() const; + const GcStatisticsHistory & gc_history() const { return gc_history_; } /** true iff GC permitted in current state **/ bool is_gc_enabled() const { return gc_enabled_ == 0; } /** true during (and only during) a GC cycle **/ bool gc_in_progress() const { return runstate_.in_progress(); } + /** @return reserved size of Nursery to-space **/ + std::size_t nursery_to_reserved() const; /** @return committed size of Nursery to-space **/ std::size_t nursery_to_committed() const; /** @return nursery bytes used before checkpoint **/ std::size_t nursery_before_checkpoint() const; /** @return nursery bytes used after checkpoint **/ std::size_t nursery_after_checkpoint() const; + /** @return allocated memory range for nursery **/ + std::pair nursery_span(role role) const; + /** @return reserved size of Tenured to-space **/ + std::size_t tenured_to_reserved() const; /** @return committed size of Tenured to-space **/ std::size_t tenured_to_committed() const; /** @return tenured bytes used before checkpoint **/ @@ -167,8 +176,24 @@ namespace xo { /** @return generation to which object at @p x belongs **/ generation_result tospace_generation_of(const void * x) const; + /** @return generation to which object at @p x belongs, + * location relative to base address for that generation, + * and allocated size of that generation + * @p role chooses between to-space and from-space + **/ + std::tuple location_of(role role, const void * x) const; + /** @return generation to which object at @p x belongs, + * location relative to base address for @p x, + * and allocated size of generation + **/ + std::tuple tospace_location_of(const void * x) const; /** @return generation that contains @p x, given it's in from-space **/ generation_result fromspace_generation_of(const void * x) const; + /** @return generation to which object at @p x belongs, + * location relative to base address for @p x, + * and allocated size of generation + **/ + std::tuple fromspace_location_of(const void * x) const; /** true iff from-space contains @p x **/ bool fromspace_contains(const void * x) const; /** @return free pointer for generation @p gen, i.e. nursery or tenured space **/ @@ -361,6 +386,9 @@ namespace xo { /** enabled when 0. disabled when <0 **/ int gc_enabled_ = 0; + /** rotating per-gc statistics history **/ + GcStatisticsHistory gc_history_; + /** for (optional) viz: invoke when copying individual objects **/ GcCopyCallbackSet gc_copy_cbset_; }; diff --git a/include/xo/alloc/GcStatistics.hpp b/include/xo/alloc/GcStatistics.hpp index 24cac461..d73f8947 100644 --- a/include/xo/alloc/GcStatistics.hpp +++ b/include/xo/alloc/GcStatistics.hpp @@ -6,6 +6,7 @@ #pragma once #include "generation.hpp" +#include "CircularBuffer.hpp" #include "xo/reflect/TypeDescr.hpp" #include "xo/indentlog/print/pretty.hpp" #include @@ -57,6 +58,8 @@ namespace xo { **/ class GcStatistics { public: + GcStatistics() = default; + /** update statistics after a GC cycle * @param upto. nursery -> incremental collection; tenured -> full collection * @param alloc_z. new allocations (since preceding GC) @@ -108,6 +111,7 @@ namespace xo { **/ class GcStatisticsExt : public GcStatistics { public: + GcStatisticsExt() = default; explicit GcStatisticsExt(const GcStatistics & x) : GcStatistics{x} {} /** @param os. write stats on this output stream **/ @@ -128,6 +132,63 @@ namespace xo { return os; } + /** @class GcStatisticsHistoryItem + * @brief info we want to record over time (won't have cumulative things in it) + **/ + class GcStatisticsHistoryItem { + public: + GcStatisticsHistoryItem() = default; + GcStatisticsHistoryItem(generation upto, + std::size_t new_alloc_z, + std::size_t survive_z, + std::size_t promote_z, + std::size_t persist_z, + std::size_t effort_z, + std::size_t garbage0_z, + std::size_t garbage1_z, + std::size_t garbageN_z) + : upto_{upto}, + new_alloc_z_{new_alloc_z}, + survive_z_{survive_z}, + promote_z_{promote_z}, + persist_z_{persist_z}, + effort_z_{effort_z}, + garbage0_z_{garbage0_z}, + garbage1_z_{garbage1_z}, + garbageN_z_{garbageN_z} + {} + + /** @param os. write stats on this output stream **/ + void display(std::ostream & os) const; + + /** type of GC that generated this record **/ + generation upto_; + /** #of bytes new allocation **/ + std::size_t new_alloc_z_ = 0; + /** #of bytes surviving their first collection (i.e. N0->N1) **/ + std::size_t survive_z_ = 0; + /** #of bytes promoted to tenured. + * Comprises all objects surviving their 2nd collection (i.e. N1->T) + **/ + std::size_t promote_z_ = 0; + /** #of bytes surviving 3rd of later collection **/ + std::size_t persist_z_ = 0; + /** #of bytes copied **/ + std::size_t effort_z_ = 0; + /** #of bytes garbage from N0 (i.e. survived 0 GCs) **/ + std::size_t garbage0_z_ = 0; + /** #of bytes garbage from N1 (i.e. survived 1 GCs) **/ + std::size_t garbage1_z_ = 0; + /** #of bytes garbage from T (i.e. survived 2+ GCs) **/ + std::size_t garbageN_z_ = 0; + }; + + inline std::ostream & operator<< (std::ostream & os, const GcStatisticsHistoryItem & x) { + x.display(os); + return os; + } + + using GcStatisticsHistory = CircularBuffer; } /*namespace gc*/ namespace print { @@ -145,6 +206,11 @@ namespace xo { struct ppdetail { static bool print_pretty(const ppindentinfo &, const xo::gc::GcStatisticsExt &); }; + + template<> + struct ppdetail { + static bool print_pretty(const ppindentinfo &, const xo::gc::GcStatisticsHistoryItem &); + }; } /*namespace print*/ } /*namespace xo*/ diff --git a/include/xo/alloc/ListAlloc.hpp b/include/xo/alloc/ListAlloc.hpp index 7b592b4e..75b148ac 100644 --- a/include/xo/alloc/ListAlloc.hpp +++ b/include/xo/alloc/ListAlloc.hpp @@ -34,6 +34,9 @@ namespace xo { static up make(const std::string & name, std::size_t cz, std::size_t nz, bool debug_flag); + /** page size used by underlying ArenaAlloc **/ + std::size_t page_size() const; + /** reset to have at least @p z bytes of storage **/ bool reset(std::size_t z); diff --git a/include/xo/alloc/generation.hpp b/include/xo/alloc/generation.hpp index 82c48808..2ed89276 100644 --- a/include/xo/alloc/generation.hpp +++ b/include/xo/alloc/generation.hpp @@ -30,6 +30,7 @@ namespace xo { tenured, not_found }; + } /*namespace gc*/ } /*namespace xo*/ diff --git a/src/alloc/ArenaAlloc.cpp b/src/alloc/ArenaAlloc.cpp index 49a2d16d..f3145bb2 100644 --- a/src/alloc/ArenaAlloc.cpp +++ b/src/alloc/ArenaAlloc.cpp @@ -113,6 +113,7 @@ namespace xo { } this->committed_z_ = align_offset_z; + this->limit_ = this->lo_ + committed_z_; return true; } @@ -133,6 +134,16 @@ namespace xo { } } + std::pair + ArenaAlloc::location_of(const void * x) const + { + if ((lo_ <= x) && (x < hi_)) { + return std::make_pair(true, reinterpret_cast(x) - lo_); + } else { + return std::make_pair(false, 0); + } + } + void ArenaAlloc::capture_object_statistics(capture_phase phase, ObjectStatistics * p_dest) const @@ -276,6 +287,7 @@ namespace xo { xtag("z0", z0), xtag("+pad", dz), xtag("z1", z1), + xtag("size", this->size()), xtag("avail", this->available())); this->free_ptr_ += z1; diff --git a/src/alloc/GC.cpp b/src/alloc/GC.cpp index 6ccc4d15..a552ae1e 100644 --- a/src/alloc/GC.cpp +++ b/src/alloc/GC.cpp @@ -79,6 +79,8 @@ namespace xo { mutation_log_[role2int(role::to_space)] = std::make_unique(); defer_mutation_log_ = std::make_unique(); + this->gc_history_ = CircularBuffer(config.stats_history_z_); + this->checkpoint(); } @@ -191,6 +193,12 @@ namespace xo { return retval; } + std::size_t + GC::nursery_to_reserved() const + { + return nursery_to()->reserved(); + } + std::size_t GC::nursery_to_committed() const { @@ -209,6 +217,17 @@ namespace xo { return nursery_to()->after_checkpoint(); } + std::pair + GC::nursery_span(role role) const { + return nursery(role)->allocated_span(); + } + + std::size_t + GC::tenured_to_reserved() const + { + return tenured_to()->reserved(); + } + std::size_t GC::tenured_to_committed() const { @@ -239,6 +258,40 @@ namespace xo { return generation_result::not_found; } + std::tuple + GC::location_of(role role, const void *x) const + { + { + auto space = this->tenured(role); + auto [is_tenured, offset] = space->location_of(x); + + if (is_tenured) + return std::make_tuple(generation_result::tenured, offset, space->allocated()); + } + + { + auto space = this->nursery(role); + auto [is_nursery, offset] = nursery(role)->location_of(x); + + if (is_nursery) + return std::make_tuple(generation_result::nursery, offset, space->allocated()); + } + + return std::make_tuple(generation_result::not_found, 0, 0); + } + + std::tuple + GC::tospace_location_of(const void * x) const + { + return location_of(role::to_space, x); + } + + std::tuple + GC::fromspace_location_of(const void * x) const + { + return location_of(role::from_space, x); + } + generation_result GC::fromspace_generation_of(const void * x) const { @@ -988,8 +1041,11 @@ namespace xo { { scope log(XO_DEBUG(config_.debug_flag_)); - std::size_t N_allocated = nursery_from()->after_checkpoint(); - std::size_t T_allocated = tenured_from()->after_checkpoint(); + std::size_t N0_before_gc = nursery_from()->after_checkpoint(); + std::size_t N1_before_gc = nursery_from()->before_checkpoint(); + + std::size_t T0_before_gc = tenured_from()->after_checkpoint(); + std::size_t T1_before_gc = tenured_from()->before_checkpoint(); std::size_t N_before_gc = nursery_from()->allocated(); std::size_t T_before_gc = tenured_from()->allocated(); @@ -998,9 +1054,37 @@ namespace xo { std::size_t T_after_gc = tenured_to()->allocated(); //std::byte * N_free_ptr = nursery_[role2int(role::to_space)]->free_ptr(); + std::size_t new_alloc_z = N0_before_gc; + /* survive_z: bytes surviving first collection */ + std::size_t survive_z = N_after_gc; + /* promote_z: bytes surviving 2nd collection */ std::size_t promote_z = (gc_statistics_.total_promoted_ - gc_statistics_.total_promoted_sab_); + /* #of bytes copied by this collection cycle */ + std::size_t effort_z = 0; + if (upto == generation::nursery) { + effort_z = N_after_gc + promote_z; + } else { + effort_z += N_after_gc + T_after_gc; + } + + /* persist_z: bytes surviving 3rd or later collection */ + std::size_t persist_z = 0; + if (upto == generation::tenured) + persist_z = T_after_gc - promote_z; + /* #of bytes found to be garbage on first collection + * (reminder: N_after_gc consists *entirely* of survives from N0_before_gc; + * + all such survivors are in N_after_gc) + */ + std::size_t garbage0_z = (N0_before_gc - N_after_gc); + /* #of bytes found to be garbage on 2nd collection */ + std::size_t garbage1_z = (N1_before_gc - promote_z); + /* #of bytes found to be garbage on 3rd or later collection */ + std::size_t garbageN_z = 0; + if (upto == generation::tenured) + garbageN_z = (T_before_gc - T_after_gc + promote_z); + /* Don't reset from-space here, it's unnecessary. * Would be permissible, but interferes with GC object modelling in * xo-object/utest/GC.test.cpp @@ -1014,25 +1098,38 @@ namespace xo { this->tenured_to()->checkpoint(); if (log) { - log(xtag("N_allocated", N_allocated)); - log(xtag("N_before_gc", N_before_gc)); + log(xtag("N0_before_gc", N0_before_gc)); + log(xtag("N1_before_gc", N1_before_gc)); log(xtag("N_after_gc", N_after_gc)); - log(xtag("T_allocated", T_allocated)); - log(xtag("T_before_gc", T_before_gc)); + + log(xtag("T0_before_gc", T0_before_gc)); + log(xtag("T1_before_gc", T1_before_gc)); log(xtag("T_after_gc", T_after_gc)); } + GcStatisticsHistoryItem item(upto, + new_alloc_z, + survive_z, + promote_z, + persist_z, + effort_z, + garbage0_z, + garbage1_z, + garbageN_z); + + this->gc_history_.push_back(item); + this->incr_gc_pending_ = false; - this->gc_statistics_.include_gc(generation::nursery, N_allocated, N_before_gc, N_after_gc, promote_z); + this->gc_statistics_.include_gc(generation::nursery, N0_before_gc, N_before_gc, N_after_gc, promote_z); if (upto == generation::tenured) { this->full_gc_pending_ = false; - this->gc_statistics_.include_gc(generation::tenured, T_allocated, T_before_gc, T_after_gc, 0); + this->gc_statistics_.include_gc(generation::tenured, T0_before_gc, T_before_gc, T_after_gc, 0); } else { // still want to update tenured stats for current alloc size this->gc_statistics_.update_snapshot(generation::tenured, T_after_gc); } - } + } /*cleanup_phase*/ void GC::execute_gc(generation upto) @@ -1090,6 +1187,9 @@ namespace xo { this->runstate_ = GCRunstate(); + // not this way.. reports cumulative stats + // this->gc_history_.push_back(this->get_gc_statistics()); + log && log("statistics:"); log && log(gc_statistics_); } diff --git a/src/alloc/GcStatistics.cpp b/src/alloc/GcStatistics.cpp index 7b7a6ff9..cb7ebebf 100644 --- a/src/alloc/GcStatistics.cpp +++ b/src/alloc/GcStatistics.cpp @@ -92,6 +92,22 @@ namespace xo { // << xtag("per_type_stats", per_type_stats_) << ">"; } + + void + GcStatisticsHistoryItem::display(std::ostream & os) const + { + os << ""; + } + } /*namespace gc*/ namespace print { @@ -148,7 +164,21 @@ namespace xo { refrtag("tenured_z", x.tenured_z_)); } - + bool + ppdetail::print_pretty(const ppindentinfo & ppii, + const xo::gc::GcStatisticsHistoryItem & x) + { + return ppii.pps()->pretty_struct(ppii, + "GcStatisticsHistoryItem", + refrtag("upto", gen2str(x.upto_)), + refrtag("survive_z", x.survive_z_), + refrtag("promote_z", x.promote_z_), + refrtag("persist_z", x.persist_z_), + refrtag("effort_z", x.effort_z_), + refrtag("garbage0_z", x.garbage0_z_), + refrtag("garbage1_z", x.garbage1_z_), + refrtag("garbageN_z", x.garbageN_z_)); + } } /*namespace print*/ } /*namespace xo*/ diff --git a/src/alloc/ListAlloc.cpp b/src/alloc/ListAlloc.cpp index 29cdb477..8b0a2dbb 100644 --- a/src/alloc/ListAlloc.cpp +++ b/src/alloc/ListAlloc.cpp @@ -70,6 +70,11 @@ namespace xo { return s_default_name; } + std::size_t + ListAlloc::page_size() const { + return hd_->page_size(); + } + std::size_t ListAlloc::size() const { return total_z_; @@ -109,8 +114,9 @@ namespace xo { ListAlloc::allocated() const { std::size_t total = 0; - if (hd_) + if (hd_) { total += hd_->allocated(); + } for (const auto & alloc : full_l_) total += alloc->allocated(); @@ -363,10 +369,17 @@ namespace xo { ListAlloc::alloc(std::size_t z) { scope log(XO_DEBUG(debug_flag_)); + /* ArenaAlloc::alloc() may modify its own size */ + + std::size_t z_pre = hd_->size(); std::byte * retval = hd_->alloc(z); - if (retval) + if (retval) { + std::size_t z_post = hd_->size(); + this->total_z_ += (z_post - z_pre); + return retval; + } log && log("space exhausted -> expand"); diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 907bafcb..379ed925 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -12,6 +12,7 @@ set(UTEST_SRCS GcStatistics.test.cpp ObjectStatistics.test.cpp Forwarding1.test.cpp + CircularBuffer.test.cpp generation.test.cpp ) diff --git a/utest/GcStatistics.test.cpp b/utest/GcStatistics.test.cpp index 5f2fbe96..f5700f25 100644 --- a/utest/GcStatistics.test.cpp +++ b/utest/GcStatistics.test.cpp @@ -76,7 +76,7 @@ namespace xo { tag_config::tag_color_enabled = false; - GcStatisticsExt stats({}); + GcStatisticsExt stats; std::string s = tostr(stats); @@ -154,7 +154,7 @@ namespace xo { std::stringstream ss; ppconfig ppc; - GcStatisticsExt stats({}); + GcStatisticsExt stats; std::string actual = toppstr2(ppc, stats); std::string expected diff --git a/utest/ListAlloc.test.cpp b/utest/ListAlloc.test.cpp index 2db22bf2..0a62fd0b 100644 --- a/utest/ListAlloc.test.cpp +++ b/utest/ListAlloc.test.cpp @@ -12,7 +12,10 @@ namespace xo { namespace ut { TEST_CASE("ListAlloc", "[alloc][gc]") { - /** teeny weeny allocator **/ + /** teeny weeny allocator. + * but underlying ArenaAlloc works in multiples of VM page size + * (most likely 4k) + **/ up alloc = ListAlloc::make("test", 16, 32, false); REQUIRE(alloc->name() == "test"); @@ -24,7 +27,7 @@ namespace xo { std::byte * mem1 = alloc->alloc(20); REQUIRE(mem1); - REQUIRE(alloc->size() == 16 + 32); + REQUIRE(alloc->size() == alloc->page_size()); /* round up to multiple of 8 */ REQUIRE(alloc->before_checkpoint() == 24); REQUIRE(alloc->after_checkpoint() == 0); @@ -34,7 +37,7 @@ namespace xo { std::byte * mem2 = alloc->alloc(30); REQUIRE(mem2); - REQUIRE(alloc->size() == 16 + 32 + 48); + REQUIRE(alloc->size() == alloc->page_size()); REQUIRE(alloc->before_checkpoint() == 24); /* round up to multiple of 8 */ REQUIRE(alloc->after_checkpoint() == 32); @@ -42,7 +45,7 @@ namespace xo { std::byte * mem3 = alloc->alloc(40); REQUIRE(mem3); - REQUIRE(alloc->size() == 16 + 32 + 48 + 80); + REQUIRE(alloc->size() == alloc->page_size()); REQUIRE(alloc->before_checkpoint() == 24); /* already multiple of 8 */ REQUIRE(alloc->after_checkpoint() == 32 + 40);