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>
impl<T: Marshal + Copy + 'static> AdaptiveIpc<T>
Sourcepub fn create(
base_path: impl Into<PathBuf>,
initial_shape: MmfWorkloadShape,
capacity: usize,
n_consumers: usize,
) -> Result<Self, ApiError>
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.
Sourcepub 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>
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.
Sourcepub fn set_ordering(&self, ordering: QosOrdering) -> Result<(), ApiError>
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):GlobalFifoflips the merge flag ON (OrderingMode::MergeByStamp- the cheap ordered switch, retroactive over the backlog),PerProducerflips it OFF. - Unstamped ring (plain
create):GlobalFifomorphs the ring to the Vyukov shape (the proven global-FIFO structure);PerProducermorphs back to the counts-based composed shape.
Sourcepub fn ordering(&self) -> QosOrdering
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).
Sourcepub fn inversions(&self) -> u64
pub fn inversions(&self) -> u64
Cross-producer inversions the stamped ring has observed (0 for unstamped rings).
Sourcepub fn send(&self, item: &T) -> Result<(), ApiError>
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.
Sourcepub fn send_u64(&self, item: u64) -> Result<(), ApiError>
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.
Sourcepub fn send_batch(&self, items: &[T]) -> Result<(), ApiError>
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.
Sourcepub fn recv(&self) -> Result<T, ApiError>
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.
Sourcepub fn send_blocking(
&self,
item: &T,
timeout: Option<Duration>,
) -> Result<(), ApiError>
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.
Sourcepub fn recv_blocking(&self, timeout: Option<Duration>) -> Result<T, ApiError>
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.
Sourcepub fn send_async(&self, item: &T) -> AdaptiveSendFut<'_, T> ⓘ
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.
Sourcepub fn recv_async(&self) -> AdaptiveRecvFut<'_, T> ⓘ
pub fn recv_async(&self) -> AdaptiveRecvFut<'_, T> ⓘ
Async recv. Resolves to the next item, suspending while empty.
Sourcepub fn profile_snapshot(&self) -> ProfileSnapshot
pub fn profile_snapshot(&self) -> ProfileSnapshot
Read the profile counters (snapshot).
Sourcepub fn active_family(&self) -> MmfFamily
pub fn active_family(&self) -> MmfFamily
Currently active family.
Sourcepub fn migrate_to(&self, target_family: MmfFamily) -> Result<(), ApiError>
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.
Sourcepub fn pin_generation(&self) -> u64
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.
Sourcepub fn ring_handle(&self) -> &AdaptiveRing
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.
Sourcepub fn pin_current_family(&self) -> PinnedIpc<'_, T>
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.
Sourcepub fn maybe_promote(&self) -> Result<Option<MmfFamily>, ApiError>
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:
- Profile counters (
total_sends,batch_sends,batch_size_sum,max_batch_size) for quantitative history. - The
Bloom64shape filter for O(1) qualitative pattern detection (verified 2.92x faster thanHashSetfor this use case, seebenches/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.