Skip to content

Latest commit

 

History

History
111 lines (74 loc) · 7.35 KB

File metadata and controls

111 lines (74 loc) · 7.35 KB

pbr-cpp-memory-pool v0.6.0 — Milestone 6: Observability & Decorators

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.

What's in the box

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 logging

PoolStats 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 / destroyed

Three events fire: exhausted (try_allocatenullptr or allocatestd::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_t detection idiom proves the stats() / add_observer() surface is absent from Pool and present on InstrumentedPool, so a Pool holder cannot even name an instrumentation operation.
  • Byte-identical footprintsizeof(Pool) == sizeof(memory_pool_t*) and Pool stays standard-layout: the decorator adds no member, vtable, or padding to it.
  • Contained overheadInstrumentedPool is larger than Pool by exactly its atomic counters plus the growth watermark; the cost lives wholly inside the wrapper.
  • Behavioural equivalence — a bare Pool and an InstrumentedPool over the same configuration are indistinguishable: identical metadata_bytes() (the C struct memory_pool does 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.

Architecture Decision Records

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.

Design-patterns catalogue

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.

Spec Coverage Map

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).

What this release does not contain

  • 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 wraps Pool; a templated InstrumentedPool<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.

Verifying the release

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

Build and use

#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
}

Links