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
50 changes: 50 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,56 @@ jobs:
"$bin" --iterations 10000 --repeats 3
echo "OK — bench binary ran to completion."

# ---------------------------------------------------------------------------
# ROADMAP §9.4 — external-allocator baselines + tail-latency percentiles
# (ADR-0045). External baselines are measured the safe way: re-run the SAME
# bench under LD_PRELOAD, which swaps the whole process allocator (jemalloc /
# tcmalloc take over global malloc on load, so they cannot be linked or dlopen'd
# beside the system allocator). Exercises the opt-in --percentiles table too.
# Linux only; never touches the MSVC leg. Asserts exit code 0, not numbers.
# ---------------------------------------------------------------------------
bench-baselines:
name: bench / external baselines + percentiles
runs-on: ubuntu-24.04
steps:
- name: Check out
uses: actions/checkout@v6

- name: Install CMake and Ninja
uses: lukka/get-cmake@latest

- name: Install jemalloc and tcmalloc runtime libraries
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libjemalloc2 libtcmalloc-minimal4t64

- name: Configure and build (bench preset)
shell: bash
run: |
export CC=gcc CXX=g++
cmake --preset bench
cmake --build --preset bench

- name: Baseline via LD_PRELOAD + the percentile table
shell: bash
run: |
set -euo pipefail
bin="build/bench/src/bench/cpp/it/d4np/memorypool/pool_vs_malloc_bench"
# LD_PRELOAD resolves a soname through the loader cache (ldconfig ran on
# install), so no filesystem search is needed.
run() { # label, LD_PRELOAD value ("" = none), expected "# allocator:" text
echo "=== allocator: $1 ==="
out="$(LD_PRELOAD="$2" "$bin" --scenario all --iterations 20000 --repeats 3 --percentiles)"
echo "$out"
echo "$out" | grep -q "# allocator: $3" || { echo "FAIL: allocator disclosure ($1)"; exit 1; }
echo "$out" | grep -q "p99_ns/op" || { echo "FAIL: percentile table missing ($1)"; exit 1; }
}
run "system malloc" "" "system malloc"
run "jemalloc" "libjemalloc.so.2" "libjemalloc.so.2"
run "tcmalloc" "libtcmalloc_minimal.so.4" "libtcmalloc_minimal.so.4"
echo "OK — pool benchmarked against system malloc, jemalloc, and tcmalloc; percentile table present."

# ---------------------------------------------------------------------------
# thread-safety — build and run the existing (single-threaded) test suite
# under each non-default PBR_MEMORY_POOL_THREAD_SAFETY policy (ADR-0020 /
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ dated version block (`## [X.Y.Z] — YYYY-MM-DD`) when a release PR closes a mil

### Added

- **Benchmark extension — tail-latency percentiles and optional jemalloc/tcmalloc baselines.**
The pool-vs-malloc microbenchmark gains an opt-in `--percentiles` mode that emits a separate
per-operation **p50/p90/p99/p999** table — the dynamic-pool growth row surfaces the
microsecond-scale growth spike the aggregate median hides — and **jemalloc** / **tcmalloc**
baselines measured the safe way, by re-running the bench under **`LD_PRELOAD`** (those
allocators take over global `malloc` on load, so they cannot be linked or `dlopen`'d beside
the system allocator without crashing). A `# allocator:` header line discloses which allocator
each run measured; the bench carries no allocator-specific code, so the default build stays
zero-external-dependency (spec §3.3). Both are strictly additive: the
[ADR-0014](docs/adr/0014-microbenchmark-methodology-pool-vs-malloc.md) aggregate ns/op table
and its committed numbers are unchanged. A Linux `bench-baselines` CI cell installs both
allocators and exercises the new paths (non-asserting on numbers, per ADR-0014 §8).
[ADR-0045](docs/adr/0045-benchmark-percentiles-and-external-baselines.md). Closes #111.
- **Coverage-guided fuzzing harness for the pool surface.** A new
[`pool_fuzz.cpp`](src/test/cpp/it/d4np/memorypool/pool_fuzz.cpp) drives randomized
`alloc` / `free(valid)` / `free(NULL)` / `free(foreign)` sequences — over both fixed and
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Goal: a coherent, post-`v1.0.0` wave of **additive, ABI-compatible** capabilitie
- [x] 9.1 **`std::pmr::memory_resource` adapter** (`PoolMemoryResource`) — the "door left open" in [ADR-0018](docs/adr/0018-stl-allocator-adapter.md): a `std::pmr::memory_resource` subclass binding one `Pool` so any `std::pmr`-aware container can draw from it through `std::pmr::polymorphic_allocator`, without the `PoolAllocator<T>` per-type rebind. Deterministic `(bytes, alignment)` routing to the bound pool — over-sized / over-aligned requests delegate to a configurable upstream resource, and exhaustion of a pool-eligible request throws `std::bad_alloc` rather than falling back (preserving the deterministic deallocate routing) — with `is_equal` by `(pool, upstream)` identity, gated behind `PBR_MEMORY_POOL_HAS_PMR` where `<memory_resource>` is available. Header-only, additive, ABI-compatible. Decided in [ADR-0042](docs/adr/0042-pmr-memory-resource-adapter.md); implemented in [`pool_memory_resource.hpp`](src/main/cpp/it/d4np/memorypool/pool_memory_resource.hpp) with [`pool_memory_resource_test.cpp`](src/test/cpp/it/d4np/memorypool/pool_memory_resource_test.cpp) (issue #107).
- [x] 9.2 **Opt-in debug hardening** — freed-block poisoning, canaries, and free-list safe-linking (which also yields double-free detection); zero cost when the gate is off (issue #109). Decided in [ADR-0043](docs/adr/0043-opt-in-debug-hardening.md); implemented in [`memory_pool.cpp`](src/main/cpp/it/d4np/memorypool/memory_pool.cpp) behind the compile-time `PBR_MEMORY_POOL_HARDENING` knob (a `harden` CMake preset), with the swappable violation-handler surface in [`pool_hardening.hpp`](src/main/cpp/it/d4np/memorypool/pool_hardening.hpp) and [`pool_hardening_test.cpp`](src/test/cpp/it/d4np/memorypool/pool_hardening_test.cpp) (CTest `pool_hardening`). The "canary" is realized as one trailing **guard word** living in *added* slot stride — so the user-visible `block_size` and the ADR-0009 alignment guarantee are unchanged and the default build is byte-for-byte unchanged (the mechanism is fully compiled out). Poisoning (`0xDE`) catches use-after-free on the next allocation, the guard word catches a write past `block_size` and a double-free, and glibc-style safe-linking (`ptr XOR (slot_addr >> 12)`) protects the in-band next-link. A `harden` CI matrix cell builds and tests the hardened configuration on each Tier-1 platform. Works with fixed and dynamic pools across all three thread-safety policies.
- [x] 9.3 **Coverage-guided fuzzing harness** for the pool surface — a libFuzzer target, time-boxed in CI (issue #108). Decided in [ADR-0044](docs/adr/0044-coverage-guided-fuzzing-harness.md); implemented in [`pool_fuzz.cpp`](src/test/cpp/it/d4np/memorypool/pool_fuzz.cpp) as a stateful opcode interpreter with a shadow oracle that asserts the no-alias, canary-intact, foreign/NULL-no-op ([ADR-0012](docs/adr/0012-foreign-pointer-and-out-of-range-pointer-policy.md)) and `InstrumentedPool` accounting invariants across fixed and dynamic pools. One engine-agnostic source yields both the Clang-only libFuzzer target `pool_fuzz` (opt-in `PBR_MEMORY_POOL_BUILD_FUZZERS`, a `fuzz` preset under `-fsanitize=fuzzer,address,undefined`) and the always-built standalone [`pool_fuzz_replay`](src/test/cpp/it/d4np/memorypool/pool_fuzz_corpus/) target that replays the seed corpus as a portable regression gate (CTest `pool_fuzz_replay`) on every platform, including MSVC. A dedicated `fuzz` CI job replays the corpus and fuzzes for a bounded time on every PR; a crash is filed in the bug ledger ([ADR-0039](docs/adr/0039-bug-ledger-and-triage-protocol.md)). Test-only and additive — the release build and the benchmark numbers are untouched.
- [ ] 9.4 **Benchmark extension** — external allocator baselines (jemalloc / tcmalloc) and p99 tail-latency reporting (issue #111).
- [x] 9.4 **Benchmark extension** — external allocator baselines (jemalloc / tcmalloc) and p99 tail-latency reporting (issue #111). Decided in [ADR-0045](docs/adr/0045-benchmark-percentiles-and-external-baselines.md) (extends [ADR-0014](docs/adr/0014-microbenchmark-methodology-pool-vs-malloc.md)); implemented in [`pool_vs_malloc_bench.cpp`](src/bench/cpp/it/d4np/memorypool/pool_vs_malloc_bench.cpp). An opt-in `--percentiles` mode adds a separate per-operation p50/p90/p99/p999 table (the dynamic-pool growth row surfaces the amortized-growth spike the median hides); jemalloc / tcmalloc baselines are measured by re-running the bench under `LD_PRELOAD` (they take over global `malloc` on load, so they cannot be linked or `dlopen`'d beside the system allocator without crashing — an in-process attempt was tried and rejected); a `# allocator:` header discloses which allocator each run measured. The bench carries no allocator-specific code, so the default build keeps zero external dependencies (spec §3.3) and the [ADR-0014](docs/adr/0014-microbenchmark-methodology-pool-vs-malloc.md) aggregate table and its committed numbers are unchanged. A Linux `bench-baselines` CI cell re-runs the bench under each allocator's preload and asserts the disclosure + percentile table (non-asserting on numbers).

---

Expand Down
6 changes: 4 additions & 2 deletions docs/adr/0014-microbenchmark-methodology-pool-vs-malloc.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# ADR-0014: Microbenchmark methodology — pool vs. `malloc`

- **Status:** Accepted
- **Status:** Accepted (extended by [ADR-0045](0045-benchmark-percentiles-and-external-baselines.md))
- **Date:** 2026-06-11
- **Deciders:** Daniel Polo (maintainer), Claude (architect agent)
- **Related:** spec §6.3, ROADMAP §2.9, [ADR-0005](0005-toolchain-matrix-and-supported-platforms.md) §3 (compiler matrix), [ADR-0006](0006-code-style-and-static-analysis-baseline.md) (style + lint baseline), [ADR-0009](0009-free-list-layout-block-size-constraints-and-alignment-guarantee.md) §2 (block-size constraints the benchmark must respect), [ADR-0013](0013-doxygen-for-api-markdown-for-narrative.md) (narrative goes in Markdown — bench reports too)
- **Related:** spec §6.3, ROADMAP §2.9, [ADR-0005](0005-toolchain-matrix-and-supported-platforms.md) §3 (compiler matrix), [ADR-0006](0006-code-style-and-static-analysis-baseline.md) (style + lint baseline), [ADR-0009](0009-free-list-layout-block-size-constraints-and-alignment-guarantee.md) §2 (block-size constraints the benchmark must respect), [ADR-0013](0013-doxygen-for-api-markdown-for-narrative.md) (narrative goes in Markdown — bench reports too), [ADR-0045](0045-benchmark-percentiles-and-external-baselines.md) (adds tail-latency percentiles + optional jemalloc/tcmalloc baselines)

> **Extended by [ADR-0045](0045-benchmark-percentiles-and-external-baselines.md)** (2026-07-09): an opt-in `--percentiles` per-operation table (p50/p90/p99/p999) and optional feature-detected jemalloc/tcmalloc baselines. Both are strictly additive — the §4 aggregate statistics, the §6 output contract, and the committed numbers below are unchanged.

## Context

Expand Down
Loading
Loading