diff --git a/src/lib/allocator-handle.hpp b/src/lib/allocator-handle.hpp index 55b6a2d37..ce66b3381 100644 --- a/src/lib/allocator-handle.hpp +++ b/src/lib/allocator-handle.hpp @@ -164,6 +164,7 @@ namespace lib { } + /** create new element using the embedded allocator */ template TY* @@ -198,6 +199,92 @@ namespace lib { } } }; + + + + + /** Metafunction: probe if the given base factory is possibly monostate */ + template + struct is_Stateless + : std::__and_< std::is_empty + , std::is_default_constructible + > + { }; + template + auto is_Stateless_v = is_Stateless{}; + + + + + + /** + * Adapter to use a _generic factory_ \a FAC for + * creating managed object instances with unique ownership. + * Generated objects are attached to a `std::unique_ptr` handle, + * which enforces scoped ownership and destroys automatically. + * The factory can either be stateless (≙monostate) or tied + * to a distinct, statefull allocator or manager backend. + * In the latter case, this adapter must be created with + * appropriate wiring and each generated `unique_ptr` handle + * will also carry a back-reference to the manager instance. + */ + template + class OwnUniqueAdapter + : protected FAC + { + template + static void + dispose (TY* elm) ///< @internal callback for unique_ptr using stateless FAC + { + FAC factory; + factory.dispose (elm); + }; + + template + struct StatefulDeleter ///< @internal callback for unique_ptr using statefull FAC + : protected FAC + { + void + operator() (TY* elm) + { + FAC::dispose (elm); + } + + StatefulDeleter (FAC const& anchor) + : FAC{anchor} + { } + }; + + + public: + OwnUniqueAdapter (FAC const& factory) + : FAC{factory} + { } + using FAC::FAC; + + /** + * Factory function: generate object with scoped ownership and automated clean-up. + */ + template + auto + make_unique (ARGS&& ...args) + { + if constexpr (is_Stateless_v) + { + using Handle = std::unique_ptr; + return Handle{FAC::template create (std::forward (args)...) + , &OwnUniqueAdapter::dispose + }; + } + else + { + using Handle = std::unique_ptr>; + return Handle{FAC::template create (std::forward (args)...) + , StatefulDeleter{*this} + }; + } + } + }; } diff --git a/src/lib/test/event-log.hpp b/src/lib/test/event-log.hpp index 447b4eb7f..7ec2eb7b1 100644 --- a/src/lib/test/event-log.hpp +++ b/src/lib/test/event-log.hpp @@ -364,6 +364,14 @@ namespace test{ */ EventLog& event (string classifier, string text); + template + EventLog& + event (string classifier, ELMS const& ...initialiser) + { + log ("event", ArgSeq{"ID="+classifier}, collectStr (initialiser...)); + return *this; + } + /** Log occurrence of a function call with no arguments. * @param target the object or scope on which the function is invoked * @param function name of the function being invoked diff --git a/src/lib/test/tracking-allocator.cpp b/src/lib/test/tracking-allocator.cpp index a4fe83383..c3e4b23fe 100644 --- a/src/lib/test/tracking-allocator.cpp +++ b/src/lib/test/tracking-allocator.cpp @@ -1,5 +1,5 @@ /* - TrackingAllocator - test dummy objects for tracking ctor/dtor calls + TrackingAllocator - custom allocator for memory management diagnostics Copyright (C) Lumiera.org 2024, Hermann Vosseler @@ -23,6 +23,18 @@ /** @file tracking-allocator.cpp ** Implementation of the common storage backend for the tracking test allocator. + ** - PoolRegistry maintains a common hashtable with all MemoryPool instances; + ** when using the standard accessors, registration and pool creating happens + ** automatically. + ** - in this setup, the pool with ID="GLOBAL" is just one further pool, which + ** will be established when a front-end function is used with the default + ** pool-ID (i.e. without explicit pool specification). Note however, that + ** such a factory is still statefull (since it embeds a shared-ownership + ** smart-pointer) + ** - each MemoryPool contains a hashtable, where each active allocation is + ** stored, using the storage-location as hashtable key. Each such entry + ** gets a further consecutive internal ID, which is visible in the EventLog + ** - ////////////////////OOO Mutex locking ** ** @see tracking-allocator.hpp ** @see TestTracking_test#demonstrate_checkAllocator() @@ -30,28 +42,24 @@ */ -//#include "lib/test/test-helper.hpp" #include "lib/test/tracking-allocator.hpp" -//#include "lib/format-string.hpp" -//#include "lib/format-cout.hpp" -//#include "lib/unique-malloc-owner.hpp" +#include "lib/uninitialised-storage.hpp" #include "lib/iter-explorer.hpp" #include "lib/depend.hpp" -#include "lib/uninitialised-storage.hpp" #include "lib/util.hpp" -//#include +#include #include -//using util::_Fmt; +using std::string; using std::make_shared; using std::make_pair; -//using std::string; using util::contains; using util::joinDash; using util::showAddr; + namespace lib { namespace test{ @@ -101,6 +109,8 @@ namespace test{ HashVal getChecksum() const; size_t getAllocationCnt() const; size_t calcAllocSize() const; + + Literal getPoolID() const { return poolID_; } }; @@ -176,19 +186,30 @@ namespace test{ { } - void* - TrackingAllocator::allocate (size_t cnt) + /** + * Allot a memory block with size \a bytes. + * This allocation is recorded in the associated MemoryPool + * and proper deallocation can thus be verified. + * @return a `void*` to the start of the bare memory location + */ + TrackingAllocator::Location + TrackingAllocator::allocate (size_t bytes) { ENSURE (mem_); - return mem_->addAlloc (cnt) + return mem_->addAlloc (bytes) .buff.front(); } + /** + * Discard and forget an allocation created through this allocator. + * The \a bytes argument serves as sanity check (since the actual allocation + * size is recorded anyway); a mismatch is logged as error, yet silently ignored. + */ void - TrackingAllocator::deallocate (Location loc, size_t cnt) noexcept + TrackingAllocator::deallocate (Location loc, size_t bytes) noexcept { ENSURE (mem_); - mem_->discardAlloc (loc, cnt); + mem_->discardAlloc (loc, bytes); } MemoryPool::Allocation& @@ -199,7 +220,7 @@ namespace test{ Location loc = newAlloc.buff.front(); ASSERT (not contains (allocs_, loc)); newAlloc.entryID = ++entryNr_; - logAlloc (poolID_, "allocate", entryNr_, bytes, showAddr(loc)); + logAlloc (poolID_, "allocate", bytes, entryNr_, showAddr(loc)); checksum_ += entryNr_ * bytes; return allocs_.emplace (loc, move(newAlloc)) .first->second; @@ -213,9 +234,10 @@ namespace test{ auto& entry = allocs_[loc]; ASSERT (entry.buff); ASSERT (entry.buff.front() == loc); - if (entry.buff.size() != bytes) // *deliberately* tolerating wrong data - logAlarm ("SizeMismatch", entry.entryID, bytes, showAddr(loc)); // but log as incident to support diagnostics - logAlloc (poolID_, "deallocate", entry.entryID, bytes, showAddr(loc)); + if (entry.buff.size() != bytes) // *deliberately* tolerating wrong data, but log incident to support diagnostics + logAlarm ("SizeMismatch", bytes, "≠", entry.buff.size(), entry.entryID, showAddr(loc)); + + logAlloc (poolID_, "deallocate", bytes, entry.entryID, bytes, showAddr(loc)); checksum_ -= entryNr_ * bytes; // Note: using the given size (if wrong ⟿ checksum mismatch) allocs_.erase(loc); } @@ -295,6 +317,7 @@ namespace test{ bool TrackingAllocator::manages (Location memLoc) const { + ENSURE (mem_); return bool(mem_->findAlloc (memLoc)); } @@ -302,6 +325,7 @@ namespace test{ size_t TrackingAllocator::getSize(Location memLoc) const { + ENSURE (mem_); auto* entry = mem_->findAlloc (memLoc); return entry? entry->buff.size() : 0; @@ -311,11 +335,20 @@ namespace test{ HashVal TrackingAllocator::getID (Location memLoc) const { + ENSURE (mem_); auto* entry = mem_->findAlloc (memLoc); return entry? entry->entryID : 0; } + Literal + TrackingAllocator::poolID() const + { + ENSURE (mem_); + return mem_->getPoolID(); + } + + }} // namespace lib::test diff --git a/src/lib/test/tracking-allocator.hpp b/src/lib/test/tracking-allocator.hpp index 8e5a6f2c0..2135a2638 100644 --- a/src/lib/test/tracking-allocator.hpp +++ b/src/lib/test/tracking-allocator.hpp @@ -1,8 +1,8 @@ /* - TRACKING-ALLOCATOR.hpp - test dummy objects for tracking ctor/dtor calls + TRACKING-ALLOCATOR.hpp - custom allocator for memory management diagnostics Copyright (C) Lumiera.org - 2023, Hermann Vosseler + 2024, Hermann Vosseler This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as @@ -25,6 +25,25 @@ ** 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 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` + ** @see TestTracking_test */ @@ -32,15 +51,14 @@ #define LIB_TEST_TRACKING_ALLOCATOR_H #include "lib/error.hpp" +#include "lib/symbol.hpp" #include "lib/nocopy.hpp" #include "lib/hash-value.h" -#include "lib/symbol.hpp" #include "lib/test/event-log.hpp" +#include "lib/format-string.hpp" -//#include #include #include -#include using std::byte; @@ -60,6 +78,12 @@ namespace test { + /** + * 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_; @@ -96,6 +120,7 @@ namespace test { 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); @@ -106,6 +131,14 @@ namespace test { }; + /** + * 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 class TrackAlloc : public TrackingAllocator @@ -129,6 +162,61 @@ namespace test { }; + + /** + * 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 + TY* + create (ARGS&& ...args) + { + Location loc = allocate(sizeof(TY)); + log.call (poolID(),"create-"+util::typeStr(), std::forward (args)...); + try { + return new(loc) TY (std::forward (args)...); + } + catch (std::exception& mishap) + { + log.error (util::_Fmt{"CtorFail: type=%s, problem:%s"} % util::typeStr() % mishap.what()); + throw; + } + catch (...) + { + log.error ("CtorFail: unknown cause"); + throw; + } + } + + /** destroy the given element and discard the associated memory and registration */ + template + void + dispose (TY* elm) noexcept + { + if (not elm) + { + log.warn("dispose(nullptr)"); + return; + } + log.call (poolID(),"destroy-"+util::typeStr()); + elm->~TY(); + deallocate(elm, sizeof(TY)); + } + }; + + + /** * C++ standard allocator API : allot raw memory for \a cnt elements of type \a TY */ diff --git a/tests/library/test/test-tracking-test.cpp b/tests/library/test/test-tracking-test.cpp index 1483708bb..11789fccc 100644 --- a/tests/library/test/test-tracking-test.cpp +++ b/tests/library/test/test-tracking-test.cpp @@ -29,6 +29,7 @@ #include "lib/test/test-helper.hpp" #include "lib/test/tracking-dummy.hpp" #include "lib/test/tracking-allocator.hpp" +#include "lib/allocator-handle.hpp" #include "lib/format-cout.hpp" #include "lib/format-util.hpp" #include "lib/test/diagnostic-output.hpp"///////////////////////TODO @@ -76,19 +77,19 @@ namespace test{ Tracker alpha; // (1) create α auto randomAlpha = toString(alpha.val); - log.note("type=ID",alpha.val); // (2) α has an random ID + log.event("ID",alpha.val); // (2) α has an random ID { Tracker beta{55}; // (3) create β alpha = beta; // (4) assign α ≔ β } - log.note("type=ID",alpha.val); // (5) thus α now also bears the ID 55 of β + log.event ("ID",alpha.val); // (5) thus α now also bears the ID 55 of β Tracker gamma = move(alpha); // (6) create γ by move-defuncting α { Tracker delta(23); // (7) create δ with ID 23 delta = move(gamma); // (8) move-assign δ ⟵ γ - log.note("type=ID",delta.val); // (9) thus δ now bears the ID 55 (moved α ⟶ γ ⟶ δ) + log.event ("ID",delta.val); // (9) thus δ now bears the ID 55 (moved α ⟶ γ ⟶ δ) } - log.note("type=ID",alpha.val); // (X) and thus α is now a zombie object + log.event("ID",alpha.val); // (X) and thus α is now a zombie object cout << "____Tracker-Log_______________\n" << util::join(Tracker::log, "\n") @@ -159,7 +160,11 @@ namespace test{ } - /** @test custom allocator to track memory handling. + /** @test custom allocator to track memory handling + * - use the base allocator to perform raw memory allocation + * - demonstrate checksum and diagnostic functions + * - use a standard adapter to create objects with `unique_ptr` + * - use as _custom allocator_ within STL containers */ void demonstrate_checkAllocator() @@ -169,9 +174,12 @@ namespace test{ Tracker::log.clear("Tracking-Allocator-Test"); Tracker::log.joinInto(log); + // everything is safe and sound initially.... CHECK (TrackingAllocator::checksum() == 0, "Testsuite is broken"); CHECK (TrackingAllocator::use_count() == 0); - { + + { // Test-1 : raw allocations.... + log.event("Test-1"); TrackingAllocator allo; CHECK (TrackingAllocator::use_count() == 1); CHECK (TrackingAllocator::numAlloc() == 0); @@ -195,35 +203,109 @@ namespace test{ CHECK (TrackingAllocator::numAlloc() == 0); CHECK (TrackingAllocator::numBytes() == 0); } - + CHECK (log.verify("EventLogHeader").on("Tracking-Allocator-Test") + .before("logJoin") + .beforeEvent("Test-1") + .beforeCall("allocate").on(GLOBAL).argPos(0, 55) + .beforeEvent("error", "SizeMismatch-42-≠-55") + .beforeCall("deallocate").on(GLOBAL).argPos(0, 42) + ); CHECK (TrackingAllocator::checksum() == 0); - { + + + { // Test-2 : attach scoped-ownership-front-End + log.event("Test-2"); + + allo::OwnUniqueAdapter uniFab; + CHECK (sizeof(uniFab) == sizeof(TrackingFactory)); + CHECK (sizeof(uniFab) == sizeof(std::shared_ptr)); + CHECK (not allo::is_Stateless_v); + + CHECK (TrackingAllocator::use_count() == 1); + CHECK (TrackingAllocator::numAlloc() == 0); + CHECK (TrackingAllocator::numBytes() == 0); + { + log.event("fabricate unique"); + auto uniqueHandle = uniFab.make_unique (77); + CHECK (uniqueHandle); + CHECK (uniqueHandle->val == 77); + CHECK (TrackingAllocator::use_count() == 2); + CHECK (TrackingAllocator::numAlloc() == 1); + CHECK (TrackingAllocator::numBytes() == sizeof(Tracker)); + + // all the default tracking allocators indeed attach to the same pool + TrackingAllocator allo; + void* mem = uniqueHandle.get(); + CHECK (allo.manages (mem)); + HashVal memID = allo.getID (mem); + CHECK (0 < memID); + CHECK (TrackingAllocator::checksum() == memID*sizeof(Tracker)); + + }// and it's gone... + CHECK (TrackingAllocator::use_count() == 1); + CHECK (TrackingAllocator::numAlloc() == 0); + CHECK (TrackingAllocator::numBytes() == 0); + } + + CHECK (log.verifyEvent("Test-2") + .beforeEvent("fabricate unique") + .beforeCall("allocate").on(GLOBAL).argPos(0, sizeof(Tracker)) + .beforeCall("create-Tracker").on(GLOBAL).arg(77) + .beforeCall("ctor").on("Tracker").arg(77) + .beforeCall("destroy-Tracker").on(GLOBAL) + .beforeCall("dtor").on("Tracker").arg(77) + .beforeCall("deallocate").on(GLOBAL).argPos(0, sizeof(Tracker)) + ); + CHECK (TrackingAllocator::checksum() == 0); + + + Tracker *t1, *t2, *t3, *t4; + + { // Test-3 : use as STL allocator + log.event("Test-3"); using SpyVec = std::vector>; + log.event("fill with 3 default instances"); SpyVec vec1(3); int v3 = vec1.back().val; -SHOW_EXPR(v3); -SHOW_EXPR(join(vec1)) SpyVec vec2; + log.event("move last instance over into other vector"); vec2.emplace_back (move (vec1[2])); -SHOW_EXPR(join(vec1)) -SHOW_EXPR(join(vec2)) + CHECK (vec2.back().val == v3); + CHECK (vec1.back().val == Tracker::DEFUNCT); + // capture object locations for log verification + t1 = & vec1[0]; + t2 = & vec1[1]; + t3 = & vec1[2]; + t4 = & vec2.front(); + log.event("leave scope"); } - CHECK (TrackingAllocator::checksum() == 0); + CHECK (log.verifyEvent("Test-3") + .beforeEvent("fill with 3 default instances") + .beforeCall("allocate").on(GLOBAL) + .beforeCall("ctor").on(t1) + .beforeCall("ctor").on(t2) + .beforeCall("ctor").on(t3) + .beforeEvent("move last instance over into other vector") + .beforeCall("allocate").on(GLOBAL) + .beforeCall("ctor-move").on(t4) + .beforeEvent("leave scope") + .beforeCall("dtor").on(t4) + .beforeCall("deallocate").on(GLOBAL) + .beforeCall("dtor").on(t1) // (problematic test? order may be implementation dependent) + .beforeCall("dtor").on(t2) + .beforeCall("dtor").on(t3) + .beforeCall("deallocate").on(GLOBAL) + ); cout << "____Tracking-Allo-Log_________\n" << util::join(Tracker::log, "\n") << "\n───╼━━━━━━━━━━━━━━━━━╾────────"< + + + + + + + + + + + + + + + @@ -83557,7 +83572,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + @@ -83584,6 +83600,10 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + @@ -83596,17 +83616,21 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + - - + + - - - + + + + + + +