Contents Menu Expand Light mode Dark mode Auto light/dark, in light mode Auto light/dark, in dark mode Skip to content
libtracer
libtracer

Getting started

  • Getting started
  • Examples
    • In-process pub/sub
    • Pub/sub fan-out & dispatch cost
    • Wire codec round-trip
    • Wire codec deep-dive & throughput
    • Rope scatter-gather
    • Two nodes over a wire — FWD delivery
    • Composition axes
    • Register a vertex, and address it
    • Read and write
    • await
    • Write-creates
    • :children[]
    • Retirement
    • A HANDLER vertex
    • A STREAM vertex
    • Subscribe to one vertex
    • One edge, a whole subtree
    • Delivery terminates at the target
    • The delivery policy is per subscription
    • Unsubscribe & the release hook
    • Unsubscribing from inside a delivery
    • Retire drops a producer's subscriptions
    • The TLV header and the opt byte
    • Structured or opaque — one bit decides
    • A PATH body is packed segment records
    • The escape record
    • The trailer: CRC and timestamp
    • What decode refuses
    • decode_into: a flat arena
    • tlv_view_t: a scattered frame
    • The segment, and its refcount
    • subview
    • borrow: the app's own bytes
    • The rope
    • subrope and the iovec egress
    • A bounded backend
    • A DEVICE link
    • A shared seam needs a thread-safe backend
    • The failable block seam
    • Two L0 seams, and the question that picks
    • A bump source
    • The upstream decides what the buffer means
    • A long-lived seam has to recycle
    • Classes do not share
    • A container that fails by value
    • The std::pmr adapter
    • Open by default, and the first ACE is the lock
    • The caller context is not the subject
    • access_mask is a bitfield
    • EVERYONE@ is reserved both ways
    • Effective ACL = own + inherited
    • expires_ns is checked against your clock
    • Two evaluators, and DENY
    • An ACL is a security document
    • The dst is a source route
    • Terminus or forward — one test decides
    • One NAME, one slot
    • A mount run is consumed whole
    • Three nodes, and a forwarder that stores nothing
    • The src you accumulated is the way home
    • A repeating flow buys its route back
    • A stale label is dropped and NACK'd
    • One seam, every wire technology
    • A kind is a NAME, resolved twice
    • DIAL and LISTEN are two constructors
    • A datagram already has boundaries
    • A stream has none, so the kind supplies them
    • No frame crosses until the Upgrade completes
    • The one BUS kind
    • One listener, many slots

Specification

  • The specification
    • Protocol v1 — the wire format

Reference

  • Reference (descriptive)
    • Overview — the six-layer model
    • Module catalog & composition
    • Deployment profiles
    • Concurrency & scaling
    • Reclamation policy
    • Memory substrate
    • Views & ownership
    • Data format
    • Protocol-defined TLVs
    • Graph model
    • Addressing
    • Communication flows
    • User data packing
    • Vertex roles & aggregation
    • Host embedding
    • Network formation
    • CAN transport
    • WebSocket session authentication
    • Composition over the network
    • Transports are vertices
    • Bindings map
    • ROS 2 integration (rmw_tracer)
    • Backpressure & sizing
  • Design notes
    • Concurrency & scaling
      • Scaling and serialization
      • Write and delivery path
    • Zero-copy and flatten
    • Build configuration
      • The configuration space
    • Failable allocation and backpressure

C++ API reference

  • C++ API reference
    • Interface map
    • status & errors — result taxonomy
    • config — the build's traits type
    • instrumentation — reachability counters
    • segment — refcounted bytes
    • backends — allocators
    • views — view_t / rope_t / cast
    • frame-codec — TLV codec + CRC
    • Wire format, bit by bit
    • path — addressing
    • graph — vertices & dispatch
    • security & ACL — access control
    • fwd-router — FWD routing and the /net plane
    • transport — the wire
    • connection config — the SPEC config keys
    • can — the header-elided CAN stack

Interoperate

  • Interoperability
  • Build a custom device
  • A production ESP32 node
  • Capability matrix
  • Implementation registry

Evidence

  • Performance & conformance
  • Test report

Glossary

  • Context glossary

Start here

  • Route by intent
Back to top

A DEVICE link, and why NOT_HOST is permanent (L0/L1 substrate)¶

A segment carries the address space of the backend that made it. HOST bytes are CPU-addressable; DEVICE bytes — GPU or accelerator memory (ADR-0024) — are not, and the codec must never dereference them. A rope may therefore be heterogeneous: a host header link chained to a device payload link. all_host() is the one question a host-side operation asks before it touches a byte.

What to notice¶

  • NOT_HOST is a property of the rope, not of the moment. No retry ever fixes it; the payload has to leave via its device path. That is a different verdict from NO_MEMORY (the pool page), which is transient — and keeping them apart is exactly what #917 bought.

  • The refusal is about the link, not the rope type. The example takes the host sub-range of the same rope and it is host, and flattenable. Nothing about a heterogeneous rope is poisoned wholesale.

  • borrow_device tags ordinary host memory DEVICE. A vendor-free stand-in: it registers no byte-mover, so mem::transfer declines it — and it declines it for the space tag, which is the same refusal a real device link gets. Every verdict on this page is deterministic in a stock build with no accelerator present.

  • The real device backend is a backends/-tier module. It registers its own transfer hook through register_device_backend; core assigns the space and nothing else. That tiering is why this example needs no CUDA and takes no skip.

  • 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 DEVICE link, and why refusing to flatten it is PERMANENT.
 9 *
10 * A segment carries the address space of the backend that made it. `HOST` bytes are
11 * CPU-addressable; `DEVICE` bytes (GPU/accelerator memory, ADR-0024) are not, and the codec
12 * must never dereference them. A rope may be HETEROGENEOUS — a host header link chained to a
13 * device payload link — and `all_host()` is the one question a host-side operation asks
14 * before touching bytes.
15 *
16 * The pay-off is in the error channel. `flatten_err_t::NOT_HOST` is a property of the rope
17 * itself: no retry ever fixes it, and the payload must go out via its device path. That is a
18 * different verdict from `NO_MEMORY`, which is transient backpressure (see
19 * `view_pool_backend`) — collapsing the two is exactly what let a local OOM be reported to a
20 * peer as a malformed frame (#917).
21 *
22 * `borrow_device` tags ordinary host memory `DEVICE` and registers no byte-mover, so
23 * `mem::transfer` refuses it — a real device backend lives in the `backends/` tier and
24 * registers its own. Nothing here is conditional: every verdict below is deterministic in a
25 * stock build with no accelerator present.
26 *
27 * Runs under ctest as `example_view_device_rope`; returns non-zero on any failed check.
28 */
29
30#include <array>
31#include <cstddef>
32#include <cstdio>
33#include <optional>
34#include <span>
35
36#include "libtracer/tracer.hpp"
37
38namespace {
39
40/** @brief Report expectation @p what and record a failure on @p ok. */
41void check(bool& ok, bool cond, const char* what) {
42    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
43    ok = ok && cond;
44}
45
46}  // namespace
47
48int main() {
49    bool ok = true;
50    std::array<std::byte, 8> header_bytes{};
51    std::array<std::byte, 8> payload_bytes{};
52
53    const tr::view::view_t host = tr::view::view_t::over(tr::view::borrow(header_bytes));
54    tr::view::segment_ptr_t dev_seg = tr::view::borrow_device(payload_bytes);
55    const tr::view::view_t device = tr::view::view_t::over(dev_seg);
56    check(ok, host.is_host() && !host.is_device(), "a borrowed host link is CPU-addressable");
57    check(ok, device.is_device(), "a borrow_device link reports DEVICE space");
58
59    tr::view::rope_t frame;
60    frame.append(host);
61    frame.append(device);
62    std::printf("heterogeneous rope: %zu links, %zu bytes, all_host=%d\n", frame.link_count(),
63                frame.total_length(), static_cast<int>(frame.all_host()));
64    check(ok, !frame.all_host(), "one DEVICE link makes the whole rope non-host");
65
66    const auto refused = frame.try_flatten();
67    check(ok, !refused && refused.error() == tr::view::flatten_err_t::NOT_HOST,
68          "try_flatten refuses it as NOT_HOST — permanent, and never a retry");
69
70    // The out-of-core arm: no backend registered a byte-mover for this segment, so the
71    // transfer is declined by value rather than guessed at with a memcpy.
72    std::array<std::byte, 8> staging{};
73    check(ok, !tr::mem::transfer(dev_seg.get(), staging, tr::mem::io_dir_t::DEVICE_TO_CPU),
74          "and mem::transfer declines a DEVICE segment no backends/ module claims");
75
76    // The host half of the same rope still flattens — the refusal is about the link, not the
77    // rope type.
78    check(ok, frame.subrope(0, 8).all_host(), "the host sub-range is host, and stays flattenable");
79    return ok ? 0 : 1;
80}

See also: backends · views module · memory substrate reference.

Next
A shared seam needs a thread-safe backend (L0/L1 substrate)
Previous
A bounded backend: exhaustion by value (L0/L1 substrate)
Copyright © 2026, avatarsd LLC
Made with Sphinx and @pradyunsg's Furo
On this page
  • A DEVICE link, and why NOT_HOST is permanent (L0/L1 substrate)
    • What to notice
    • Source