segment — refcounted bytes (L0↔L1)¶
In one paragraph
A tr::view::segment_t is real bytes owned by a backend, plus an intrusive
atomic refcount — the boundary object where L0’s bytes acquire L1’s ownership.
The tr::view::segment_ptr_t handle threads that one buffer’s lifetime
through fan-out: copying a handle is a relaxed increment (a clone), dropping
the last one reclaims the bytes through the backend. This is what lets many
views share one buffer with no copies, and what makes a decoded TLV safe to hold
past the receive call.
What it does¶
L0 is “real bytes in real memory owned by some real allocator”; L1 adds the
ownership. segment_t is the control block over one such buffer and the single
sanctioned object on that boundary — L0 backends vend it, L1 views hold it, and
no other type crosses. segment_ptr_t is the owning handle. The refcount lives
inside the segment (not in a side shared_ptr block) so a static MMIO
descriptor, a pool slot, and a heap allocation all carry their own count.
A segment is never copied or moved; it is always handled through
segment_ptr_t, and it caches its backend’s address space and module-set tag at
construction (segment_t, core/include/libtracer/segment.hpp). When the last
handle drops, segment_ptr_t::reset calls tr::mem::destroy_dispatch, which
switches on that tag to a direct call for a linked backend and falls back to the
backend’s virtual destroy for any other — the result is identical to
seg->backend->destroy(seg) for every backend
(core/include/libtracer/backend.hpp:destroy_dispatch;
ADR-0047 — build-time-closed module sets, compile-time seams §2).
There is no separate release() step.
The atomic orderings are the canonical intrusive_ptr pattern, specified once in
reference/02 §required atomic operations:
increment relaxed (core/include/libtracer/segment.hpp:count_.fetch_add(1, std::memory_order_relaxed) — the caller already
holds a reference, so the data dependency travels through it), decrement
acq_rel (core/include/libtracer/segment.hpp:return count_.fetch_sub(1, std::memory_order_acq_rel) — release the writes before another thread observes the count
drop, acquire on observing the drop to zero), inspect acquire (core/include/libtracer/segment.hpp:return count_.load(std::memory_order_acquire)). The
decrement returns the value before it, so a return of 1 identifies the caller
that dropped the last reference.
On a core with no atomic read-modify-write — Cortex-M0/M0+ (no LDREX/STREX), rv32imc —
the count takes its guarded binding instead: a load and a store inside one section of
the build’s guard, config_t::guard_t (core/include/libtracer/segment.hpp:inline constexpr bool kNativeRefCount,
core/include/libtracer/segment.hpp:class basic_ref_count_t). The choice is made from the target, the same
way tr::rmw_counter_t makes it for a vertex’s write sequence; nothing is defined on the
command line. A single-threaded node makes the guard free by binding tr::no_guard_t in
its libtracer/config_override.hpp, which is what the footprint sentinels do
(core/tests/footprint/config/libtracer/config_override.hpp). On a host the substrate test
drives the guarded binding by naming it, under several threads
(core/tests/substrate_test.cpp:test_refcount_bindings_under_threads). The old
LIBTRACER_NO_ATOMIC macro was removed in #1722 and is refused at compile time.
API reference¶
-
struct segment_t¶
A refcounted span of real bytes: the L0↔L1 boundary object.
Real bytes + the backend that reclaims them + an intrusive refcount. Never copied or moved (the atomic refcount pins it in place); always handled through segment_ptr_t.
Note
bytesis writable at the type level, but whether writes are legal is the backend’s contract — a const/ROM borrow must not be written through (seemem_borrowed.hpp).Public Functions
-
inline segment_t(mem::mem_backend_t *b, std::span<std::byte> by, std::uint_least32_t initial = 1) noexcept¶
Construct a segment over
by, reclaimed byb, withinitialrefcount.The address space and module-set tag are taken from the backend (
b->space()/b->tag()); aDEVICEsegment must not be CPU-dereferenced (docs/adr/0024).
Public Members
-
detail::ref_count_t refcount¶
Intrusive refcount (spec orderings).
-
mem::mem_backend_t *backend¶
Reclaimer; non-const (cache hooks mutate it).
-
std::span<std::byte> bytes¶
The backing bytes this segment holds a reference to.
-
mem::mem_space_t space¶
Address space (HOST/DEVICE), inherited from backend.
-
mem::backend_tag btag¶
Module-set tag, inherited from backend (ADR-0047 §2).
-
std::uint8_t rx_loan = 0¶
Nonzero iff the first tr::mem::kRxLoanBytes of bytes are an INGRESS-LOAN reserve (RFC-0028 §6.9, #1626), not payload.
Set only by alloc_rx, on a receive block a transport allocated with room for the record the graph will need, so the terminus that stores a value shared out of this frame builds that record IN the block instead of allocating one. A structural bit, never an in-band marker: the reserve’s bytes are never read to decide whether it exists, so a peer cannot forge one. Rides the padding after btag (
sizeof(segment_t)is unchanged on every target).
-
inline segment_t(mem::mem_backend_t *b, std::span<std::byte> by, std::uint_least32_t initial = 1) noexcept¶
-
class segment_ptr_t¶
Intrusive owning handle for a segment_t.
Copy = clone (refcount bump, relaxed); destruction = release (acq_rel); the backend’s
destroyfires when the last handle drops. This is what makes a borrowed (zero-copy) view safe to hold: the spans in a decoded TLV stay valid as long as the view — and thus this handle — lives.Public Functions
-
inline segment_ptr_t(const segment_ptr_t &other) noexcept¶
Clone — a new shared reference to the same segment (relaxed increment).
-
inline segment_ptr_t(segment_ptr_t &&other) noexcept¶
Transfer ownership of
other'sreference, leaving it empty.
-
inline segment_ptr_t &operator=(segment_ptr_t other) noexcept¶
Copy-and-swap assignment — one operator covers copy- and move-assign.
-
inline void reset() noexcept¶
Drop this reference (acq_rel); fires the backend’s
destroyat zero.
-
inline explicit operator bool() const noexcept¶
True when this handle owns a segment.
-
inline std::uint_least32_t use_count() const noexcept¶
Current refcount — debug / metrics only (acquire load), NOT a sync primitive.
Public Static Functions
-
static inline segment_ptr_t adopt(segment_t *seg) noexcept¶
Adopt an existing reference (e.g. from
alloc, refcount = 1) WITHOUT bumping.
-
static inline segment_ptr_t retain(segment_t *seg) noexcept¶
Take a NEW shared reference to an already-live segment (bumps the count).
-
inline segment_ptr_t(const segment_ptr_t &other) noexcept¶
The ingress-loan reserve (RFC-0028 §6.9): a receive block at or above the share
threshold carries this many bytes in front of the frame, and the value stored from
that frame is placed in them. view::alloc_rx (below, with the other
handle-producing conveniences) sets the segment’s rx_loan bit.
-
constexpr std::size_t tr::mem::kRxLoanBytes = sizeof(void*) >= 8 ? 48 : 32¶
Bytes a loaned receive block reserves in front of its frame (RFC-0028 §6.9, #1626): the claim word, then room for a one-link
tr::graph::value_theader.Sized for the value’s header and its one link — 16 + 24 B on a 64-bit host, 12 + 12 B on rv32 — after an aligned claim word.
value.hppasserts the fit, so a change to either side fails the build rather than overrunning a frame.
The handle-producing conveniences live in tr::view rather than with the
backends, because what they produce is an L1 handle:
-
segment_ptr_t tr::view::heap_alloc(std::size_t size)¶
Allocate a fresh, owned heap segment of
sizebytes, wrapped in an adoptingsegment_ptr_t.An L1 helper (it produces an owning handle), so it lives in
tr::view, nottr::mem(docs/adr/0016 §2). Exactly segment_alloc over mem::heap_backend.- Return values:
{} – An empty handle on allocation failure.
-
inline segment_ptr_t tr::view::borrow(std::span<std::byte> bytes)¶
Wrap writable caller-owned
bytesin a segment without owning them.An L1 handle producer (docs/adr/0016 §2). The caller guarantees the bytes outlive every view that holds them.
-
inline segment_ptr_t tr::view::borrow_const(std::span<const std::byte> bytes)¶
Wrap read-only caller-owned
bytes(ROM, a const table, an MMIO read view).The span is const; libtracer never writes through a borrowed-const segment, so the
const_castonly restores the segment’s uniform writable-at-the-type- level base.
-
inline segment_ptr_t tr::view::borrow_device(std::span<std::byte> bytes)¶
Wrap caller-owned
bytesas aDEVICE-space (non-CPU) segment.The resulting view reports view_t::is_device; the codec must not CPU-dereference it (docs/adr/0024). A real device backend lives in the
backends/tier and registers its own byte-move (register_device_backend); this borrow tags existing memoryDEVICE(e.g. for tests or a custom binding), registers nothing, and somem::transferrefuses it.
-
segment_ptr_t tr::view::cuda_alloc(std::size_t size)¶
Allocate a CUDA device segment of
sizebytes (DEVICE space).An L1 handle producer (docs/adr/0016 §2). The bytes live in GPU memory; the resulting view reports view_t::is_device and must not be CPU-dereferenced.
- Return values:
{} – An empty handle if
cudaMallocfails.
Refcount lifecycle (fan-out)¶
sequenceDiagram
participant TX as producer
participant V as views
participant S1 as subscriber 1
participant S2 as subscriber 2
participant B as backend
TX->>V: make segment (count=1)
V->>S1: clone (relaxed ++ → 2)
V->>S2: clone (relaxed ++ → 3)
TX->>V: drop producer ref (acq_rel -- → 2)
S1->>V: release (acq_rel -- → 1)
S2->>V: release (acq_rel -- → 0)
V->>B: destroy_dispatch(seg) — bytes reclaimed
Consequences¶
Zero-copy fan-out — N subscribers share one buffer; delivery is N relaxed increments, no
memcpy.A decoded TLV outlives its receive call — a
tlv_node_tborrows segment bytes via spans; thesegment_ptr_tkeeps them alive exactly as long as some view needs them, which is what makes borrowed (zero-copy) decode safe at all.No hidden allocation — the count is in the segment, so MMIO, pool and borrowed segments need no separate control block.
Reclaim is devirtualizable — the cached module-set tag turns per-release reclaim into a
switch, foldable to one direct call on a target that links a single backend.Portable to cores without atomics — the guarded binding counts under the build’s guard, which a single-threaded node binds to
tr::no_guard_tfor a plain counter.
Pitfalls¶
adoptandretainare not interchangeable.adopttakes over an existing reference without bumping — the shapemem_backend_t::allocreturns (a rawsegment_t*at refcount 1);retainadds a new reference to an already-live segment (segment.hpp:segment_ptr_t::adopt,segment.hpp:segment_ptr_t::retain). Adopting a segment twice double-frees it; retaining anallocresult leaks it, because the referenceallocalready created is never dropped.use_countis not a synchronization primitive. It is an acquire load for debug and metrics (segment.hpp:segment_ptr_t::use_count). A count of 1 does not mean no other thread is about to clone the handle, and branching on it reintroduces the race the refcount exists to remove.tr::no_guard_tis an application promise, not a portability switch. With it bound, the guarded refcount is a plain load and store, and one cross-thread clone or release races the count and corrupts the lifetime silently. Bind it only where the application serializes all access to libtracer state.bytesis writable at the type level; legality is the backend’s contract. A borrow over ROM or a caller’sconstbuffer hands out a mutablestd::span<std::byte>all the same (segment.hpp:segment_t,segment.hpp:segment_t::bytes); writing through it is undefined even though it compiles.A
DEVICEsegment must not be CPU-dereferenced. The span looks ordinary, butspacerecords that the bytes are not CPU-addressable (segment.hpp:segment_t::space,backend.hpp:mem_space_t); such a segment may back only an opaque VALUE payload, with header and trailer kept inHOSTsegments (ADR-0024 — mem_cuda GPU backend, heterogeneous rope).
See: backends (who creates segments), views (who holds them), and the interface map.