Skip to content

[M0-CORE-05] Memory pools: ArenaPool<T> + Pool<T> - #5

Merged
offdev merged 2 commits into
masterfrom
m0-core-05-pools
Sep 11, 2026
Merged

offdev merged 2 commits into
masterfrom
m0-core-05-pools

Conversation

@offdev

@offdev offdev commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Implements roadmap step M0-CORE-05 (PRD §10.4 memory model, §9.1 S-2 "no per-frame heap in the safe API"): laige::ArenaPool<T> and laige::Pool<T> in a new header-only src/laige-core/include/laige/pools.h.

What

  • laige::ArenaPool<T> — arena-scoped, bump-only, budgeted storage: one contiguous aligned block; create() advances a cursor, reset() (the per-frame release) destroys the live prefix and rewinds it (O(inUse), idempotent). Slots are uint32_t, valid until the next reset(); no in-frame recycling (that is Pool's job).
  • laige::Pool<T> — stable-handle pool (CPP-007): handle = {uint32 index, uint32 generation}. LIFO free list, O(1) create/destroy; destroy() bumps the slot's generation so every stale handle fails isValid() (the one 2^32-wrap case is defined unsigned wrap, documented in the header). clear() is O(capacity).
  • Common: declared budget fixed at construction (Options{capacity}, S-6/API-006) — overflow → ErrorCode::BudgetExhausted (4, already registered; no new codes), destroy() on invalid/stale handle → ErrorCode::InvalidArgument (2). One backing allocation at construction (setup path); every hot-path op O(1), allocation-free, lock-free, noexcept (PERF-003/006). Move-only (O(1); a moved-from pool is a valid empty pool); destructor destroys every live element (no leak). The pool does not log — the owning system logs the failure under its subsystem name (LOG-002, G-R1), keeping a failing create() a branch on the cold path.
  • Accounting (feeds the M1 profiler, FR-11.4): stats() returns laige::PoolStats — element counts, peakInUse (peak tracking), totalCreated (G-R4 churn), and bytes (capacity/in-use, incl. per-slot bookkeeping for Pool). The pool publishes; the profiler pulls — no registration/callback yet (CORE-004: the sink interface lands with the M1 profiler).
  • Element storage is a plain ElementSlot<T> (alignas(T) std::byte[...]) instead of std::aligned_storage — deprecated since C++23, and GCC 16's C++26-era libstdc++ makes the outer class an empty 1-byte wrapper (using it directly would silently allocate 1-byte slots; caught by the byte-accounting tests during development).

Docs

  • New API contract: docs/api/pools.md (ownership, threading, complexity/allocations, determinism, handle contract, Performance section, performant example + misuse warnings).
  • src/laige-core/README.md and docs/getting-started/building.md status updated (incl. the new pools CTest entry).
  • Roadmap step recorded with Decision/Verify/Size.

Verification (local, 2026-09-11)

  • ctest -R pools: 25 GTest cases green — ArenaPoolBasics, ArenaPoolBudget, PoolBasics, PoolBudget, PoolStale, PoolDestruction, PoolStats, PoolMove.
    • Exhaustion tests assert the Status error (BudgetExhausted), no crash, no silent growth, and reusability after reset/destroy.
    • Reset semantics: destructors run exactly once per element (ctor/dtor counters), cursor rewinds, idempotent.
    • Stale handles: destroy/clear/reuse-slot/default handles all fail isValid(); generation bump verified (gen+1 on reuse); double-destroy → InvalidArgument with no double free; the stale-handle at() assert is proven in a forked child dying on SIGABRT (POSIX jobs; Windows skips with a reason).
    • Leak-free: every placement-new has a matching destroy (counters + ASan).
  • Full suite 11/11 (incl. real-tree include-lint) on GCC 16.2.1: build (static), build-shared, build-asan (ASan+UBSan fatal), build-tsan (TSan halt_on_error=1), and a Clang 22.1.8 tree — zero warnings under the NFR-8.10 policy (-Wall -Werror -fno-exceptions -fno-rtti).

The CI lane will additionally cover the P0 Windows job (MSVC compile of the new header) and macOS.

PRD §10.4 / §9.1 S-2: engine-owned, budgeted, accounted pools.

- laige::ArenaPool<T>: arena-scoped, bump-only, reset-per-frame;
  slots valid until the next reset(); O(inUse) reset, idempotent.
- laige::Pool<T>: stable generation-checked handles (CPP-007);
  LIFO free list, O(1) create/destroy; generation bump on free makes
  stale handles undetectable-free (2^32 wrap documented).
- Both: declared budget fixed at construction (S-6), overflow ->
  ErrorCode::BudgetExhausted (never grows silently); one backing
  allocation at construction; O(1) allocation-free hot paths;
  move-only (moved-from = valid empty pool); destructor destroys all
  live elements (no leak).
- Accounting: laige::PoolStats (counts, peak, churn, bytes) that the
  M1 profiler pulls; no registration/callback (CORE-004).
- Element storage is a plain ElementSlot<T> (alignas(T) std::byte[]) —
  not std::aligned_storage (deprecated C++23+; GCC 16's C++26-era
  libstdc++ makes the outer class an empty 1-byte wrapper).
- Header-only; no new error codes; pools do not log (the owning
  system logs BudgetExhausted per G-R1/LOG-002).

Docs: docs/api/pools.md (AGENTS §9 contract); module README +
building.md status updated; roadmap step recorded with
Decision/Verify/Size.

Verified locally 2026-09-11: 11/11 ctest on GCC 16.2.1 (static,
shared, ASan+UBSan, TSan) and Clang 22.1.8 trees, zero warnings;
25 GTest cases in ctest -R pools (exhaustion -> Status error, reset
semantics, generation-checked stale handles incl. a forked-child
SIGABRT assert proof on POSIX, leak-free destruction counters);
include-lint real-tree check green.
@offdev
offdev merged commit 4a572af into master Sep 11, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant