lumiera_documentation/src/test/tracking-allocator.hpp
Ichthyostega 9c74466778 Tests: build an link a test-support library
This is something I wanted to clean-up since a very long time indeed,
but always postponed it, since it requires a lot of nasty clean-up and disentangling....

Several parts had been stashed away into `src/lib/test`, which are not really related:
 * the testrunner and testsuite builder
 * generic helper utilities
 * specialised test-support facilities, e.g. the EventLog

This changeset introduces a new top-level test folder and a namespace `::test`
Code from this folder is linked into a shared library `libtestsupport.so`,
which resides in the linkage hierarchy between »Vessel« and »Vault«,
yet is no longer part of the Lumiera application and only linked into the test-suite.
2026-03-06 23:16:32 +01:00

246 lines
7.7 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
TRACKING-ALLOCATOR.hpp - custom allocator for memory management diagnostics
Copyright (C)
2024, Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
* *****************************************************************/
/** @file tracking-allocator.hpp
** Unittest helper code: a custom allocator to track memory usage.
** By registering each allocation and deallocation, correct memory handling
** can be verified and memory usage can be investigated in practice.
** \par TrackingAllocator
** The foundation is to allow raw memory allocations, which are attached
** and tracked within some memory pool, allowing to investigate the number
** of allocations, number of currently allotted bytes and a checksum.
** Moreover, all relevant actions are logged into an lib::test::EventLog.
** By default a common global MemoryPool is used, while it is possible
** to carry out all those operations also on a dedicated pool; the user
** visible »allocators« are actually shared-ownership smart-handles.
** \par TrackingFactory
** Built on top is a standard factory to create and destroy arbitrary
** object instances, with the corresponding allocations attached to
** the underlying MemoryPool
** \par C++ standard allocator adapter
** A variation of this is the TrackAlloc<TY> template, which can be
** used as custom allocator adapter within STL containers
** @remark these classes also work in concert with the building blocks
** from allocator-handle.hpp; notably it is possible to create
** a OwnUniqueAdapter front-end for fabricating `unique_ptr`
** @warning deliberately *not threadsafe*
** - generally speaking, allocation should be kept outside of
** any multithreaded environment, or at least requires
** dedicated care beyond any standard scheme
** - this is a test feature...
** @see TestTracking_test
*/
#ifndef TESTSUPPORT_TRACKING_ALLOCATOR_H
#define TESTSUPPORT_TRACKING_ALLOCATOR_H
#include "lib/error.hpp"
#include "lib/symbol.hpp"
#include "lib/nocopy.hpp"
#include "lib/hash-value.h"
#include "lib/format-string.hpp"
#include "test/event-log.hpp"
#include <utility>
#include <memory>
using std::byte;
using lib::HashVal;
namespace test {
namespace {
const Symbol GLOBAL{"GLOBAL"};
}
/** common registration table and memory pool */
class MemoryPool;
using PoolHandle = std::shared_ptr<MemoryPool>;
/**
* Generic low-level allocator attached to tracking MemoryPool.
* Effectively this is a shared handle front-end to the MemoryPool, and
* new distinct pools are generated (and discarded) on demand, keyed by a pool-ID.
* A global (singleton) pool is used when no pool-ID is explicitly given.
*/
class TrackingAllocator
{
PoolHandle mem_;
public:
/** can be default created to attach to a common pool */
TrackingAllocator();
/** create a separate, marked memory pool */
TrackingAllocator (Literal id);
// standard copy operations acceptable
using Location = void*;
[[nodiscard]] Location allocate (size_t n);
void deallocate (Location, size_t =0) noexcept;
friend bool
operator== (TrackingAllocator const& a1, TrackingAllocator const& a2)
{
return a1.mem_ == a2.mem_;
}
friend bool
operator!= (TrackingAllocator const& a1, TrackingAllocator const& a2)
{
return not (a1 == a2);
}
/* ===== Diagnostics ===== */
bool manages (Location) const;
size_t getSize(Location) const;
HashVal getID (Location) const;
Literal poolID () const;
static HashVal checksum (Literal pool =GLOBAL);
static size_t use_count (Literal pool =GLOBAL);
static size_t numAlloc (Literal pool =GLOBAL);
static size_t numBytes (Literal pool =GLOBAL);
static EventLog log;
};
/**
* C++ standard compliant _custom allocator adapter_
* backed by the TrackingAllocator and the MemoryPool denoted at construction.
* TrackAlloc adapters can be default constructed (thereby using the GLOBAL pool),
* or created with a designated pool-ID or copy/move constructed from any other
* TrackAlloc adapter (then using the same backing pool)
* @tparam TY the type of intended product object, to determine the size
*/
template<typename TY>
class TrackAlloc
: public TrackingAllocator
{
public:
using TrackingAllocator::TrackingAllocator;
/** cross-building for another type, using a common pool */
template<typename X>
TrackAlloc (TrackAlloc<X> const& anchor)
: TrackingAllocator{anchor}
{ }
/* ===== C++ standard allocator interface ===== */
using value_type = TY;
// define that (non-equivalent) allocators migrate alongside on assignments....
using propagate_on_container_copy_assignment = std::true_type; ///< for sake of consistency
using propagate_on_container_move_assignment = std::true_type; ///< otherwise all elements must be copied
using propagate_on_container_swap = std::true_type; ///< otherwise they would have to deallocate cross-wise
[[nodiscard]] TY* allocate (size_t cnt);
void deallocate (TY*, size_t) noexcept;
};
/**
* Generic object factory backed by TrackingAllocator.
*/
class TrackingFactory
: public TrackingAllocator
{
public:
using TrackingAllocator::TrackingAllocator;
/** attach to the given TrackingAllocator and MemoryPool */
TrackingFactory (TrackingAllocator const& anchor)
: TrackingAllocator{anchor}
{ }
/** create new element with an allocation registered in the backing MemoryPool */
template<class TY, typename...ARGS>
TY*
create (ARGS&& ...args)
{
Location loc = allocate(sizeof(TY));
log.call (poolID(),"create-"+util::typeStr<TY>(), std::forward<ARGS> (args)...);
try {
return new(loc) TY (std::forward<ARGS> (args)...);
}
catch (std::exception& mishap)
{
log.error (util::_Fmt{"CtorFail: type=%s, problem:%s"} % util::typeStr<TY>() % mishap.what());
throw;
}
catch (...)
{
log.error ("CtorFail: unknown cause");
throw;
}
}
/** destroy the given element and discard the associated memory and registration */
template<class TY>
void
dispose (TY* elm) noexcept
{
if (not elm)
{
log.warn("dispose(nullptr)");
return;
}
log.call (poolID(),"destroy-"+util::typeStr<TY>());
elm->~TY();
deallocate(elm, sizeof(TY));
}
};
/**
* C++ standard allocator API : allot raw memory for \a cnt elements of type \a TY
*/
template<typename TY>
TY*
TrackAlloc<TY>::allocate (size_t cnt)
{
return static_cast<TY*> (TrackingAllocator::allocate (cnt * sizeof(TY)));
}
/**
* C++ standard allocator API : clear an existing allocation,
* which must have been allocated into the same pool, with given element cnt.
*/
template<typename TY>
void
TrackAlloc<TY>::deallocate (TY* loc, size_t cnt) noexcept
{
TrackingAllocator::deallocate (loc, cnt * sizeof(TY));
}
}// namespace test
#endif /*TESTSUPPORT_TRACKING_ALLOCATOR_H*/