ESP-IDF: the component, and nothing else

The smallest ESP-IDF app that uses libtracer: a main component that says REQUIRES libtracer, and a graph_t built with no argument. On an ESP-IDF build that graph draws every byte it holds from one static arena in .bss, the MCU default root (ADR-0083 Decision 4). The app registers one vertex, writes it, reads it back, and prints what the arena carved for it.

What to notice

  • No source is passed, and none is needed. The component binds kSlabPool = false, so tr::mem::default_root() is the static arena of CONFIG_LIBTRACER_ARENA_BYTES (32 KiB on a chip). The graph’s tables and the value it stores both come from it; the root’s census (stats()) shows the bytes carved.

  • The value is built on the graph’s own backend. over_bytes(bytes, g.value_backend()) copies the app’s bytes into a segment drawn from the arena’s value sub-pool, the same place the graph keeps what it stores.

  • No transport is compiled. sdkconfig.defaults turns CONFIG_LIBTRACER_TRANSPORT_UDP, _TCP and _WS off: an in-process node sheds those translation units from libtracer.a.

  • “No heap” is checked at link time, not here. CI’s no-heap link check reads libtracer.a with nm and fails on any heap call not pinned in tools/no_heap_baseline.json, a list that only shrinks.

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 — the component and nothing else: `REQUIRES libtracer`, a `graph_t` built
 9 *        with no argument, and every byte it holds carved from one static arena.
10 *
11 * On an ESP-IDF build the component binds `kSlabPool = false`, so the default root
12 * (`tr::mem::default_root()`) is a static arena of `CONFIG_LIBTRACER_ARENA_BYTES` in `.bss`
13 * (ADR-0083 Decision 4). A graph built without a source draws its tables, and the values it
14 * stores, from that arena. This app registers one vertex, writes it, reads it back, and prints
15 * what the arena carved for it. The transports are switched off in `sdkconfig.defaults`: an
16 * in-process node needs none of them.
17 */
18
19#include <cstdio>
20#include <cstdlib>
21#include <cstring>
22#include <span>
23
24#include "libtracer/tracer.hpp"
25#include "sdkconfig.h"
26
27namespace {
28
29using tr::graph::path_t;
30using tr::graph::role_t;
31
32/** @brief Failed checks so far. */
33int g_failures = 0;
34
35/** @brief Print @p what with its verdict and count a failure. */
36void check(bool ok, const char* what) {
37    std::printf("  [%s] %s\n", ok ? "ok" : "FAIL", what);
38    if (!ok) ++g_failures;
39}
40
41/** @brief Print the verdict; on the `linux` target also exit with it, so CI can run this. */
42void finish() {
43    std::printf("RESULT %s\n", g_failures == 0 ? "ok" : "FAIL");
44#if CONFIG_IDF_TARGET_LINUX
45    std::exit(g_failures == 0 ? 0 : 1);
46#endif
47}
48
49}  // namespace
50
51extern "C" void app_main(void) {
52    tr::mem::block_source_t& root = tr::mem::default_root();
53    const std::size_t before = root.stats().in_use;
54
55    tr::graph::graph_t g;  // no source: the default root, the static arena
56    const auto temp = g.register_vertex(path_t("/sensor/temp"), role_t::STORED_VALUE);
57
58    static constexpr char kText[] = "21.5C";
59    const auto bytes = std::as_bytes(std::span(kText, sizeof kText - 1));
60    const auto value = tr::view::over_bytes(bytes, g.value_backend());
61    check(value.has_value(), "the value's bytes came from the graph's value backend");
62    check(value && g.write(temp, *value).has_value(), "write /sensor/temp");
63
64    const auto read = g.read(temp);
65    const auto got = read ? (*read)->only().bytes() : std::span<const std::byte>{};
66    check(got.size() == bytes.size() && std::memcmp(got.data(), bytes.data(), got.size()) == 0,
67          "read /sensor/temp serves what was written");
68
69    const tr::mem::source_stats_t s = root.stats();
70    std::printf("default root \"%s\": %zu of %zu arena bytes carved, %zu of them by this node\n",
71                root.name(), s.in_use, s.capacity, s.in_use - before);
72    check(s.capacity == tr::mem::kArenaBytes, "the root is the arena config_t sized");
73    check(s.in_use > before, "the graph's tables and its value were carved from it");
74    finish();
75}

Build and run

$ cd integrations/esp-idf/examples/concepts/minimal
$ idf.py set-target esp32c6 build
$ idf.py flash monitor        # board-only

CI builds it for esp32c6, and also builds and runs it on the ESP-IDF linux target, where it exits non-zero on a failed check. See also: the ESP-IDF one-concept set · backends.