A bump source: carve, and never take back (L0 substrate)

tr::mem::bump_source_t is the nothrow twin of std::pmr::monotonic_buffer_resource: a cursor over a span the caller owns. try_alloc moves the cursor forward, release of a block from that span is a no-op, and reset() is the only way storage comes back — all of it, at once.

That is a lifetime rule rather than a performance note, and it decides where a bump source may be wired at all.

What to notice

  • It is a scope-lifetime object. Construct it, do one unit of work, drop it or reset() it. The branch-write decode does exactly that with a 4 KiB stack buffer; so does wire_arena_decode.

  • Parked as a long-lived seam it dies, and the number is arithmetic rather than anecdote. An 8 KiB bump source wired as a router’s rx, decoding a 53-byte FWD, decoded 6 frames and rejected the next 194 — 8192 bytes divided by the arena footprint of one decode of that frame, so the figure does not vary with host or build flags. A long-lived bounded seam wants pool_source_t, which recycles.

  • release of a bump block returns nothing, and the example asserts it. used() does not retreat. This is the same behaviour a monotonic resource has, stated where a reader will trip over it rather than in a footnote.

  • reset() is deliberately not called release. On a monotonic_buffer_resource that name means exactly this whole-buffer operation, while on a block_source_t it means “return one block” — two meanings one token apart at a call site is a defect waiting to be written, so the seam refuses to spell them the same.

  • Padding is visible. used() counts alignment padding as well as payload, so an over-aligned request costs more than its size. The example asks for 40 bytes and then a 16-aligned 16, and watches the cursor land on 64.

  • Every block from the buffer dangles after a reset(). It is only safe when the previous unit of work is entirely finished — which is what “scope-lifetime” means operationally.

  • Nothing here is conditional — the target builds and runs under every CI leg.

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 bump source hands out a caller buffer and never takes it back.
 9 *
10 * `tr::mem::bump_source_t` is the nothrow twin of `std::pmr::monotonic_buffer_resource`: a
11 * cursor over a span the CALLER owns. `try_alloc` moves the cursor forward, `release` on a
12 * block from that span is a **no-op**, and `reset()` is the only way storage comes back —
13 * all of it at once.
14 *
15 * That is a lifetime rule, not a performance note. A bump source is a SCOPE-LIFETIME object:
16 * construct it, do one unit of work, drop it (or `reset()` it) — which is exactly what the
17 * branch-write decode does with a 4 KiB stack buffer. Parked as a LONG-LIVED seam it fills
18 * monotonically and then refuses everything: an 8 KiB bump source wired as a router's `rx`
19 * decoded 6 frames and rejected the next 194, measured. A long-lived bounded seam wants
20 * `pool_source_t`, which recycles (see mem_pool_source).
21 *
22 * Runs under ctest as `example_mem_bump_source`; returns non-zero on any failed check.
23 */
24
25#include <array>
26#include <cstddef>
27#include <cstdio>
28
29#include "libtracer/mem_source.hpp"
30
31namespace {
32
33/** @brief Report expectation @p what and record a failure on @p ok. */
34void check(bool& ok, bool cond, const char* what) {
35    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
36    ok = ok && cond;
37}
38
39}  // namespace
40
41int main() {
42    bool ok = true;
43    // `alignas(64)` rather than the usual `alignas(std::max_align_t)` so the padding arithmetic
44    // below is the same on every host this example runs on, not a property of one ABI.
45    alignas(64) std::array<std::byte, 256> scratch{};
46    tr::mem::bump_source_t bump{scratch};
47    check(ok, bump.used() == 0, "a fresh bump source has carved nothing");
48
49    void* const first = bump.try_alloc(40, 8);
50    check(ok, first == scratch.data(), "the first block starts at the caller's buffer");
51    check(ok, bump.used() == 40, "and the cursor moved by exactly the request — no header");
52
53    // Alignment is padding, and padding is visible: 40 is not a multiple of 16.
54    void* const wide = bump.try_alloc(16, 16);
55    check(ok, wide != nullptr && bump.used() == 64,
56          "an over-aligned request pays 8 bytes of padding, and used() says so");
57    std::printf("two blocks: %zu of %zu buffer bytes carved\n", bump.used(), scratch.size());
58
59    // The defining property. A bump block is not individually reclaimable, so handing one
60    // back is a no-op — the cursor does not retreat and the bytes are not reusable.
61    bump.release(first, 40, 8);
62    check(ok, bump.used() == 64, "release() of a bump block returns NOTHING — the cursor stands");
63
64    // reset() is the whole-buffer answer, and it is why a bump source is scope-lifetime:
65    // it is only safe when every block carved from the buffer is already dead.
66    bump.reset();
67    check(ok, bump.used() == 0, "reset() hands the whole buffer back at once");
68    check(ok, bump.try_alloc(40, 8) == first, "so the next unit of work starts at byte 0 again");
69    return ok ? 0 : 1;
70}

See also: backends · memory substrate reference · the upstream is the bound.