Skip to content
Reading Observations

Reading sidecar observations

The cross-process round-trip used a bare SharedHashMap. To have the sidecar observe a primitive - drain its op stream into InstanceStats - you register it by wrapping it in a SidecarBox. This chapter wraps a map, drives some ops, and shows what the sidecar then saw, in detail.

The Observation record

Each primitive op pushes one Observation to its thread-local ring. The struct is 24 bytes, three per cache line, no straddling:

#[repr(C)]
pub struct Observation {
    pub instance_id: u32,         // who emitted it
    pub op_kind: u16,              // primitive-specific (1, 2, 3, ...)
    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,
}

Note

op_kind is per-primitive. Each primitive defines its own op_kind constants. SharedRing uses ring::OP_PUSH = 1 and ring::OP_POP = 2; SharedHashMap uses hash_map::OP_INSERT = 1, OP_GET = 2, OP_REMOVE = 3, OP_CONTAINS = 4, OP_CLEAR = 5, OP_COMPACT = 6. See subetha_cxc::sidecar_ops for the full enumeration.

Inspecting what the sidecar accumulated

InstanceStats is a snapshot of the drain-and-fold accumulator:

use subetha_cxc::SharedHashMap;
use subetha_sidecar::{global, SidecarBox};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // SidecarBox::new registers the primitive with the global sidecar.
    // A bare SharedHashMap is not observed; the wrapper is what plugs
    // it into the observation pipeline.
    let m = SidecarBox::new(
        SharedHashMap::<u32, u64>::create("/tmp/observed.bin", 1024)?,
    );

    for i in 0..1_000u32 {
        m.insert(i, i as u64)?;   // pushes an OP_INSERT observation
        let _ = m.get(&i);        // pushes an OP_GET observation
    }
    // Force one synchronous scan so the ring drains into stats
    // (the 200 us poll would otherwise pick it up on its own).
    global().scan_now();

    let s = m.stats().unwrap();
    println!("ops_observed          = {}", s.ops_observed);
    println!("contention_rate       = {:.3}", s.contention_rate());
    println!("op_kind_counts        = {:?}", &s.op_kind_counts[..]);
    println!("average_latency_ticks = {}", s.average_latency_ticks());
    println!("total_latency_ticks   = {}", s.total_latency_ticks);
    println!("migrations_triggered  = {}", s.migrations_triggered);
    Ok(())
}

Tip

contention_rate() is the fraction of ops that reported flags & 1 != 0. Primitives set this bit when they took the slow path (e.g. a Mutex lock() that had to park, a CAS that lost the race, a SeqLock reader that retried). A workload-driven Policy reads this number and decides whether to migrate.

The op_kind histogram

op_kind_counts is a fixed-size array indexed by the op_kind field. The sidecar caps it at N_OP_KINDS = 8, so each primitive gets seven distinct op_kinds (1..=7; index 0 is reserved for “unspecified”). The histogram is what a Policy consults to distinguish insert-heavy from get-heavy workloads on the same hash map.

For SharedHashMap:

Indexop_kindWhat it counts
0(reserved)observations with op_kind = 0
1OP_INSERTinsert() calls (flag bit 0 set when Err(Full))
2OP_GETget() calls (flag bit 1 set on None)
3OP_REMOVEremove() calls (flag bit 1 set on None)
4OP_CONTAINScontains_key() calls
5OP_CLEARclear() calls
6OP_COMPACTcompact() calls
7unusedspare slot if the primitive gains a seventh op_kind

The latency aggregates

InstanceStats keeps two latency fields: total_latency_ticks (the running sum across every drained observation) and ops_observed (the divisor). The convenience accessor average_latency_ticks() divides them, returning 0 when ops_observed == 0. There is no percentile state - the sidecar folds latency into a sum-and-count pair so the per-instance stats footprint stays at the fixed-size struct, never growing with op-count.

Important

latency_ticks is raw TSC, not nanoseconds. Convert with the host TSC frequency if you want wall-clock numbers - on Zen+ this is ~3.4 GHz, so 1 ns ≈ 3.4 ticks. Most policies work in ticks directly because they compare ratios, not absolutes.

Tip

Want percentile latency? Track it primitive-side. The observation push site has the raw latency_ticks value in scope; a primitive that needs p50/p99 can keep a small t-digest or reservoir in its own state and expose it via a primitive-specific accessor. The sidecar’s stats struct stays minimal on purpose.

What to do next

That closes the tutorial. From here, the How-To Guides cover task-shaped questions and the Reference pages document every public type. The architecture overview is the right next read if you want to understand the substrate / sidecar / primitives split before reaching for the spec.