godspeed_sdk/trace.rs
1// SPDX-License-Identifier: Apache-2.0
2//! IPC trace emission - the client half of the trace ring that `events` holds (`utilities/47_events.md`).
3//!
4//! # Why the instrumentation is HERE and not in the kernel
5//!
6//! The kernel is at the routing point holding sender, receiver and endpoint, so a trace ring there
7//! looks obvious. It is the wrong place, and the reason is worth stating where the code is: the kernel
8//! is FORBIDDEN to know what a message means (§4.4, §26.10), so a kernel ring can only ever record
9//! `endpoint 7, op 11`. A service knows its own protocol exactly, so instrumenting the SDK - the layer
10//! every service already calls - is the only place `fs.read` can come from. The constraint the
11//! constitution imposes and the feature the requirement wanted point the same way.
12//!
13//! It also means the kernel gains nothing at all: no ring, no retention policy, no message-identity
14//! scheme, no control syscall, no new capability, and no write on the IPC fast path.
15//!
16//! # Cost when not tracing
17//!
18//! One `Relaxed` load, branch not taken. A service is tracing only if its spawn request granted it a
19//! send cap to `events` (declared in its contract as `ipc_send = ["events"]`, CLAUDE.md 13.6) and the
20//! lazy resolution below found that cap - so tracing is AUTHORITY,
21//! visible in `caps <service>`, revocable, and absent by default (§3.1: no ambient anything).
22//!
23//! # Never blocks, never fails loudly
24//!
25//! Emission is `try_send` and the result is discarded. An observer must not be able to slow, block or
26//! break the thing it observes: a full events queue costs the emitting service nothing and loses one
27//! event, which the ring counts and `events status` reports. That is the correct trade here, and the
28//! opposite of the one made on a correctness path.
29
30use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
31
32/// Bytes of the peer's NAME carried in an event. Short on purpose: it identifies a service, and the
33/// longest that matters here is `block-driver` (12).
34pub const PEER_LEN: usize = 12;
35
36/// Wire length of one event:
37/// seq(4) + at_s(4, STAMPED BY THE SINK) + caller[12] + peer[12] + op(1) + kind(1).
38///
39/// The peer is carried as a NAME, not an endpoint or a cap slot, and that is the whole point. A cap
40/// slot is local to the emitter and means nothing to a reader; an endpoint id needs a second lookup.
41/// The emitter called `request_with_reply("fs", ...)` - it KNOWS the name, and a name is what a reader
42/// wants. This is the "symbolic" half the requirement asked for, and it is only reachable out here:
43/// the kernel may not interpret a protocol, but the service that owns one may (§4.4, §26.10).
44pub const EV_LEN: usize = 4 + 4 + PEER_LEN + PEER_LEN + 1 + 1;
45
46/// The service that holds the ring. A call to THIS peer is never traced: the reader reaches the ring
47/// through it, so recording those calls would fill the ring with the reader's own questions.
48pub const SINK_NAME: &str = "events";
49
50/// Message opcodes understood by the ring in `events` (byte 0 of the payload).
51pub const TRACE_OP_EVENT: u8 = 1;
52/// Ask for the most recent events; byte 1 = how many are wanted.
53pub const TRACE_OP_DUMP: u8 = 2;
54/// Ask for ring capacity / accepted / dropped.
55pub const TRACE_OP_STATUS: u8 = 3;
56
57/// Publish a METRIC sample: `[4][owner:12][name:20][value(8, le)]` - value at offset 33.
58///
59/// The name field is [`MET_NAME_LEN`] = 20, not 12. This line said 12, which is the bug the
60/// note on that constant records fixing (a metric truncated to `ring.recorde`); decoding by the
61/// old layout reads eight bytes of NAME as the u64 value.
62///
63/// A metric is a SET, not an increment. The emitting service already holds the counter - it is that
64/// service's own state, with an owner (§3.8) - and publishes the current value; `events` remembers the
65/// last one published. That keeps the sink free of accumulation semantics it would otherwise have to
66/// define (what does an increment mean across a restart of either side?), and it means a service that
67/// dies leaves its final published value behind instead of taking it along - the one useful thing an
68/// observer can still learn from a service that is gone.
69pub const TRACE_OP_METRIC: u8 = 4;
70/// Ask for the metric table.
71pub const TRACE_OP_METRICS: u8 = 5;
72
73/// A LOG COPY: `[6][owner:12][utf-8 text]`, variable length.
74///
75/// **This is a copy, never the log itself.** `ctx.log()` performs its syscall FIRST and
76/// unconditionally - the kernel ring and serial are written whether or not `events` is alive, exists,
77/// or is reachable - and only then offers this duplicate so the line can be queried later. The
78/// distinction is the whole design: re-pointing logs AT the service would make observing a failure
79/// depend on a service that can fail, which is the storage argument in CLAUDE.md 15 one layer up.
80/// Adding a copy costs a `try_send` whose result is discarded and takes nothing away from the floor.
81///
82/// What this cannot carry, stated rather than discovered: lines written BEFORE `events` exists. Those
83/// live in the kernel's 16 KiB ring, which no syscall exposes to userspace, so `events log` begins at
84/// the moment `events` does. Boot output is serial's job, and always was.
85/// (2026-10-09: the kernel now also keeps a fixed copy of the first 32 KiB ever logged, readable as
86/// `events log boot` through InspectKernel query 27 - CLAUDE.md 11.4, amendment 2026-10-08. The
87/// wrapping 16 KiB ring is still exposed by no syscall.)
88pub const TRACE_OP_LOG: u8 = 6;
89/// Ask for recent log lines: `[7][since:u64 le]`, where `since` is the last line number the caller
90/// has already seen (absent or 0 = everything held). The reply is a 25-byte header - next cursor
91/// (u64), oldest line still held (u64), lines in the window (u64), wrapped (u8) - then the text,
92/// one line per `\n`, owner and text separated by 0x1f. (This said byte 1 was a byte count.)
93pub const TRACE_OP_LOGS: u8 = 7;
94
95/// Bytes of a METRIC's name. Deliberately NOT `PEER_LEN`.
96///
97/// `PEER_LEN` is 12 because the longest SERVICE name that matters is `block-driver`. A metric name is a
98/// different kind of thing and wants more room - `ring.recorded` is already 13 - so reusing the service
99/// bound for it was wrong, and it bit immediately: the sink's own first metric was truncated to
100/// `ring.recorde`. Sizing a field by what it will hold, rather than by what an adjacent field holds, is
101/// the whole fix.
102pub const MET_NAME_LEN: usize = 20;
103
104/// Wire length of one metric: owner[12] + name[20] + value(8).
105///
106/// FIXED, like an event, and for the same reason: no allocation anywhere on the path, and a bound a
107/// reader can read off the source (§26.6.1). A name longer than `MET_NAME_LEN` is still TRUNCATED
108/// rather than refused - a truncated name identifies the sample to a human, where a refused sample is
109/// a hole in the instrument - but the truncation is now REPORTED once by `ServiceContext::metric`,
110/// because a name silently becoming a different name is exactly the quiet loss invariant 12 forbids.
111pub const MET_LEN: usize = PEER_LEN + MET_NAME_LEN + 8;
112
113/// A request was sent and the caller is now awaiting a reply.
114pub const KIND_REQUEST: u8 = 1;
115/// A reply came back.
116pub const KIND_REPLY: u8 = 2;
117/// The call reached its deadline with no reply - the peer is alive but did not answer in time.
118pub const KIND_TIMEOUT: u8 = 3;
119/// The peer's endpoint died while the call was outstanding (`ReplyDead`, §8.6), or the send failed.
120pub const KIND_PEER_LOST: u8 = 4;
121/// The peer is ALIVE and its queue is FULL - congestion, not absence. A distinct kind because
122/// answering it like a lost peer is a real bug this project has already paid for once (`net-stack`
123/// reacquiring a capability that was never stale; see `DeadlineOutcome::QueueFull`).
124pub const KIND_QUEUE_FULL: u8 = 5;
125/// The USER abandoned the wait (`q` at a `ReqOutcome::Aborted` call). Not a failure of anything, and
126/// recorded so a gap in a chain is explained rather than mysterious.
127pub const KIND_ABORTED: u8 = 6;
128
129/// The emitting service's OWN name, as it declared it (`ServiceContext::trace_as`).
130///
131/// # Why the caller is self-declared, and why that costs nothing
132///
133/// A service cannot ask what it is called: there is no name in its context page and no query for it,
134/// because identity is not ambient in this system (3.1). The kernel DOES know - every syscall send
135/// stamps `Message.sender_ep`, the sender's primary endpoint - but that is deliberately
136/// kernel-internal ("never crosses to userspace ... so no ABI change"), and surfacing it would grow
137/// the syscall surface for a diagnostic. It is not worth that.
138///
139/// So the caller says who it is. The objection writes itself - a self-declared name is a claim, not a
140/// fact - and it does not survive contact with what the event already is: a service holding
141/// `ipc_send = ["events"]` can already write any `peer` and any `outcome` it likes, because the whole
142/// event is its testimony. A `caller` field is exactly as trustworthy as the two fields beside it,
143/// which is to say as trustworthy as the service you granted trace authority to. It opens nothing.
144///
145/// Unset (a service that never called `trace_as`) reads as `?` rather than a guess.
146static CALLER: spin_name::Name = spin_name::Name::new();
147
148/// A fixed 12-byte name cell. No lock: a service is single-threaded, and this is written once at
149/// startup and read on every emit.
150pub mod spin_name {
151 use core::sync::atomic::{AtomicU8, Ordering};
152 /// A `PEER_LEN`-byte name, byte-atomic so a torn read is impossible even in principle.
153 pub struct Name {
154 b: [AtomicU8; super::PEER_LEN],
155 }
156 impl Name {
157 #[allow(clippy::declare_interior_mutable_const)]
158 pub const fn new() -> Self {
159 const Z: AtomicU8 = AtomicU8::new(0);
160 Self { b: [Z; super::PEER_LEN] }
161 }
162 pub fn set(&self, s: &str) {
163 let n = s.len().min(super::PEER_LEN);
164 for i in 0..super::PEER_LEN {
165 let v = if i < n { s.as_bytes()[i] } else { 0 };
166 self.b[i].store(v, Ordering::Relaxed);
167 }
168 }
169 pub fn read_into(&self, out: &mut [u8]) {
170 for i in 0..super::PEER_LEN.min(out.len()) {
171 out[i] = self.b[i].load(Ordering::Relaxed);
172 }
173 }
174 pub fn is_set(&self) -> bool { self.b[0].load(Ordering::Relaxed) != 0 }
175 }
176}
177
178/// Where a given PEER's protocol keeps its opcode, when it is not byte 0.
179///
180/// # Why this exists
181///
182/// The trace recorded byte 0 of the request and called it the opcode, because that is what most
183/// protocols do. Two of ours do not, and they are the two you actually see: `shell -> fs` and
184/// `fs -> block-driver` both PREPEND a one-byte correlation tag (added to stop replies being matched
185/// by arrival order, which had `fs` accepting one block's data as another's). So byte 0 was a request
186/// id and the column was showing noise while claiming to show opcodes.
187///
188/// The SDK cannot know this - it is generic across every peer - and the KERNEL certainly cannot, since
189/// it may not interpret a payload at all. The service that owns the protocol can, and that is exactly
190/// where the rest of this design already puts protocol knowledge. So it declares it, once, per peer.
191///
192/// Bounded: four entries, no heap, a linear scan of at most four short names per emit. A fifth
193/// declaration is dropped rather than growing - and dropped LOUDLY is not possible here (this is the
194/// emit path), so the bound is set well above the two entries the system actually needs.
195const OP_AT_MAX: usize = 4;
196static OP_AT_NAMES: [spin_name::Name; OP_AT_MAX] = [
197 spin_name::Name::new(), spin_name::Name::new(),
198 spin_name::Name::new(), spin_name::Name::new(),
199];
200static OP_AT_OFFSETS: [AtomicU32; OP_AT_MAX] = [
201 AtomicU32::new(0), AtomicU32::new(0), AtomicU32::new(0), AtomicU32::new(0),
202];
203
204/// Declare that `peer`'s protocol keeps its opcode at byte `at` of the request.
205pub fn set_op_offset(peer: &str, at: u8) {
206 for i in 0..OP_AT_MAX {
207 if !OP_AT_NAMES[i].is_set() {
208 OP_AT_NAMES[i].set(peer);
209 OP_AT_OFFSETS[i].store(at as u32, Ordering::Relaxed);
210 return;
211 }
212 }
213}
214
215/// The byte of a request to `peer` that carries its opcode. 0 unless declared otherwise.
216pub fn op_offset(peer: &str) -> usize {
217 let mut want = [0u8; PEER_LEN];
218 let n = peer.len().min(PEER_LEN);
219 want[..n].copy_from_slice(&peer.as_bytes()[..n]);
220 for i in 0..OP_AT_MAX {
221 if !OP_AT_NAMES[i].is_set() { break; }
222 let mut have = [0u8; PEER_LEN];
223 OP_AT_NAMES[i].read_into(&mut have);
224 if have == want { return OP_AT_OFFSETS[i].load(Ordering::Relaxed) as usize; }
225 }
226 0
227}
228
229/// Declare the emitting service's own name; see [`CALLER`].
230#[inline]
231pub fn set_caller(name: &str) { CALLER.set(name); }
232
233/// True once this service has declared a name.
234#[inline]
235pub fn caller_set() -> bool { CALLER.is_set() }
236
237/// Whether resolution has been attempted yet. Emission arms itself LAZILY, on the first call, because
238/// a service is handed a context and has no init hook to run in - so there is nowhere to arm it from.
239static RESOLVED: AtomicBool = AtomicBool::new(false);
240/// The cap slot for `events`: `u32::MAX` means "resolved, and this service has no events send cap", which
241/// is the normal case and costs one relaxed load per call forever after.
242static TRACER_SLOT: AtomicU32 = AtomicU32::new(u32::MAX);
243/// Per-service event counter, so a reader can see gaps where events were dropped.
244static SEQ: AtomicU32 = AtomicU32::new(0);
245
246/// True once resolution has run - the fast path's only check on the common (untraced) call.
247#[inline]
248pub fn resolved() -> bool {
249 RESOLVED.load(Ordering::Relaxed)
250}
251
252/// How often a service that has NOT resolved the sink may try again, in emissions. Bounded on purpose
253/// (26.6): a service which genuinely holds no `events` cap pays one short in-memory scan per this many
254/// emissions rather than one on every emission.
255const RESOLVE_RETRY_EVERY: u64 = 64;
256static RESOLVE_TICK: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
257
258/// Record the outcome of resolution. A VALID slot latches for the life of the service; a MISS does
259/// NOT.
260///
261/// It used to latch either way, on the reading that `u32::MAX` means "this service holds no `events`
262/// send cap" - a permanent fact deserving a permanent answer. It is not permanent. It also happens
263/// when the sink is simply mid-restart at the instant of this service's FIRST emission, and chaos
264/// restarts the sink constantly. A service unlucky in that window then returned early on every
265/// subsequent emission forever: it never sent, so it never saw a send failure, so it never reached the
266/// reacquire path that exists precisely to recover from a restarted sink.
267///
268/// The damage would be silent and permanent, invisible except as a missing row - which is the one
269/// thing `msgs.received` exists to rule out, since a service with no row is indistinguishable from a
270/// dead one.
271///
272/// HONESTY ABOUT WHAT THIS DID AND DID NOT FIX. This was written while chasing a T630 run where `fs`
273/// published six metric rows before a chaos storm and none afterwards, and the latch looked like the
274/// cause. It was not: `events` restarted 0.5 s BEFORE `fs` there, so `fs` resolved successfully and
275/// never latched at all. That run was the capless-publisher bug (`metric`/`trace_emit` never ASKING
276/// for a cap) on top of the kernel dropping the peer declaration. This change stands on its own
277/// merits - a permanent answer to a transient condition is wrong however rarely it fires - but it is
278/// not the fix that closed that failure, and saying otherwise would leave the next reader chasing a
279/// cause that was already ruled out.
280#[inline]
281pub fn set_sink_slot(slot: u32) {
282 let prev = TRACER_SLOT.swap(slot, Ordering::Relaxed);
283 RESOLVED.store(slot != u32::MAX, Ordering::Release);
284 if slot != u32::MAX && prev != slot {
285 SINK_CHANGED.store(true, Ordering::Relaxed);
286 }
287}
288
289/// Set when the sink slot changes to a live one - a first resolve, or a reacquire after the sink
290/// restarted. Consumed by the first emission that follows.
291static SINK_CHANGED: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
292
293/// True ONCE after the sink changed, then false until it changes again.
294///
295/// A restarted sink starts EMPTY, and a publisher on an interval does not fill it again until its
296/// next boundary - up to 64 messages for `msgs.received`. For a quiet service that is a long time to
297/// have no row, and a service with no row is indistinguishable from a dead one, which is the single
298/// question this metric answers. 14.3 says a client reacquires AND re-establishes what hung off the
299/// old instance; a counter the new sink has never seen is exactly that.
300///
301/// Cheap on the hot path: a relaxed LOAD on the common (unchanged) call, and the swap only in the
302/// rare tick that follows a restart.
303#[inline]
304pub fn take_sink_changed() -> bool {
305 if !SINK_CHANGED.load(Ordering::Relaxed) {
306 return false;
307 }
308 SINK_CHANGED.swap(false, Ordering::Relaxed)
309}
310
311/// Number of resolution attempts that MISSED. Read only to report a late resolve (see
312/// `take_late_resolve`), so the silent window is measured rather than argued about.
313static RESOLVE_MISSES: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
314
315/// If this service resolved the sink only AFTER one or more misses, return how many and consume the
316/// fact so it is reported exactly once. `None` on the healthy path (resolved first try) and on every
317/// call thereafter, so a normal boot prints nothing.
318///
319/// This exists because the failure it reports was invisible: a service that missed simply stopped
320/// publishing forever, and the only evidence was a row that never appeared. Invariant 12 - a silent
321/// degradation is the bug.
322#[inline]
323pub fn take_late_resolve() -> Option<u64> {
324 let n = RESOLVE_MISSES.swap(0, Ordering::Relaxed);
325 if n == 0 { None } else { Some(n) }
326}
327
328/// Whether the caller should attempt (re)resolution now. False once resolved; after a miss, true only
329/// every `RESOLVE_RETRY_EVERY`th call, so recovery is bounded work rather than a lookup per emission.
330#[inline]
331pub fn should_resolve() -> bool {
332 if RESOLVED.load(Ordering::Relaxed) {
333 return false;
334 }
335 let n = RESOLVE_TICK.fetch_add(1, Ordering::Relaxed);
336 if n > 0 && n % RESOLVE_RETRY_EVERY != 0 {
337 return false;
338 }
339 RESOLVE_MISSES.fetch_add(1, Ordering::Relaxed);
340 true
341}
342
343/// The cap slot to emit on, or `u32::MAX` if this service is not tracing.
344#[inline]
345pub fn sink_slot() -> u32 {
346 TRACER_SLOT.load(Ordering::Relaxed)
347}
348
349#[inline]
350/// Encode one log copy. Text past [`LOG_TEXT_MAX`] bytes is cut HERE, at that byte count and not at
351/// a character boundary, so a clipped copy can end mid-character. (This said the CALLER truncated at
352/// a char boundary; `ServiceContext::log_copy` passes the text through unclipped.)
353pub fn encode_log(text: &[u8], out: &mut [u8; 1 + PEER_LEN + LOG_TEXT_MAX]) -> usize {
354 out[0] = TRACE_OP_LOG;
355 CALLER.read_into(&mut out[1..1 + PEER_LEN]);
356 let n = text.len().min(LOG_TEXT_MAX);
357 out[1 + PEER_LEN..1 + PEER_LEN + n].copy_from_slice(&text[..n]);
358 1 + PEER_LEN + n
359}
360
361/// Longest log text carried in one copy. A longer line still reaches serial in full - only the
362/// queryable copy is clipped - so the loss is bounded and the authoritative record is untouched.
363pub const LOG_TEXT_MAX: usize = 240;
364
365/// Encode one metric sample.
366///
367/// The OWNER is this service's declared name (`trace_as`), exactly as for an event: identity is not
368/// ambient, so the emitter says who it is, and the sample is its own testimony - no more and no less
369/// trustworthy than the value beside it.
370///
371/// No clock read. The sink stamps the sample, for the reason recorded on `trace_emit`: a CMOS RTC read
372/// on every publish would put up to a millisecond of port I/O on the emitting service's path, and that
373/// once cost the shell its keystrokes. An observer must not be able to slow what it observes.
374pub fn encode_metric(name: &str, value: u64) -> [u8; 1 + MET_LEN] {
375 let mut b = [0u8; 1 + MET_LEN];
376 b[0] = TRACE_OP_METRIC;
377 CALLER.read_into(&mut b[1..1 + PEER_LEN]);
378 let n = name.len().min(MET_NAME_LEN);
379 b[1 + PEER_LEN..1 + PEER_LEN + n].copy_from_slice(&name.as_bytes()[..n]);
380 b[1 + PEER_LEN + MET_NAME_LEN..1 + PEER_LEN + MET_NAME_LEN + 8]
381 .copy_from_slice(&value.to_le_bytes());
382 b
383}
384
385/// Build one event payload. Split out from the send so the caller owns the buffer and this stays
386/// allocation-free (§26.6.1 - fixed stack, no heap).
387///
388/// **The timestamp field is left ZERO and stamped by the SINK** - reading a clock here is a CMOS RTC
389/// read on the caller's hot path (see `ServiceContext::trace_emit`), and an observer that costs the
390/// observed a millisecond of port I/O per IPC is not an observer, it is a brake.
391pub fn encode(peer: &str, op: u8, kind: u8) -> [u8; 1 + EV_LEN] {
392 let seq = SEQ.fetch_add(1, Ordering::Relaxed);
393 let mut b = [0u8; 1 + EV_LEN];
394 b[0] = TRACE_OP_EVENT;
395 b[1..5].copy_from_slice(&seq.to_le_bytes());
396 CALLER.read_into(&mut b[9..9 + PEER_LEN]);
397 let n = peer.len().min(PEER_LEN);
398 b[9 + PEER_LEN..9 + PEER_LEN + n].copy_from_slice(&peer.as_bytes()[..n]);
399 b[9 + 2 * PEER_LEN] = op;
400 b[10 + 2 * PEER_LEN] = kind;
401 b
402}