Sixth tagged release of pbr-cpp-memory-pool. Milestone 6 adds optional observability — statistics, on-demand logging, and lifecycle event notification — "without touching the hot path of release builds" (ROADMAP §6 goal). Everything is opt-in by type: a program that keeps using Pool directly is byte-identical to v0.5.0 and pays nothing.
An instrumented pool — the Decorator (ADR-0025)
The new header-only it::d4np::memorypool::InstrumentedPool composes a Pool and re-exposes its allocation surface, counting activity as it forwards:
#include <it/d4np/memorypool/instrumented_pool.hpp>
using namespace it::d4np::memorypool;
InstrumentedPool pool{Pool(64, 1024)}; // wrap a fixed pool
void* block = pool.try_allocate();
// ...
pool.deallocate(block);
const PoolStats s = pool.stats(); // copyable snapshot
pool.write_summary(std::cout); // on-demand loggingPoolStats carries allocations_, deallocations_, allocation_failures_, the live count live_, and its high-water mark peak_live_. Because a fixed-block pool's allocation-size histogram is degenerate (one bucket), peak_live_ is the capacity-planning signal instead. The counters are relaxed atomics, so the decorator is safe to wrap a thread-safe (MUTEX / LOCKFREE) pool and drive it concurrently; a hand-written move keeps the type factory-returnable (InstrumentedPool::make / make_dynamic mirror Pool).
It is the Decorator in its idiomatic-C++ (composition) form — Pool is a concrete, move-only value type with no virtual surface, so the decorator wraps it rather than inheriting, exactly as TypedPool and PoolAllocator already do.
Lifecycle events — the Observer (ADR-0026)
A runtime Observer is wired into the same interception points, so statistics and events compose in one observability type rather than two non-stackable wrappers:
struct Logger : PoolObserver {
void on_pool_event(PoolEvent e, const PoolStats& s) noexcept override { /* ... */ }
};
Logger logger;
pool.add_observer(logger); // notified on exhausted / grew / destroyedThree events fire: exhausted (try_allocate → nullptr or allocate → std::bad_alloc), grew (a dynamic pool acquired an overflow chunk), and destroyed (the dtor notifies once; a moved-from instance notifies nobody). on_pool_event is noexcept by contract — it may fire from the noexcept try_allocate and from the destructor. Notification is not internally synchronized: register observers before concurrent use and make them thread-safe, or observe single-threaded (documented).
Detecting grew cheaply is the one place Milestone 6 touches the C core: growth happens inside the C pop_head, invisible above the C boundary, and diffing metadata_bytes per allocation would be O(chunks). So struct memory_pool gains a std::atomic<std::size_t> grow_count_, incremented (relaxed) only in grow_pool (the rare slow path — the hot path is untouched), exposed by a new always-present, NULL-tolerant, ANSI-C C89-clean accessor:
size_t memory_pool_growths(const memory_pool_t* pool); /* O(1) */The decorator reads it in O(1) after each allocation and notifies on a rise. The field adds 8 bytes (NONE struct memory_pool → 64, MUTEX → 144) — comfortably within the ADR-0015 192-byte budget.
Zero overhead when disabled, verified (ADR-0025 §5)
"Instrumentation disabled" means using Pool directly. The new zero_overhead test binary discharges the contract structurally, not with a noisy wall-clock gate:
- Opt-in by type — a
std::void_tdetection idiom proves thestats()/add_observer()surface is absent fromPooland present onInstrumentedPool, so aPoolholder cannot even name an instrumentation operation. - Byte-identical footprint —
sizeof(Pool) == sizeof(memory_pool_t*)andPoolstays standard-layout: the decorator adds no member, vtable, or padding to it. - Contained overhead —
InstrumentedPoolis larger thanPoolby exactly its atomic counters plus the growth watermark; the cost lives wholly inside the wrapper. - Behavioural equivalence — a bare
Pooland anInstrumentedPoolover the same configuration are indistinguishable: identicalmetadata_bytes()(the Cstruct memory_pooldoes not grow — instrumentation is header-only C++ state),block_size(), capacity / exhaustion point, and LIFO re-allocation signature.
These are static_asserts plus a runtime check, so they hold in Release exactly as in Debug, and the binary runs in every CI matrix cell including the Release cells.
Two ADRs accepted in Milestone 6, taking the running total from 24 to 26:
- ADR-0025 — Decorator for an instrumented pool variant.
- ADR-0026 — Observer for pool-lifecycle events.
Decorator (row #10) and Observer (row #11) flip to Implemented in docs/patterns/README.md. With these, every pattern planned through Milestone 6 — RAII, Pimpl, Factory Method, Builder, Adapter, Iterator, Strategy, Template Method, Composite, Decorator, Observer — is now implemented.
No change — observability is additive instrumentation, not a spec requirement, so no row maps to Milestone 6. Coverage stays at the eleven ✅ rows reached at v0.5.0; the remaining work toward full acceptance is the Milestone 7 audit (M7.6).
- Double-free detection — explicitly deferred from Milestone 2 (ADR-0012) to a future instrumentation pass; the Decorator's interception points are the natural home, but it is not in this release.
- Instrumentation of
TypedPool— the Decorator wrapsPool; a templatedInstrumentedPool<PoolLike>is the deferred generalisation (ADR-0025 alternatives). - Lock-free dynamic growth, pool shrink-on-idle, per-thread caches — still deferred (ADR-0020 §4, ADR-0022, ADR-0024 §2).
- Doxygen-rendered API site, install / packaging (vcpkg, Conan), the full usage / compatibility README — Milestone 7 →
v1.0.0.
Each platform tarball produced by release.yml contains the public headers, the static archive, and LICENSE / README.md / CHANGELOG.md. SHA-256 checksums live in SHA256SUMS:
sha256sum --check SHA256SUMS#include <it/d4np/memorypool/instrumented_pool.hpp>
int main() {
using namespace it::d4np::memorypool;
InstrumentedPool pool{Pool(64, 1024)};
void* block = pool.try_allocate();
// ... work ...
pool.deallocate(block);
pool.write_summary(std::cout); // allocations=1 deallocations=1 ... peak_live=1
}- Changelog entry:
CHANGELOG.md—[0.6.0] - Milestone plan:
ROADMAP.md— Milestone 6 - Specification:
docs/specs/01_spec_cpp_memory_pool.md - Previous release:
docs/releases/v0.5.0.md