libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_tail.h
1/*-
2 * Copyright (c) 2026, The XTC Project
3 * @stability stable
4 * Use of this source code is governed by the ISC License.
5 *
6 * src/inc/xtc_tail.h
7 * A runtime microscope: cheap, high-volume recording of what the
8 * scheduler and runtime are actually doing -- "tail -f a system" --
9 * so hard async bugs (lost/late wakeups, long polls, mailbox backups,
10 * scheduler imbalance) become obvious after the fact. Inspired by
11 * dial9 ("a microscope for Tokio").
12 *
13 * Unlike xtc_stats (aggregate counters) and xtc_trace (the causal
14 * message trace), xtc_tail records every INDIVIDUAL runtime event
15 * tied to a precise instant, so a degraded window can be diffed
16 * against a normal one. It is OFF by default and one branch when
17 * disabled -- observability must not tax production.
18 *
19 * Phase 1 (this): the SCHED source (proc spawn / exit / wake / run)
20 * over a bounded per-process ring, an in-process read callback, and a
21 * versioned binary dump to an fd. The IO/OS sources, on-disk spill
22 * with rotation, and the offline viewer are staged follow-ons (see
23 * the roadmap); the record format and API are designed to accept them
24 * without a break.
25 */
26
27#ifndef XTC_TAIL_H
28#define XTC_TAIL_H
29
30#include "xtc_export.h"
31
32#include <stddef.h>
33#include <stdint.h>
34
35#include "xtc.h"
36#include "xtc_proc.h"
37
38/* Event sources, an enable mask (dial9-style: pay only for what you turn
39 * on). Phase 1 implements SCHED; the others are reserved so the mask is
40 * stable across the staged build-out. */
41#define XTC_TAIL_SCHED (1u << 0) /* proc spawn/exit/wake/run */
42#define XTC_TAIL_MSG (1u << 1) /* reserved: send/recv/mailbox depth */
43#define XTC_TAIL_IO (1u << 2) /* reserved: fd reg/del/completion */
44#define XTC_TAIL_OS (1u << 3) /* reserved: per-loop CPU/RSS sampling */
45#define XTC_TAIL_ALL (XTC_TAIL_SCHED | XTC_TAIL_MSG | XTC_TAIL_IO | XTC_TAIL_OS)
46
47/* Event kinds recorded by the SCHED source. */
48enum xtc_tail_kind {
49 XTC_TAIL_SPAWN = 0, /* a proc was spawned */
50 XTC_TAIL_EXIT = 1, /* a proc exited (detail = reason) */
51 XTC_TAIL_WAKE = 2, /* an I/O completion was DISPATCHED to a task
52 * (the waker side). pid.loop_id is the
53 * DISPATCHING loop; local_id/gen are 0
54 * because dispatch has a task, not a proc.
55 * detail = the xtc_task_t * as an integer,
56 * which is the join key: match it against
57 * the `task` column of xtc-procs, or against
58 * the XTC_TAIL_PARK_TASK event the parking
59 * fiber emits just before its PARK.
60 *
61 * NOTE: XTC_TAIL_PARK's detail is the aio OP,
62 * NOT a task pointer. An earlier version of
63 * this comment said otherwise and sent a
64 * consumer chasing a join that could not be
65 * performed; PARK_TASK exists because of it.
66 *
67 * This is the event that separates "the wake
68 * was never generated" from "the wake was
69 * generated and the task still never ran":
70 * a WAKE for task T with no following RUN
71 * for T's pid means dispatch DID run and the
72 * loss is after it. */
73 XTC_TAIL_RUN = 3, /* a proc began running after a wake
74 * (detail = wake-to-run latency, ns) */
75 XTC_TAIL_PARK = 4, /* a proc parked (blocked on recv/timer/fd) */
76 /* MSG source: */
77 XTC_TAIL_SEND = 5, /* pid sent a message (detail = payload bytes) */
78 XTC_TAIL_RECV = 6, /* pid received a message (detail = bytes) */
79 XTC_TAIL_MBOX_HWM = 7, /* pid's mailbox depth reached a new high-water
80 * (detail = the new peak depth) */
81 /* Loop liveness (SCHED source). pid.loop_id identifies the loop;
82 * local_id/gen are 0 because a LOOP is not a proc. detail = the
83 * number of events the poll dispatched.
84 *
85 * This exists to answer one question a park/run timeline cannot:
86 * when a fiber's park has no matching RUN, did its loop KEEP WORKING
87 * (so the loop is alive and that fiber specifically was skipped) or
88 * did the loop stop entirely? Those have different causes and
89 * different fixes, and without a per-loop event the only way to tell
90 * them apart is to infer from the absence of other pids' events --
91 * which cannot distinguish "loop dead" from "we stopped recording",
92 * and a consumer hit exactly that ambiguity.
93 *
94 * Emitted after each completed xtc_io_poll on the loop's own ring, so
95 * a loop that is still polling produces a steady stream even when it
96 * dispatches nothing (detail = 0). */
97 XTC_TAIL_LOOP_POLL = 8,
98 /*
99 * The identity of the task a proc is parked as. pid is the PARKING
100 * proc; detail is its xtc_task_t * as an integer.
101 *
102 * This exists purely to make XTC_TAIL_WAKE joinable. WAKE is emitted
103 * from completion dispatch, which holds a task and no pid (there is no
104 * task-to-proc back pointer), so it carries the task pointer. PARK
105 * carries the pid and, for an aio park, the OP in detail -- which a
106 * consumer relies on to tell an fdatasync park from a read park.
107 * Overwriting that op with the task pointer would trade one key for
108 * the other; emitting this alongside gives both.
109 *
110 * Emitted immediately before the PARK it describes, from the same
111 * fiber, so the pairing is unambiguous:
112 *
113 * PARK_TASK pid=25.1.1 detail=<task*>
114 * PARK pid=25.1.1 detail=3 (XTC_AIO_FDATASYNC)
115 * ...
116 * WAKE pid=<loop> detail=<task*> <- joins on detail
117 * RUN pid=25.1.1 detail=<ns>
118 *
119 * A PARK_TASK whose task pointer never appears in a later WAKE means
120 * the completion never reached dispatch. One that does appear, with
121 * no following RUN for that pid, means dispatch ran and the loss is
122 * downstream of it. Those are different bugs.
123 */
124 XTC_TAIL_PARK_TASK = 9,
125 /*
126 * A CQE was REAPED from a ring and its life ended here. pid.loop_id
127 * is the REAPING loop; local_id/gen are 0 (the reaper holds a ring,
128 * not a proc). detail is the tag the CQE resolved to -- the same
129 * xtc_task_t * that PARK_TASK and WAKE carry -- or 0 when it resolved
130 * to no tag at all.
131 *
132 * This closes the LAST gap in the chain. PARK_TASK/WAKE/RUN can prove
133 * a completion never reached dispatch, but not WHY: the CQE might never
134 * have been posted, or been posted and consumed by the reaper without
135 * ever being handed on. Those are different subsystems. A REAP event
136 * for task T with no WAKE for T means the reaper consumed T's
137 * completion and dropped it; no REAP at all means the kernel never
138 * posted it (or we never looked).
139 *
140 * Emitted at every point where a CQE is consumed, INCLUDING the paths
141 * that deliberately discard one, because a deliberate discard is
142 * indistinguishable from a bug in a timeline that cannot see it:
143 *
144 * detail = <task*> the completion was stored for dispatch
145 * detail = 0 consumed and NOT handed on. Expected for the
146 * wakeup-pipe CQE and for a poll_remove cancel
147 * (user_data NULL by design), and the signature of
148 * a completion dropped because its registration was
149 * already torn down.
150 *
151 * Rides XTC_TAIL_SCHED like the other kinds. It is the highest-volume
152 * event here -- one per CQE -- so on a busy ring it WILL dominate the
153 * buffer; check xtc_tail_dropped() before believing any absence, and
154 * prefer a short capture window. (The LOOP_POLL lesson: an event that
155 * crowds out the data it explains is worse than none.)
156 */
157 XTC_TAIL_REAP = 10,
158 /*
159 * An SQE was handed to the kernel. pid.loop_id is the SUBMITTING loop;
160 * local_id/gen are 0. detail is the task pointer the submission is on
161 * behalf of -- the same key PARK_TASK, REAP and WAKE carry -- or 0 when
162 * the submission is not tied to a parked task.
163 *
164 * This separates the two halves of "no REAP", which REAP alone cannot:
165 * a completion that never came back may never have been ASKED FOR.
166 *
167 * SUBMIT then REAP normal.
168 * SUBMIT, never REAPed the kernel took the request and no
169 * completion came back -- look at the ring,
170 * the wait, or the request itself.
171 * no SUBMIT at all we never asked. The fiber parked for a
172 * completion that was never queued, which no
173 * amount of polling can deliver.
174 */
175 XTC_TAIL_SUBMIT = 11,
176 /*
177 * A submission did NOT reach the kernel. pid.loop_id is the submitting
178 * loop; detail is the negated errno, or 0 if there was none.
179 *
180 * This is the one kind here whose mere PRESENCE is a fault -- every
181 * other needs a join to mean anything. If one appears, a fiber is
182 * parked on a request the kernel never accepted, and no reap-side or
183 * dispatch-side investigation can explain it.
184 *
185 * Reachable today: io_uring_submit returns the number of SQEs consumed
186 * or a negative errno, and every submit site in the uring backend
187 * discarded that value, so a partial or failed submit left a fiber
188 * parked forever with nothing in flight, silently.
189 */
190 XTC_TAIL_SUBMIT_FAIL = 12,
191 /*
192 * A poll drained its FULL per-call budget and therefore may have left
193 * completions behind. pid.loop_id is the polling loop; detail is the
194 * budget it filled (the caller's `max`).
195 *
196 * This closes a blind spot in XTC_TAIL_LOOP_POLL, which is emitted only
197 * for an IDLE poll (one that dispatched nothing). That restriction is
198 * deliberate -- an every-poll event crowded the ring and evicted the
199 * data it existed to explain -- but it means a poll that reaped
200 * some-but-not-all events is INVISIBLE, and "I polled, took what I
201 * could, and left the rest" is exactly the state to look for when a
202 * ring has a pinned backlog while its loop is demonstrably alive.
203 *
204 * Low volume by construction: it fires only when a single poll hits its
205 * ceiling, not on every poll. A steady stream of these for one loop_id
206 * means that ring is receiving completions faster than one poll can
207 * take them.
208 */
209 XTC_TAIL_POLL_FULL = 13,
210 /*
211 * A LIFECYCLE notification could not be delivered. pid is the proc
212 * whose exit was being reported (for a dropped DOWN/EXIT) or the pid
213 * whose registry enrollment failed; detail is the negated xtc_err
214 * from the send.
215 *
216 * Like SUBMIT_FAIL, its mere PRESENCE is a fault: it means a watcher
217 * or supervisor will never learn about a terminal event it asked to
218 * observe, so it can wait forever on a DOWN that is not coming, or
219 * believe a monitored registry name will be reaped when nothing is
220 * watching it.
221 *
222 * Reachable because the delivery sites discard the send result:
223 * __notify_links_and_monitors does (void)xtc_send for each DOWN/EXIT
224 * (src/ptc/proc.c), and xtc_reg_register_mon does the same for its
225 * reaper enrollment while still returning XTC_OK (src/orc/reg.c). A
226 * mailbox saturated by ORDINARY traffic therefore silently defeats
227 * lifecycle observation -- the bounded mailbox is correct
228 * backpressure, but a lifecycle event losing that race must not be
229 * invisible. Emitting this does not FIX the loss (a reserved
230 * lifecycle route or an explicit failed-observation state is the
231 * real fix, and is a consumer-visible design change); it makes the
232 * loss DIAGNOSABLE instead of silent, which is the prerequisite.
233 */
234 XTC_TAIL_LIFECYCLE_DROP = 14
235};
236
237/*
238 * XTC_TAIL_SUBMIT_FAIL detail encoding. Below this value the detail is a
239 * plain errno (the submit was refused); at or above it, subtract the base to
240 * get the number of SQEs left UNSUBMITTED by a short submit, which has no
241 * errno of its own. Two failure shapes, one field, no ambiguity: a short
242 * submit returns a POSITIVE count, so an errno-only report would call it
243 * success while an SQE never reached the kernel.
244 */
245#define XTC_TAIL_SHORT_SUBMIT_BASE 4096
246
247/*
248 * loop_id reported for a ring that has NO exec-relative loop id -- a loop
249 * created by a bare xtc_loop_init rather than as part of an executor.
250 *
251 * This exists because the obvious fallback is a lie: reporting 0 makes an
252 * unlabelled ring indistinguishable from the executor's REAL loop 0, and a
253 * consumer read a stack of "loop 0" SUBMIT/REAP events as evidence that
254 * loop 0 owned a stranded completion. That inference may well be right, but
255 * the number they read could not distinguish the two cases, so it could not
256 * support it. 0xFFFFFFFF cannot be a real exec loop id and is visibly
257 * not an index. (1.x, with a 16-bit loop_id, used 0xFFFF.)
258 */
259#define XTC_TAIL_LOOP_NONE 0xFFFFFFFFu
260
261/* One recorded event. Fixed layout; the binary dump writes it verbatim
262 * behind a versioned header, so a reader across the wire/disk decodes it
263 * without guessing. */
264typedef struct xtc_tail_rec {
265 uint64_t ts_ns; /* monotonic timestamp (ns) */
266 uint32_t source; /* which XTC_TAIL_* source produced it */
267 uint32_t kind; /* enum xtc_tail_kind */
268 xtc_pid_t pid; /* the proc the event concerns */
269 uint64_t detail; /* kind-specific (EXIT reason / RUN latency ns) */
271
272/* Visit callback for xtc_tail_read: return 0 to continue, nonzero stops. */
273typedef int (*xtc_tail_fn)(const xtc_tail_rec_t *rec, void *user);
274
275/*
276 * PUBLIC: unsigned xtc_tail_enable __P((unsigned));
277 * PUBLIC: void xtc_tail_disable __P((void));
278 * PUBLIC: int xtc_tail_reset __P((void));
279 * PUBLIC: int xtc_tail_read __P((xtc_tail_fn, void *));
280 * PUBLIC: int xtc_tail_dump __P((int));
281 * PUBLIC: int xtc_tail_dump_dial9 __P((int));
282 * PUBLIC: unsigned xtc_tail_from_env __P((void));
283 * PUBLIC: int xtc_tail_spill_dial9 __P((const char *));
284 * PUBLIC: size_t xtc_tail_count __P((void));
285 * PUBLIC: uint64_t xtc_tail_dropped __P((void));
286 */
287
288/* Enable the named sources (a bitwise-OR of XTC_TAIL_*). Returns the
289 * previously enabled mask. Enabling is idempotent; call with the full
290 * mask you want each time (it replaces, not ORs). */
291XTC_API unsigned xtc_tail_enable(unsigned source_mask);
292
293/* Disable all recording (equivalent to xtc_tail_enable(0)). */
294XTC_API void xtc_tail_disable(void);
295
296/* Drop all buffered records. Returns XTC_OK. */
297XTC_API int xtc_tail_reset(void);
298
299/* Visit every buffered record oldest-first (a stable snapshot). */
300XTC_API int xtc_tail_read(xtc_tail_fn cb, void *user);
301
302/* Write the buffered records to `fd` as a versioned binary trace:
303 * a small header (magic, version, record count, record size) followed
304 * by the records verbatim. A separate offline tool renders it. */
305XTC_API int xtc_tail_dump(int fd);
306
307/* Write the buffered records to `fd` as a dial9 trace (TRC\0 wire format,
308 * version 1), so a libxtc trace opens in the dial9 GUI viewer unchanged.
309 * Each libxtc event kind is registered as a self-describing schema under a
310 * name the viewer recognizes where a Tokio analogue exists (PollStartEvent,
311 * TaskSpawnEvent, WakeEventEvent, ...) and under an Xtc* name otherwise (the
312 * io_uring reap/submit chain), which the viewer shows as a custom event.
313 * See the dial9 project for the viewer; the format is dial9's, pinned to v1. */
314XTC_API int xtc_tail_dump_dial9(int fd);
315
316/* Enable xtc_tail from the environment (XTC_TAIL_ENABLE=1/sched/all), for
317 * zero-code deployment. Returns the enabled source mask (0 = off). */
318XTC_API unsigned xtc_tail_from_env(void);
319
320/* Spill the current ring to a new dial9 segment file in `dir` (which must
321 * exist), named xtc-tail-<pid>-<ns>.d9. The way to get a trace off a
322 * deployed box; rotation/budget is the operator's (a dir + a cleanup rule). */
323XTC_API int xtc_tail_spill_dial9(const char *dir);
324
325/* Number of records currently buffered. */
326XTC_API size_t xtc_tail_count(void);
327
328/*
329 * How many records have been OVERWRITTEN (evicted) because the ring
330 * wrapped -- total emitted minus what is still buffered.
331 *
332 * Check this before drawing any conclusion from the ABSENCE of an event.
333 * With a non-zero dropped count, "pid X has no events" and "loop L never
334 * polled" are unfalsifiable: the events may simply have been evicted. A
335 * consumer reported two verdicts that were exactly this artifact -- 23 of
336 * 33 loops appeared to have stopped polling when their events had merely
337 * been overwritten -- so this accessor exists to make a real zero
338 * distinguishable from an evicted one.
339 *
340 * Conclusions drawn from events that are PRESENT stay valid regardless.
341 */
342XTC_API uint64_t xtc_tail_dropped(void);
343
344/* The internal hook-point primitives __xtc_tail_emit / __xtc_tail_on
345 * are library-internal (the __ prefix) and live in "tail_int.h", not in
346 * this installed public header. Consumers use the public xtc_tail_*
347 * API (enable + read/dump/count) below. */
348
349/* On-disk binary dump header (also used by the offline reader).
350 *
351 * Format v2+ is COMPACT and PORTABLE (dial9-style): all header fields are
352 * written as explicit little-endian bytes (no struct memcpy, so it is
353 * byte-identical across endianness and padding), and each event is
354 * varint/delta encoded rather than a fixed 32-byte record:
355 *
356 * header: magic[4]="XTCL" version(LE u32)=3 flags(LE u32)
357 * count(LE u32) base_ts_ns(LE u64)
358 * flags bit0 = 1 -> little-endian canonical stream (always set today)
359 * per event (oldest first):
360 * kind : 1 byte
361 * source : 1 byte
362 * ts_delta: LEB128 varint, ns since the previous event (base for the
363 * first) -- monotonic timestamps make this 1-2 bytes
364 * loop_id : LEB128 varint (pid.loop_id, u32; XTC_TAIL_LOOP_NONE
365 * 0xFFFFFFFF = a ring with no exec loop id)
366 * local_id: LEB128 varint (pid.local_id, u32)
367 * gen : LEB128 varint (pid.gen, u64)
368 * detail : LEB128 varint (EXIT reason / RUN latency ns)
369 *
370 * Version 3 (2.0) widened the pid (u32 loop_id, u32 local_id, u64 gen)
371 * and the loop sentinel (0xFFFF -> 0xFFFFFFFF); the byte layout is the
372 * same varint sequence, so only those values distinguish it from v2.
373 * A reader accepts 2 and 3 and maps a v2 loop_id of 0xFFFF to NONE.
374 * Typical ~6-14 bytes/event vs 40 for the raw struct, and portable.
375 * xtc_tail_read (in-process) still hands back the fixed xtc_tail_rec_t. */
376#define XTC_TAIL_MAGIC 0x5854434Cu /* "XTCL" */
377#define XTC_TAIL_VERSION 3u
378#define XTC_TAIL_FLAG_LE 1u /* little-endian canonical stream */
379typedef struct xtc_tail_hdr {
380 uint32_t magic; /* XTC_TAIL_MAGIC */
381 uint32_t version; /* XTC_TAIL_VERSION */
382 uint32_t flags; /* XTC_TAIL_FLAG_* (endianness marker) */
383 uint32_t count; /* number of events that follow */
384 uint64_t base_ts_ns; /* timestamp of the first event (deltas from here) */
386
387#endif /* XTC_TAIL_H */