A long-lived bounded seam has to recycle (L0 substrate)

heap_source_t recycles but is unbounded. bump_source_t is bounded but never recycles. A seam that outlives one unit of work — a graph’s control source, a receiver’s decode arena — needs both, and until tr::mem::pool_source_t existed such a seam could not be bounded at all (ADR-0067).

It is segregated free lists over a slab the caller owns, with no per-block header — the seam’s sized release makes one unnecessary.

What to notice

  • used() is a high-water mark, not a running total. That one sentence is the whole example: ten thousand alloc/release rounds over a 512-byte slab settle at 128 bytes, because once every live block has been carved once, a round costs nothing. A bump source in the same position would have wanted 1.28 MB and refused after the first eight rounds.

  • Both bounds are injected. The caller supplies the slab and the span of size_class_t slots, so neither the byte ceiling nor the class count is a constant inside the library — a bounded node is a property the deployer states, not one the library grants.

  • Recycling is LIFO on the exact shape. A released block is the next one handed out, which the example pins by pointer identity — the cheapest possible reuse, and the reason the free list can live inside the free blocks.

  • Exact packing is the visible consequence of the header-free scheme. A 512-byte slab holds exactly eight 64-byte blocks. No rounding, no bookkeeping, nothing to subtract.

  • Full is still nullptr. A bounded source never falls back to the platform heap and never aborts; the ninth block is a refusal the caller answers with backpressure.

  • Own one per receiver — do not share one across receive threads. A shared free-list pool collapses to roughly a fifteenth of its own single-thread rate on a 12-core host (8.3 M → 1.36 M ops/s, p50 60 ns → 3587 ns) while the platform heap scales, so the problem is the shared cacheline and not the flavour of the guard — a lock-free CAS on the list head replaces one contended word with the same word (ADR-0060 erratum 1). A Sync policy belongs only at wiring frequency, never per frame.

  • Nothing here is conditional — the target builds and runs under every CI leg, and it uses no threads.

Source

 1/*
 2 * SPDX-License-Identifier: Apache-2.0
 3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
 4 */
 5
 6/**
 7 * @file
 8 * @brief ONE CONCEPT — a LONG-LIVED bounded seam has to recycle, not just to bound.
 9 *
10 * `heap_source_t` recycles but is unbounded; `bump_source_t` is bounded but never recycles.
11 * A seam that outlives one unit of work — a graph's control source, a receiver's decode
12 * arena — needs both, and that is `tr::mem::pool_source_t`: segregated free lists over a
13 * slab the CALLER owns, with no per-block header (the seam's sized `release` makes one
14 * unnecessary).
15 *
16 * The property to watch is `used()`. It is the slab high-water mark, not a running total:
17 * once every LIVE block has been carved once, a release/alloc round costs no new bytes at
18 * all. Ten thousand rounds over a 512-byte slab therefore settle where two rounds do — which
19 * is exactly what the bump source in the same position cannot do (see mem-bump-source).
20 *
21 * Both bounds are injected, deliberately: the caller supplies the slab AND the span of
22 * `size_class_t` slots, so neither the byte ceiling nor the class count is a constant inside
23 * the library (ADR-0067).
24 *
25 * Runs under ctest as `example_mem_pool_source`; returns non-zero on any failed check.
26 */
27
28#include <array>
29#include <cstddef>
30#include <cstdio>
31
32#include "libtracer/mem_source.hpp"
33
34namespace {
35
36/** @brief Report expectation @p what and record a failure on @p ok. */
37void check(bool& ok, bool cond, const char* what) {
38    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
39    ok = ok && cond;
40}
41
42}  // namespace
43
44int main() {
45    bool ok = true;
46    alignas(std::max_align_t) std::array<std::byte, 512> slab{};
47    std::array<tr::mem::size_class_t, 4> classes{};
48    tr::mem::pool_source_t<> pool{slab, classes};
49
50    // The long-lived shape: two blocks live at a time, ten thousand times over.
51    bool served_every_round = true;
52    for (int round = 0; round < 10000; ++round) {
53        void* const a = pool.try_alloc(64, 8);
54        void* const b = pool.try_alloc(64, 8);
55        if (a == nullptr || b == nullptr) {
56            served_every_round = false;
57            break;
58        }
59        pool.release(a, 64, 8);
60        pool.release(b, 64, 8);
61    }
62    check(ok, served_every_round, "10,000 alloc/release rounds on a 512-byte slab all served");
63    check(ok, pool.used() == 128,
64          "used() settled at the 2-block high-water — a bump source would have wanted 1.28 MB");
65    std::printf("512-byte slab, 10,000 rounds: %zu bytes carved, %zu class slot(s) in use\n",
66                pool.used(), pool.classes_used());
67
68    // Recycling is LIFO on the exact shape, and the freed block is the one that comes back.
69    void* const first = pool.try_alloc(64, 8);
70    pool.release(first, 64, 8);
71    check(ok, pool.try_alloc(64, 8) == first, "a released block is the next one handed out");
72    check(ok, pool.used() == 128, "and reuse carves nothing new");
73
74    // Bounded is still bounded: the slab is the ceiling, and reaching it is a value.
75    int more = 0;
76    while (pool.try_alloc(64, 8) != nullptr) ++more;
77    check(ok, more == 7,
78          "seven more came out — with the one in hand, exactly 8 × 64 B in 512: no header");
79    check(ok, pool.try_alloc(64, 8) == nullptr,
80          "a full slab answers nullptr, never the platform heap and never an abort()");
81    return ok ? 0 : 1;
82}

See also: backends · memory substrate reference · classes do not share · concurrency & scaling reference.