xo-allod: + per-type stats + pretty printing

This commit is contained in:
Roland Conybeare 2025-08-06 22:34:20 -05:00
commit 593dc064f9
16 changed files with 379 additions and 191 deletions

View file

@ -6,6 +6,7 @@
#pragma once
#include "IAlloc.hpp"
#include "ObjectStatistics.hpp"
namespace xo {
namespace gc {
@ -48,6 +49,9 @@ namespace xo {
std::byte * free_ptr() const { return free_ptr_; }
void set_free_ptr(std::byte * x);
void capture_object_statistics(capture_phase phase,
ObjectStatistics * p_dest) const;
// inherited from IAlloc...
virtual const std::string & name() const final override { return name_; }

View file

@ -13,6 +13,7 @@ namespace xo {
// inherited from Object..
virtual TaggedPtr self_tp() const final override;
virtual void display(std::ostream & os) const final override;
virtual bool _is_forwarded() const final override { return true; }
virtual Object * _offset_destination(Object * src) const final override;
virtual Object * _destination() final override;

View file

@ -42,6 +42,8 @@ namespace xo {
std::size_t initial_tenured_z_ = 0;
/** true to permit incremental garbage collection **/
bool allow_incremental_gc_ = true;
/** true to report statistics **/
bool stats_flag_ = false;
/** true to enable debug logging **/
bool debug_flag_ = false;
};
@ -207,6 +209,8 @@ namespace xo {
void swap_mutation_log();
/** swap roles of FromSpace/ToSpace **/
void swap_spaces(generation g);
/** scan to-space for object statistics before GC */
void capture_object_statistics(generation upto, capture_phase phase);
/** copy object **/
void copy_object(Object ** addr, generation upto, ObjectStatistics * object_stats);
/** copy everything reachable from global gc roots **/
@ -273,6 +277,10 @@ namespace xo {
/** allocation/collection counters **/
GcStatistics gc_statistics_;
/** optional per-object-type counters. snapshot at beginning of collection cycle **/
std::array<ObjectStatistics, gen2int(generation::N)> object_statistics_sab_;
/** optional per-object-type counters. snapshot at end of collection cycle **/
std::array<ObjectStatistics, gen2int(generation::N)> object_statistics_sae_;
/** trigger full GC whenever this much data arrives in tenured generation **/
std::size_t full_gc_threshold_ = 0;

View file

@ -6,20 +6,13 @@
#pragma once
#include "generation.hpp"
#include "xo/reflect/TypeDescr.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
**/
@ -103,9 +96,6 @@ namespace xo {
* (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) {

View file

@ -6,6 +6,7 @@
#pragma once
#include "IAlloc.hpp"
#include "ObjectStatistics.hpp"
#include <list>
#include <memory>
#include <cstdint>
@ -43,6 +44,14 @@ namespace xo {
/** current free pointer **/
std::byte * free_ptr() const;
/** scan space (must not contain forwarding pointers, because loses size info)
* + gather stats by object type
*
* See @ref Object::self_tp
**/
void capture_object_statistics(capture_phase phase,
ObjectStatistics * p_dest) const;
// inherited from IAlloc..
virtual const std::string & name() const final override;

View file

@ -157,6 +157,9 @@ namespace xo {
**/
virtual TaggedPtr self_tp() const = 0;
/** print on stream @p os **/
virtual void display(std::ostream & os) const = 0;
// GC support
/** true iff this object represents a forwarding pointer.
@ -244,6 +247,9 @@ namespace xo {
rhs.ptr());
}
std::ostream &
operator<< (std::ostream & os, gp<Object> x);
/** @class Cpof
* @brief argument to operator new used for garbage collector evacuation phase
*

View file

@ -0,0 +1,86 @@
/* file ObjectStatistics.hpp
*
* author: Roland Conybeare, Aug 2025
*/
#pragma once
#include "xo/indentlog/print/pretty.hpp"
#include <vector>
#include <cstdint>
namespace xo {
namespace reflect { class TypeDescrBase; }
namespace gc {
enum class capture_phase {
/** snapshot-at-beginning **/
sab,
/** snapshot-at-end **/
sae,
};
/** @class PerObjectTypeStatistics
* @brief statistics for a particular object type
*
* Gathered for each leaf type descended from xo::obj::Object.
* See @ref xo::obj::Object::self_tp
*
* See @ref GC::capture_object_statistics
* (gathers @ref scanned_n_, @ref scanned_z_)
**/
struct PerObjectTypeStatistics {
using TypeDescr = xo::reflect::TypeDescrBase const *;
void display(std::ostream & os) const;
/** stats here are for objects of this type **/
TypeDescr td_ = nullptr;
/** number of objects scanned **/
std::size_t scanned_n_ = 0;
/** number of bytes scanned **/
std::size_t scanned_z_ = 0;
/** number of objects surviving **/
std::size_t survive_n_ = 0;
/** number of bytes from surviving objects **/
std::size_t survive_z_ = 0;
};
inline std::ostream & operator<< (std::ostream & os, const PerObjectTypeStatistics & x) {
x.display(os);
return os;
}
/** @class ObjectStatistics
* @brief placeholder for type-driven allocation statistics
*
* Passed to @ref Object::deep_move for example
**/
struct ObjectStatistics {
void display(std::ostream & os) const;
/** per-object-type statistics, indexed by TypeId **/
std::vector<PerObjectTypeStatistics> per_type_stats_v_;
};
inline std::ostream & operator<< (std::ostream & os, const ObjectStatistics & x) {
x.display(os);
return os;
}
} /*namespace gc*/
namespace print {
template <>
struct ppdetail<xo::gc::PerObjectTypeStatistics> {
static bool print_pretty(const ppindentinfo &, const xo::gc::PerObjectTypeStatistics &);
};
template <>
struct ppdetail<xo::gc::ObjectStatistics> {
static bool print_pretty(const ppindentinfo &, const xo::gc::ObjectStatistics &);
};
} /*namespace print*/
} /*namespace xo*/
/* end ObjectStatistics.hpp */

View file

@ -4,6 +4,8 @@
*/
#include "ArenaAlloc.hpp"
#include "Object.hpp"
#include "ObjectStatistics.hpp"
#include "xo/indentlog/scope.hpp"
#include "xo/indentlog/print/tag.hpp"
#include <cassert>
@ -64,6 +66,43 @@ namespace xo {
}
}
void
ArenaAlloc::capture_object_statistics(capture_phase phase,
ObjectStatistics * p_dest) const
{
using xo::reflect::TaggedPtr;
std::byte * p = lo_;
while (p < free_ptr_) {
Object * obj = reinterpret_cast<Object *>(p);
TaggedPtr tp = obj->self_tp();
std::size_t z = obj->_shallow_size();
std::uint32_t id = tp.td()->id().id();
if (p_dest->per_type_stats_v_.size() < id + 1)
p_dest->per_type_stats_v_.resize(id + 1);
PerObjectTypeStatistics & dest = p_dest->per_type_stats_v_.at(id);
dest.td_ = tp.td();
switch (phase) {
case capture_phase::sab:
++dest.scanned_n_;
dest.scanned_z_ += z;
break;
case capture_phase::sae:
++dest.survive_n_;
dest.survive_z_ += z;
break;
}
p += z;
}
assert(p == free_ptr_);
}
std::size_t
ArenaAlloc::size() const {
return limit_ - lo_;

View file

@ -7,6 +7,7 @@ set(SELF_SRCS
ListAlloc.cpp
GC.cpp
GcStatistics.cpp
ObjectStatistics.cpp
Object.cpp
Forwarding1.cpp
)

View file

@ -23,6 +23,12 @@ namespace xo {
return Reflect::make_tp(const_cast<Forwarding1*>(this));
}
void
Forwarding1::display(std::ostream & os) const
{
os << "<fwd" << xtag("dest", (void*)dest_.ptr()) << ">";
}
Object *
Forwarding1::_offset_destination(Object * src) const
{

View file

@ -12,91 +12,6 @@
namespace xo {
namespace gc {
void
PerGenerationStatistics::include_gc(std::size_t alloc_z,
std::size_t before_z,
std::size_t after_z,
std::size_t promote_z)
{
this->update_snapshot(after_z);
new_alloc_z_ += alloc_z;
scanned_z_ += before_z;
survive_z_ += after_z;
promote_z_ += promote_z;
}
void
PerGenerationStatistics::update_snapshot(std::size_t after_z)
{
used_z_ = after_z;
}
void
PerGenerationStatistics::display(std::ostream & os) const
{
os << "<PerGenerationStatistics"
<< xtag("used", used_z_)
<< xtag("n_gc", n_gc_)
<< xtag("new_alloc_z", new_alloc_z_)
<< xtag("scanned_z", scanned_z_)
<< xtag("survive_z", survive_z_)
<< xtag("promote_z", promote_z_)
<< ">";
}
void
GcStatistics::include_gc(generation upto,
std::size_t alloc_z,
std::size_t before_z,
std::size_t after_z,
std::size_t promote_z)
{
gen_v_[static_cast<std::size_t>(upto)].include_gc(alloc_z, before_z, after_z, promote_z);
}
void
GcStatistics::update_snapshot(generation upto,
std::size_t after_z)
{
gen_v_[static_cast<std::size_t>(upto)].update_snapshot(after_z);
}
void
GcStatistics::display(std::ostream & os) const
{
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_)
<< ">";
}
bool
MutationLogEntry::is_child_forwarded() const
{
@ -542,6 +457,22 @@ namespace xo {
} /*swap_spaces*/
void
GC::capture_object_statistics(generation upto, capture_phase phase)
{
/* scan nursery */
this->nursery_[role2int(role::to_space)]->capture_object_statistics
(phase,
&object_statistics_sab_[gen2int(generation::nursery)]);
if (upto == generation::tenured) {
/* scan tenured */
this->tenured_[role2int(role::to_space)]->capture_object_statistics
(phase,
&object_statistics_sab_[gen2int(generation::tenured)]);
}
}
void
GC::copy_object(Object ** pp_object, generation upto, ObjectStatistics * object_stats)
{
@ -566,7 +497,7 @@ namespace xo {
GC::copy_globals(generation upto)
{
for (Object ** pp_root : gc_root_v_) {
this->copy_object(pp_root, upto, &gc_statistics_.per_type_stats_);
this->copy_object(pp_root, upto, &object_statistics_sae_[gen2int(upto)]);
}
}
@ -802,7 +733,7 @@ namespace xo {
if (upto == generation::tenured) {
log && log("TODO: forward mutation log for full GC");
} else {
this->incremental_gc_forward_mlog(&gc_statistics_.per_type_stats_);
this->incremental_gc_forward_mlog(&object_statistics_sae_[gen2int(generation::nursery)]);
}
}
@ -858,7 +789,7 @@ namespace xo {
void
GC::execute_gc(generation upto)
{
scope log(XO_DEBUG(config_.debug_flag_));
scope log(XO_DEBUG(config_.stats_flag_));
bool full_move = (upto == generation::tenured);
@ -876,7 +807,11 @@ namespace xo {
log && log(xtag("new_alloc", new_alloc));
log && log("step 1: swap to/from roles");
log && log("step 0: (optional) scan for object statistics");
this->capture_object_statistics(upto, capture_phase::sab);
log && log("step 1 : swap to/from roles");
this->swap_spaces(upto);
@ -886,18 +821,25 @@ namespace xo {
log && log("step 2b: TODO: copy pinned");
log && log("step 3: forward mutation log");
log && log("step 3 : forward mutation log");
this->forward_mutation_log(upto);
log && log("step 4: TODO: notify destructor log");
log && log("step 4 : TODO: notify destructor log");
log && log("step 5: TODO: keep reachable weak pointers");
log && log("step 5 : TODO: keep reachable weak pointers");
log && log("step 6: cleanup");
log && log("step 6 : cleanup");
this->cleanup_phase(upto);
this->capture_object_statistics(upto, capture_phase::sae);
log && log("object statistics [nursery]:");
log && log(refrtag("stats", object_statistics_sab_[gen2int(generation::nursery)]));
log && log("object statistics [tenured]:");
log && log(refrtag("stats", object_statistics_sab_[gen2int(generation::tenured)]));
this->runstate_ = GCRunstate();
log && log("statistics:");

View file

@ -7,6 +7,93 @@
#include "xo/indentlog/print/pretty_vector.hpp"
namespace xo {
namespace gc {
void
PerGenerationStatistics::include_gc(std::size_t alloc_z,
std::size_t before_z,
std::size_t after_z,
std::size_t promote_z)
{
this->update_snapshot(after_z);
new_alloc_z_ += alloc_z;
scanned_z_ += before_z;
survive_z_ += after_z;
promote_z_ += promote_z;
}
void
PerGenerationStatistics::update_snapshot(std::size_t after_z)
{
used_z_ = after_z;
}
void
PerGenerationStatistics::display(std::ostream & os) const
{
os << "<PerGenerationStatistics"
<< rtag("used", used_z_)
<< rtag("n_gc", n_gc_)
<< rtag("new_alloc_z", new_alloc_z_)
<< rtag("scanned_z", scanned_z_)
<< rtag("survive_z", survive_z_)
<< rtag("promote_z", promote_z_)
<< ">";
}
void
GcStatistics::include_gc(generation upto,
std::size_t alloc_z,
std::size_t before_z,
std::size_t after_z,
std::size_t promote_z)
{
gen_v_[static_cast<std::size_t>(upto)].include_gc(alloc_z, before_z, after_z, promote_z);
}
void
GcStatistics::update_snapshot(generation upto,
std::size_t after_z)
{
gen_v_[static_cast<std::size_t>(upto)].update_snapshot(after_z);
}
void
GcStatistics::display(std::ostream & os) const
{
os << "<GcStatistics"
<< rtag("gen_v", gen_v_)
<< rtag("total_allocated", total_allocated_)
<< rtag("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"
<< rtag("gen_v", gen_v_)
<< rtag("total_allocated", total_allocated_)
<< rtag("total_promoted_sab", total_promoted_)
<< rtag("nursery_z", nursery_z_)
<< rtag("nursery_before_ckp_z", nursery_before_checkpoint_z_)
<< rtag("nursery_after_ckp_z", nursery_after_checkpoint_z_)
<< rtag("tenured_z", tenured_z_)
<< rtag("n_mutation", n_mutation_)
<< rtag("n_logged_mutation", n_logged_mutation_)
<< rtag("n_xgen_mutation", n_xgen_mutation_)
<< rtag("n_xkcp_mutation", n_xckp_mutation_)
// << xtag("per_type_stats", per_type_stats_)
<< ">";
}
} /*namespace gc*/
namespace print {
bool
ppdetail<xo::gc::PerGenerationStatistics>::print_pretty(const ppindentinfo & ppii,

View file

@ -50,6 +50,17 @@ namespace xo {
return retval;
}
void
ListAlloc::capture_object_statistics(capture_phase phase,
ObjectStatistics * p_dest) const
{
hd_->capture_object_statistics(phase, p_dest);
for (const auto & arena : full_l_)
arena->capture_object_statistics(phase, p_dest);
}
const std::string &
ListAlloc::name() const {
if (hd_) {

View file

@ -191,6 +191,18 @@ namespace xo {
(void)fwd;
}
std::ostream &
operator<< (std::ostream & os, gp<Object> x)
{
if (x.ptr()) {
x->display(os);
} else {
os << "<nullptr>";
}
return os;
}
} /*namespace xo*/
/* end Object.cpp*/

View file

@ -0,0 +1,73 @@
/* file ObjectStatistics.cpp
*
* author: Roland Conybeare, Aug 2025
*/
#include "ObjectStatistics.hpp"
#include "xo/reflect/TypeDescr.hpp"
#include "xo/indentlog/print/pretty_vector.hpp"
namespace xo {
namespace gc {
void
PerObjectTypeStatistics::display(std::ostream & os) const
{
os << "<PerObjectTypeStatistics";
if (td_)
os << rtag("td", td_->short_name());
else
os << rtag("td", "nullptr");
os << rtag("scanned_n", scanned_n_)
<< rtag("scanned_z", scanned_z_)
<< rtag("survive_n", survive_n_)
<< rtag("survive_z", survive_z_)
<< ">";
}
void
ObjectStatistics::display(std::ostream & os) const
{
os << "<ObjectStatistics";
std::size_t i = 0;
for (const auto & x : per_type_stats_v_) {
os << " :[" << i << "] " << x;
}
os << ">";
}
} /*namespace gc*/
namespace print {
bool
ppdetail<xo::gc::PerObjectTypeStatistics>::print_pretty(const ppindentinfo & ppii,
const xo::gc::PerObjectTypeStatistics & x)
{
static constexpr std::string_view c_nullptr_str = "nullptr";
if (x.td_) {
return ppii.pps()->pretty_struct(ppii,
"PerObjectTypeStatistics",
refrtag("td", x.td_ ? x.td_->short_name() : c_nullptr_str),
refrtag("scanned_n", x.scanned_n_),
refrtag("scanned_z", x.scanned_z_),
refrtag("survive_n", x.survive_n_),
refrtag("survive_z", x.survive_z_));
} else {
/* print nothing -- empty struct */
return true;
}
}
bool
ppdetail<xo::gc::ObjectStatistics>::print_pretty(const ppindentinfo & ppii,
const xo::gc::ObjectStatistics & x)
{
return ppii.pps()->pretty_struct(ppii,
"ObjectTypeStatistics",
refrtag("per_type_stats_v", x.per_type_stats_v_));
}
} /*namespace gc*/
} /*namespace xo*/
/* end ObjectStatistics.cpp */

View file

@ -1,87 +0,0 @@
/* @file LinearAlloc.test.cpp
*
* author: Roland Conybeare, Jul 2025
*/
#include "xo/alloc/ArenaAlloc.hpp"
#include <catch2/catch.hpp>
namespace xo {
using xo::gc::LinearAlloc;
namespace ut {
namespace {
struct testcase_alloc {
testcase_alloc(std::size_t rz, std::size_t z)
: redline_z_{rz}, arena_z_{z} {}
std::size_t redline_z_;
std::size_t arena_z_;
};
std::vector<testcase_alloc>
s_testcase_v = {
testcase_alloc(0, 4096)
};
}
TEST_CASE("linearalloc", "[alloc]")
{
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
const testcase_alloc & tc = s_testcase_v[i_tc];
constexpr bool c_debug_flag = false;
auto alloc = LinearAlloc::make("linearalloc", tc.redline_z_, tc.arena_z_, c_debug_flag);
REQUIRE(alloc.get());
REQUIRE(alloc->name() == "linearalloc");
REQUIRE(alloc->size() == tc.arena_z_);
REQUIRE(alloc->available() == tc.arena_z_);
REQUIRE(alloc->allocated() == 0);
REQUIRE(alloc->is_before_checkpoint(alloc->free_ptr()) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == 0);
auto free0 = alloc->free_ptr();
auto mem = alloc->alloc(tc.arena_z_);
REQUIRE(mem != nullptr);
REQUIRE(mem == free0);
REQUIRE(alloc->size() == tc.arena_z_);
REQUIRE(alloc->available() == 0);
REQUIRE(alloc->allocated() == tc.arena_z_);
REQUIRE(alloc->is_before_checkpoint(mem) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == tc.arena_z_);
alloc->clear();
REQUIRE(alloc->free_ptr() == free0);
REQUIRE(alloc->available() == tc.arena_z_);
REQUIRE(alloc->allocated() == 0);
REQUIRE(alloc->is_before_checkpoint(free0) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == 0);
mem = alloc->alloc(1);
auto used = sizeof(void*);
REQUIRE(alloc->size() == tc.arena_z_);
REQUIRE(alloc->available() == tc.arena_z_ - used);
REQUIRE(alloc->allocated() == used);
REQUIRE(alloc->is_before_checkpoint(free0) == false);
REQUIRE(alloc->before_checkpoint() == 0);
REQUIRE(alloc->after_checkpoint() == used);
}
}
} /*namespace ut */
} /*namespace xo*/