ObservationRing + Observation
The observation pipeline is how primitive op-streams reach the
sidecar. Each primitive op pushes one 24-byte Observation to a
thread-local 4096-slot SPSC ring; the sidecar’s scan thread drains
the ring asynchronously into per-instance InstanceStats.
Observation layout
24 bytes, three per cache line, no straddling:
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct Observation {
pub instance_id: u32, // who emitted it
pub op_kind: u16, // primitive-specific (1..=7)
pub flags: u16, // bit 0 contention, bit 1 empty/miss
pub latency_ticks: u64, // raw TSC ticks
pub producer_thread_id: u32, // auto-stamped if 0
pub _reserved: u32, // alignment padding
}
impl Observation {
pub const ZERO: Self = /* all zeros */;
}Note
producer_thread_id is process-local and lazy. thread_id()
returns the same value for every call from the current thread, is
allocated on first call via an atomic counter (no syscalls), and
is never 0 (that value is the “unspecified” sentinel so the
default Observation::ZERO is distinguishable from a real
producer). First thread to call gets id 1.
ObservationRing layout
64-byte aligned, SPSC, 4096 slots:
#[repr(C, align(64))]
pub struct ObservationRing {
head: AtomicU32, // cache line 0 - consumer writes, producer reads
_pad0: [u8; 60],
tail: AtomicU32, // cache line 1 - producer writes, consumer reads
_pad1: [u8; 60],
buf: [UnsafeCell<Observation>; 4096],
}Important
Head and tail live on separate cache lines. The consumer
(sidecar drain thread) is the only writer of head; the
producer (primitive op thread) is the only writer of tail.
Splitting them prevents false-sharing between the two roles.
Push (producer)
pub fn push(&self, mut obs: Observation) -> bool;- Auto-stamps
producer_thread_idif it is0. - Loads
tailrelaxed (producer is the only writer). - Loads
headacquire (synchronises with the consumer’s release on pop). - Checks
(tail + 1) - head > capacity→ ring full, returnfalse(observation dropped silently; sampling, not coordination). - Writes the observation into
buf[tail % capacity]. - Stores
tail + 1release (publishes to the consumer).
Push cost is ~3 cycles steady-state (~2.8 ns on Zen+). Producer never blocks; full-ring observations are dropped.
Pop (consumer)
pub fn pop(&self) -> Option<Observation>;- Loads
headrelaxed (consumer is the only writer). - Loads
tailacquire (synchronises with the producer’s release on push). - Returns
Noneifhead == tail. - Reads
buf[head % capacity], storeshead + 1release.
Single-consumer: the caller must serialise pops. The sidecar’s scan thread is the only caller.
thread_id()
pub fn thread_id() -> u32;Process-local sequential thread id. Stable for the lifetime of the
thread; not valid across forks (the child keeps the parent’s
counter but reissues new ids to its own threads). First thread to
call gets 1.
Test invariants
The unit tests in crates/subetha-core/src/observation.rs assert:
- Push/pop round-trip preserves all fields and auto-stamps
producer_thread_idwhen the caller passes0. - Ring fills exactly at capacity (4096 pushes succeed, the 4097th
returns
false). thread_id()is stable across calls from the same thread.thread_id()is distinct across threads.thread_id()never returns0.pushauto-stamps thread id when the caller passes0.
See also
InstanceStats- what the sidecar accumulates from drained observations.- Sidecar observation pipeline - the end-to-end flow from op push through scan-thread drain to policy decision.