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_tslots, 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
Syncpolicy 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.