xo-object: generative GC utest + reinstate coverage build
This commit is contained in:
parent
14ff9f9592
commit
e80304a09b
18 changed files with 625 additions and 28 deletions
13
README.md
13
README.md
|
|
@ -75,6 +75,19 @@ $ nix-build -A xo-userenv-slow
|
|||
Same result as `$nix-build -A xo-userenv`, but builds each package serially
|
||||
using `xo-build`.
|
||||
|
||||
### Coverage Build
|
||||
|
||||
Prepare build
|
||||
```
|
||||
# phase 2
|
||||
$ cmake -B .build -S . -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_BUILD_TYPE=Debug -DCODE_COVERAGE=ON
|
||||
```
|
||||
|
||||
Build coverage-enabled libraries and executables
|
||||
```
|
||||
$ (cd .build && make ccov)
|
||||
```
|
||||
|
||||
## To view docs from WSL
|
||||
|
||||
1. find wsl IP address
|
||||
|
|
|
|||
|
|
@ -92,6 +92,11 @@ Aternatively can enter nix environment, then follow instructions for cmake build
|
|||
# etc
|
||||
|
||||
|
||||
coverage build
|
||||
--------------
|
||||
|
||||
See ``Test Coverage Setup`` under ``Development`` below
|
||||
|
||||
Development
|
||||
===========
|
||||
|
||||
|
|
@ -109,6 +114,30 @@ To setup xo-umbrella2 build to work with a language server:
|
|||
In this case subsystem LSP setup should be omitted, git root is ``path/to/xo-umbrella2``,
|
||||
not ``path/to/xo-umbrella2/xo-ratio`` etc.
|
||||
|
||||
Test Coverage Setup
|
||||
-------------------
|
||||
|
||||
To setup a unit test coverage build/ccov/all-merged
|
||||
|
||||
.. code-block::
|
||||
|
||||
# can reuse phase 1 cmake-macros-install
|
||||
|
||||
# phase 2
|
||||
$ cmake -B .build -S . -DCMAKE_INSTALL_PREFIX=$PREFIX -DCMAKE_BUILD_TYPE=coverage
|
||||
|
||||
Then run unit tests
|
||||
|
||||
$ (cd .build && ctest)
|
||||
|
||||
To build coverage report
|
||||
|
||||
$ (.build/gen-ccov)
|
||||
|
||||
Html report in ``.build/ccov/html/index.html``
|
||||
|
||||
|
||||
|
||||
Sphinx Autobuild Setup
|
||||
----------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ namespace xo {
|
|||
public:
|
||||
/** create new GC instance with configuration @p config **/
|
||||
explicit GC(const Config & config);
|
||||
virtual ~GC();
|
||||
|
||||
/** create GC allocator.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ namespace xo {
|
|||
|
||||
if (lo_ <= x && x < limit_) {
|
||||
this->free_ptr_ = x;
|
||||
if (this->checkpoint_ > free_ptr_)
|
||||
this->checkpoint_ = free_ptr_;
|
||||
} else {
|
||||
throw std::runtime_error(tostr("LinearAllog::set_free_ptr(x): expected lo <= x < limit",
|
||||
xtag("lo", lo_), xtag("x", x), xtag("limit", limit_)));
|
||||
|
|
@ -102,8 +104,7 @@ namespace xo {
|
|||
void
|
||||
ArenaAlloc::clear()
|
||||
{
|
||||
this->checkpoint_ = lo_;
|
||||
this->free_ptr_ = lo_;
|
||||
this->set_free_ptr(lo_);
|
||||
this->limit_ = hi_ - redline_z_;
|
||||
}
|
||||
|
||||
|
|
@ -133,14 +134,14 @@ namespace xo {
|
|||
|
||||
std::byte * retval = this->free_ptr_;
|
||||
|
||||
this->free_ptr_ += z1;
|
||||
|
||||
log && log(xtag("self", name_), xtag("z0", z0), xtag("+pad", dz), xtag("z1", z1));
|
||||
|
||||
if (free_ptr_ > limit_) {
|
||||
if (free_ptr_ + z1 > limit_) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
this->free_ptr_ += z1;
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,23 +26,29 @@ namespace xo {
|
|||
return dest_.ptr();
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START
|
||||
std::size_t
|
||||
Forwarding1::_shallow_size() const {
|
||||
assert(false);
|
||||
return 0;
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
// LCOV_EXCL_START
|
||||
Object *
|
||||
Forwarding1::_shallow_copy() const {
|
||||
assert(false);
|
||||
return nullptr;
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
// LCOV_EXCL_START
|
||||
std::size_t
|
||||
Forwarding1::_forward_children() {
|
||||
assert(false);
|
||||
return 0;
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
} /*namespace obj*/
|
||||
} /*namespace xo*/
|
||||
|
|
|
|||
|
|
@ -141,6 +141,21 @@ namespace xo {
|
|||
this->checkpoint();
|
||||
}
|
||||
|
||||
GC::~GC() {
|
||||
/* hygiene */
|
||||
this->clear();
|
||||
|
||||
nursery_[role2int(role::from_space)].reset();
|
||||
nursery_[role2int(role::to_space) ].reset();
|
||||
|
||||
tenured_[role2int(role::from_space)].reset();
|
||||
tenured_[role2int(role::to_space) ].reset();
|
||||
|
||||
mutation_log_[role2int(role::from_space)].reset();
|
||||
mutation_log_[role2int(role::to_space) ].reset();
|
||||
defer_mutation_log_.reset();
|
||||
}
|
||||
|
||||
up<GC>
|
||||
GC::make(const Config & config)
|
||||
{
|
||||
|
|
@ -245,8 +260,10 @@ namespace xo {
|
|||
return nursery_[role2int(role::to_space)]->free_ptr();
|
||||
case generation::tenured:
|
||||
return tenured_[role2int(role::to_space)]->free_ptr();
|
||||
// LCOV_EXCL_START
|
||||
case generation::N:
|
||||
assert(false);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
|
@ -647,7 +664,7 @@ namespace xo {
|
|||
|
||||
Object * parent_to = from_entry.parent_destination();
|
||||
|
||||
log(xtag("parent_to", (void*)parent_to));
|
||||
log && log(xtag("parent_to", (void*)parent_to));
|
||||
|
||||
assert(tospace_generation_of(parent_to) == generation_result::tenured);
|
||||
|
||||
|
|
|
|||
|
|
@ -47,12 +47,14 @@ namespace xo {
|
|||
*lhs = rhs;
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START
|
||||
std::byte *
|
||||
IAlloc::alloc_gc_copy(std::size_t /*z*/, const void * /*src*/)
|
||||
{
|
||||
assert(false);
|
||||
return nullptr;
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
} /*namespace gc*/
|
||||
} /*namespace xo*/
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
set(UTEST_EXE utest.alloc)
|
||||
set(UTEST_SRCS
|
||||
alloc_utest_main.cpp
|
||||
IAlloc.test.cpp
|
||||
ArenaAlloc.test.cpp
|
||||
GC.test.cpp)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ namespace xo {
|
|||
/** the empty list. unique sentinel object **/
|
||||
static gp<List> nil;
|
||||
|
||||
/** @return non-null iff @p x is actually a List cell (or nil) **/
|
||||
static gp<List> from(gp<Object> x);
|
||||
|
||||
/** @return list with first element @p car, and tail @p cdr **/
|
||||
static gp<List> cons(gp<Object> car, gp<List> cdr);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ namespace xo {
|
|||
namespace obj {
|
||||
class String : public Object {
|
||||
public:
|
||||
enum Owner { unique, shared };
|
||||
enum class owner { unique, shared };
|
||||
|
||||
/** donwcast from @p x iff x is actually a String. Otherwise nullptr **/
|
||||
static gp<String> from(gp<Object> x);
|
||||
|
||||
/** create shared string @p s, using allocator @ref Object::mm **/
|
||||
static gp<String> share(const char * s);
|
||||
/** create copy of string @p s, using allocator @ref Object::mm **/
|
||||
static gp<String> copy(const char * s);
|
||||
/** create copy of string @p s, using allocator @p mm **/
|
||||
|
|
@ -35,14 +37,14 @@ namespace xo {
|
|||
virtual std::size_t _forward_children() override;
|
||||
|
||||
private:
|
||||
String(Owner owner, std::size_t z, char * s);
|
||||
String(owner owner, std::size_t z, char * s);
|
||||
/** create instance, copying string contents (when @p copy_flag is true) using allocator @p mm **/
|
||||
String(gc::IAlloc * mm, Owner owner, std::size_t z, char * s, bool copy);
|
||||
String(gc::IAlloc * mm, owner owner, std::size_t z, char * s);
|
||||
|
||||
private:
|
||||
/** true iff storage in @ref chars_ is owned by this String.
|
||||
**/
|
||||
Owner owner_ = Owner::shared;
|
||||
owner owner_ = owner::shared;
|
||||
/** length of @ref chars_ in bytes (storage allocated, not necessarily string length) **/
|
||||
std::size_t z_chars_ = 0;
|
||||
/** string contents. always null-terminated **/
|
||||
|
|
|
|||
|
|
@ -19,15 +19,29 @@ namespace xo {
|
|||
return s_boolean_v[static_cast<std::size_t>(x)];
|
||||
}
|
||||
|
||||
gp<Boolean>
|
||||
Boolean::true_obj()
|
||||
{
|
||||
return boolean_obj(true);
|
||||
}
|
||||
|
||||
gp<Boolean>
|
||||
Boolean::false_obj()
|
||||
{
|
||||
return boolean_obj(false);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Boolean::_shallow_size() const
|
||||
{
|
||||
return sizeof(Boolean);
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START
|
||||
Object *
|
||||
Boolean::_shallow_copy() const
|
||||
{
|
||||
|
||||
/* Boolean instances not created in GC-owned space,
|
||||
* so GC will not traverse them.
|
||||
*
|
||||
|
|
@ -38,14 +52,18 @@ namespace xo {
|
|||
|
||||
assert(false);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
// LCOV_EXCL_START
|
||||
std::size_t
|
||||
Boolean::_forward_children()
|
||||
{
|
||||
assert(false);
|
||||
return 0;
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
} /*namespace xo*/
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ namespace xo {
|
|||
gp<List>
|
||||
List::nil = new List(nullptr, nullptr);
|
||||
|
||||
gp<List>
|
||||
List::from(gp<Object> x) {
|
||||
return dynamic_cast<List *>(x.ptr());
|
||||
}
|
||||
|
||||
gp<List>
|
||||
List::cons(gp<Object> car, gp<List> cdr) {
|
||||
return new (MMPtr(mm)) List(car, cdr);
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@
|
|||
|
||||
namespace xo {
|
||||
namespace obj {
|
||||
String::String(Owner owner, std::size_t z, char * s)
|
||||
String::String(owner owner, std::size_t z, char * s)
|
||||
: owner_{owner}, z_chars_{z}, chars_{s}
|
||||
{}
|
||||
|
||||
String::String(gc::IAlloc * mm, Owner owner, std::size_t z, char * s, bool copy)
|
||||
String::String(gc::IAlloc * mm, owner owner, std::size_t z, char * s)
|
||||
: owner_{owner}, z_chars_{z}
|
||||
{
|
||||
if (copy) {
|
||||
if (owner_ == owner::unique) {
|
||||
chars_ = reinterpret_cast<char *>(mm->alloc(z));
|
||||
|
||||
assert(chars_);
|
||||
|
|
@ -35,6 +35,14 @@ namespace xo {
|
|||
return dynamic_cast<String*>(x.ptr());
|
||||
}
|
||||
|
||||
gp<String>
|
||||
String::share(const char * s) {
|
||||
const char * chars = s ? s : "";
|
||||
std::size_t z = 1 + ::strlen(chars);
|
||||
|
||||
return new (MMPtr(mm)) String(mm, owner::shared, z, const_cast<char *>(chars));
|
||||
}
|
||||
|
||||
gp<String>
|
||||
String::copy(const char * s) {
|
||||
return copy(Object::mm, s);
|
||||
|
|
@ -47,13 +55,13 @@ namespace xo {
|
|||
const char * chars = s ? s : "";
|
||||
|
||||
// const-cast ok since chars copied with Owner::unique
|
||||
return new (MMPtr(mm)) String(mm, Owner::unique, z, const_cast<char *>(chars), true /*copy*/);
|
||||
return new (MMPtr(mm)) String(mm, owner::unique, z, const_cast<char *>(chars));
|
||||
}
|
||||
|
||||
gp<String>
|
||||
String::allocate(std::size_t z)
|
||||
{
|
||||
return new (MMPtr(Object::mm)) String(mm, Owner::unique, z, const_cast<char *>(""), true /*copy*/);
|
||||
return new (MMPtr(Object::mm)) String(mm, owner::unique, z, const_cast<char *>(""));
|
||||
}
|
||||
|
||||
gp<String>
|
||||
|
|
@ -63,10 +71,11 @@ namespace xo {
|
|||
std::size_t z2 = s2->length();
|
||||
std::size_t z = z1 + z2;
|
||||
|
||||
gp<String> retval = allocate(z);
|
||||
// +1 for null terminator
|
||||
gp<String> retval = allocate(z+1);
|
||||
|
||||
strlcpy(retval->chars_, s1->chars_, z1);
|
||||
strlcpy(retval->chars_ + z1, s2->chars_, z2);
|
||||
strlcpy(retval->chars_, s1->chars_, z1+1);
|
||||
strlcpy(retval->chars_ + z1, s2->chars_, z2+1);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
|
@ -87,7 +96,7 @@ namespace xo {
|
|||
*/
|
||||
std::size_t retval = gc::IAlloc::with_padding(sizeof(String));
|
||||
|
||||
if (owner_ == Owner::unique)
|
||||
if (owner_ == owner::unique)
|
||||
retval += gc::IAlloc::with_padding(z_chars_);
|
||||
|
||||
return retval;
|
||||
|
|
@ -109,7 +118,7 @@ namespace xo {
|
|||
//
|
||||
gp<String> copy = new (cpof) String(owner_, z_chars_, chars_);
|
||||
|
||||
if (owner_ == Owner::unique) {
|
||||
if (owner_ == owner::unique) {
|
||||
std::byte * mem = reinterpret_cast<std::byte *>(chars_);
|
||||
|
||||
copy->chars_ = reinterpret_cast<char *>(Object::mm->alloc_gc_copy(z_chars_, mem));
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
# build unittest object/utest
|
||||
|
||||
set(SELF_EXE utest.object)
|
||||
set(SELF_SRCS
|
||||
set(UTEST_EXE utest.object)
|
||||
set(UTEST_SRCS
|
||||
object_utest_main.cpp
|
||||
Boolean.test.cpp
|
||||
String.test.cpp
|
||||
List.test.cpp
|
||||
GC.test.cpp)
|
||||
|
||||
xo_add_utest_executable(${SELF_EXE} ${SELF_SRCS})
|
||||
xo_self_dependency(${SELF_EXE} xo_object)
|
||||
xo_external_target_dependency(${SELF_EXE} Catch2 Catch2::Catch2)
|
||||
xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS})
|
||||
xo_self_dependency(${UTEST_EXE} xo_object)
|
||||
xo_dependency(${UTEST_EXE} randomgen)
|
||||
xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,12 @@
|
|||
#include "xo/alloc/GC.hpp"
|
||||
#include "xo/object/List.hpp"
|
||||
#include "xo/object/Integer.hpp"
|
||||
#include "xo/randomgen/random_seed.hpp"
|
||||
#include "xo/randomgen/xoshiro256.hpp"
|
||||
#include "xo/indentlog/scope.hpp"
|
||||
#include "xo/indentlog/print/tag.hpp"
|
||||
#include <catch2/catch.hpp>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace xo {
|
||||
using xo::obj::List;
|
||||
|
|
@ -15,6 +20,9 @@ namespace xo {
|
|||
using xo::gc::generation_result;
|
||||
using xo::gc::generation;
|
||||
|
||||
using xo::rng::Seed;
|
||||
using xo::rng::xoshiro256ss;
|
||||
|
||||
namespace ut {
|
||||
|
||||
// Also see GC unit tests in xo-alloc/utest
|
||||
|
|
@ -36,7 +44,7 @@ namespace xo {
|
|||
{
|
||||
.initial_nursery_z_ = 1024,
|
||||
.initial_tenured_z_ = 2048,
|
||||
.debug_flag_ = true
|
||||
.debug_flag_ = false
|
||||
});
|
||||
|
||||
REQUIRE(gc->gc_statistics().n_mutation_ == 0);
|
||||
|
|
@ -49,15 +57,24 @@ namespace xo {
|
|||
|
||||
gp<List> l = List::list(Integer::make(1));
|
||||
gc->add_gc_root(reinterpret_cast<Object**>(l.ptr_address()));
|
||||
|
||||
gp<List> l2 = List::list(Integer::make(10));
|
||||
gc->add_gc_root(reinterpret_cast<Object**>(l2.ptr_address()));
|
||||
|
||||
{
|
||||
REQUIRE(l->size() == 1);
|
||||
REQUIRE(l2->size() == 1);
|
||||
|
||||
REQUIRE(gc->tospace_generation_of(l.ptr()) == generation_result::nursery);
|
||||
REQUIRE(gc->tospace_generation_of(l->head().ptr()) == generation_result::nursery);
|
||||
|
||||
REQUIRE(gc->is_before_checkpoint(l.ptr()) == false);
|
||||
REQUIRE(gc->is_before_checkpoint(l->head().ptr()) == false);
|
||||
|
||||
REQUIRE(gc->tospace_generation_of(l2.ptr()) == generation_result::nursery);
|
||||
REQUIRE(gc->tospace_generation_of(l2->head().ptr()) == generation_result::nursery);
|
||||
REQUIRE(gc->is_before_checkpoint(l2.ptr()) == false);
|
||||
REQUIRE(gc->is_before_checkpoint(l2->head().ptr()) == false);
|
||||
|
||||
REQUIRE(gc->mlog_size() == 0);
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +155,358 @@ namespace xo {
|
|||
REQUIRE(gc->mlog_size() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
enum class object_type {
|
||||
nil,
|
||||
integer,
|
||||
cons,
|
||||
};
|
||||
|
||||
struct ObjectModel {
|
||||
/* 1:1 with address */
|
||||
std::size_t index_;
|
||||
/* nil|integer|cons */
|
||||
object_type type_;
|
||||
/* value for model of Integer::value_, if type_ is object_type::integer */
|
||||
std::size_t int_value_;
|
||||
/* index# for model of List::head_, if type_ is object_type::list */
|
||||
std::size_t head_ix_;
|
||||
/* index# for model of List::rest_, if type_ is object_type::list */
|
||||
std::size_t rest_ix_;
|
||||
};
|
||||
|
||||
struct ObjectGraphModel {
|
||||
/**
|
||||
* @param from_graph
|
||||
* @param from_ix
|
||||
* @param to_graph
|
||||
* @param to_ix
|
||||
* @param p_visited_set
|
||||
**/
|
||||
static bool verify_equal_aux(ObjectGraphModel & from_graph,
|
||||
std::size_t from_ix,
|
||||
ObjectGraphModel & to_graph,
|
||||
std::size_t to_ix,
|
||||
std::unordered_set<std::uintptr_t> * p_visited_set);
|
||||
|
||||
/** compare models for structural equivalence; will be comparing before/after a garbage collection cycle
|
||||
* @param from_graph model before GC
|
||||
* @param to_graph model after GC
|
||||
* @return true iff models are equivalent
|
||||
**/
|
||||
static bool verify_equal_models(ObjectGraphModel & from_model, ObjectGraphModel & to_model);
|
||||
|
||||
/* build model for object graph from a vector of object pointers */
|
||||
void from_root_vector(const std::vector<gp<Object>> & object_v);
|
||||
/* include everything reachable from @p x in this object model */
|
||||
std::size_t traverse_from_object(gp<Object> x);
|
||||
|
||||
/* one node per xo::Object instance. */
|
||||
std::vector<ObjectModel> nodes_;
|
||||
/* map from root index to node index number */
|
||||
std::vector<std::size_t> roots_;
|
||||
/* map from (original) address to index number */
|
||||
std::unordered_map<std::uintptr_t, std::size_t> addr2node_map_;
|
||||
};
|
||||
|
||||
bool
|
||||
ObjectGraphModel::verify_equal_aux(ObjectGraphModel & from_graph,
|
||||
std::size_t from_ix,
|
||||
ObjectGraphModel & to_graph,
|
||||
std::size_t to_ix,
|
||||
std::unordered_set<std::size_t> * p_visited_set)
|
||||
{
|
||||
if (p_visited_set->contains(from_ix))
|
||||
return true;
|
||||
|
||||
const ObjectModel & from = from_graph.nodes_.at(from_ix);
|
||||
const ObjectModel & to = from_graph.nodes_.at(to_ix);
|
||||
|
||||
REQUIRE(from.type_ == to.type_);
|
||||
|
||||
p_visited_set->insert(from.index_);
|
||||
|
||||
if (from.type_ == object_type::nil)
|
||||
return true;
|
||||
|
||||
if (from.type_ == object_type::integer) {
|
||||
REQUIRE(from.int_value_ == to.int_value_);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (from.type_ == object_type::cons) {
|
||||
return (verify_equal_aux(from_graph, from.head_ix_,
|
||||
to_graph, to.head_ix_,
|
||||
p_visited_set)
|
||||
&& verify_equal_aux(from_graph, from.rest_ix_,
|
||||
to_graph, to.rest_ix_,
|
||||
p_visited_set));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
ObjectGraphModel::verify_equal_models(ObjectGraphModel & from_model,
|
||||
ObjectGraphModel & to_model)
|
||||
{
|
||||
REQUIRE(from_model.roots_.size() == to_model.nodes_.size());
|
||||
REQUIRE(from_model.nodes_.size() == to_model.nodes_.size());
|
||||
|
||||
std::unordered_set<std::uintptr_t> visited_set;
|
||||
|
||||
for (std::size_t i = 0, n = from_model.roots_.size(); i < n; ++i) {
|
||||
INFO(tostr(xtag("i", i), xtag("n", n)));
|
||||
|
||||
REQUIRE(verify_equal_aux(from_model,
|
||||
from_model.roots_.at(i),
|
||||
to_model,
|
||||
to_model.roots_.at(i),
|
||||
&visited_set));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
ObjectGraphModel::traverse_from_object(gp<Object> x)
|
||||
{
|
||||
std::uintptr_t x_addr = reinterpret_cast<std::uintptr_t>(x.ptr());
|
||||
|
||||
auto addr2node_ix = addr2node_map_.find(x_addr);
|
||||
|
||||
if (addr2node_ix != addr2node_map_.end()) {
|
||||
/* already imported (or import on call stack) */
|
||||
|
||||
return addr2node_ix->second;
|
||||
} else {
|
||||
ObjectModel new_model;
|
||||
auto x_int = Integer::from(x);
|
||||
auto x_list = List::from(x);
|
||||
|
||||
std::size_t new_index = this->nodes_.size();
|
||||
{
|
||||
if (x_int.is_null() && x_list.is_null())
|
||||
throw std::runtime_error("expecting object graph containing int|cons only");
|
||||
|
||||
if (!x_int.is_null()) {
|
||||
new_model.index_ = new_index;
|
||||
new_model.type_ = object_type::integer;
|
||||
new_model.int_value_ = x_int->value();
|
||||
new_model.head_ix_ = 0;
|
||||
new_model.rest_ix_ = 0;
|
||||
}
|
||||
|
||||
if (!x_list.is_null()) {
|
||||
|
||||
if (x_list->is_nil()) {
|
||||
new_model.index_ = 0;
|
||||
new_model.type_ = object_type::nil;
|
||||
new_model.int_value_ = 0;
|
||||
new_model.head_ix_ = 0;
|
||||
new_model.rest_ix_ = 0;
|
||||
} else {
|
||||
new_model.index_ = new_index;
|
||||
new_model.type_ = object_type::cons;
|
||||
new_model.int_value_ = 0;
|
||||
/* fill below */
|
||||
new_model.head_ix_ = 0;
|
||||
new_model.rest_ix_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->nodes_.push_back(new_model);
|
||||
this->addr2node_map_[x_addr] = new_index;
|
||||
|
||||
if (!x_list.is_null() && !(x_list->is_nil())) {
|
||||
ObjectModel & model = this->nodes_.at(new_index);
|
||||
|
||||
model.head_ix_ = traverse_from_object(x_list->head());
|
||||
model.rest_ix_ = traverse_from_object(x_list->rest());
|
||||
}
|
||||
|
||||
return new_index;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ObjectGraphModel::from_root_vector(const std::vector<gp<Object>> & root_v)
|
||||
{
|
||||
assert(nodes_.empty());
|
||||
assert(addr2node_map_.empty());
|
||||
|
||||
/* sentinel = List::nil */
|
||||
{
|
||||
ObjectModel sentinel;
|
||||
sentinel.index_ = 0;
|
||||
sentinel.type_ = object_type::nil;
|
||||
sentinel.int_value_ = 0;
|
||||
sentinel.head_ix_ = 0;
|
||||
sentinel.rest_ix_ = 0;
|
||||
|
||||
this->nodes_.push_back(sentinel);
|
||||
}
|
||||
|
||||
/* it's possible that object_v is complete.
|
||||
* seed model by importing all the nodes in object_v[]
|
||||
*/
|
||||
for (gp<Object> x : root_v)
|
||||
this->roots_.push_back(traverse_from_object(x));
|
||||
}
|
||||
|
||||
struct testcase_stresstest {
|
||||
testcase_stresstest(std::size_t nz, std::size_t tz, std::size_t m, std::size_t n, std::size_t r, std::size_t k, bool debug_flag)
|
||||
: nursery_z_{nz}, tenured_z_{tz}, m_{m}, n_{n}, r_{r}, k_{k}, debug_flag_{debug_flag}
|
||||
{}
|
||||
|
||||
std::size_t nursery_z_;
|
||||
std::size_t tenured_z_;
|
||||
|
||||
/* #of random list cells to create */
|
||||
std::size_t m_;
|
||||
/* #of random integers to create */
|
||||
std::size_t n_;
|
||||
/* #of gc roots to create */
|
||||
std::size_t r_;
|
||||
/* #of random mutations */
|
||||
std::size_t k_;
|
||||
|
||||
bool debug_flag_;
|
||||
};
|
||||
|
||||
std::vector<testcase_stresstest> s_testcase_v =
|
||||
{
|
||||
testcase_stresstest(1024, 1024, 3, 7, 5, 10, true)
|
||||
};
|
||||
} /*namespace*/
|
||||
|
||||
TEST_CASE("gc-stresstest", "[alloc][gc][gc_mutation]")
|
||||
{
|
||||
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
|
||||
const testcase_stresstest & tc = s_testcase_v[i_tc];
|
||||
|
||||
scope log(XO_DEBUG(tc.debug_flag_));
|
||||
|
||||
up<GC> gc = GC::make(
|
||||
{
|
||||
.initial_nursery_z_ = tc.nursery_z_,
|
||||
.initial_tenured_z_ = tc.tenured_z_,
|
||||
.debug_flag_ = tc.debug_flag_
|
||||
});
|
||||
|
||||
REQUIRE(gc->gc_statistics().n_mutation_ == 0);
|
||||
REQUIRE(gc->gc_statistics().n_logged_mutation_ == 0);
|
||||
|
||||
REQUIRE(gc.get());
|
||||
|
||||
/* use gc for all Object allocs */
|
||||
Object::mm = gc.get();
|
||||
|
||||
// Plan:
|
||||
// - create vector of m cons cells w1[].
|
||||
// - prepend w1[] to a vector of n integers; call this w2[].
|
||||
// - create vector root_v[] of r gc roots. Assign each root_v[j] to some random w2[i]
|
||||
// - make some random mutations.
|
||||
// - traverse root_v[] to construct model from_model for reachable objects
|
||||
// - run gc
|
||||
// - traverse root_v[] again, to construct to_model for eachable objects
|
||||
// - verify from_model ~=~ to_model
|
||||
|
||||
uint64_t seed = 8365237040761243362UL;
|
||||
//Seed<xoshiro256ss> seed; // to seed from /dev/random
|
||||
//std::cerr << "seed=" << seed << std::endl;
|
||||
auto rgen = xoshiro256ss(seed);
|
||||
|
||||
/* create m random list cells */
|
||||
size_t m = tc.m_;
|
||||
/* create n random integers */
|
||||
size_t n = tc.n_;
|
||||
/* #of roots */
|
||||
size_t r = tc.r_;
|
||||
/* #of random mutations */
|
||||
size_t k = tc.k_;
|
||||
|
||||
REQUIRE(m > 0);
|
||||
REQUIRE(n > 0);
|
||||
|
||||
/* w1[] contains some random list cells */
|
||||
std::vector<gp<List>> w1;
|
||||
{
|
||||
for (size_t i = 0; i < m; ++i) {
|
||||
w1.push_back(List::cons(List::nil, List::nil));
|
||||
}
|
||||
REQUIRE(w1.size() == m);
|
||||
}
|
||||
|
||||
/* w2[] has all of w1[], also contains some integers */
|
||||
std::vector<gp<Object>> w2;
|
||||
{
|
||||
std::copy(w1.begin(), w1.end(), std::back_inserter(w2));
|
||||
for (size_t j = 0; j < n; ++j) {
|
||||
w2.push_back(Integer::make(j));
|
||||
}
|
||||
REQUIRE(w2.size() == m + n);
|
||||
}
|
||||
|
||||
/* create some random roots. always pick at least one list cell */
|
||||
std::vector<gp<Object>> root_v;
|
||||
std::size_t w1_ix = rgen() % n;
|
||||
{
|
||||
root_v.push_back(w2.at(w1_ix));
|
||||
for (std::size_t i = 1; i < r; ++i) {
|
||||
std::size_t w2_ix = rgen() % (m + n);
|
||||
|
||||
root_v.push_back(w2.at(w2_ix));
|
||||
}
|
||||
|
||||
for (auto & root : root_v)
|
||||
gc->add_gc_root(root.ptr_address());
|
||||
}
|
||||
|
||||
/* random mutations -- these will get logged */
|
||||
{
|
||||
for (std::size_t i = 0; i < k; ++i) {
|
||||
/* pick a list cell at random */
|
||||
gp<List> l1 = w1.at(rgen() % w1.size());
|
||||
|
||||
if (rgen() % 2 == 0) {
|
||||
/* pick another list cell at random, and link it to l1 */
|
||||
gp<List> l2 = w1.at(rgen() % w1.size());
|
||||
|
||||
l1->assign_rest(l2);
|
||||
} else {
|
||||
/* pick a value at random (could be list or integer),
|
||||
* assign to head
|
||||
*/
|
||||
gp<Object> x2 = w2.at(rgen() % w2.size());
|
||||
|
||||
l1->assign_head(x2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log && log("stats.before", gc->gc_statistics());
|
||||
|
||||
/* make model for contents of w2[] */
|
||||
ObjectGraphModel from_model;
|
||||
from_model.from_root_vector(root_v);
|
||||
|
||||
gc->request_gc(generation::nursery);
|
||||
|
||||
/* collector cycle changed object addresses.
|
||||
* build a new object model, and verify that they're equivalent
|
||||
*/
|
||||
|
||||
ObjectGraphModel to_model;
|
||||
to_model.from_root_vector(root_v);
|
||||
|
||||
REQUIRE(ObjectGraphModel::verify_equal_models(from_model, to_model));
|
||||
|
||||
log && log("stats.after", gc->gc_statistics());
|
||||
}
|
||||
}
|
||||
} /*namespace ut*/
|
||||
} /*namespace xo*/
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ namespace xo {
|
|||
|
||||
std::size_t expected_alloc_z = 0;
|
||||
|
||||
// TODO: consolidate: root setup shared with "List" unit test
|
||||
|
||||
/* construct example Lists from testcase info */
|
||||
for (const std::vector<std::string> & v : tc.v_)
|
||||
{
|
||||
|
|
@ -179,6 +181,102 @@ namespace xo {
|
|||
}
|
||||
} /*TEST_CASE(List, ..)*/
|
||||
|
||||
TEST_CASE("List-cyclic", "[List][gc][cycles]")
|
||||
{
|
||||
for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) {
|
||||
const Testcase_List & tc = s_testcase_v[i_tc];
|
||||
|
||||
constexpr bool c_debug_flag = false;
|
||||
|
||||
up<GC> gc = GC::make(
|
||||
{.initial_nursery_z_ = tc.nursery_z_,
|
||||
.initial_tenured_z_ = tc.tenured_z_,
|
||||
.debug_flag_ = c_debug_flag});
|
||||
|
||||
REQUIRE(gc.get());
|
||||
|
||||
/* use gc for all Object allocs */
|
||||
Object::mm = gc.get();
|
||||
|
||||
{
|
||||
scope log(XO_DEBUG(c_debug_flag));
|
||||
log && log(xtag("i_tc", i_tc), xtag("tc.v_.size", tc.v_.size()));
|
||||
|
||||
std::vector<gp<List>> root_v(tc.v_.size());
|
||||
std::size_t i = 0;
|
||||
|
||||
std::size_t expected_alloc_z = 0;
|
||||
|
||||
// TODO: consolidate: root setup shared with "List" unit test
|
||||
|
||||
/* construct example Lists from testcase info */
|
||||
for (const std::vector<std::string> & v : tc.v_)
|
||||
{
|
||||
/* building l1 in reverse order */
|
||||
gp<List> l1 = List::nil;
|
||||
gp<List> last = List::nil;
|
||||
|
||||
for (std::size_t ip1 = v.size(); ip1 > 0; --ip1) {
|
||||
const std::string & si = v.at(ip1 - 1);
|
||||
log && log(xtag("i", ip1-1), xtag("si", si));
|
||||
gp<String> sobj = String::copy(si.c_str());
|
||||
l1 = List::cons(sobj, l1);
|
||||
log && log(xtag("l1.size", l1->size()));
|
||||
if (ip1 == v.size()) {
|
||||
// capture last
|
||||
last = l1;
|
||||
}
|
||||
|
||||
std::size_t alloc_z = l1->_shallow_size() + l1->head()->_shallow_size();
|
||||
expected_alloc_z += alloc_z;
|
||||
|
||||
// replace tail to make a cycle
|
||||
if (last.ptr()) {
|
||||
last->assign_rest(l1);
|
||||
}
|
||||
}
|
||||
|
||||
REQUIRE(l1->is_nil() == (v.size() == 0));
|
||||
//REQUIRE(l1->size() == v.size()); // lwill loop forever
|
||||
|
||||
root_v[i] = l1;
|
||||
gc->add_gc_root(reinterpret_cast<Object **>(root_v[i].ptr_address()));
|
||||
|
||||
REQUIRE(gc->allocated() % sizeof(std::uintptr_t) == 0);
|
||||
REQUIRE(gc->allocated() == expected_alloc_z);
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
gc->request_gc(generation::nursery);
|
||||
|
||||
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->allocated() == expected_alloc_z);
|
||||
|
||||
/* verify GC preserved list structure and contents */
|
||||
for (std::size_t i = 0, n = root_v.size(); i < n; ++i) {
|
||||
std::size_t nj = tc.v_.at(i).size();
|
||||
|
||||
// REQUIRE(root_v.at(i)->size() == nj); // will loop forever
|
||||
if (!(root_v.at(i)->is_nil()))
|
||||
REQUIRE(gc->contains(reinterpret_cast<std::byte *>(root_v.at(i).ptr())));
|
||||
|
||||
for (std::size_t j = 0; j < nj; ++j) {
|
||||
gp<String> s = String::from(root_v.at(i)->list_ref(j));
|
||||
REQUIRE(s.ptr());
|
||||
REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0);
|
||||
|
||||
REQUIRE(gc->tospace_generation_of(reinterpret_cast<std::byte*>(s.ptr()))
|
||||
== generation_result::nursery);
|
||||
}
|
||||
|
||||
REQUIRE(root_v.at(i)->list_ref(nj).ptr() == root_v.at(i)->list_ref(0).ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} /*namespace ut*/
|
||||
} /*namespace xo*/
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include "xo/object/String.hpp"
|
||||
#include "xo/alloc/GC.hpp"
|
||||
#include "xo/alloc/ArenaAlloc.hpp"
|
||||
#include "xo/indentlog/scope.hpp"
|
||||
#include "xo/indentlog/print/quoted.hpp"
|
||||
#include <catch2/catch.hpp>
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
namespace xo {
|
||||
using xo::gc::IAlloc;
|
||||
using xo::gc::GC;
|
||||
using xo::gc::ArenaAlloc;
|
||||
using xo::gc::generation;
|
||||
using xo::obj::String;
|
||||
|
||||
|
|
@ -151,6 +153,25 @@ namespace xo {
|
|||
}
|
||||
}
|
||||
|
||||
TEST_CASE("String.append", "[String]")
|
||||
{
|
||||
const bool c_debug_flag = false;
|
||||
up<ArenaAlloc> arena = ArenaAlloc::make("testarena", 0, 16*1024, c_debug_flag);
|
||||
|
||||
Object::mm = arena.get();
|
||||
|
||||
gp<String> s1 = String::share("the");
|
||||
gp<String> s2 = String::share(" quick");
|
||||
|
||||
gp<String> s3 = String::append(s1, s2);
|
||||
|
||||
REQUIRE(::strcmp(s1->c_str(), "the") == 0);
|
||||
REQUIRE(::strcmp(s2->c_str(), " quick") == 0);
|
||||
REQUIRE(s3.ptr());
|
||||
REQUIRE(s3->length() == s1->length() + s2->length());
|
||||
REQUIRE(::strcmp(s1->c_str(), "the quick"));
|
||||
}
|
||||
|
||||
} /*namespace ut*/
|
||||
} /*namespace xo*/
|
||||
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@ endif()
|
|||
# needs to have been installed somewhere on CMAKE_MODULE_PATH,
|
||||
# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX)
|
||||
#
|
||||
include(xo_macros/xo-project-macros)
|
||||
include(xo_macros/xo_cxx)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue