libe3 0.2.5
C++ library for E3AP RAN vendor integration
Loading...
Searching...
No Matches
latrec.h
Go to the documentation of this file.
1/**
2 * @file latrec.h
3 * @brief Lock-free per-thread latency stamp recorder (header-only, C99).
4 *
5 * One mmap-backed ring per writer thread, single producer, no locks: a stamp
6 * is one CLOCK_MONOTONIC read and four stores, with no syscall, allocation or
7 * formatting. Records are 32 bytes (seq:48 | cpu:8 | stage:8, t_ns, aux,
8 * aux2); seq is assigned once at the producer and joins the stages offline,
9 * aux/aux2 carry the per-stage payloads listed in the stage catalog below.
10 *
11 * CLOCK_MONOTONIC shares an origin across processes, so RAN, dApp and xApp
12 * stamps join without calibration.
13 *
14 * t_ns is stored last, with release ordering: a reader that acquire-loads a
15 * nonzero t_ns also observes that record's sc, aux and aux2. That makes one
16 * record self-consistent; it does not stop the slot being reused, so a reader
17 * of a ring that has wrapped may pair one write's t_ns with the next write's
18 * payload. A live reader must therefore keep up, or detect the wrap from
19 * rec_count and the single descent in t_ns.
20 *
21 * Gated once, at compile time, by LIBE3_ENABLE_LATREC: off by default, so a
22 * normal build carries none of this, not even a disabled-ring branch. With it
23 * on, tracing runs -- there is no second per-run opt-in. See the bottom of
24 * this file, and docs/latrec.md for where the rings are written.
25 *
26 * File layout: a 4 KiB latrec_hdr followed by `entries` 32-byte records. v1
27 * field offsets are frozen and v2 fields appended inside the header pad, so a
28 * v1 reader parses a v2 file. Readers detect a byte-swapped LATREC_MAGIC.
29 * 64-bit hosts only: a 32-bit host can tear the t_ns store.
30 *
31 * Installed as <libe3/latrec.h>, so a component that links libe3 includes it
32 * from there. Components that do not link libe3 mirror the layout instead, and
33 * the stage catalog below carries their ids too, so it stays the one place a
34 * new id is checked for collisions.
35 *
36 * @see \ref latrec_guide "docs/latrec.md" for the full guide: the clock
37 * model, ring naming/sizing, the stage catalog table, and the
38 * capture-to-CSV workflow.
39 * @see \ref path_a_e3_loop "docs/path-a-e3-loop.md" and
40 * \ref path_b_e2_e3_loop "docs/path-b-e2-e3-loop.md" for the loop boxes
41 * the stage catalog names.
42 *
43 * SPDX-FileCopyrightText: Copyright (c) 2026 Northeastern University
44 * SPDX-License-Identifier: Apache-2.0
45 */
46#ifndef LIBE3_LATREC_H
47#define LIBE3_LATREC_H
48
49/* clock_gettime() needs a feature-test macro under a strict -std=. Act only
50 * when the TU selected none, and use _GNU_SOURCE: _POSIX_C_SOURCE would
51 * suppress the _DEFAULT_SOURCE a -std=gnu* build otherwise gets, dropping the
52 * BSD/SVID declarations from a TU that includes this header first. Effective
53 * only before the first system header; the #error below catches the rest. */
54#if !defined(_GNU_SOURCE) && !defined(_POSIX_C_SOURCE) && \
55 !defined(_DEFAULT_SOURCE) && !defined(_XOPEN_SOURCE)
56#define _GNU_SOURCE 1
57#endif
58
59#include <fcntl.h>
60#include <stdint.h>
61#include <stdio.h>
62#include <stdlib.h>
63#include <string.h>
64#include <sys/mman.h>
65#include <sys/stat.h>
66#include <time.h>
67#include <unistd.h>
68
69#if !defined(CLOCK_MONOTONIC)
70#error "latrec.h: CLOCK_MONOTONIC undeclared - build with a POSIX-enabled dialect (-std=gnu11 / -std=gnu++17) or define _POSIX_C_SOURCE >= 199309L before any system header."
71#endif
72
73#ifdef __cplusplus
74extern "C" {
75#endif
76
77/* ---- portability shims ---------------------------------------------------- */
78
79#if defined(__GNUC__) || defined(__clang__)
80#define LATREC_STORE_RELEASE(p, v) __atomic_store_n((p), (v), __ATOMIC_RELEASE)
81#define LATREC_LOAD_ACQUIRE(p) __atomic_load_n((p), __ATOMIC_ACQUIRE)
82#define LATREC_PREFETCH_W(p) __builtin_prefetch((p), 1, 0)
83#define LATREC_UNLIKELY(x) __builtin_expect(!!(x), 0)
84#else
85/* Fallback without the builtins: correct on TSO hosts only. */
86#define LATREC_STORE_RELEASE(p, v) do { *(p) = (v); } while (0)
87#define LATREC_LOAD_ACQUIRE(p) (*(p))
88#define LATREC_PREFETCH_W(p) ((void)(p))
89#define LATREC_UNLIKELY(x) (x)
90#endif
91
92#if defined(__linux__)
93#define LATREC_HAVE_GETCPU 1
94/* glibc exports the symbol unconditionally; only the declaration is gated
95 * behind _GNU_SOURCE, which a TU may have locked in before including us. The
96 * C++ spelling has to carry noexcept to match glibc's __THROW, otherwise a
97 * <sched.h> included after this header is a conflicting redeclaration. */
98#if defined(__cplusplus)
99extern int sched_getcpu(void) noexcept;
100#else
101extern int sched_getcpu(void);
102#endif
103#else
104#define LATREC_HAVE_GETCPU 0
105#endif
106
107/* Read the cpu id in the stamp only where sched_getcpu() resolves in user
108 * space (x86-64 glibc goes through rseq). Elsewhere it can be a getcpu
109 * syscall, so the stamp uses the id cached by latrec_refresh_cpu(). */
110#if LATREC_HAVE_GETCPU && (defined(__x86_64__) || defined(__i386__))
111#define LATREC_CPUID_ON_STAMP 1
112#else
113#define LATREC_CPUID_ON_STAMP 0
114#endif
115
116#define LATREC_CAT_(a, b) a##b
117#define LATREC_CAT(a, b) LATREC_CAT_(a, b)
118#if defined(__cplusplus)
119#define LATREC_STATIC_ASSERT(c, m) static_assert(c, m)
120#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
121#define LATREC_STATIC_ASSERT(c, m) _Static_assert(c, m)
122#else
123#define LATREC_STATIC_ASSERT(c, m) \
124 typedef char LATREC_CAT(latrec_static_assert_, __LINE__)[(c) ? 1 : -1]
125#endif
126
127/* ---- stage catalog ------------------------------------------------------- */
128/*
129 * One identifier per *operation* on the E3AP / E3SM / E2SM-DAPP path -- not
130 * one per (operation, component) pair. Several components implement the same
131 * operation: an indication is encoded by whichever Service Model produced it,
132 * and E3AP framing is the same libe3 call whether the RAN or the dApp made it.
133 * They share the identifier. Which component wrote a given record comes from
134 * its ring name (<role>.<tid>.latrec), and which concrete message it carried
135 * from the aux payloads, so a component prefix on the stage id would only
136 * duplicate what the reader already knows.
137 *
138 * Identifiers name the boxes of the two documented loops:
139 * docs/path-a-e3-loop.md the E3-only loop (RAN <-> dApp), boxes A1..A20
140 * docs/path-b-e2-e3-loop.md the E2-E3 loop (dApp <-> xApp), boxes B1..B15
141 * Each comment below cites the box(es) it bounds. Boxes that hand off inside
142 * one function on one thread share a single boundary stamp rather than
143 * carrying a separate exit and entry.
144 *
145 * Stable IDs: from this baseline on, never renumber, only append.
146 */
147enum {
148 /* --- Source side: recording and processing what a Service Model reports.
149 * A1 (E3 data recording) and A2 (Processing). Stamped by whichever
150 * component is the data source -- an OAI PHY thread, an OCUDU jbpf hook
151 * feeding its E3 controller, a cuBB L1 data-lake reader, or the Simple SM
152 * shipped in examples/sm_simple. seq = the source's own record counter;
153 * aux = a source-defined slot/frame key (e.g. sfn<<16|slot). */
154 LATREC_RECORD_BEGIN = 0x10, /* A1 entry: raw input capture begins */
155 LATREC_PROCESS_BEGIN = 0x11, /* A1 exit / A2 entry: input in hand,
156 SM-side processing begins */
157
158 /* --- E3SM codec. A3 and A12 (encode), A9 and A18 (decode). The same
159 * Service Model codec on either leg and in either role; aux2 = PduType,
160 * which is what separates an indication from a control or a report.
161 * ENCODE_E3SM_BEGIN doubles as A2's exit. */
162 LATREC_ENCODE_E3SM_BEGIN = 0x18, /* A2 exit / A3 (or A12) entry */
163 LATREC_ENCODE_E3SM_DONE = 0x19, /* A3 (or A12) exit; aux = encoded bytes.
164 Hands to EMIT_ENTER */
165 LATREC_DECODE_E3SM_BEGIN = 0x1A, /* A9 (or A18) entry; aux = payload bytes */
166 LATREC_DECODE_E3SM_DONE = 0x1B, /* A9 (or A18) exit; aux = a decoded count
167 the SM cares about (PRBs, slots, ...) */
168
169 /* --- E3AP codec, queue and transport: libe3 itself. A4..A8 on the
170 * forward leg, A13..A17 on the return leg, B10..B13 on the policy leg --
171 * the same five outbound and three inbound stamps each time.
172 * seq = Pdu::enqueue_seq outbound (allocated at EMIT_ENTER so this record
173 * and the rest of the leg share one key), allocated at RECV inbound. */
174 LATREC_EMIT_ENTER = 0x20, /* A4 entry: the producer's thread crosses
175 into libe3. aux = the producer's own
176 trace seq (whatever it last passed to
177 latrec_ctx_set(), 0 if none), which is
178 what joins the producer's stages to
179 this leg; aux2 = RAN function id.
180 EMIT_ENTER->ENQUEUE is Pdu construction */
181 LATREC_ENQUEUE = 0x21, /* A4 first half exit / A5 entry;
182 aux2 = PduType */
183 LATREC_DEQUEUE = 0x22, /* A5 exit / A4 second half entry, on the
184 publisher thread; aux2 = PduType */
185 LATREC_ENCODE_E3AP_DONE = 0x23, /* A4 exit / A6 entry; aux = wire bytes */
186 LATREC_SEND_DONE = 0x24, /* A6 exit: the connector's send() returned;
187 aux = message_id. NOT the wire: a ZMQ PUB
188 send only copies into ZMQ's queue and the
189 socket write happens later on ZMQ's own
190 io thread, so ENCODE_E3AP_DONE->SEND_DONE
191 excludes that hop for ZMQ but includes it
192 for the POSIX connectors */
193 LATREC_RECV = 0x25, /* A7 / A8 entry; aux = wire bytes read */
194 LATREC_DECODE_E3AP_DONE = 0x26, /* A8 exit; aux = message_id,
195 aux2 = PduType */
196 LATREC_DELIVER_BEGIN = 0x27, /* libe3's inbound filter passed; about to
197 invoke the application. A9's entry seen
198 from the library side. A message the
199 dApp-id filter rejects gets neither this
200 nor DELIVER_DONE: see
201 LATREC_DROP_FILTERED */
202 LATREC_DELIVER_DONE = 0x28, /* the application handler returned;
203 aux2 = PduType */
204 LATREC_REPORT_QUEUED = 0x29, /* A19b entry: report handed to the report
205 worker */
206 LATREC_REPORT_DONE = 0x2A, /* A19b exit */
207 LATREC_SESSION_QUEUED = 0x2B, /* the batching seam a language binding
208 drains, inside A9 / B13; aux = kind */
209 LATREC_SESSION_POLLED = 0x2C, /* handed to the binding; aux = position in
210 batch, aux2 = batch size */
211
212 LATREC_DROP = 0x2F, /* a PDU went no further; aux2 = reason
213 (LATREC_DROP_*). An outcome marker, not
214 a box boundary */
215
216 /* --- Bootstrap: the setup and subscription handshakes, and Service Model
217 * lifecycle. These precede the measured loop rather than sitting on it, so
218 * they are bracketed per handshake instead of per box. Either role stamps
219 * the same pair -- the ring tells you which side you are looking at.
220 * seq = the request's message id. */
221 LATREC_SETUP_BEGIN = 0x30, /* setup request sent (initiator) or
222 reached the handler (responder) */
223 LATREC_SETUP_DONE = 0x31, /* setup response written, or the session
224 is ready to carry traffic; aux2 = 1
225 accepted, 0 rejected. A setup that never
226 decoded into a request is rejected with
227 no SETUP_BEGIN to pair with */
228 LATREC_SUB_BEGIN = 0x32, /* subscription request sent or received */
229 LATREC_SUB_DONE = 0x33, /* subscription response queued or
230 confirmed; aux2 = 1 accepted,
231 0 rejected; aux = granted id */
232 LATREC_SM_START = 0x34, /* first subscription: SM worker up */
233 LATREC_SM_STOP = 0x35, /* last unsubscribe: worker joined;
234 aux2 = batches emitted */
235 LATREC_SM_STATUS_BEGIN = 0x36, /* peer connect/disconnect handler entered;
236 aux = RAN function id */
237 LATREC_SM_STATUS_DONE = 0x37, /* handler returned, cadences recomputed
238 and any RIC update sent */
239
240 /* --- Receiving application, dApp side: A10 (Process), A11 (Create control
241 * or report), and B15 (Apply policy). Bespoke application logic rather
242 * than a library call, so these unify across dApp implementations (the
243 * Python framework and the C++ e3_manager dApps) but never across roles.
244 * seq = the key of the message being handled. */
245 LATREC_PROCESS_DONE = 0x40, /* A10 exit: the dApp reached a decision.
246 A10's entry is DECODE_E3SM_DONE */
247 LATREC_CREATE_OUTPUT = 0x41, /* A11: control or report built and queued;
248 aux = bytes. Hands to ENCODE_E3SM_BEGIN */
249 LATREC_APPLY_POLICY_DONE = 0x42, /* B15 exit: an xApp policy applied in the
250 dApp. B15's entry is DECODE_E3SM_DONE */
251 LATREC_ADMITTED = 0x43, /* the handler decided whether to do the
252 work at all; aux2 = outcome
253 (LATREC_OUT_*). An outcome marker */
254
255 /* --- Receiving application, RAN side: A19a (Apply control). The MAC's own
256 * slot-internal scheduler timing is not an E3 operation and is not in this
257 * catalog. seq = the E3 message id of the control being applied. */
258 LATREC_APPLY_CONTROL_DONE = 0x48, /* A19a: mask or policy installed in the
259 MAC; aux = ok. Takes the scheduler
260 lock, so this couples the control
261 thread to the slot-deadline thread */
262 LATREC_LIVE_ON_AIR = 0x49, /* A19a terminal: the first scheduler tick
263 that put this install (or its clear)
264 on air. aux = sfn<<16|slot,
265 aux2 = installs coalesced into this
266 tick (>1 means the ones behind the
267 keyed install rode along). Stamped on
268 the MAC thread but keyed on the control
269 that installed the mask. A clear with
270 no control behind it carries seq 0 */
271
272 /* --- Acknowledgment tail. Either direction: the receiver emits an ack,
273 * the sender observes it. seq = the acknowledged message's id. */
274 LATREC_ACK_SENT = 0x4A, /* aux = response code, aux2 = reject
275 reason, 0 when accepted */
277
278 /* --- E2-E3 bridge: A20 (a dApp report going up toward E2) and B9 (an xApp
279 * control coming down toward E3). One pair for both directions;
280 * aux2 = 0 report-up, 1 control-down. seq = a bridge-local counter, since
281 * the E2 envelope carries no id the bridge could propagate. */
282 LATREC_BRIDGE_IN = 0x50, /* message reached the bridge; aux = bytes */
283 LATREC_BRIDGE_OUT = 0x51, /* forwarded on; aux = peers or
284 subscriptions it went to */
285
286 /* --- E2AP codec and SCTP transport. B1 and B6 (encode and send), B2 and
287 * B7 (decode). The same codec whether the xApp side or the E2 agent calls
288 * it, on either flow; aux2 = PDU type. seq = the stamping process's own
289 * counter: no id crosses the RIC, so the legs pair by time, not by seq. */
290 LATREC_ENCODE_E2AP_BEGIN = 0x58, /* aux = payload bytes */
291 LATREC_ENCODE_E2AP_DONE = 0x59, /* SCTP send returned; aux = wire bytes */
292 LATREC_DECODE_E2AP_BEGIN = 0x5A, /* aux = wire bytes received */
293 LATREC_DECODE_E2AP_DONE = 0x5B, /* aux2 = PDU type */
294
295 /* --- E2SM (including E2SM-DAPP) codec, and the xApp's own decision. B3
296 * and B8 (decode), B5 (encode), B4 (xApp processing). The codec pairs are
297 * shared by every framework that speaks E2SM -- the E2 agent's own SM
298 * handling and any xApp framework, Python or otherwise. XAPP_PROCESS_DONE
299 * stays separate from the codecs: it is application logic, not a library
300 * call. seq = the request id the stamping side keys on. */
301 LATREC_DECODE_E2SM_BEGIN = 0x60, /* B3 / B8 entry; the E2AP layer hands the
302 inner payload over; aux = bytes */
303 LATREC_DECODE_E2SM_DONE = 0x61, /* B3 / B8 exit; also B4's entry */
304 LATREC_XAPP_PROCESS_DONE = 0x62, /* B4 exit: the xApp returned a decision.
305 Also B5's entry */
306 LATREC_ENCODE_E2SM_BEGIN = 0x63, /* B5 (or B1) entry, where no xApp
307 decision precedes it */
308 LATREC_ENCODE_E2SM_DONE = 0x64, /* B5 / B1 exit; aux = bytes. Hands to
309 ENCODE_E2AP_BEGIN */
310
311 /* --- Off-path records. Not boxes: these answer "how loaded was the
312 * process" and "why did this row end early", so the one-stamp-per-box
313 * rule does not apply to them. */
314 LATREC_WAIT_ENTER = 0x70, /* a worker went back to its wait carrying
315 the item it just handled. WAIT_ENTER ->
316 the next row's RECORD_BEGIN is time
317 blocked; stamped once per item, so a
318 periodic wait that times out and loops
319 does not restamp it */
320 LATREC_SKIPPED = 0x71, /* a fetched item produced no indication;
321 aux2 = reason (LATREC_SKIP_*) */
322 LATREC_CONTEXT = 0x72 /* slow lane, ~1 Hz on its own ring:
323 aux = involuntary context switches
324 since the last tick, aux2 = current CPU
325 frequency in kHz (0 if unavailable).
326 seq = 0: no per-message key */
327};
328
329/* D5 admission outcomes (aux2) */
330enum {
335};
336
337/* LATREC_SKIPPED reasons (aux2). A telemetry SM drops a fetched publish for
338 * reasons libe3 never sees, so they are numbered separately from
339 * LATREC_DROP_*. The last two are reachable only on the sensing path. */
340enum {
341 LATREC_SKIP_NO_SUBSCRIBERS = 1, /* fetched, then the last dApp went away */
342 LATREC_SKIP_ENCODE = 2, /* indication encode failed */
343 LATREC_SKIP_COALESCED = 3, /* superseded by a newer publish inside the
344 * same emission period (periodic mode) */
345 LATREC_SKIP_NO_SHM = 4, /* /e3_l2_sensing unavailable, so the
346 * indication has no ring entry to name */
347 LATREC_SKIP_RANGES_LOST = 5 /* the publish's seqlock slot was recycled
348 * by newer writes before it was read out */
349};
350
351/* LATREC_DROP reasons (aux2) */
352enum {
361 /* A push rejected because shutdown() had already been called on that
362 * queue, distinct from LATREC_DROP_QUEUE_PUSH/REPORT_QUEUE (the ring was
363 * actually full). E3Interface::stop() shuts response_queue_/report_queue_
364 * down before joining a registered ServiceModel's own producer thread
365 * (which it does not own directly -- that happens later, via
366 * SmRegistry::clear()), so a handful of these during teardown is expected
367 * and does not indicate undersized capacity. */
369};
370
371#define LATREC_MAGIC 0x31524C41u /* "ALR1" */
372#define LATREC_VERSION 2u
373#define LATREC_HDR_LEN 4096
374
375/* Measured timestamp cost above which latrec_open warns. A vDSO read is tens
376 * of ns; a syscall fallback is several hundred. */
377#define LATREC_CLOCK_SLOW_NS 100u
378
379typedef struct {
380 uint64_t sc; /* seq:48 (low) | cpu:8 | stage:8 (high byte) */
381 uint64_t t_ns; /* CLOCK_MONOTONIC; released last: nonzero == valid record */
382 uint64_t aux;
383 uint64_t aux2;
384} latrec_rec;
385
386/* v1 layout through name[]; v2 appends inside the pad. v1 field offsets are
387 * frozen. */
388typedef struct {
389 uint32_t magic, version;
390 uint64_t entries; /* ring capacity (power of two) */
391 uint64_t rec_count; /* final index; refreshed by latrec_heartbeat */
392 uint64_t t0_mono_ns; /* paired clocks at open: mono<->wall mapping */
393 uint64_t t0_real_ns;
394 char name[64];
395 /* --- v2 --- */
396 uint32_t rec_size; /* sizeof(latrec_rec), for reader validation */
397 uint32_t clock_ns_per_call; /* measured timestamp cost (0 = not measured) */
398 uint64_t t1_mono_ns; /* paired clocks refreshed by heartbeat/close */
399 uint64_t t1_real_ns;
400 uint8_t pad[LATREC_HDR_LEN - 4 * 4 - 8 * 6 - 64];
401} latrec_hdr;
402
403LATREC_STATIC_ASSERT(sizeof(latrec_rec) == 32, "latrec_rec must stay 32 bytes");
404LATREC_STATIC_ASSERT(sizeof(latrec_hdr) == LATREC_HDR_LEN, "latrec_hdr must be 4 KiB");
405
406typedef struct {
408 uint64_t idx;
409 uint64_t mask;
410 void* map;
411 size_t map_len;
413 uint32_t cpu; /* cpu id used by the stamp when LATREC_CPUID_ON_STAMP
414 * is 0; set by latrec_refresh_cpu() */
415 uint64_t ctx_seq; /* seq for code too deep to be passed one; see
416 * latrec_ctx_set() */
417} latrec_t;
418
419static inline uint64_t latrec_now_ns(void)
420{
421 struct timespec ts;
422 clock_gettime(CLOCK_MONOTONIC, &ts);
423 return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
424}
425
426static inline uint64_t latrec_real_ns(void)
427{
428 struct timespec ts;
429 clock_gettime(CLOCK_REALTIME, &ts);
430 return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
431}
432
433/* Re-read the cpu id into the ring. Call off the hot path: sched_getcpu() is
434 * a syscall on targets where LATREC_CPUID_ON_STAMP is 0. */
435static inline void latrec_refresh_cpu(latrec_t* r)
436{
437#if LATREC_HAVE_GETCPU
438 const int cpu = sched_getcpu();
439 if (cpu >= 0) r->cpu = (uint32_t)cpu;
440#else
441 (void)r;
442#endif
443}
444
445/* Mean cost of one latrec_now_ns() over 1000 calls, about 30 us in total.
446 * Called once per ring at open and recorded in the header. */
447static inline uint32_t latrec_measure_clock_ns(void)
448{
449 enum { ITERS = 1000 };
450 uint64_t sink = 0;
451 const uint64_t t0 = latrec_now_ns();
452 for (int i = 0; i < ITERS; i++) sink += latrec_now_ns();
453 const uint64_t t1 = latrec_now_ns();
454 if (sink == 0 || t1 <= t0) return 0;
455 return (uint32_t)((t1 - t0) / (uint64_t)ITERS);
456}
457
458/* Where rings go when the caller does not name a directory. A build-time
459 * setting, like the recorder itself: -DLATREC_DEFAULT_DIR=... at configure
460 * time. Runtime placement is chosen with latrec_open_in(), or process-wide
461 * with latrec_set_output_dir(). */
462#ifndef LATREC_DEFAULT_DIR
463#define LATREC_DEFAULT_DIR "/tmp/latrec"
464#endif
465
466/* Open a ring under `dir`. name ("process.thread") becomes
467 * <dir>/<name>.latrec and must be unique per writer thread across every
468 * process sharing the directory: the file is opened O_TRUNC, so a collision
469 * discards the other writer's ring. entries_log2 sets the capacity (22 = 4 M
470 * records = 128 MiB). `dir` is created if missing (one level only).
471 * Returns 1 on success, -1 on error.
472 *
473 * There is no "tracing disabled" return: in a build with LIBE3_ENABLE_LATREC
474 * the recorder records. Whether it exists at all is the build's decision. */
475static inline int latrec_open_in(latrec_t* r, const char* dir, const char* name,
476 unsigned entries_log2)
477{
478 memset(r, 0, sizeof(*r));
479 if (!dir || !*dir) dir = LATREC_DEFAULT_DIR;
480 mkdir(dir, 0755); /* EEXIST is the normal case; ignore */
481
482 const uint64_t entries = 1ull << entries_log2;
483 const size_t len = LATREC_HDR_LEN + (size_t)entries * sizeof(latrec_rec);
484
485 /* Built under a temporary name and renamed once the header is written:
486 * rename() is atomic, so a <name>.latrec always carries a valid header. */
487 char path[512], tmp[520];
488 snprintf(path, sizeof(path), "%s/%s.latrec", dir, name);
489 snprintf(tmp, sizeof(tmp), "%s.part", path);
490 int fd = open(tmp, O_CREAT | O_RDWR | O_TRUNC, 0644);
491 if (fd < 0) return -1;
492 if (ftruncate(fd, (off_t)len) != 0) { close(fd); unlink(tmp); return -1; }
493 void* map = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
494 close(fd); /* mapping keeps the file */
495 if (map == MAP_FAILED) { unlink(tmp); return -1; }
496 memset(map, 0, len); /* pre-fault every page */
497
498 const uint32_t clock_ns = latrec_measure_clock_ns();
499
500 latrec_hdr* h = (latrec_hdr*)map;
501 h->magic = LATREC_MAGIC;
503 h->entries = entries;
504 h->rec_size = (uint32_t)sizeof(latrec_rec);
505 h->clock_ns_per_call = clock_ns;
508 h->t1_mono_ns = h->t0_mono_ns;
509 h->t1_real_ns = h->t0_real_ns;
510 snprintf(h->name, sizeof(h->name), "%s", name);
511 if (rename(tmp, path) != 0) { munmap(map, len); unlink(tmp); return -1; }
512
513 r->recs = (latrec_rec*)((uint8_t*)map + LATREC_HDR_LEN);
514 r->mask = entries - 1;
515 r->map = map;
516 r->map_len = len;
517 r->enabled = 1;
519
520 if (clock_ns > LATREC_CLOCK_SLOW_NS) {
521 fprintf(stderr,
522 "[latrec] WARNING %s: CLOCK_MONOTONIC costs %u ns/call; the vDSO "
523 "is unavailable (check the clocksource)\n", name, clock_ns);
524 }
525 return 1;
526}
527
528/* Open a ring under the compiled-in default directory. */
529static inline int latrec_open(latrec_t* r, const char* name, unsigned entries_log2)
530{
532}
533
534/* Stamp carrying a timestamp the caller already took, for the case where the
535 * moment being recorded is not the moment the stamp can be issued: handing an
536 * item to another thread is only known to have *succeeded* after the handoff,
537 * by which time the consumer may already have run and stamped its own stage.
538 * Reading the clock before the handoff and passing it here keeps the interval
539 * meaningful; issuing a fresh stamp afterwards can order the producer's record
540 * after the consumer's.
541 *
542 * t_ns must be a CLOCK_MONOTONIC reading from latrec_now_ns(). Zero is treated
543 * as "not taken" and replaced with the current time, since zero is also how an
544 * unwritten slot reads. */
545static inline void latrec_stamp_at(latrec_t* r, uint64_t seq, uint8_t stage,
546 uint64_t aux, uint64_t aux2, uint64_t t_ns)
547{
548 if (LATREC_UNLIKELY(!r->enabled)) return;
549 const uint64_t i = r->idx++;
550 latrec_rec* rec = &r->recs[i & r->mask];
551#if LATREC_CPUID_ON_STAMP
552 const uint64_t cpu = (uint64_t)(unsigned)sched_getcpu();
553#else
554 const uint64_t cpu = (uint64_t)r->cpu;
555#endif
556 rec->sc = (seq & 0x0000FFFFFFFFFFFFull)
557 | ((cpu & 0xFFull) << 48)
558 | ((uint64_t)stage << 56);
559 rec->aux = aux;
560 rec->aux2 = aux2;
561 /* Last store, released: makes the record observable as valid only once
562 * sc/aux/aux2 are visible. */
563 LATREC_STORE_RELEASE(&rec->t_ns, t_ns ? t_ns : latrec_now_ns());
564 /* Take the next record's line for write while the caller runs. */
565 LATREC_PREFETCH_W(&r->recs[(i + 1) & r->mask]);
566}
567
568/* Hot path: one clock read and four stores. Single writer per ring.
569 *
570 * The disabled-ring check is repeated here deliberately. latrec_now_ns() is an
571 * argument, so it would be evaluated before the call even on a thread with no
572 * ring, putting a clock read on a path whose whole point is to be free --
573 * tests/bench_latrec.cpp holds that path to a 10 ns ceiling and catches it. */
574static inline void latrec_stamp(latrec_t* r, uint64_t seq, uint8_t stage,
575 uint64_t aux, uint64_t aux2)
576{
577 if (LATREC_UNLIKELY(!r->enabled)) return;
578 latrec_stamp_at(r, seq, stage, aux, aux2, latrec_now_ns());
579}
580
581/* Refresh rec_count and the closing clock pair without closing the ring. Call
582 * periodically so a ring that is never closed still carries both. */
583static inline void latrec_heartbeat(latrec_t* r)
584{
585 if (!r->enabled) return;
586 latrec_hdr* h = (latrec_hdr*)r->map;
587 h->rec_count = r->idx;
590}
591
592static inline void latrec_close(latrec_t* r)
593{
594 if (!r->enabled) return;
596 msync(r->map, r->map_len, MS_SYNC);
597 munmap(r->map, r->map_len);
598 r->enabled = 0;
599}
600
601/* ---- per-thread rings (backed by latrec.c) -------------------------------- */
602
603/* @see \ref latrec_guide "docs/latrec.md" for why this is a build-time
604 * option, and for what a consumer of an installed libe3 has to match. */
605#ifdef LIBE3_ENABLE_LATREC
606
607/* Ring of the calling thread. Never NULL: it refers to a disabled ring until
608 * the thread calls latrec_tls_open(), so a stamp before that is discarded. */
609extern __thread latrec_t* latrec_tls;
610
611/* Directory the per-thread rings are written to, for the whole process. Call
612 * before the first latrec_tls_open*(); rings already open are unaffected.
613 * NULL or empty restores LATREC_DEFAULT_DIR. This chooses *where*, never
614 * *whether*: with LIBE3_ENABLE_LATREC on, the recorder records regardless.
615 * Two processes that should not share rings (concurrent tests, one benchmark
616 * per directory) each set their own. */
617void latrec_set_output_dir(const char* dir);
618
619/* Opens the calling thread's ring as <role>.<tid>, or <program>.<tid> when
620 * role is NULL or empty; idempotent. Capacity comes from
621 * LATREC_ENTRIES_LOG2_<ROLE> (role uppercased, non-alphanumerics as '_'),
622 * else LATREC_ENTRIES_LOG2, else the compiled-in default. The open
623 * pre-faults the whole mapping, which takes tens of ms; the stamp path does
624 * not call it. */
625latrec_t* latrec_tls_open_as(const char* role);
627
628/* Next value of the process-wide sequence counter (starts at 1). */
629uint64_t latrec_seq_next(void);
630
631/* Stamp into the calling thread's ring, carrying a timestamp taken earlier.
632 * See latrec_stamp_at for when that is the correct thing to do. */
633static inline void latrec_tstamp_at(uint64_t seq, uint8_t stage, uint64_t aux,
634 uint64_t aux2, uint64_t t_ns)
635{
636 latrec_stamp_at(latrec_tls, seq, stage, aux, aux2, t_ns);
637}
638
639/* Take a timestamp for a later latrec_tstamp_at(). Pairing these two, rather
640 * than calling latrec_now_ns() directly, keeps the clock read out of a build
641 * with the recorder compiled out: this is the point where the pair collapses
642 * to nothing. */
643static inline uint64_t latrec_tnow(void) { return latrec_now_ns(); }
644
645/* Stamp into the calling thread's ring. */
646static inline void latrec_tstamp(uint64_t seq, uint8_t stage, uint64_t aux, uint64_t aux2)
647{
648 latrec_stamp(latrec_tls, seq, stage, aux, aux2);
649}
650
651/* Publishes the seq of the message this thread is carrying, for code whose
652 * signature cannot receive it: the connector is handed a byte buffer, not a
653 * Pdu, libe3's emit entry points are handed neither, and an application's
654 * indication or control handler is handed the message alone. Caller and callee
655 * run on the same thread. latrec_ctx() reads back 0 from a thread with no ring.
656 *
657 * The value is sticky until overwritten, so a producer that wants its records
658 * joined to libe3's outbound leg (via LATREC_EMIT_ENTER's aux) sets it once
659 * per message, immediately before emitting. Readers must tolerate a stale or
660 * zero value rather than treating it as authoritative.
661 *
662 * Both directions use it. Outbound, the producer sets it and libe3 reads it
663 * into LATREC_EMIT_ENTER's aux. Inbound, libe3 sets it before invoking the
664 * application handler, so the handler's own stages can key on the same seq as
665 * the DELIVER_BEGIN/DELIVER_DONE pair around them; a dApp reads it with
666 * latrec_ctx() on entry rather than being passed it. */
667static inline void latrec_ctx_set(uint64_t seq)
668{
669 /* Threads without a ring share one disabled ring, so an unconditional
670 * store here would be a write race between them. */
671 latrec_t* r = latrec_tls;
672 if (r->enabled) r->ctx_seq = seq;
673}
674static inline uint64_t latrec_ctx(void) { return latrec_tls->ctx_seq; }
675
676#else /* !LIBE3_ENABLE_LATREC */
677
678/* True no-op stubs, not just a disabled ring: every instrumented call site
679 * collapses to nothing here, with no dependency on latrec.c (excluded from
680 * the build in this configuration) and no linker symbol to resolve. */
681static inline void latrec_set_output_dir(const char* dir) { (void)dir; }
682static inline latrec_t* latrec_tls_open_as(const char* role) { (void)role; return NULL; }
683static inline latrec_t* latrec_tls_open(void) { return NULL; }
684static inline uint64_t latrec_seq_next(void) { return 0; }
685static inline void latrec_tstamp(uint64_t seq, uint8_t stage, uint64_t aux, uint64_t aux2)
686{
687 (void)seq; (void)stage; (void)aux; (void)aux2;
688}
689static inline void latrec_tstamp_at(uint64_t seq, uint8_t stage, uint64_t aux,
690 uint64_t aux2, uint64_t t_ns)
691{
692 (void)seq; (void)stage; (void)aux; (void)aux2; (void)t_ns;
693}
694/* No clock read at all here: the whole pair is compiled out. */
695static inline uint64_t latrec_tnow(void) { return 0; }
696static inline void latrec_ctx_set(uint64_t seq) { (void)seq; }
697static inline uint64_t latrec_ctx(void) { return 0; }
698
699#endif /* LIBE3_ENABLE_LATREC */
700
701#ifdef __cplusplus
702}
703#endif
704
705#endif /* LIBE3_LATREC_H */
static unsigned entries_log2(const char *role)
Definition latrec.c:86
__thread latrec_t * latrec_tls
Definition latrec.c:45
static void latrec_tstamp(uint64_t seq, uint8_t stage, uint64_t aux, uint64_t aux2)
Definition latrec.h:685
static uint64_t latrec_tnow(void)
Definition latrec.h:695
static void latrec_ctx_set(uint64_t seq)
Definition latrec.h:696
static void latrec_set_output_dir(const char *dir)
Definition latrec.h:681
static int latrec_open_in(latrec_t *r, const char *dir, const char *name, unsigned entries_log2)
Definition latrec.h:475
#define LATREC_STATIC_ASSERT(c, m)
Definition latrec.h:123
#define LATREC_VERSION
Definition latrec.h:372
static uint64_t latrec_seq_next(void)
Definition latrec.h:684
static latrec_t * latrec_tls_open(void)
Definition latrec.h:683
static void latrec_heartbeat(latrec_t *r)
Definition latrec.h:583
static void latrec_tstamp_at(uint64_t seq, uint8_t stage, uint64_t aux, uint64_t aux2, uint64_t t_ns)
Definition latrec.h:689
static uint32_t latrec_measure_clock_ns(void)
Definition latrec.h:447
static uint64_t latrec_now_ns(void)
Definition latrec.h:419
@ LATREC_SUB_BEGIN
Definition latrec.h:228
@ LATREC_EMIT_ENTER
Definition latrec.h:174
@ LATREC_DECODE_E2SM_BEGIN
Definition latrec.h:301
@ LATREC_DECODE_E2AP_BEGIN
Definition latrec.h:292
@ LATREC_DEQUEUE
Definition latrec.h:183
@ LATREC_ACK_RECV
Definition latrec.h:276
@ LATREC_PROCESS_BEGIN
Definition latrec.h:155
@ LATREC_SETUP_BEGIN
Definition latrec.h:221
@ LATREC_SESSION_POLLED
Definition latrec.h:209
@ LATREC_SKIPPED
Definition latrec.h:320
@ LATREC_CONTEXT
Definition latrec.h:322
@ LATREC_ENCODE_E3SM_DONE
Definition latrec.h:163
@ LATREC_SM_START
Definition latrec.h:232
@ LATREC_ENCODE_E2AP_BEGIN
Definition latrec.h:290
@ LATREC_DECODE_E2SM_DONE
Definition latrec.h:303
@ LATREC_DECODE_E3SM_DONE
Definition latrec.h:166
@ LATREC_DECODE_E2AP_DONE
Definition latrec.h:293
@ LATREC_RECORD_BEGIN
Definition latrec.h:154
@ LATREC_ENCODE_E3AP_DONE
Definition latrec.h:185
@ LATREC_CREATE_OUTPUT
Definition latrec.h:247
@ LATREC_RECV
Definition latrec.h:193
@ LATREC_DECODE_E3AP_DONE
Definition latrec.h:194
@ LATREC_DECODE_E3SM_BEGIN
Definition latrec.h:165
@ LATREC_REPORT_QUEUED
Definition latrec.h:204
@ LATREC_ENCODE_E3SM_BEGIN
Definition latrec.h:162
@ LATREC_DELIVER_BEGIN
Definition latrec.h:196
@ LATREC_BRIDGE_IN
Definition latrec.h:282
@ LATREC_SETUP_DONE
Definition latrec.h:223
@ LATREC_ADMITTED
Definition latrec.h:251
@ LATREC_APPLY_CONTROL_DONE
Definition latrec.h:258
@ LATREC_PROCESS_DONE
Definition latrec.h:245
@ LATREC_SEND_DONE
Definition latrec.h:186
@ LATREC_WAIT_ENTER
Definition latrec.h:314
@ LATREC_ENCODE_E2SM_DONE
Definition latrec.h:308
@ LATREC_SM_STATUS_DONE
Definition latrec.h:237
@ LATREC_SUB_DONE
Definition latrec.h:229
@ LATREC_SESSION_QUEUED
Definition latrec.h:207
@ LATREC_ACK_SENT
Definition latrec.h:274
@ LATREC_SM_STOP
Definition latrec.h:233
@ LATREC_XAPP_PROCESS_DONE
Definition latrec.h:304
@ LATREC_REPORT_DONE
Definition latrec.h:206
@ LATREC_APPLY_POLICY_DONE
Definition latrec.h:249
@ LATREC_LIVE_ON_AIR
Definition latrec.h:262
@ LATREC_ENCODE_E2AP_DONE
Definition latrec.h:291
@ LATREC_BRIDGE_OUT
Definition latrec.h:283
@ LATREC_ENQUEUE
Definition latrec.h:181
@ LATREC_SM_STATUS_BEGIN
Definition latrec.h:235
@ LATREC_DELIVER_DONE
Definition latrec.h:202
@ LATREC_DROP
Definition latrec.h:212
@ LATREC_ENCODE_E2SM_BEGIN
Definition latrec.h:306
static uint64_t latrec_ctx(void)
Definition latrec.h:697
static void latrec_close(latrec_t *r)
Definition latrec.h:592
#define LATREC_STORE_RELEASE(p, v)
Definition latrec.h:86
#define LATREC_DEFAULT_DIR
Definition latrec.h:463
static void latrec_refresh_cpu(latrec_t *r)
Definition latrec.h:435
#define LATREC_UNLIKELY(x)
Definition latrec.h:89
#define LATREC_HDR_LEN
Definition latrec.h:373
static uint64_t latrec_real_ns(void)
Definition latrec.h:426
#define LATREC_PREFETCH_W(p)
Definition latrec.h:88
@ LATREC_OUT_PROCESSED
Definition latrec.h:331
@ LATREC_OUT_SHED_AGE
Definition latrec.h:332
@ LATREC_OUT_SHED_LAG
Definition latrec.h:333
@ LATREC_OUT_NO_RANGE
Definition latrec.h:334
static int latrec_open(latrec_t *r, const char *name, unsigned entries_log2)
Definition latrec.h:529
static void latrec_stamp(latrec_t *r, uint64_t seq, uint8_t stage, uint64_t aux, uint64_t aux2)
Definition latrec.h:574
#define LATREC_CLOCK_SLOW_NS
Definition latrec.h:377
static latrec_t * latrec_tls_open_as(const char *role)
Definition latrec.h:682
#define LATREC_MAGIC
Definition latrec.h:371
@ LATREC_DROP_SHUTDOWN
Definition latrec.h:368
@ LATREC_DROP_FILTERED
Definition latrec.h:360
@ LATREC_DROP_DECODE
Definition latrec.h:356
@ LATREC_DROP_SESSION_QUEUE
Definition latrec.h:359
@ LATREC_DROP_ENCODE
Definition latrec.h:354
@ LATREC_DROP_REPORT_QUEUE
Definition latrec.h:357
@ LATREC_DROP_QUEUE_PUSH
Definition latrec.h:353
@ LATREC_DROP_SEND
Definition latrec.h:355
@ LATREC_DROP_NO_HANDLER
Definition latrec.h:358
@ LATREC_SKIP_RANGES_LOST
Definition latrec.h:347
@ LATREC_SKIP_COALESCED
Definition latrec.h:343
@ LATREC_SKIP_NO_SUBSCRIBERS
Definition latrec.h:341
@ LATREC_SKIP_ENCODE
Definition latrec.h:342
@ LATREC_SKIP_NO_SHM
Definition latrec.h:345
static void latrec_stamp_at(latrec_t *r, uint64_t seq, uint8_t stage, uint64_t aux, uint64_t aux2, uint64_t t_ns)
Definition latrec.h:545
uint32_t magic
Definition latrec.h:389
uint64_t t1_mono_ns
Definition latrec.h:398
uint32_t rec_size
Definition latrec.h:396
uint8_t pad[4096 - 4 *4 - 8 *6 - 64]
Definition latrec.h:400
uint64_t entries
Definition latrec.h:390
uint64_t rec_count
Definition latrec.h:391
char name[64]
Definition latrec.h:394
uint64_t t0_real_ns
Definition latrec.h:393
uint64_t t1_real_ns
Definition latrec.h:399
uint32_t clock_ns_per_call
Definition latrec.h:397
uint32_t version
Definition latrec.h:389
uint64_t t0_mono_ns
Definition latrec.h:392
uint64_t aux2
Definition latrec.h:383
uint64_t aux
Definition latrec.h:382
uint64_t sc
Definition latrec.h:380
uint64_t t_ns
Definition latrec.h:381
uint64_t mask
Definition latrec.h:409
uint64_t ctx_seq
Definition latrec.h:415
size_t map_len
Definition latrec.h:411
int enabled
Definition latrec.h:412
uint64_t idx
Definition latrec.h:408
uint32_t cpu
Definition latrec.h:413
latrec_rec * recs
Definition latrec.h:407
void * map
Definition latrec.h:410