Skip to main content

AdaptiveIpc

Struct AdaptiveIpc 

Source
pub struct AdaptiveIpc<T: Marshal + Copy + 'static> { /* private fields */ }
Expand description

Runtime profile-and-migrate IPC endpoint with zero-overhead hot path. Both possible backings (a SharedRing and a SharedDeque<PassSlot>) are pre-allocated at construction; an AtomicU32 tag selects which is active. Migration is a single Release-store on the MMF-resident control atom.

Implementations§

Source§

impl<T: Marshal + Copy + 'static> AdaptiveIpc<T>

Source

pub fn create( base_path: impl Into<PathBuf>, initial_shape: MmfWorkloadShape, capacity: usize, n_consumers: usize, ) -> Result<Self, ApiError>

Create a new AdaptiveIpc at base_path with an initial workload shape. Both backings (ring + deque) are pre-allocated; the initial shape selects which one is active at start.

Source

pub fn create_with_ordering( base_path: impl Into<PathBuf>, initial_shape: MmfWorkloadShape, capacity: usize, n_consumers: usize, ordering: QosOrdering, auto_order: Option<f64>, ) -> Result<Self, ApiError>

As create with the ordering axis wired in: the inner AdaptiveRing is constructed STAMPED (push stamps plus the cross-process ordering header), the ordering declaration is applied immediately, and auto_order - when set - pre-authorizes the sidecar’s maybe_promote poll to arm the merge flag once the observed inversion rate crosses the threshold (inversions/sec).

The payload cap is unchanged: T::PAYLOAD_BYTES <= 56 was already the AdaptiveIpc contract (the Vyukov backing’s slot size), and the stamped slot leaves the same 56 bytes.

Source

pub fn set_ordering(&self, ordering: QosOrdering) -> Result<(), ApiError>

Apply an ordering declaration at runtime. Routing follows the substrate’s two paths:

  • Stamped ring (constructed via create_with_ordering): GlobalFifo flips the merge flag ON (OrderingMode::MergeByStamp - the cheap ordered switch, retroactive over the backlog), PerProducer flips it OFF.
  • Unstamped ring (plain create): GlobalFifo morphs the ring to the Vyukov shape (the proven global-FIFO structure); PerProducer morphs back to the counts-based composed shape.
Source

pub fn ordering(&self) -> QosOrdering

The ordering guarantee currently provided, derived from the live substrate state (merge flag for stamped rings, shape for unstamped ones).

Source

pub fn inversions(&self) -> u64

Cross-producer inversions the stamped ring has observed (0 for unstamped rings).

Source

pub fn send(&self, item: &T) -> Result<(), ApiError>

Send one item. Hot path: read tag (Acquire on MMF-resident atom, no kernel touch, plain MOV on x86), match-dispatch to pre-allocated concrete backing, push, record send. No Arc clone, no vtable lookup.

Type-specialized fast paths are dispatched automatically at compile time via TypeId::of::<T>() constant comparisons. For T = u64, the branch monomorphizes to a direct call to send_u64, guaranteeing the 8-byte stack-buffer path instead of the generic 56-byte Marshal buffer. The A/B harness (benches/adaptive_send_specialized_ab.rs) measures the two paths within noise on the current toolchain (~1.05x on Zen+ R7 2700: generic 2.01 ms vs specialized 1.92 ms) - LLVM already inlines the generic u64 Marshal path to equivalent code, so the branch’s value is the small-buffer GUARANTEE across toolchains, not a separate measured win. For other T, the branch monomorphizes away to the generic path.

Source

pub fn send_u64(&self, item: u64) -> Result<(), ApiError>

Specialized u64 send fast path. The T: Marshal indirection is eliminated, the payload buffer is exactly 8 bytes (not 56), and the SharedRing / SharedDeque dispatch sees a concrete known-size payload that LLVM can inline directly.

Same wire format as send(&u64_value): receivers see the same 8-byte payload prefix in the slot. Use this when sending homogeneous u64 streams (tokens, sequence numbers, message IDs) where the generic Marshal path is overhead. send itself auto-routes here via a TypeId-monomorphised branch when T = u64, so callers rarely need to name send_u64 directly.

Source

pub fn send_batch(&self, items: &[T]) -> Result<(), ApiError>

Send a batch of items. All-or-nothing: this returns Ok only after every item is in the backing. The implementation re-reads the active tag per item so a migration that lands mid-batch routes the remaining items to the new backing rather than the old; it spins on Full (backpressure) the same way single send callers spin on Err, and propagates any non-Full error immediately.

The atomic-or-spin guarantee is what makes naive caller loops of the form while send_batch(&b).is_err() { spin } safe: without it, partial-success-then-Err returns would prompt the caller to retry the whole batch, double-sending the items that already landed.

Source

pub fn recv(&self) -> Result<T, ApiError>

Receive one item. Drains the KHL side-backing first (batched sends land there), then BOTH ring/deque backings: the inactive (stale) backing first, then the active one.

Source

pub fn send_blocking( &self, item: &T, timeout: Option<Duration>, ) -> Result<(), ApiError>

Blocking send: parks the calling thread until the active backing accepts the item (or timeout elapses). None waits forever.

Source

pub fn recv_blocking(&self, timeout: Option<Duration>) -> Result<T, ApiError>

Blocking recv: parks the calling thread until an item arrives (or timeout elapses). None waits forever.

Source

pub fn send_async(&self, item: &T) -> AdaptiveSendFut<'_, T>

Async send. Resolves once the item is accepted, suspending the task while the active backing is full.

Source

pub fn recv_async(&self) -> AdaptiveRecvFut<'_, T>

Async recv. Resolves to the next item, suspending while empty.

Source

pub fn profile_snapshot(&self) -> ProfileSnapshot

Read the profile counters (snapshot).

Source

pub fn active_family(&self) -> MmfFamily

Currently active family.

Source

pub fn migrate_to(&self, target_family: MmfFamily) -> Result<(), ApiError>

Explicitly migrate to target_family. Both backings are pre-allocated; migration is a single Release-store on the MMF-resident control atom. ZERO kernel touch.

The pin_generation is bumped BEFORE the family-tag store so pinned-handle holders see invalidation on their next is_still_valid() check at or after the migration boundary.

Source

pub fn pin_generation(&self) -> u64

Current pin generation. Pinned handles capture this at pin time; a non-equal current value means the pin is stale and the holder should release + re-acquire via pin_current_family.

Source

pub fn ring_handle(&self) -> &AdaptiveRing

Direct access to the composed AdaptiveRing backing.

The override hatch for callers who want shape-axis control without going through the pin protocol: register additional producers / consumers, call morph_to(RingShape::Vyukov) to lock global-FIFO behavior, attach a separate AdaptiveRingSidecar, etc. The IPC-level family migration continues to work independently on top.

Source

pub fn pin_current_family(&self) -> PinnedIpc<'_, T>

Pin the current family and return a PinnedIpc handle exposing typed access to the active backing.

Hot-path use: call once, then drive ops through as_ring() or as_deque() for as long as is_still_valid() returns true. On false, release this pin and call pin_current_family() again to capture the new family.

Source

pub fn maybe_promote(&self) -> Result<Option<MmfFamily>, ApiError>

Inspect the current profile and migrate to the dispatcher’s preferred family if it differs from the active family.

The decision uses TWO signals in production:

  1. Profile counters (total_sends, batch_sends, batch_size_sum, max_batch_size) for quantitative history.
  2. The Bloom64 shape filter for O(1) qualitative pattern detection (verified 2.92x faster than HashSet for this use case, see benches/bloom_filter_ab.rs).

The Bloom check rejects calls where no batched shape has ever been observed (early exit without re-deriving from counters); when the Bloom says “might-have-been-batched”, the counter-based inference runs.

Trait Implementations§

Source§

impl<T: Marshal + Copy + 'static> Drop for AdaptiveIpc<T>

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

§

impl<T> !Freeze for AdaptiveIpc<T>

§

impl<T> !RefUnwindSafe for AdaptiveIpc<T>

§

impl<T> !UnwindSafe for AdaptiveIpc<T>

§

impl<T> Send for AdaptiveIpc<T>
where PhantomData<T>: Send,

§

impl<T> Sync for AdaptiveIpc<T>
where PhantomData<T>: Sync,

§

impl<T> Unpin for AdaptiveIpc<T>
where PhantomData<T>: Unpin,

§

impl<T> UnsafeUnpin for AdaptiveIpc<T>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.