xo-object: improve GC unittest + prep to integrate w/ xo::reflect

This commit is contained in:
Roland Conybeare 2025-08-06 13:53:31 -05:00
commit 432e0efce2
8 changed files with 305 additions and 120 deletions

View file

@ -6,6 +6,7 @@
#pragma once
#include "ListAlloc.hpp"
#include "GcStatistics.hpp"
#include "xo/indentlog/print/array.hpp"
#include <vector>
#include <array>
@ -15,20 +16,6 @@ namespace xo {
class Object;
namespace gc {
enum class generation {
nursery,
tenured,
N
};
constexpr std::size_t gen2int(generation x) { return static_cast<std::size_t>(x); }
enum class generation_result {
nursery,
tenured,
not_found
};
enum class role {
/** nursery: generation for new objects **/
from_space,
@ -59,104 +46,6 @@ namespace xo {
bool debug_flag_ = false;
};
/** @class ObjectStatistics
* @brief placeholder for type-driven allocation statistics
*
* Passed to @ref Object::deep_move for example
**/
class ObjectStatistics {
};
/** @class PerGenerationStatistics
* @brief garbage collection statistics for particular GC generation
**/
class PerGenerationStatistics {
public:
/** update statistics after a GC cycle
* @param alloc_z. new allocations (since preceding GC)
* @param before_z. generation size (bytes allocated) before collection
* @param after_z. generation size after collection
* @param promote_z. bytes promoted to next generation
**/
void include_gc(std::size_t alloc_z, std::size_t before_z, std::size_t after_z,
std::size_t promote_z);
/** update with current state (use at end of gc cycle) **/
void update_snapshot(std::size_t after_z);
/** @param os. write stats on this output stream **/
void display(std::ostream & os) const;
/** number of bytes currently in use **/
std::size_t used_z_ = 0;
/** number of collection cycles completed **/
std::size_t n_gc_ = 0;
/** sum of new alloc bytes, sampled at start of each collection cycle **/
std::size_t new_alloc_z_ = 0;
/** sum of allocated bytes sampled at beginning of each collection cycle **/
std::size_t scanned_z_ = 0;
/** sum of bytes remaining after collection cycle **/
std::size_t survive_z_ = 0;
/** sum of bytes promoted to next generation **/
std::size_t promote_z_ = 0;
};
inline std::ostream & operator<< (std::ostream & os, const PerGenerationStatistics & x) {
x.display(os);
return os;
}
/** @class GcStatistics
* @brief garbage collection statistics
**/
class GcStatistics {
public:
/** update statistics after a GC cycle
* @param upto. nursery -> incremental collection; tenured -> full collection
* @param alloc_z. new allocations (since preceding GC)
* @param before_z. generation size (bytes allocated) before collection
* @param after_z. generation size after collection
* @param promote_z. bytes promoted to next generation
**/
void include_gc(generation upto, std::size_t alloc_z,
std::size_t before_z, std::size_t after_z, std::size_t promote_z);
/** update snapshot for current state.
* Use with tenured stats after incremental gc
**/
void update_snapshot(generation upto, std::size_t after_z);
/** @param os. write stats on this output stream **/
void display(std::ostream & os) const;
/** statistics gathered across {incr, full} GCs respectively **/
std::array<PerGenerationStatistics, static_cast<std::size_t>(generation::N)> gen_v_;
/** total bytes allocated since inception **/
std::size_t total_allocated_ = 0;
/** snapshot of total bytes promoted asof beginning of last gc cycle **/
std::size_t total_promoted_sab_ = 0;
/** total bytes promoted from nursery->tenured since inception **/
std::size_t total_promoted_ = 0;
/** total number of mutations to already-allocated objects,
* whether or not GC needs to log them.
**/
std::size_t n_mutation_ = 0;
/** total number of mutation eligible for logging **/
std::size_t n_logged_mutation_ = 0;
/** total number of cross-generation mutations (tenured->nursery when reported) **/
std::size_t n_xgen_mutation_ = 0;
/** total number of cross-checkpoint mutations (N0 -> N1 when reported) **/
std::size_t n_xckp_mutation_ = 0;
/** per-type statistics (placeholder) **/
ObjectStatistics per_type_stats_;
};
inline std::ostream & operator<< (std::ostream & os, const GcStatistics & x) {
x.display(os);
return os;
}
/** @class GCRunstate
* @brief encapsulate state needed while GC is running
*
@ -228,7 +117,8 @@ namespace xo {
static up<GC> make(const Config & config);
const GCRunstate & runstate() const { return runstate_; }
const GcStatistics & gc_statistics() const { return gc_statistics_; }
const GcStatistics & native_gc_statistics() const { return gc_statistics_; }
GcStatisticsExt get_gc_statistics() const;
/** true iff GC permitted in current state **/
bool is_gc_enabled() const { return gc_enabled_ == 0; }

View file

@ -0,0 +1,161 @@
/* GcStatistics.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#pragma once
#include "generation.hpp"
#include "xo/indentlog/print/pretty.hpp"
#include <ostream>
#include <array>
namespace xo {
namespace gc {
/** @class ObjectStatistics
* @brief placeholder for type-driven allocation statistics
*
* Passed to @ref Object::deep_move for example
**/
class ObjectStatistics {
};
/** @class PerGenerationStatistics
* @brief garbage collection statistics for particular GC generation
**/
class PerGenerationStatistics {
public:
/** update statistics after a GC cycle
* @param alloc_z. new allocations (since preceding GC)
* @param before_z. generation size (bytes allocated) before collection
* @param after_z. generation size after collection
* @param promote_z. bytes promoted to next generation
**/
void include_gc(std::size_t alloc_z, std::size_t before_z, std::size_t after_z,
std::size_t promote_z);
/** update with current state (use at end of gc cycle) **/
void update_snapshot(std::size_t after_z);
/** @param os. write stats on this output stream **/
void display(std::ostream & os) const;
/** number of bytes currently in use **/
std::size_t used_z_ = 0;
/** number of collection cycles completed **/
std::size_t n_gc_ = 0;
/** sum of new alloc bytes, sampled at start of each collection cycle **/
std::size_t new_alloc_z_ = 0;
/** sum of allocated bytes sampled at beginning of each collection cycle **/
std::size_t scanned_z_ = 0;
/** sum of bytes remaining after collection cycle **/
std::size_t survive_z_ = 0;
/** sum of bytes promoted to next generation **/
std::size_t promote_z_ = 0;
};
inline std::ostream & operator<< (std::ostream & os, const PerGenerationStatistics & x) {
x.display(os);
return os;
}
/** @class GcStatistics
* @brief garbage collection statistics
**/
class GcStatistics {
public:
/** update statistics after a GC cycle
* @param upto. nursery -> incremental collection; tenured -> full collection
* @param alloc_z. new allocations (since preceding GC)
* @param before_z. generation size (bytes allocated) before collection
* @param after_z. generation size after collection
* @param promote_z. bytes promoted to next generation
**/
void include_gc(generation upto, std::size_t alloc_z,
std::size_t before_z, std::size_t after_z, std::size_t promote_z);
/** update snapshot for current state.
* Use with tenured stats after incremental gc
**/
void update_snapshot(generation upto, std::size_t after_z);
/** @param os. write stats on this output stream **/
void display(std::ostream & os) const;
/** statistics gathered across {incr, full} GCs respectively **/
std::array<PerGenerationStatistics, static_cast<std::size_t>(generation::N)> gen_v_;
/** total bytes allocated since inception **/
std::size_t total_allocated_ = 0;
/** snapshot of total bytes promoted asof beginning of last gc cycle **/
std::size_t total_promoted_sab_ = 0;
/** total bytes promoted from nursery->tenured since inception **/
std::size_t total_promoted_ = 0;
/** total number of mutations to already-allocated objects,
* whether or not GC needs to log them.
**/
std::size_t n_mutation_ = 0;
/** total number of mutation eligible for logging (cumulative across GCs) **/
std::size_t n_logged_mutation_ = 0;
/** total number of cross-generation mutations
* (tenured->nursery when reported; cumulative across GCs) **/
std::size_t n_xgen_mutation_ = 0;
/** total number of cross-checkpoint mutations
* (N0 -> N1 when reported; cumulative across GCs)
**/
std::size_t n_xckp_mutation_ = 0;
/** per-type statistics (placeholder) **/
ObjectStatistics per_type_stats_;
};
inline std::ostream & operator<< (std::ostream & os, const GcStatistics & x) {
x.display(os);
return os;
}
/** @class GcStatisticsExt
* @brief extend GcStatistics for application convenience
**/
class GcStatisticsExt : public GcStatistics {
public:
explicit GcStatisticsExt(const GcStatistics & x) : GcStatistics{x} {}
/** @param os. write stats on this output stream **/
void display(std::ostream & os) const;
/** current capacity of nursery generation **/
std::size_t nursery_z_ = 0;
/** current nursery survivor size **/
std::size_t nursery_before_checkpoint_z_ = 0;
/** current nursery new alloc size **/
std::size_t nursery_after_checkpoint_z_ = 0;
/** current capacity of tenured generation **/
std::size_t tenured_z_ = 0;
};
inline std::ostream & operator<< (std::ostream & os, const GcStatisticsExt & x) {
x.display(os);
return os;
}
} /*namespace gc*/
namespace print {
template <>
struct ppdetail<xo::gc::PerGenerationStatistics> {
static bool print_pretty(const ppindentinfo &, const xo::gc::PerGenerationStatistics &);
};
template<>
struct ppdetail<xo::gc::GcStatistics> {
static bool print_pretty(const ppindentinfo &, const xo::gc::GcStatistics &);
};
template<>
struct ppdetail<xo::gc::GcStatisticsExt> {
static bool print_pretty(const ppindentinfo &, const xo::gc::GcStatisticsExt &);
};
} /*namespace print*/
} /*namespace xo*/
/* end GcStatistics.hpp */

View file

@ -5,6 +5,7 @@
#pragma once
#include "xo/reflect/SelfTagging.hpp"
#include "IAlloc.hpp"
#include <concepts>
#include <cstdint>

View file

@ -0,0 +1,26 @@
/* generation.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#include <cstdint>
namespace xo {
namespace gc {
enum class generation {
nursery,
tenured,
N
};
constexpr std::size_t gen2int(generation x) { return static_cast<std::size_t>(x); }
enum class generation_result {
nursery,
tenured,
not_found
};
} /*namespace gc*/
} /*namespace xo*/
/* end generation.hpp */

View file

@ -6,11 +6,12 @@ set(SELF_SRCS
ArenaAlloc.cpp
ListAlloc.cpp
GC.cpp
GcStatistics.cpp
Object.cpp
Forwarding1.cpp
)
xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS})
xo_dependency(${SELF_LIB} indentlog)
xo_dependency(${SELF_LIB} reflect)
#end CMakeLists.txt

View file

@ -3,6 +3,7 @@
* author: Roland Conybeare, Jul 2025
*/
#include "GcStatistics.hpp"
#include "GC.hpp"
#include "Object.hpp"
#include "xo/indentlog/scope.hpp"
@ -67,6 +68,31 @@ namespace xo {
os << "<GcStatistics"
<< xtag("gen_v", gen_v_)
<< xtag("total_allocated", total_allocated_)
<< xtag("total_promoted_sab", total_promoted_sab_)
// total_promoted
// n_mtuation
// n_logged_mutation
// n_xgen_mutation
// n_xckp_mutation
// << xtag("per_type_stats", per_type_stats_)
<< ">";
}
void
GcStatisticsExt::display(std::ostream & os) const
{
os << "<GcStatisticsExt"
<< xtag("gen_v", gen_v_)
<< xtag("total_allocated", total_allocated_)
<< xtag("total_promoted_sab", total_promoted_)
<< xtag("nursery_z", nursery_z_)
<< xtag("nursery_before_ckp_z", nursery_before_checkpoint_z_)
<< xtag("nursery_after_ckp_z", nursery_after_checkpoint_z_)
<< xtag("tenured_z", tenured_z_)
<< xtag("n_mutation", n_mutation_)
<< xtag("n_logged_mutation", n_logged_mutation_)
<< xtag("n_xgen_mutation", n_xgen_mutation_)
<< xtag("n_xkcp_mutation", n_xckp_mutation_)
// << xtag("per_type_stats", per_type_stats_)
<< ">";
}
@ -228,6 +254,18 @@ namespace xo {
return config_.debug_flag_;
}
GcStatisticsExt
GC::get_gc_statistics() const
{
GcStatisticsExt retval = GcStatisticsExt(this->native_gc_statistics());
retval.nursery_z_ = nursery_[role2int(role::to_space)]->size();
retval.nursery_before_checkpoint_z_ = nursery_[role2int(role::to_space)]->before_checkpoint();
retval.tenured_z_ = tenured_[role2int(role::to_space)]->size();
return retval;
}
generation_result
GC::fromspace_generation_of(const void * x) const
{

View file

@ -0,0 +1,68 @@
/* GcStatistics.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "GcStatistics.hpp"
#include "xo/indentlog/print/pretty_vector.hpp"
namespace xo {
namespace print {
bool
ppdetail<xo::gc::PerGenerationStatistics>::print_pretty(const ppindentinfo & ppii,
const xo::gc::PerGenerationStatistics & x)
{
return ppii.pps()->pretty_struct(ppii,
"PerGenerationStatistics",
refrtag("used_z", x.used_z_),
refrtag("n_gc", x.n_gc_),
refrtag("new_alloc_z", x.new_alloc_z_),
refrtag("scanned_z", x.scanned_z_),
refrtag("survive_z", x.survive_z_),
refrtag("promote_z", x.promote_z_)
);
}
bool
ppdetail<xo::gc::GcStatistics>::print_pretty(const ppindentinfo & ppii,
const xo::gc::GcStatistics & x)
{
return ppii.pps()->pretty_struct(ppii,
"GcStatistics",
refrtag("gen_v", x.gen_v_),
refrtag("total_allocated", x.total_allocated_),
refrtag("total_promoted_sab", x.total_promoted_sab_),
refrtag("total_promoted", x.total_promoted_),
refrtag("n_mutation", x.n_mutation_),
refrtag("n_logged_mutation", x.n_logged_mutation_),
refrtag("n_xgen_mutation", x.n_xgen_mutation_),
refrtag("n_xckp_mutation", x.n_xckp_mutation_)
);
}
bool
ppdetail<xo::gc::GcStatisticsExt>::print_pretty(const ppindentinfo & ppii,
const xo::gc::GcStatisticsExt & x)
{
return ppii.pps()->pretty_struct(ppii,
"GcStatisticsExt",
refrtag("gen_v", x.gen_v_),
refrtag("total_allocated", x.total_allocated_),
refrtag("total_promoted_sab", x.total_promoted_sab_),
refrtag("total_promoted", x.total_promoted_),
refrtag("n_mutation", x.n_mutation_),
refrtag("n_logged_mutation", x.n_logged_mutation_),
refrtag("n_xgen_mutation", x.n_xgen_mutation_),
refrtag("n_xckp_mutation", x.n_xckp_mutation_),
refrtag("nursery_z", x.nursery_z_),
refrtag("nursery_before_checkpoint_z", x.nursery_before_checkpoint_z_),
refrtag("nursery_after_checkpoint_z", x.nursery_after_checkpoint_z_),
refrtag("tenured_z", x.tenured_z_));
}
} /*namespace print*/
} /*namespace xo*/
/* end GcStatistics.cpp */

View file

@ -47,22 +47,22 @@ namespace xo {
REQUIRE(gc->gc_in_progress() == false);
REQUIRE(gc->is_gc_enabled() == true);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 0);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 0);
REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
/* gc with empty state */
gc->request_gc(generation::nursery);
REQUIRE(gc->gc_in_progress() == false);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0);
/* still empty state */
gc->request_gc(generation::tenured);
REQUIRE(gc->gc_in_progress() == false);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
REQUIRE(gc->gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 1);
REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1);
REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 1);
}
}