Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<size_t>
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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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 |
|-----------|------------------------------------|-------------|
Expand All @@ -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).

Expand Down
19 changes: 18 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<PoolLike>`, 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<std::size_t> 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<size_t>) + 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

Expand Down Expand Up @@ -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<std::size_t> 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).
Expand Down
Loading
Loading