diff --git a/CHANGELOG.md b/CHANGELOG.md index d701d5f..e39ce96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,24 @@ under *Changed* or *Removed*. The `Unreleased` block accumulates entries during development and is rolled into a dated version block (`## [X.Y.Z] — YYYY-MM-DD`) when a release PR closes a milestone. +## [0.6.0] — 2026-06-14 + +**Milestone 6 — Observability & Decorators.** Optional logging / statistics / tracing +"without touching the hot path of release builds" (ROADMAP §6 goal). A new header-only +`it::d4np::memorypool::InstrumentedPool` **Decorator** composes a `Pool` and counts +allocation activity (allocations, deallocations, failures, live blocks, and the +`peak_live_` high-water mark), exposed as a `PoolStats` snapshot + `write_summary`. A +runtime **Observer** (`PoolObserver` registered via `add_observer`) delivers +pool-lifecycle events — `exhausted`, `grew`, `destroyed` — reusing the Decorator's +interception points. Observability is **opt-in by type**: a program that uses `Pool` +directly pays nothing — no counter, no branch, no atomic — verified structurally by the +new `zero_overhead` test. The single library-side addition is one `std::atomic +grow_count_` on `struct memory_pool` (incremented only on the growth slow path) exposed +by the O(1) `memory_pool_growths` accessor, within the ADR-0015 192-byte budget. Two new +ADRs (0025–0026) take the running total to 26; the patterns catalogue gains **Decorator** +and **Observer** as Implemented. No spec row changes (observability is additive). Full +release notes in [`docs/releases/v0.6.0.md`](docs/releases/v0.6.0.md). + ### Added (M6.3) - Dedicated `zero_overhead` CTest binary @@ -742,7 +760,8 @@ Milestone 2 → `v0.2.0`. Full release notes in --- -[Unreleased]: https://github.com/danielPoloWork/pbr-cpp-memory-pool/compare/v0.5.0...HEAD +[Unreleased]: https://github.com/danielPoloWork/pbr-cpp-memory-pool/compare/v0.6.0...HEAD +[0.6.0]: https://github.com/danielPoloWork/pbr-cpp-memory-pool/releases/tag/v0.6.0 [0.5.0]: https://github.com/danielPoloWork/pbr-cpp-memory-pool/releases/tag/v0.5.0 [0.4.0]: https://github.com/danielPoloWork/pbr-cpp-memory-pool/releases/tag/v0.4.0 [0.3.0]: https://github.com/danielPoloWork/pbr-cpp-memory-pool/releases/tag/v0.3.0 diff --git a/README.md b/README.md index 1b3b301..c54a0e1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![docs](https://github.com/danielPoloWork/pbr-cpp-memory-pool/actions/workflows/docs.yml/badge.svg)](https://github.com/danielPoloWork/pbr-cpp-memory-pool/actions/workflows/docs.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Standard: C++17 / ANSI C](https://img.shields.io/badge/Standard-C%2B%2B17%20%2F%20ANSI%20C-blue.svg)](docs/specs/01_spec_cpp_memory_pool.md) -[![Status: v0.5.0 dynamic growth](https://img.shields.io/badge/Status-v0.5.0%20dynamic%20growth-green.svg)](https://github.com/danielPoloWork/pbr-cpp-memory-pool/releases/tag/v0.5.0) +[![Status: v0.6.0 observability](https://img.shields.io/badge/Status-v0.6.0%20observability-green.svg)](https://github.com/danielPoloWork/pbr-cpp-memory-pool/releases/tag/v0.6.0) > Part of the **Purpose-Built References (PBR)** series — small, didactic, production-quality C/C++ reference implementations of high-performance building blocks. @@ -18,6 +18,7 @@ Many high-performance systems — graphics engines, financial trading servers, d - **Standards:** ANSI C public surface, C++17 internals and wrapper. No external dependencies. - **Thread safety:** opt-in, configurable at compile time (Milestone 4). - **Dynamic growth:** optional contiguous overflow chunks (Milestone 5). +- **Observability:** opt-in `InstrumentedPool` Decorator (stats, occupancy) + `PoolObserver` for lifecycle events — zero cost to undecorated pools (Milestone 6). - **Quality gates:** `clang-tidy` clean, ASan + UBSan + (when threading lands) TSan green, Valgrind clean, Doxygen-documented public surface. - **Benchmark target:** measured against `malloc`/`free` over 1,000,000 iterations. @@ -77,7 +78,7 @@ Reports for other host × compiler combinations (Linux / GCC, Linux / Clang, mac ## Status -`v0.5.0` — dynamic growth mode. Optional, runtime, per-pool dynamic growth (spec §2.2): a pool created with `memory_pool_create_dynamic` / `Pool::make_dynamic` / `PoolBuilder::with_growth_factor` acquires a new geometric contiguous chunk on exhaustion instead of failing; the default stays fixed-size (v0.4.0 behaviour, bit-for-bit). The pool is now a **Composite** — an inline first chunk plus an append-only list of overflow chunks under one shared free list — so `alloc`/`free` stay O(1) (only the `free` safety check and `destroy` are O(log N) in dynamic mode) and per-block overhead stays zero. Growth runs under the policy's synchronization (NONE / MUTEX); lock-free + dynamic is rejected at creation (deferred). Three new ADRs (0022–0024) bring the total to 24; **Composite** flips to Implemented; the per-pool metadata budget is renegotiated 128 → 192. Release notes for `v0.5.0` live in [`docs/releases/v0.5.0.md`](docs/releases/v0.5.0.md). +`v0.6.0` — observability & decorators. Optional statistics, on-demand logging, and lifecycle-event notification, all **opt-in by type** — a program that uses `Pool` directly is byte-identical to v0.5.0 and pays nothing. The header-only `InstrumentedPool` **Decorator** composes a `Pool` and counts allocation activity (allocations / deallocations / failures / live + the `peak_live_` high-water mark) via relaxed atomics, exposed as a `PoolStats` snapshot plus `write_summary`; a runtime **Observer** (`PoolObserver` via `add_observer`) delivers `exhausted` / `grew` / `destroyed` events from the same interception points. The one library-side addition is a `grow_count_` atomic on `struct memory_pool` (bumped only on the growth slow path) behind the O(1) `memory_pool_growths` accessor, within the ADR-0015 192-byte budget. The new `zero_overhead` test verifies the zero-cost-when-disabled contract structurally. Two new ADRs (0025–0026) bring the total to 26; **Decorator** and **Observer** flip to Implemented. Release notes for `v0.6.0` live in [`docs/releases/v0.6.0.md`](docs/releases/v0.6.0.md). | Milestone | Title | Status | |-----------|------------------------------------|-------------| @@ -87,8 +88,8 @@ Reports for other host × compiler combinations (Linux / GCC, Linux / Clang, mac | 3 | C++ Wrapper & Type Safety | ✅ complete | | 4 | Thread-Safe Variant | ✅ complete | | 5 | Dynamic Growth Mode | ✅ complete | -| 6 | Observability & Decorators | ⏳ next | -| 7 | Release & Polish | ⏳ planned | +| 6 | Observability & Decorators | ✅ complete | +| 7 | Release & Polish | ⏳ next | See [`ROADMAP.md`](ROADMAP.md) for the per-task breakdown and the Spec Coverage Map at the bottom (traceability from spec sections to roadmap items). diff --git a/ROADMAP.md b/ROADMAP.md index aa348ee..7d642d7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -102,7 +102,7 @@ Goal: optional logging / statistics / tracing without touching the hot path of r - [x] 6.1 ADR + impl: **Decorator** for an instrumented pool variant (counters, allocation histogram, optional logging). Implemented per [ADR-0025](docs/adr/0025-decorator-for-instrumented-pool.md) as the header-only [`instrumented_pool.hpp`](src/main/cpp/it/d4np/memorypool/instrumented_pool.hpp): `InstrumentedPool` **composes** a `Pool` (Decorator by composition — `Pool` is concrete + move-only with no virtual surface, so it wraps rather than inherits, like `TypedPool` / `PoolAllocator`) and re-exposes its allocation verbs, counting `allocations_` / `deallocations_` / `allocation_failures_` and tracking `live_` and its high-water mark `peak_live_` (a relaxed compare-exchange max on each alloc). A copyable `PoolStats` snapshot via `stats()` and a `write_summary(std::ostream&)` provide on-demand logging; per-event lifecycle notification is the M6.2 Observer's job, not here. **Counters are relaxed atomics**, so the decorator is safe to wrap a thread-safe (`MUTEX`/`LOCKFREE`) pool and drive it concurrently (the live/peak high-water mark is an approximate diagnostic under contention); `std::atomic` is not movable, so a **hand-written move ctor/assign** loads and re-seeds the counters, keeping the type factory-returnable (`make` / `make_dynamic` mirror `Pool`). **Zero overhead when disabled = opt-in by type** — a program using `Pool` directly pays nothing (no counter, no branch, no atomic); M6.3 verifies the plain-`Pool` path is unchanged. The roadmap's "allocation histogram" is **degenerate for a fixed-block pool** (every block is `block_size`, one bucket), so `peak_live_` is the occupancy signal instead — recorded in ADR-0025 §3. Six rejected alternatives recorded (GoF virtual-interface Decorator, template `Decorator`, non-atomic counters, compile-time-gated instrumentation, per-op `std::function` logger, size histogram). A dedicated `instrumented_pool` CTest binary (five cases — counters/live/peak, exhaustion-failure counting + LIFO forwarding, `write_summary`, move semantics + pass-throughs, and an over-a-dynamic-pool case that never fails) passes under NONE / MUTEX / LOCKFREE (MSVC 19.51). **Decorator** flips to `Implemented` (row #10) in [`docs/patterns/README.md`](docs/patterns/README.md). - [x] 6.2 ADR + impl: **Observer** for pool-lifecycle events (exhaustion, growth, destruction). Implemented per [ADR-0026](docs/adr/0026-observer-for-pool-lifecycle-events.md): the GoF **runtime** Observer — a `PoolObserver` abstract interface (`virtual void on_pool_event(PoolEvent, const PoolStats&) noexcept`) registered via `InstrumentedPool::add_observer` — is wired into the M6.1 Decorator's existing alloc/free interception points rather than a separate, non-stackable wrapper, so Decorator (stats) and Observer (events) compose in one observability type. Events: **`exhausted`** (`try_allocate` → `nullptr` / `allocate` → `std::bad_alloc`), **`destroyed`** (the dtor notifies once; a moved-from instance has an emptied observer list so it notifies nobody), and **`grew`** — detected via a new **O(1)** core counter. Because growth happens inside the C `pop_head` (invisible above the C boundary, and diffing `metadata_bytes` per alloc would be O(chunks)), `struct memory_pool` gains `std::atomic grow_count_` incremented (relaxed) **only in `grow_pool`** (the rare slow path — hot path untouched), exposed by a new always-present, NULL-tolerant, C89-clean accessor `size_t memory_pool_growths(const memory_pool_t*)` (the `InstrumentedPool` reads it after each allocation in O(1) and notifies on a rise). `on_pool_event` is `noexcept` by contract (it may fire from the `noexcept` `try_allocate` and the dtor), so `try_allocate` keeps its `noexcept`. **Zero overhead**: plain `Pool` is unchanged; an observer-less `InstrumentedPool` does an empty-vector check + one relaxed load per alloc (M6.3 verifies). The `grow_count_` field adds 8 bytes (NONE struct → 64, MUTEX → 144, within the 192 budget). Notification is not internally synchronized — observers must out-live the pool and be thread-safe for concurrent observable use (documented). `c_consumer_min.c` exercises `memory_pool_growths` under C89/C99; three new `instrumented_pool` `TEST_CASE`s cover exhaustion / destruction / growth notification, passing under NONE / MUTEX / LOCKFREE. Six rejected alternatives recorded in ADR-0026. **Observer** flips to `Implemented` (row #11) in [`docs/patterns/README.md`](docs/patterns/README.md). (clang-format + clang-tidy run locally before push — both clean.) - [x] 6.3 Tests verifying zero-overhead in release builds when instrumentation is disabled. Implemented as the dedicated `zero_overhead` CTest binary [`zero_overhead_test.cpp`](src/test/cpp/it/d4np/memorypool/zero_overhead_test.cpp), which discharges the [ADR-0025](docs/adr/0025-decorator-for-instrumented-pool.md) §5 contract ("instrumentation disabled" = **opt-in by type**: a program using `Pool` directly pays nothing). The two §5 obligations — the plain-`Pool` path is **byte-identical**, and the overhead lives **only inside `InstrumentedPool`** — are *structural* facts, so they are verified by `static_assert` (compile-time, config-independent — they hold in Release exactly as in Debug) plus a runtime behavioural-equivalence check, **not** a wall-clock benchmark: a timing gate on shared CI runners is too noisy to be meaningful ([ADR-0014](docs/adr/0014-microbenchmark-methodology-pool-vs-malloc.md) §8) and a measured delta can never *prove* zero overhead the way the type structure does (`pool_vs_malloc_bench` remains the home for indicative numbers). Three structural proofs: **(1) opt-in by type** — a `std::void_t` detection idiom asserts the `stats()` / `add_observer()` surface is absent from `Pool` and present on `InstrumentedPool`, so a `Pool` holder cannot even name an instrumentation operation; **(2) byte-identical footprint** — `sizeof(Pool) == sizeof(memory_pool_t*)` and `Pool` stays standard-layout (the decorator adds no member, vtable, or padding to it); **(3) contained overhead** — `sizeof(InstrumentedPool) >= sizeof(Pool) + 5 × sizeof(atomic) + sizeof(size_t)`, so the counter cost is wholly inside the decorator. The runtime case proves a bare `Pool` and an `InstrumentedPool` over the same configuration are behaviourally indistinguishable: identical `metadata_bytes()` (the C `struct memory_pool` does not grow — instrumentation is header-only C++ state, ADR-0015), identical `block_size()`, identical capacity / exhaustion point, and the identical LIFO re-allocation signature (drain → free → re-drain yields the reverse of the first sequence on both paths, ADR-0009 §1). Four `TEST_CASE`s / 77 assertions; the binary is an ordinary CTest target, so it runs in **every** CI matrix cell including the Release cells — the "in release builds" coverage the item asks for, without a bespoke Release-only job. No new ADR — the methodology is fixed by ADR-0025 §5 (clang-format + clang-tidy run locally before push — both clean). -- [ ] 6.4 **Close Milestone 6 → `v0.6.0`**: bump `version.hpp`, roll `CHANGELOG.md`, draft `docs/releases/v0.6.0.md`, open release PR (ADR-0004 §2). +- [x] 6.4 **Close Milestone 6 → `v0.6.0`**: bump `version.hpp`, roll `CHANGELOG.md`, draft `docs/releases/v0.6.0.md`, open release PR (ADR-0004 §2). [`version.hpp`](src/main/cpp/it/d4np/memorypool/version.hpp) is bumped to `MINOR=6 PATCH=0` and `PBR_MEMORY_POOL_VERSION_STRING` to `"0.6.0"`; the `pool_smoke` version-check `TEST_CASE` asserts the new components. [`CHANGELOG.md`](CHANGELOG.md) `[Unreleased]` is rolled into a sealed `## [0.6.0] — 2026-06-14` block (milestone headline + the M6.1–M6.3 `Added` subsections + a *no-flip* Spec Coverage Map note — observability is additive instrumentation, no spec row maps to it), with the bottom-of-file link references rewritten (`[Unreleased]` → `compare/v0.6.0...HEAD`, new `[0.6.0]` → `releases/tag/v0.6.0`). [`docs/releases/v0.6.0.md`](docs/releases/v0.6.0.md) carries the human-prose release notes for `release.yml`. [`README.md`](README.md) status badge (green `v0.5.0 dynamic growth` → green `v0.6.0 observability`), the *At a glance* observability bullet, status paragraph, and milestone table (Milestone 6 `⏳ next` → `✅ complete`, Milestone 7 `⏳ planned` → `⏳ next`) are refreshed. The maintainer reviews and merges this PR, then the agent tags `v0.6.0` from `master` per [ADR-0008](docs/adr/0008-delegate-tag-creation-and-push-to-the-agent.md), and the maintainer clicks *Publish* on the draft GitHub Release produced by `release.yml`. ## Milestone 7 — Release & Polish @@ -152,6 +152,23 @@ When a roadmap item flips from ⏳ to ✅, update the corresponding cell(s) in t > Living, dated note describing where the project stands at the end of the most recent work session. Updated at the close of each session so the next session resumes from a known point without re-reading the full PR history. Latest entry first; older entries are kept for trail. +### 2026-06-14 — End of M6 release session (v0.6.0) + +- **Done in this session** — the whole of Milestone 6: 6.1 (ADR-0025 `InstrumentedPool` Decorator, PR #47), 6.2 (ADR-0026 Observer for pool-lifecycle events + `memory_pool_growths` accessor + `grow_count_`, PR #48), 6.3 (the `zero_overhead` zero-overhead-when-disabled verification test, PR #49), and 6.4 (this release PR — bump to `0.6.0`, roll `CHANGELOG.md` into `[0.6.0] — 2026-06-14`, add `docs/releases/v0.6.0.md`, refresh README). +- **Library state on `master`** — the v0.5.0 surface plus opt-in observability, all header-only on the C++ side. `InstrumentedPool` (Decorator) composes a `Pool`, counting allocations / deallocations / failures / live + `peak_live_` via relaxed atomics, with a `PoolStats` snapshot + `write_summary`; a runtime `PoolObserver` (registered via `add_observer`) fires `exhausted` / `grew` / `destroyed`. The one library-side (C) change is a `std::atomic grow_count_` on `struct memory_pool`, bumped only in `grow_pool`, exposed by the O(1), C89-clean `memory_pool_growths` accessor; the ADR-0015 budget stays 192 (NONE struct → 64, MUTEX → 144). Observability is **opt-in by type** — a plain `Pool` is byte-identical to v0.5.0. CTest registers ten binaries (the eight from M5 + `instrumented_pool` + `zero_overhead`). `CHANGELOG.md` has a sealed `[0.6.0]` block and a fresh empty `Unreleased`. +- **ADRs accepted to date** — 0001–0024 (see prior checkpoints) plus 0025 (Decorator for an instrumented pool) and 0026 (Observer for pool-lifecycle events). Total: 26. +- **Patterns catalogue** — RAII, Pimpl, Factory Method, Builder, Adapter, Iterator, Strategy, Template Method, Composite, **Decorator**, and **Observer** are all Implemented. Every pattern planned through Milestone 6 is now realised. +- **Spec Coverage Map state at `v0.6.0`** — unchanged from v0.5.0: eleven rows ✅. Milestone 6 added no spec row (observability is additive instrumentation, not a spec requirement). The full row-by-row acceptance audit is M7.6. +- **Open issues / follow-ups carried into the next session** — none blocking. Double-free detection (deferred from M2 / ADR-0012), `TypedPool` instrumentation, lock-free dynamic growth, pool shrink-on-idle, and per-thread caches are all deferred (documented). After this release PR merges, the agent tags `v0.6.0` from `master` (ADR-0008) and the maintainer clicks *Publish*. +- **Resume the next session with**, in order (Milestone 7 — Release & Polish, the road to `v1.0.0`): + 1. **M7.1** — Doxygen-generated API documentation published as a static site. + 2. **M7.2** — README: full usage example, performance summary, compatibility matrix. + 3. **M7.3** — `CHANGELOG.md` audit for the v1.0.0 entry. + 4. **M7.4** — ADR: install / packaging layout (header export, pkg-config, CMake `find_package` config). + 5. **M7.5** — patterns catalogue audit (every adopted pattern has an ADR + a code location). + 6. **M7.6** — spec compliance acceptance: walk every Spec Coverage Map row; record the audit in an ADR. + 7. **M7.7** — close Milestone 7 → `v1.0.0`. + ### 2026-06-13 — End of M5 release session (v0.5.0) - **Done in this session** — the whole of Milestone 5: 5.1 (ADR-0022 dynamic-growth policy decision, PR #42), 5.2 (ADR-0023 Composite chunk-list representation, dormant, PR #43), 5.3 (ADR-0024 growth implementation + `memory_pool_create_dynamic` + lock-free deferral + budget 128→192, PR #44), 5.4 (exhaustion-and-grow tests + growth benchmark, PR #45), and 5.5 (this release PR — bump to `0.5.0`, roll `CHANGELOG.md` into `[0.5.0] — 2026-06-13`, add `docs/releases/v0.5.0.md`, refresh README). diff --git a/docs/releases/v0.6.0.md b/docs/releases/v0.6.0.md new file mode 100644 index 0000000..3d8017a --- /dev/null +++ b/docs/releases/v0.6.0.md @@ -0,0 +1,111 @@ +# 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](../adr/0025-decorator-for-instrumented-pool.md)) + +The new header-only `it::d4np::memorypool::InstrumentedPool` **composes** a `Pool` and re-exposes its allocation surface, counting activity as it forwards: + +```cpp +#include + +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](../adr/0026-observer-for-pool-lifecycle-events.md)) + +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: + +```cpp +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_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 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: + +```c +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](../adr/0025-decorator-for-instrumented-pool.md) §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 footprint** — `sizeof(Pool) == sizeof(memory_pool_t*)` and `Pool` stays standard-layout: the decorator adds no member, vtable, or padding to it. +- **Contained overhead** — `InstrumentedPool` 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_assert`s 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](../adr/0025-decorator-for-instrumented-pool.md) — Decorator for an instrumented pool variant. +- [ADR-0026](../adr/0026-observer-for-pool-lifecycle-events.md) — Observer for pool-lifecycle events. + +## Design-patterns catalogue + +**Decorator** (row #10) and **Observer** (row #11) flip to *Implemented* in [`docs/patterns/README.md`](../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` 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`: + +```bash +sha256sum --check SHA256SUMS +``` + +## Build and use + +```cpp +#include + +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 + +- Changelog entry: [`CHANGELOG.md` — `[0.6.0]`](../../CHANGELOG.md#060--2026-06-14) +- Milestone plan: [`ROADMAP.md` — Milestone 6](../../ROADMAP.md#milestone-6--observability--decorators) +- Specification: [`docs/specs/01_spec_cpp_memory_pool.md`](../specs/01_spec_cpp_memory_pool.md) +- Previous release: [`docs/releases/v0.5.0.md`](v0.5.0.md) diff --git a/src/main/cpp/it/d4np/memorypool/version.hpp b/src/main/cpp/it/d4np/memorypool/version.hpp index 3b0d432..6dc13ba 100644 --- a/src/main/cpp/it/d4np/memorypool/version.hpp +++ b/src/main/cpp/it/d4np/memorypool/version.hpp @@ -20,13 +20,13 @@ namespace it::d4np::memorypool { inline constexpr unsigned PBR_MEMORY_POOL_VERSION_MAJOR = 0; /** Minor version component (incremented with each closed milestone pre-1.0). */ -inline constexpr unsigned PBR_MEMORY_POOL_VERSION_MINOR = 5; +inline constexpr unsigned PBR_MEMORY_POOL_VERSION_MINOR = 6; /** Patch version component (incremented for hotfixes between milestones). */ inline constexpr unsigned PBR_MEMORY_POOL_VERSION_PATCH = 0; /** Pre-formatted version string, kept in lockstep with the components above. */ -inline constexpr const char* PBR_MEMORY_POOL_VERSION_STRING = "0.5.0"; +inline constexpr const char* PBR_MEMORY_POOL_VERSION_STRING = "0.6.0"; } // namespace it::d4np::memorypool diff --git a/src/test/cpp/it/d4np/memorypool/pool_smoke_test.cpp b/src/test/cpp/it/d4np/memorypool/pool_smoke_test.cpp index a9a3360..de8ca2d 100644 --- a/src/test/cpp/it/d4np/memorypool/pool_smoke_test.cpp +++ b/src/test/cpp/it/d4np/memorypool/pool_smoke_test.cpp @@ -56,11 +56,11 @@ TEST_CASE("version constants are consistent with the project version") { const std::string_view ver{mem::PBR_MEMORY_POOL_VERSION_STRING}; CHECK(ver.find('.') != std::string_view::npos); - // Milestone 5 closes at v0.5.0; these constants are bumped from this - // release PR (M5.5) in lockstep with version.hpp. Milestone 6 will - // bump them again to 0.6.0 when M6.4 lands. + // Milestone 6 closes at v0.6.0; these constants are bumped from this + // release PR (M6.4) in lockstep with version.hpp. Milestone 7 will + // bump them again to 1.0.0 when M7.7 lands. CHECK(mem::PBR_MEMORY_POOL_VERSION_MAJOR == 0U); - CHECK(mem::PBR_MEMORY_POOL_VERSION_MINOR == 5U); + CHECK(mem::PBR_MEMORY_POOL_VERSION_MINOR == 6U); CHECK(mem::PBR_MEMORY_POOL_VERSION_PATCH == 0U); }