ESP-IDF: reading the sub-pools’ :stats¶
The default root is one arena, and the graph derives three sub-pools from it: values (what
vertices store), tables (registrations and the graph’s own containers) and net (the
router’s and the links’ defaults). Each is a :stats seam on any vertex
(RFC-0010 Amendment 3): one READ of :stats.mem.values, .tables or .net answers that
sub-pool’s census as one SETTINGS block of NAME / u64 pairs.
What to notice¶
One READ, one block. The counters arrive together, sampled in one call:
capacity,in_use,peak,refusedandlargest_refused. A reader looks them up by name and ignores a name it does not know.Read in place, with nothing allocated. The example walks the block with
tr::wire::tlv_node_t::over(bytes, tr::mem::null_source()), which validates the frame and walks its children without building a tree.peakis the number to size by. The app writes values of 16, 200 and 700 bytes to one vertex. Each write replaces the last, soin_useholds only the 700-byte value, whilepeakstill counts the moment the old and the new value were both alive.A node with no link still has a net sub-pool. Its census answers, with nothing refused.
An injected root answers none of the three. A graph over the app’s own root derives no sub-pools, and each name answers
SCHEMA_NOT_FOUND(one task, no pool locks).
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 — where a running node's RAM goes: one READ of `:stats.mem.values`,
9 * `.tables` or `.net` answers that sub-pool's census.
10 *
11 * The default root is one arena, and the graph derives three sub-pools from it: values (what
12 * vertices store), tables (registrations and the graph's own containers) and net (the router's
13 * and the links' defaults). Each is a `:stats` seam on any vertex (RFC-0010 Amendment 3): a
14 * READ answers one `SETTINGS` block of `NAME` / u64 pairs, read here in place with
15 * `tlv_node_t`, which allocates nothing. The app stores a few values of different sizes and
16 * prints all three blocks: `in_use` against `peak` is the number to size the arena by.
17 */
18
19#include <cstdint>
20#include <cstdio>
21#include <cstdlib>
22#include <optional>
23#include <span>
24#include <string_view>
25
26#include "libtracer/byteorder.hpp"
27#include "libtracer/tracer.hpp"
28#include "sdkconfig.h"
29
30namespace {
31
32using tr::graph::path_t;
33using tr::graph::role_t;
34using tr::wire::type_t;
35
36/** @brief Failed checks so far. */
37int g_failures = 0;
38
39/** @brief Print @p what with its verdict and count a failure. */
40void check(bool ok, const char* what) {
41 std::printf(" [%s] %s\n", ok ? "ok" : "FAIL", what);
42 if (!ok) ++g_failures;
43}
44
45/** @brief Print the verdict; on the `linux` target also exit with it, so CI can run this. */
46void finish() {
47 std::printf("RESULT %s\n", g_failures == 0 ? "ok" : "FAIL");
48#if CONFIG_IDF_TARGET_LINUX
49 std::exit(g_failures == 0 ? 0 : 1);
50#endif
51}
52
53/** @brief The three counters this example checks, from one sub-pool's census. */
54struct census_t {
55 std::uint64_t in_use = 0; /**< @brief Bytes handed out and not returned. */
56 std::uint64_t peak = 0; /**< @brief The high-water mark of `in_use`. */
57 std::uint64_t refused = 0; /**< @brief Requests answered by a refusal. */
58};
59
60/** @brief Read the census at @p seam and print every counter in it. */
61std::optional<census_t> census(const tr::graph::graph_t& g, std::string_view seam) {
62 const auto read = g.read(*path_t::parse(seam));
63 if (!read) return std::nullopt;
64 const tr::view::view_t flat = (*read)->flatten();
65 const auto block = tr::wire::tlv_node_t::over(flat.bytes(), tr::mem::null_source());
66 if (!block || block->type() != type_t::SETTINGS) return std::nullopt;
67
68 std::printf("%.*s:", static_cast<int>(seam.size()), seam.data());
69 census_t c;
70 std::string_view name;
71 for (const tr::wire::tlv_node_t child : block->children()) {
72 const auto body = child.payload();
73 if (child.type() == type_t::NAME) {
74 name = {reinterpret_cast<const char*>(body.data()), body.size()};
75 } else if (child.type() == type_t::VALUE && body.size() == 8) {
76 const auto v = tr::detail::load_le<std::uint64_t>(body);
77 std::printf(" %.*s=%llu", static_cast<int>(name.size()), name.data(),
78 static_cast<unsigned long long>(v));
79 if (name == "in_use") c.in_use = v;
80 if (name == "peak") c.peak = v;
81 if (name == "refused") c.refused = v;
82 }
83 }
84 std::printf("\n");
85 return c;
86}
87
88} // namespace
89
90extern "C" void app_main(void) {
91 tr::graph::graph_t g;
92 const auto log = g.register_vertex(path_t("/log"), role_t::STORED_VALUE);
93 static constexpr std::byte kLine[700]{};
94 for (std::size_t n : {16U, 200U, 700U}) {
95 const auto value = tr::view::over_bytes(std::span(kLine, n), g.value_backend());
96 check(value && g.write(log, *value).has_value(), "write a value to /log");
97 }
98
99 const auto values = census(g, "/log:stats.mem.values");
100 const auto tables = census(g, "/log:stats.mem.tables");
101 const auto net = census(g, "/log:stats.mem.net");
102
103 check(values && values->in_use > 0, "the value sub-pool holds what /log stores");
104 check(values && values->peak > values->in_use,
105 "its peak still counts the two values the last write replaced");
106 check(tables && tables->in_use > 0, "the table sub-pool holds the registration");
107 check(net && net->refused == 0, "the net sub-pool answers too, and has refused nothing");
108 finish();
109}
Build and run¶
$ cd integrations/esp-idf/examples/concepts/stats_subpools
$ 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. See also:
sizing the arena ·
Rust: reading a node’s :stats.