Skip to main content

subetha_cxc/
adaptive_ipc.rs

1//! `AdaptiveIpc<T>`: runtime profile-and-migrate IPC, kernel-bypass
2//! preserved end-to-end, hot path optimised to ~zero overhead vs
3//! direct dispatch.
4//!
5//! The optimisation pattern (named by an external scheduler-agent
6//! finding and verified locally):
7//!
8//! - **Wrong**: `arc_swap::ArcSwapOption<Arc<dyn MessageTransport>>`
9//!   per slot, hot path = ArcSwap::load_full + Arc clone + vtable
10//!   indirect call. Measured: **+18-30 ns/op vs direct**, +163%.
11//! - **Right**: pre-allocate both possible backings as concrete
12//!   types in the struct, use an `AtomicU32` tag to select which
13//!   is active, dispatch through a static enum-style `match`.
14//!   On x86 TSO the Acquire-load lowers to a plain MOV; the match
15//!   is a `cmp+jmp` that branch-predicts on the rare-migration
16//!   common case; `#[inline]` collapses the wrapper into a direct
17//!   call. Measured: **~0 ns/op vs direct**, statistically
18//!   identical noise.
19//!
20//! The architectural property (kernel-bypass through live family
21//! migration via two MMF backings + MMF-resident control flag)
22//! is preserved. The migration handoff is one `mmap()` for the
23//! new backing at construction (both backings pre-mmap'd) plus a
24//! single `Release`-store on the MMF-resident control atom when
25//! the dispatcher decides to flip. No syscalls on the per-op path.
26//!
27//! Per-op cost on Zen+ R7 2700 (measured by `concurrent_methods`
28//! bench): within noise of `SharedRing::try_push` direct, despite
29//! providing runtime family migration + profile counters.
30
31#![allow(clippy::missing_errors_doc)]
32
33use std::cell::Cell;
34use std::future::Future;
35use std::marker::PhantomData;
36use std::path::{Path, PathBuf};
37use std::pin::Pin;
38use std::sync::Arc;
39use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
40use std::task::{Context, Poll, Waker};
41use std::time::Duration;
42
43use parking_lot::Mutex;
44use subetha_core::Marshal;
45
46use crate::adaptive_ring::{AdaptiveRing, RingShape};
47use crate::api::{map_waker, wait_heal, ApiError};
48use crate::cross_process_waker::{CrossProcessWaker, MAX_WAITERS_DEFAULT};
49use crate::message_transport::{PassSlot, TransportError};
50use crate::mmf_dispatcher::{MmfDispatcher, MmfFamily, MmfWorkloadShape};
51use crate::ordering::{monotonic_nanos, OrderingMode};
52use crate::qos_policy::Ordering as QosOrdering;
53use crate::shared_atomic::{SharedAtomicU32, SharedAtomicU64};
54use crate::shared_deque::SharedDeque;
55use crate::shared_deque_khl::{SharedDequeKhl, Steal as KhlSteal};
56use crate::shared_deque_khpd::{LineItem, KHPD_ITEM_BYTES};
57use crate::shared_ring::PAYLOAD_BYTES;
58
59/// Tag values for the `AtomicU32` active-backing flag.
60const TAG_RING: u32 = 0;
61const TAG_DEQUE: u32 = 1;
62
63/// Profile counters tracked at every send. `AdaptiveIpc::maybe_promote`
64/// inspects these and decides whether to migrate.
65#[derive(Debug, Default, Clone, Copy)]
66pub struct ProfileSnapshot {
67    /// Total individual sends since last reset.
68    pub total_sends: u64,
69    /// Total batched sends since last reset.
70    pub batch_sends: u64,
71    /// Sum of all batch sizes seen.
72    pub batch_size_sum: u64,
73    /// Maximum batch size observed.
74    pub max_batch_size: u64,
75}
76
77impl ProfileSnapshot {
78    /// Average batch size across observed batched calls (1 if none).
79    pub fn avg_batch_size(&self) -> u64 {
80        self.batch_size_sum.checked_div(self.batch_sends).unwrap_or(1)
81    }
82
83    /// Ratio of batched calls to total calls in [0.0, 1.0].
84    pub fn batch_ratio(&self) -> f64 {
85        let total = self.total_sends + self.batch_sends;
86        if total == 0 {
87            0.0
88        } else {
89            self.batch_sends as f64 / total as f64
90        }
91    }
92
93    /// Infer a workload shape from observed patterns.
94    pub fn inferred_shape(&self, n_consumers: usize) -> MmfWorkloadShape {
95        if self.batch_ratio() >= 0.5 || self.max_batch_size >= 8 {
96            MmfWorkloadShape::WorkStealing(
97                crate::dispatch_deque::WorkloadShape {
98                    n_thieves: n_consumers,
99                    batch_size: Some(self.avg_batch_size().max(2) as usize),
100                    wait_idle: false,
101                },
102            )
103        } else {
104            MmfWorkloadShape::StreamingMpmc {
105                n_producers: 1,
106                n_consumers,
107            }
108        }
109    }
110}
111
112/// Runtime profile-and-migrate IPC endpoint with **zero-overhead
113/// hot path**. Both possible backings (a `SharedRing` and a
114/// `SharedDeque<PassSlot>`) are pre-allocated at construction;
115/// an `AtomicU32` tag selects which is active. Migration is a
116/// single Release-store on the MMF-resident control atom.
117pub struct AdaptiveIpc<T: Marshal + Copy + 'static> {
118    /// Cross-process control flag indexing the active backing.
119    /// Lives in its own small MMF so cross-process consumers see
120    /// the same flag.
121    control: Arc<SharedAtomicU32>,
122    /// Cross-process pin generation. Bumped on every successful
123    /// `migrate_to`. Pinned-handle holders capture this value at
124    /// pin time and call `is_still_valid()` to see whether a
125    /// migration has happened. MMF-resident so remote pin holders
126    /// (cross-process consumers) see invalidation through the same
127    /// kernel-bypass channel as the control flag.
128    pin_generation: Arc<SharedAtomicU64>,
129    /// Pre-allocated `AdaptiveRing` backing (used when tag = TAG_RING).
130    /// Composes the shape-axis morph protocol underneath the
131    /// protocol-axis migration: while the IPC family is `SharedRing`,
132    /// this AdaptiveRing morphs across SPSC/MPSC/MPMC/Vyukov shapes
133    /// driven by its own sidecar based on observed peer counts.
134    /// Default-on; callers wanting Vyukov-only behavior can reach
135    /// through `ring_handle()` and `morph_to(RingShape::Vyukov)`.
136    ring: AdaptiveRing,
137    /// Pre-allocated `SharedDeque<PassSlot>` backing (used when tag
138    /// = TAG_DEQUE). The `PassSlot` newtype carries the payload
139    /// bytes in a position-independent layout.
140    deque: SharedDeque<PassSlot>,
141    /// Optional KHL (3-items-per-cache-line) batched-send fast path.
142    /// `Some` only when `T::PAYLOAD_BYTES <= KHPD_ITEM_BYTES` (16),
143    /// since KHL's `LineItem` slot is 16 bytes. A `send_batch` of >= 2
144    /// items routes here at runtime - KHL publishes 3 items per
145    /// Release-store (measured ~3x cheaper per item than the Chase-Lev
146    /// deque on batched producers). Drained by `recv` alongside the
147    /// ring + deque. Independent of the ring<->deque migration tag: it
148    /// is a parallel side-backing, not a migration target.
149    khl: Option<Arc<SharedDequeKhl>>,
150    /// Surplus from a KHL slot steal. `steal_slot` returns up to 3
151    /// `LineItem`s per slot; `recv` yields one item per call, so the
152    /// 0..=2 surplus items wait here for the next `recv`. Shared (any
153    /// consumer drains it), so a stopped consumer never strands items.
154    khl_surplus: Mutex<Vec<LineItem>>,
155    /// Base path stem. Backings live at `{base_path}.ring.bin` +
156    /// `{base_path}.deque.bin`; control flag at `{base_path}.ctl.bin`;
157    /// pin generation at `{base_path}.pingen.bin`.
158    base_path: PathBuf,
159    /// Profile counters as separate atomics so the hot path pays
160    /// exactly ONE `fetch_add` per send.
161    total_sends_atom: AtomicU64,
162    batch_sends_atom: AtomicU64,
163    batch_size_sum_atom: AtomicU64,
164    max_batch_size_atom: AtomicU64,
165    /// Bloom64 of observed workload-shape signatures. Every send /
166    /// send_batch sets the bits derived from its shape signature
167    /// (e.g. (shape_kind, log2_batch_size_bucket)); `maybe_promote`
168    /// consults this for O(1) pattern recognition instead of
169    /// re-deriving from the per-call counters.
170    shape_bloom_atom: AtomicU64,
171    /// Declared consumer count (used by `inferred_shape` when the
172    /// profile suggests a migration).
173    n_consumers: usize,
174    /// Pre-authorized automatic ordering response: when set (via
175    /// [`create_with_ordering`](Self::create_with_ordering)), the
176    /// sidecar's `maybe_promote` poll arms the stamped ring's merge
177    /// flag once the observed inversion rate (inversions/sec)
178    /// crosses this threshold. `None` = report-only.
179    auto_order_threshold: Option<f64>,
180    /// Inversion-rate bookkeeping for the auto-order check.
181    last_inversions_atom: AtomicU64,
182    last_inversion_check_nanos: AtomicU64,
183    /// Producer fires on send; a blocking / awaiting recv waits here.
184    consumer_waker: Arc<CrossProcessWaker>,
185    /// Consumer fires on recv; a blocking / awaiting send waits here.
186    producer_waker: Arc<CrossProcessWaker>,
187    /// The awaiting consumer's `Waker`, fired directly (in-process).
188    recv_slot: Arc<Mutex<Option<Waker>>>,
189    /// The awaiting producer's `Waker`.
190    send_slot: Arc<Mutex<Option<Waker>>>,
191    /// Monotonic send / recv counters: the keys blocking waiters park
192    /// on, backing-independent so they survive a ring<->deque migration.
193    published: AtomicU64,
194    consumed: AtomicU64,
195    /// Set once a recv / send blocks or awaits. Gates the wake signal so
196    /// a pure-sync endpoint pays nothing for the async machinery.
197    has_recv_waiter: AtomicBool,
198    has_send_waiter: AtomicBool,
199    _phantom: PhantomData<T>,
200}
201
202impl<T: Marshal + Copy + 'static> AdaptiveIpc<T> {
203    /// Create a new AdaptiveIpc at `base_path` with an initial
204    /// workload shape. Both backings (ring + deque) are
205    /// pre-allocated; the initial shape selects which one is
206    /// active at start.
207    pub fn create(
208        base_path: impl Into<PathBuf>,
209        initial_shape: MmfWorkloadShape,
210        capacity: usize,
211        n_consumers: usize,
212    ) -> Result<Self, ApiError> {
213        if T::PAYLOAD_BYTES > PAYLOAD_BYTES {
214            return Err(ApiError::PayloadTooLarge);
215        }
216        let base_path: PathBuf = base_path.into();
217        let ctl_path = control_path_for(&base_path);
218        let deque_path = deque_path_for(&base_path);
219        let pingen_path = pingen_path_for(&base_path);
220
221        let control = Arc::new(
222            SharedAtomicU32::create(&ctl_path, 0)
223                .map_err(|e| ApiError::Io(std::io::Error::other(format!("control: {e:?}"))))?,
224        );
225        let pin_generation = Arc::new(
226            SharedAtomicU64::create(&pingen_path, 0)
227                .map_err(|e| ApiError::Io(std::io::Error::other(format!("pingen: {e:?}"))))?,
228        );
229        // Sizing HINT only (the ring grows past it on demand):
230        // pre-allocate one backing per expected consumer so
231        // AdaptiveIpc's single-producer flow plus any callers that
232        // register additional producers through
233        // `ring_handle().register_producer()` start with warm
234        // backings instead of growing on first registration.
235        let max_producers = n_consumers.max(1);
236        let ring_prefix = ring_path_prefix_for(&base_path);
237        let ring = AdaptiveRing::create(
238            &ring_prefix,
239            max_producers,
240            n_consumers.max(1),
241            capacity,
242        )
243        .map_err(|e| ApiError::Io(std::io::Error::other(format!("ring: {e:?}"))))?;
244        // Register one producer + n_consumers consumers so the
245        // shape-axis sidecar sees the right initial peer counts.
246        ring.register_producer()
247            .map_err(|e| ApiError::Io(std::io::Error::other(format!("ring register_producer: {e:?}"))))?;
248        for _ in 0..n_consumers.max(1) {
249            ring.register_consumer()
250                .map_err(|e| ApiError::Io(std::io::Error::other(format!("ring register_consumer: {e:?}"))))?;
251        }
252        let deque = SharedDeque::<PassSlot>::create(&deque_path, capacity)?;
253        // KHL batched fast path, allocated only when the payload fits
254        // KHL's 16-byte LineItem slot. Item capacity = capacity * 3.
255        let khl = if T::PAYLOAD_BYTES <= KHPD_ITEM_BYTES {
256            Some(Arc::new(
257                SharedDequeKhl::create(khl_path_for(&base_path), capacity)
258                    .map_err(|e| ApiError::Io(std::io::Error::other(format!("khl: {e:?}"))))?,
259            ))
260        } else {
261            None
262        };
263
264        let initial_family = MmfDispatcher::pick(initial_shape);
265        let initial_tag = match initial_family {
266            MmfFamily::SharedRing => TAG_RING,
267            MmfFamily::SharedDeque(_) => TAG_DEQUE,
268            MmfFamily::SharedHashMap => {
269                return Err(ApiError::WrongFamily {
270                    wanted: "SharedRing or SharedDeque",
271                    got: initial_family,
272                });
273            }
274        };
275        control.store(initial_tag, Ordering::Release);
276
277        let consumer_waker = Arc::new(
278            CrossProcessWaker::create_anon(MAX_WAITERS_DEFAULT).map_err(|e| {
279                ApiError::Io(std::io::Error::other(format!("consumer waker: {e:?}")))
280            })?,
281        );
282        let producer_waker = Arc::new(
283            CrossProcessWaker::create_anon(MAX_WAITERS_DEFAULT).map_err(|e| {
284                ApiError::Io(std::io::Error::other(format!("producer waker: {e:?}")))
285            })?,
286        );
287
288        Ok(Self {
289            control,
290            pin_generation,
291            ring,
292            deque,
293            khl,
294            khl_surplus: Mutex::new(Vec::new()),
295            base_path,
296            total_sends_atom: AtomicU64::new(0),
297            batch_sends_atom: AtomicU64::new(0),
298            batch_size_sum_atom: AtomicU64::new(0),
299            max_batch_size_atom: AtomicU64::new(0),
300            shape_bloom_atom: AtomicU64::new(0),
301            n_consumers,
302            auto_order_threshold: None,
303            last_inversions_atom: AtomicU64::new(0),
304            last_inversion_check_nanos: AtomicU64::new(monotonic_nanos()),
305            consumer_waker,
306            producer_waker,
307            recv_slot: Arc::new(Mutex::new(None)),
308            send_slot: Arc::new(Mutex::new(None)),
309            published: AtomicU64::new(0),
310            consumed: AtomicU64::new(0),
311            has_recv_waiter: AtomicBool::new(false),
312            has_send_waiter: AtomicBool::new(false),
313            _phantom: PhantomData,
314        })
315    }
316
317    /// As [`create`](Self::create) with the ordering axis wired in:
318    /// the inner `AdaptiveRing` is constructed STAMPED (push stamps
319    /// plus the cross-process ordering header), the `ordering`
320    /// declaration is applied immediately, and `auto_order` - when
321    /// set - pre-authorizes the sidecar's `maybe_promote` poll to
322    /// arm the merge flag once the observed inversion rate crosses
323    /// the threshold (inversions/sec).
324    ///
325    /// The payload cap is unchanged: `T::PAYLOAD_BYTES <= 56` was
326    /// already the `AdaptiveIpc` contract (the Vyukov backing's
327    /// slot size), and the stamped slot leaves the same 56 bytes.
328    pub fn create_with_ordering(
329        base_path: impl Into<PathBuf>,
330        initial_shape: MmfWorkloadShape,
331        capacity: usize,
332        n_consumers: usize,
333        ordering: QosOrdering,
334        auto_order: Option<f64>,
335    ) -> Result<Self, ApiError> {
336        if T::PAYLOAD_BYTES > PAYLOAD_BYTES {
337            return Err(ApiError::PayloadTooLarge);
338        }
339        let base_path: PathBuf = base_path.into();
340        let ctl_path = control_path_for(&base_path);
341        let deque_path = deque_path_for(&base_path);
342        let pingen_path = pingen_path_for(&base_path);
343
344        let control = Arc::new(
345            SharedAtomicU32::create(&ctl_path, 0)
346                .map_err(|e| ApiError::Io(std::io::Error::other(format!("control: {e:?}"))))?,
347        );
348        let pin_generation = Arc::new(
349            SharedAtomicU64::create(&pingen_path, 0)
350                .map_err(|e| ApiError::Io(std::io::Error::other(format!("pingen: {e:?}"))))?,
351        );
352        let max_producers = n_consumers.max(1);
353        let ring_prefix = ring_path_prefix_for(&base_path);
354        let ring = AdaptiveRing::create(
355            &ring_prefix,
356            max_producers,
357            n_consumers.max(1),
358            capacity,
359        )
360        .map_err(|e| ApiError::Io(std::io::Error::other(format!("ring: {e:?}"))))?
361        .with_ordering_stamps()
362        .map_err(|e| ApiError::Io(std::io::Error::other(format!("ordering: {e:?}"))))?;
363        ring.register_producer()
364            .map_err(|e| ApiError::Io(std::io::Error::other(format!("ring register_producer: {e:?}"))))?;
365        for _ in 0..n_consumers.max(1) {
366            ring.register_consumer()
367                .map_err(|e| ApiError::Io(std::io::Error::other(format!("ring register_consumer: {e:?}"))))?;
368        }
369        let deque = SharedDeque::<PassSlot>::create(&deque_path, capacity)?;
370        // KHL batched fast path, allocated only when the payload fits
371        // KHL's 16-byte LineItem slot. Item capacity = capacity * 3.
372        let khl = if T::PAYLOAD_BYTES <= KHPD_ITEM_BYTES {
373            Some(Arc::new(
374                SharedDequeKhl::create(khl_path_for(&base_path), capacity)
375                    .map_err(|e| ApiError::Io(std::io::Error::other(format!("khl: {e:?}"))))?,
376            ))
377        } else {
378            None
379        };
380
381        let initial_family = MmfDispatcher::pick(initial_shape);
382        let initial_tag = match initial_family {
383            MmfFamily::SharedRing => TAG_RING,
384            MmfFamily::SharedDeque(_) => TAG_DEQUE,
385            MmfFamily::SharedHashMap => {
386                return Err(ApiError::WrongFamily {
387                    wanted: "SharedRing or SharedDeque",
388                    got: initial_family,
389                });
390            }
391        };
392        control.store(initial_tag, Ordering::Release);
393
394        let consumer_waker = Arc::new(
395            CrossProcessWaker::create_anon(MAX_WAITERS_DEFAULT).map_err(|e| {
396                ApiError::Io(std::io::Error::other(format!("consumer waker: {e:?}")))
397            })?,
398        );
399        let producer_waker = Arc::new(
400            CrossProcessWaker::create_anon(MAX_WAITERS_DEFAULT).map_err(|e| {
401                ApiError::Io(std::io::Error::other(format!("producer waker: {e:?}")))
402            })?,
403        );
404
405        let ipc = Self {
406            control,
407            pin_generation,
408            ring,
409            deque,
410            khl,
411            khl_surplus: Mutex::new(Vec::new()),
412            base_path,
413            total_sends_atom: AtomicU64::new(0),
414            batch_sends_atom: AtomicU64::new(0),
415            batch_size_sum_atom: AtomicU64::new(0),
416            max_batch_size_atom: AtomicU64::new(0),
417            shape_bloom_atom: AtomicU64::new(0),
418            n_consumers,
419            auto_order_threshold: auto_order,
420            last_inversions_atom: AtomicU64::new(0),
421            last_inversion_check_nanos: AtomicU64::new(monotonic_nanos()),
422            consumer_waker,
423            producer_waker,
424            recv_slot: Arc::new(Mutex::new(None)),
425            send_slot: Arc::new(Mutex::new(None)),
426            published: AtomicU64::new(0),
427            consumed: AtomicU64::new(0),
428            has_recv_waiter: AtomicBool::new(false),
429            has_send_waiter: AtomicBool::new(false),
430            _phantom: PhantomData,
431        };
432        ipc.set_ordering(ordering)?;
433        Ok(ipc)
434    }
435
436    /// Apply an ordering declaration at runtime. Routing follows
437    /// the substrate's two paths:
438    ///
439    /// - **Stamped ring** (constructed via
440    ///   [`create_with_ordering`](Self::create_with_ordering)):
441    ///   `GlobalFifo` flips the merge flag ON
442    ///   (`OrderingMode::MergeByStamp` - the cheap ordered switch,
443    ///   retroactive over the backlog), `PerProducer` flips it OFF.
444    /// - **Unstamped ring** (plain [`create`](Self::create)):
445    ///   `GlobalFifo` morphs the ring to the Vyukov shape (the
446    ///   proven global-FIFO structure); `PerProducer` morphs back
447    ///   to the counts-based composed shape.
448    pub fn set_ordering(&self, ordering: QosOrdering) -> Result<(), ApiError> {
449        if self.ring.is_stamped() {
450            let mode = match ordering {
451                QosOrdering::GlobalFifo => OrderingMode::MergeByStamp,
452                QosOrdering::PerProducer => OrderingMode::Unordered,
453            };
454            self.ring.set_ordering_mode(mode).map_err(map_ring_err)?;
455            return Ok(());
456        }
457        match ordering {
458            QosOrdering::GlobalFifo => {
459                if self.ring.current_shape() != RingShape::Vyukov {
460                    self.ring.morph_to(RingShape::Vyukov).map_err(map_ring_err)?;
461                }
462            }
463            QosOrdering::PerProducer => {
464                // Back to the automatic counts-based shape: undo the
465                // GlobalFifo pin and let the ring re-track its live
466                // peer counts (now and on every future change).
467                self.ring.resume_auto_shape();
468            }
469        }
470        Ok(())
471    }
472
473    /// The ordering guarantee currently provided, derived from the
474    /// live substrate state (merge flag for stamped rings, shape
475    /// for unstamped ones).
476    pub fn ordering(&self) -> QosOrdering {
477        if self.ring.is_stamped() {
478            match self.ring.ordering_mode() {
479                Some(OrderingMode::Unordered) | None => QosOrdering::PerProducer,
480                Some(_) => QosOrdering::GlobalFifo,
481            }
482        } else if self.ring.current_shape() == RingShape::Vyukov {
483            QosOrdering::GlobalFifo
484        } else {
485            QosOrdering::PerProducer
486        }
487    }
488
489    /// Cross-producer inversions the stamped ring has observed
490    /// (0 for unstamped rings).
491    pub fn inversions(&self) -> u64 {
492        self.ring.inversions()
493    }
494
495    /// Send one item. Hot path: read tag (Acquire on MMF-resident
496    /// atom, no kernel touch, plain MOV on x86), match-dispatch to
497    /// pre-allocated concrete backing, push, record send. No Arc
498    /// clone, no vtable lookup.
499    ///
500    /// **Type-specialized fast paths are dispatched automatically at
501    /// compile time** via `TypeId::of::<T>()` constant comparisons.
502    /// For `T = u64`, the branch monomorphizes to a direct call to
503    /// [`send_u64`](Self::send_u64), guaranteeing the 8-byte
504    /// stack-buffer path instead of the generic 56-byte `Marshal`
505    /// buffer. The A/B harness
506    /// (`benches/adaptive_send_specialized_ab.rs`) measures the two
507    /// paths within noise on the current toolchain (~1.05x on Zen+
508    /// R7 2700: generic 2.01 ms vs specialized 1.92 ms) - LLVM
509    /// already inlines the generic `u64` Marshal path to equivalent
510    /// code, so the branch's value is the small-buffer GUARANTEE
511    /// across toolchains, not a separate measured win.
512    /// For other `T`, the branch monomorphizes away to the generic
513    /// path.
514    #[inline]
515    pub fn send(&self, item: &T) -> Result<(), ApiError> {
516        // Compile-time specialization: TypeId::of is a const fn so
517        // this comparison is a known constant in each monomorphization
518        // and LLVM eliminates the dead branch.
519        if core::any::TypeId::of::<T>() == core::any::TypeId::of::<u64>() {
520            // SAFETY: TypeId equality guarantees T is u64; the
521            // transmute reads the same bytes that a `*item: T` read
522            // would. Verified by the type check above.
523            let val: u64 = unsafe { *(item as *const T as *const u64) };
524            return self.send_u64(val);
525        }
526        let tag = self.control.load(Ordering::Acquire);
527        let mut buf = [0u8; PAYLOAD_BYTES];
528        item.marshal(&mut buf[..T::PAYLOAD_BYTES]);
529        match tag {
530            TAG_RING => {
531                self.ring.try_send(0, &buf[..T::PAYLOAD_BYTES])
532                    .map_err(map_ring_err)?;
533            }
534            TAG_DEQUE => {
535                let mut slot = PassSlot([0u8; PAYLOAD_BYTES]);
536                slot.0[..T::PAYLOAD_BYTES]
537                    .copy_from_slice(&buf[..T::PAYLOAD_BYTES]);
538                self.deque.push(&slot)?;
539            }
540            _ => return Err(ApiError::Transport(TransportError::Other)),
541        }
542        self.total_sends_atom.fetch_add(1, Ordering::Relaxed);
543        self.signal_consumer();
544        Ok(())
545    }
546
547    /// Specialized `u64` send fast path. The `T: Marshal` indirection
548    /// is eliminated, the payload buffer is exactly 8 bytes (not 56),
549    /// and the `SharedRing` / `SharedDeque` dispatch sees a concrete
550    /// known-size payload that LLVM can inline directly.
551    ///
552    /// Same wire format as `send(&u64_value)`: receivers see the same
553    /// 8-byte payload prefix in the slot. Use this when sending
554    /// homogeneous u64 streams (tokens, sequence numbers, message
555    /// IDs) where the generic `Marshal` path is overhead. `send`
556    /// itself auto-routes here via a `TypeId`-monomorphised branch
557    /// when `T = u64`, so callers rarely need to name `send_u64`
558    /// directly.
559    #[inline]
560    pub fn send_u64(&self, item: u64) -> Result<(), ApiError> {
561        let tag = self.control.load(Ordering::Acquire);
562        let buf = item.to_le_bytes();
563        match tag {
564            TAG_RING => {
565                self.ring.try_send(0, &buf)
566                    .map_err(map_ring_err)?;
567            }
568            TAG_DEQUE => {
569                let mut slot = PassSlot([0u8; PAYLOAD_BYTES]);
570                slot.0[..8].copy_from_slice(&buf);
571                self.deque.push(&slot)?;
572            }
573            _ => return Err(ApiError::Transport(TransportError::Other)),
574        }
575        self.total_sends_atom.fetch_add(1, Ordering::Relaxed);
576        self.signal_consumer();
577        Ok(())
578    }
579
580    /// Send a batch of items. All-or-nothing: this returns `Ok` only
581    /// after every item is in the backing. The implementation
582    /// re-reads the active tag per item so a migration that lands
583    /// mid-batch routes the remaining items to the new backing
584    /// rather than the old; it spins on `Full` (backpressure) the
585    /// same way single `send` callers spin on `Err`, and propagates
586    /// any non-`Full` error immediately.
587    ///
588    /// The atomic-or-spin guarantee is what makes naive caller loops
589    /// of the form `while send_batch(&b).is_err() { spin }` safe:
590    /// without it, partial-success-then-Err returns would prompt the
591    /// caller to retry the whole batch, double-sending the items
592    /// that already landed.
593    pub fn send_batch(&self, items: &[T]) -> Result<(), ApiError> {
594        if items.is_empty() {
595            return Ok(());
596        }
597        // KHL batched fast path (additive: the per-item path below is
598        // unchanged). A batch of >= 2 items whose payload fits KHL's
599        // 16-byte LineItem (`khl` is `Some` only then) routes to KHL,
600        // which publishes 3 items per Release-store. Items land in the
601        // khl side-backing, drained by `recv` alongside ring + deque.
602        if items.len() >= 2
603            && let Some(khl) = self.khl.as_ref()
604        {
605            self.publish_batch_khl(khl, items)?;
606            self.record_batch_profile(items.len() as u64);
607            return Ok(());
608        }
609        let mut buf = [0u8; PAYLOAD_BYTES];
610        let mut sent = 0usize;
611        while sent < items.len() {
612            items[sent].marshal(&mut buf[..T::PAYLOAD_BYTES]);
613            let tag = self.control.load(Ordering::Acquire);
614            let result: Result<(), ApiError> = match tag {
615                TAG_RING => self
616                    .ring
617                    .try_send(0, &buf[..T::PAYLOAD_BYTES])
618                    .map_err(map_ring_err),
619                TAG_DEQUE => {
620                    let mut slot = PassSlot([0u8; PAYLOAD_BYTES]);
621                    slot.0[..T::PAYLOAD_BYTES]
622                        .copy_from_slice(&buf[..T::PAYLOAD_BYTES]);
623                    self.deque.push(&slot).map_err(ApiError::from)
624                }
625                _ => return Err(ApiError::Transport(TransportError::Other)),
626            };
627            match result {
628                Ok(()) => sent += 1,
629                Err(ApiError::Transport(TransportError::Full)) => {
630                    std::hint::spin_loop();
631                }
632                Err(e) => return Err(e),
633            }
634        }
635        let len = items.len() as u64;
636        self.batch_sends_atom.fetch_add(1, Ordering::Relaxed);
637        self.batch_size_sum_atom.fetch_add(len, Ordering::Relaxed);
638        let mut cur = self.max_batch_size_atom.load(Ordering::Relaxed);
639        while len > cur {
640            match self.max_batch_size_atom.compare_exchange_weak(
641                cur,
642                len,
643                Ordering::Relaxed,
644                Ordering::Relaxed,
645            ) {
646                Ok(_) => break,
647                Err(observed) => cur = observed,
648            }
649        }
650        // Bloom-track the batched shape: kind=1, bucket = log2(len).
651        let log2_bucket = (64u32 - len.leading_zeros()).saturating_sub(1);
652        let mut bloom = subetha_pointers::bloom_pointer::Bloom64(
653            self.shape_bloom_atom.load(Ordering::Relaxed),
654        );
655        bloom.insert(&(1u32, log2_bucket));
656        self.shape_bloom_atom.store(bloom.0, Ordering::Relaxed);
657        Ok(())
658    }
659
660    /// Publish a batch through the KHL side-backing, marshalling each
661    /// item into a 16-byte `LineItem` and publishing one slot
662    /// (`KHL_ITEMS_PER_SLOT` items) per `publish_batch` call from a
663    /// stack array (no allocation). Spins on backpressure (a partial
664    /// publish) exactly as the per-item `send_batch` path spins on
665    /// `Full`.
666    fn publish_batch_khl(&self, khl: &SharedDequeKhl, items: &[T]) -> Result<(), ApiError> {
667        use crate::shared_deque_khl::KHL_ITEMS_PER_SLOT;
668        let mut chunk = [LineItem::default(); KHL_ITEMS_PER_SLOT];
669        let mut i = 0;
670        while i < items.len() {
671            let n = (items.len() - i).min(KHL_ITEMS_PER_SLOT);
672            for j in 0..n {
673                let mut lb = [0u8; KHPD_ITEM_BYTES];
674                items[i + j].marshal(&mut lb[..T::PAYLOAD_BYTES]);
675                chunk[j] = LineItem::new(&lb).map_err(|_| ApiError::PayloadTooLarge)?;
676            }
677            let mut done = 0;
678            while done < n {
679                match khl.publish_batch(&chunk[done..n]) {
680                    Ok(c) => done += c,
681                    Err(_) => return Err(ApiError::Transport(TransportError::Other)),
682                }
683                if done < n {
684                    std::hint::spin_loop();
685                }
686            }
687            i += n;
688        }
689        Ok(())
690    }
691
692    /// Record a batch in the profile counters. Duplicated from the
693    /// per-item `send_batch` tail so the KHL fast path leaves that path
694    /// byte-for-byte unchanged.
695    fn record_batch_profile(&self, len: u64) {
696        self.batch_sends_atom.fetch_add(1, Ordering::Relaxed);
697        self.batch_size_sum_atom.fetch_add(len, Ordering::Relaxed);
698        let mut cur = self.max_batch_size_atom.load(Ordering::Relaxed);
699        while len > cur {
700            match self.max_batch_size_atom.compare_exchange_weak(
701                cur,
702                len,
703                Ordering::Relaxed,
704                Ordering::Relaxed,
705            ) {
706                Ok(_) => break,
707                Err(observed) => cur = observed,
708            }
709        }
710        let log2_bucket = (64u32 - len.leading_zeros()).saturating_sub(1);
711        let mut bloom = subetha_pointers::bloom_pointer::Bloom64(
712            self.shape_bloom_atom.load(Ordering::Relaxed),
713        );
714        bloom.insert(&(1u32, log2_bucket));
715        self.shape_bloom_atom.store(bloom.0, Ordering::Relaxed);
716    }
717
718    /// Drain one item from the KHL side-backing. Returns the buffered
719    /// surplus from a prior multi-item slot steal first; otherwise
720    /// steals one slot (up to `KHL_ITEMS_PER_SLOT` items), returns the
721    /// first, and buffers the rest in the shared surplus so any
722    /// consumer drains them (no stranding). `Retry` is transient and
723    /// retried a bounded number of times before falling through.
724    fn recv_from_khl(&self) -> Result<Option<T>, ApiError> {
725        let Some(khl) = self.khl.as_ref() else {
726            return Ok(None);
727        };
728        {
729            let mut surplus = self.khl_surplus.lock();
730            if let Some(item) = surplus.pop() {
731                return Ok(Some(Self::unmarshal_line(&item)?));
732            }
733        }
734        for _ in 0..4 {
735            match khl.steal_slot() {
736                KhlSteal::Success(res) => {
737                    let n = res.n_items;
738                    if n == 0 {
739                        return Ok(None);
740                    }
741                    if n > 1 {
742                        let mut surplus = self.khl_surplus.lock();
743                        // Push items[1..n] reversed so `pop` yields them
744                        // in producer order.
745                        for k in (1..n).rev() {
746                            surplus.push(res.items[k]);
747                        }
748                    }
749                    return Ok(Some(Self::unmarshal_line(&res.items[0])?));
750                }
751                KhlSteal::Empty => return Ok(None),
752                KhlSteal::Retry => continue,
753            }
754        }
755        Ok(None)
756    }
757
758    #[inline]
759    fn unmarshal_line(item: &LineItem) -> Result<T, ApiError> {
760        let bytes = item.bytes();
761        Ok(T::unmarshal(&bytes[..T::PAYLOAD_BYTES])?)
762    }
763
764    /// Receive one item. Drains the KHL side-backing first (batched
765    /// sends land there), then BOTH ring/deque backings: the inactive
766    /// (stale) backing first, then the active one.
767    #[inline]
768    pub fn recv(&self) -> Result<T, ApiError> {
769        if let Some(v) = self.recv_from_khl()? {
770            self.signal_producer();
771            return Ok(v);
772        }
773        let active = self.control.load(Ordering::Acquire);
774        let stale = if active == TAG_RING { TAG_DEQUE } else { TAG_RING };
775        if let Some(v) = self.try_recv_from(stale)? {
776            self.signal_producer();
777            return Ok(v);
778        }
779        match self.try_recv_from(active)? {
780            Some(v) => {
781                self.signal_producer();
782                Ok(v)
783            }
784            None => Err(ApiError::Transport(TransportError::Empty)),
785        }
786    }
787
788    #[inline]
789    fn try_recv_from(&self, tag: u32) -> Result<Option<T>, ApiError> {
790        // 64-byte buffer: AdaptiveRing's SPSC/MPSC/MPMC backings
791        // expose a 64-byte payload slot (Lamport slot is
792        // payload-only); the Vyukov shape uses 56 bytes. Sizing the
793        // buffer at the larger of the two covers every shape the
794        // AdaptiveRing can morph through. PassSlot for the deque
795        // path is 56 bytes per `PAYLOAD_BYTES`.
796        let mut out = [0u8; crate::adaptive_ring::ADAPTIVE_SPSC_PAYLOAD_BYTES];
797        match tag {
798            TAG_RING => match self.ring.try_recv(0, &mut out) {
799                Ok(n) => Ok(Some(T::unmarshal(&out[..n.min(out.len())])?)),
800                Err(_) => Ok(None),
801            },
802            TAG_DEQUE => match self.deque.steal() {
803                Some(slot) => Ok(Some(T::unmarshal(
804                    &slot.0[..PAYLOAD_BYTES.min(T::PAYLOAD_BYTES.max(1))],
805                )?)),
806                None => Ok(None),
807            },
808            _ => Err(ApiError::Transport(TransportError::Other)),
809        }
810    }
811
812    /// Wake whoever waits to RECEIVE: the awaiting task's `Waker` and
813    /// any thread parked in `recv_blocking`. The `published` counter is
814    /// the park key, advanced on every send regardless of backing.
815    fn signal_consumer(&self) {
816        if !self.has_recv_waiter.load(Ordering::Relaxed) {
817            return;
818        }
819        let n = self.published.fetch_add(1, Ordering::AcqRel) + 1;
820        if let Some(w) = self.recv_slot.lock().take() {
821            w.wake();
822        }
823        self.consumer_waker.wake_up_to(n);
824    }
825
826    /// Wake whoever waits to SEND.
827    fn signal_producer(&self) {
828        if !self.has_send_waiter.load(Ordering::Relaxed) {
829            return;
830        }
831        let n = self.consumed.fetch_add(1, Ordering::AcqRel) + 1;
832        if let Some(w) = self.send_slot.lock().take() {
833            w.wake();
834        }
835        self.producer_waker.wake_up_to(n);
836    }
837
838    /// Blocking send: parks the calling thread until the active backing
839    /// accepts the item (or `timeout` elapses). `None` waits forever.
840    pub fn send_blocking(
841        &self,
842        item: &T,
843        timeout: Option<Duration>,
844    ) -> Result<(), ApiError> {
845        self.has_send_waiter.store(true, Ordering::Relaxed);
846        let deadline = timeout.map(|d| std::time::Instant::now() + d);
847        loop {
848            match self.send(item) {
849                Ok(()) => return Ok(()),
850                Err(ApiError::Transport(TransportError::Full)) => {}
851                Err(e) => return Err(e),
852            }
853            let seen = self.consumed.load(Ordering::Acquire);
854            let token = self.producer_waker.try_park(seen + 1).map_err(map_waker)?;
855            match self.send(item) {
856                Ok(()) => {
857                    self.producer_waker.release(token);
858                    return Ok(());
859                }
860                Err(ApiError::Transport(TransportError::Full)) => {}
861                Err(e) => {
862                    self.producer_waker.release(token);
863                    return Err(e);
864                }
865            }
866            wait_heal(&self.producer_waker, token, deadline)?;
867        }
868    }
869
870    /// Blocking recv: parks the calling thread until an item arrives (or
871    /// `timeout` elapses). `None` waits forever.
872    pub fn recv_blocking(&self, timeout: Option<Duration>) -> Result<T, ApiError> {
873        self.has_recv_waiter.store(true, Ordering::Relaxed);
874        let deadline = timeout.map(|d| std::time::Instant::now() + d);
875        loop {
876            match self.recv() {
877                Ok(v) => return Ok(v),
878                Err(ApiError::Transport(TransportError::Empty)) => {}
879                Err(e) => return Err(e),
880            }
881            let seen = self.published.load(Ordering::Acquire);
882            let token = self.consumer_waker.try_park(seen + 1).map_err(map_waker)?;
883            match self.recv() {
884                Ok(v) => {
885                    self.consumer_waker.release(token);
886                    return Ok(v);
887                }
888                Err(ApiError::Transport(TransportError::Empty)) => {}
889                Err(e) => {
890                    self.consumer_waker.release(token);
891                    return Err(e);
892                }
893            }
894            wait_heal(&self.consumer_waker, token, deadline)?;
895        }
896    }
897
898    /// Async send. Resolves once the item is accepted, suspending the
899    /// task while the active backing is full.
900    pub fn send_async(&self, item: &T) -> AdaptiveSendFut<'_, T> {
901        self.has_send_waiter.store(true, Ordering::Relaxed);
902        AdaptiveSendFut { ipc: self, item: *item }
903    }
904
905    /// Async recv. Resolves to the next item, suspending while empty.
906    pub fn recv_async(&self) -> AdaptiveRecvFut<'_, T> {
907        self.has_recv_waiter.store(true, Ordering::Relaxed);
908        AdaptiveRecvFut { ipc: self }
909    }
910
911    /// Read the profile counters (snapshot).
912    pub fn profile_snapshot(&self) -> ProfileSnapshot {
913        ProfileSnapshot {
914            total_sends: self.total_sends_atom.load(Ordering::Relaxed),
915            batch_sends: self.batch_sends_atom.load(Ordering::Relaxed),
916            batch_size_sum: self.batch_size_sum_atom.load(Ordering::Relaxed),
917            max_batch_size: self.max_batch_size_atom.load(Ordering::Relaxed),
918        }
919    }
920
921    /// Currently active family.
922    pub fn active_family(&self) -> MmfFamily {
923        match self.control.load(Ordering::Acquire) {
924            TAG_RING => MmfFamily::SharedRing,
925            TAG_DEQUE => MmfFamily::SharedDeque(
926                crate::dispatch_deque::DequeVariant::Khl,
927            ),
928            _ => MmfFamily::SharedRing,
929        }
930    }
931
932    /// Explicitly migrate to `target_family`. Both backings are
933    /// pre-allocated; migration is a single Release-store on the
934    /// MMF-resident control atom. ZERO kernel touch.
935    ///
936    /// The pin_generation is bumped BEFORE the family-tag store so
937    /// pinned-handle holders see invalidation on their next
938    /// `is_still_valid()` check at or after the migration boundary.
939    pub fn migrate_to(&self, target_family: MmfFamily) -> Result<(), ApiError> {
940        let new_tag = match target_family {
941            MmfFamily::SharedRing => TAG_RING,
942            MmfFamily::SharedDeque(_) => TAG_DEQUE,
943            MmfFamily::SharedHashMap => {
944                return Err(ApiError::WrongFamily {
945                    wanted: "SharedRing or SharedDeque",
946                    got: target_family,
947                });
948            }
949        };
950        let current_tag = self.control.load(Ordering::Acquire);
951        if current_tag == new_tag {
952            return Ok(());
953        }
954        self.pin_generation.fetch_add(1, Ordering::AcqRel);
955        self.control.store(new_tag, Ordering::Release);
956        Ok(())
957    }
958
959    /// Current pin generation. Pinned handles capture this at pin
960    /// time; a non-equal current value means the pin is stale and
961    /// the holder should release + re-acquire via
962    /// [`pin_current_family`](Self::pin_current_family).
963    pub fn pin_generation(&self) -> u64 {
964        self.pin_generation.load(Ordering::Acquire)
965    }
966
967    /// Direct access to the composed `AdaptiveRing` backing.
968    ///
969    /// The override hatch for callers who want shape-axis control
970    /// without going through the pin protocol: register additional
971    /// producers / consumers, call `morph_to(RingShape::Vyukov)` to
972    /// lock global-FIFO behavior, attach a separate
973    /// `AdaptiveRingSidecar`, etc. The IPC-level family migration
974    /// continues to work independently on top.
975    pub fn ring_handle(&self) -> &AdaptiveRing {
976        &self.ring
977    }
978
979    /// Pin the current family and return a [`PinnedIpc`] handle
980    /// exposing typed access to the active backing.
981    ///
982    /// Hot-path use: call once, then drive ops through `as_ring()`
983    /// or `as_deque()` for as long as `is_still_valid()` returns
984    /// `true`. On `false`, release this pin and call
985    /// `pin_current_family()` again to capture the new family.
986    pub fn pin_current_family(&self) -> PinnedIpc<'_, T> {
987        let captured_gen = self.pin_generation.load(Ordering::Acquire);
988        let tag = self.control.load(Ordering::Acquire);
989        let family = match tag {
990            TAG_RING => MmfFamily::SharedRing,
991            TAG_DEQUE => MmfFamily::SharedDeque(
992                crate::dispatch_deque::DequeVariant::Khl,
993            ),
994            _ => MmfFamily::SharedRing,
995        };
996        PinnedIpc {
997            parent: self,
998            pinned_generation: captured_gen,
999            family,
1000            _not_sync: PhantomData,
1001        }
1002    }
1003
1004    /// Inspect the current profile and migrate to the dispatcher's
1005    /// preferred family if it differs from the active family.
1006    ///
1007    /// The decision uses TWO signals in production:
1008    /// 1. Profile counters (`total_sends`, `batch_sends`,
1009    ///    `batch_size_sum`, `max_batch_size`) for quantitative
1010    ///    history.
1011    /// 2. The `Bloom64` shape filter for O(1) qualitative pattern
1012    ///    detection (verified 2.92x faster than `HashSet` for this
1013    ///    use case, see `benches/bloom_filter_ab.rs`).
1014    ///
1015    /// The Bloom check rejects calls where no batched shape has
1016    /// ever been observed (early exit without re-deriving from
1017    /// counters); when the Bloom says "might-have-been-batched",
1018    /// the counter-based inference runs.
1019    pub fn maybe_promote(&self) -> Result<Option<MmfFamily>, ApiError> {
1020        self.maybe_auto_order();
1021        let snap = self.profile_snapshot();
1022        let total_events = snap.total_sends + snap.batch_sends;
1023        if total_events < 8 {
1024            return Ok(None);
1025        }
1026        // Bloom fast-reject: if we haven't seen any batched shape
1027        // recently, skip the migration analysis entirely.
1028        let bloom = subetha_pointers::bloom_pointer::Bloom64(
1029            self.shape_bloom_atom.load(Ordering::Relaxed),
1030        );
1031        // log2_bucket can be 1..=10 typically for our workloads.
1032        let any_batched = (1..=10).any(|b| {
1033            bloom.might_contain(&(1u32, b))
1034        });
1035        if !any_batched && self.active_family() == MmfFamily::SharedRing {
1036            // No batched shapes observed AND we are already on the
1037            // streaming family - nothing to migrate to.
1038            return Ok(None);
1039        }
1040        let target_shape = snap.inferred_shape(self.n_consumers);
1041        let target_family = MmfDispatcher::pick(target_shape);
1042        let active = self.active_family();
1043        if target_family != active {
1044            self.migrate_to(target_family)?;
1045            return Ok(Some(target_family));
1046        }
1047        Ok(None)
1048    }
1049
1050    /// The pre-authorized automatic ordering response: when an
1051    /// `auto_order` threshold was configured at construction and
1052    /// the stamped ring is still `Unordered`, compute the inversion
1053    /// rate since the previous poll and arm `MergeByStamp` once it
1054    /// crosses the threshold. One-way by design - merged pops read
1055    /// zero inversions, so there is no symmetric disarm signal; the
1056    /// caller disarms via [`set_ordering`](Self::set_ordering).
1057    fn maybe_auto_order(&self) {
1058        let Some(threshold) = self.auto_order_threshold else { return };
1059        if self.ring.ordering_mode() != Some(OrderingMode::Unordered) {
1060            return;
1061        }
1062        let now = monotonic_nanos();
1063        let then = self.last_inversion_check_nanos.swap(now, Ordering::AcqRel);
1064        let inversions = self.ring.inversions();
1065        let last = self.last_inversions_atom.swap(inversions, Ordering::AcqRel);
1066        let elapsed_secs = (now.saturating_sub(then) as f64 / 1e9).max(1e-9);
1067        let rate = inversions.saturating_sub(last) as f64 / elapsed_secs;
1068        if rate > threshold {
1069            self.ring.set_ordering_mode(OrderingMode::MergeByStamp).ok();
1070        }
1071    }
1072}
1073
1074impl<T: Marshal + Copy + 'static> Drop for AdaptiveIpc<T> {
1075    fn drop(&mut self) {
1076        let deque_p = deque_path_for(&self.base_path);
1077        let ctl_p = control_path_for(&self.base_path);
1078        let pingen_p = pingen_path_for(&self.base_path);
1079        std::fs::remove_file(&deque_p).ok();
1080        std::fs::remove_file(&ctl_p).ok();
1081        std::fs::remove_file(&pingen_p).ok();
1082        if self.khl.is_some() {
1083            std::fs::remove_file(khl_path_for(&self.base_path)).ok();
1084        }
1085
1086        // AdaptiveRing's file-backed constructor lays its files out
1087        // as: `{prefix}.spsc.bin`, `{prefix}.mpsc.{i}.bin`,
1088        // `{prefix}.mpmc.{i}.bin`, `{prefix}.vyukov.bin`. Mirror that
1089        // here so cleanup is exhaustive.
1090        let ring_prefix = ring_path_prefix_for(&self.base_path);
1091        let max_producers = self.ring.max_producers();
1092        std::fs::remove_file(with_suffix(&ring_prefix, ".spsc.bin")).ok();
1093        std::fs::remove_file(with_suffix(&ring_prefix, ".vyukov.bin")).ok();
1094        std::fs::remove_file(with_suffix(&ring_prefix, ".ordering.bin")).ok();
1095        for i in 0..max_producers {
1096            std::fs::remove_file(
1097                with_suffix(&ring_prefix, &format!(".mpsc.{i}.bin")),
1098            ).ok();
1099            std::fs::remove_file(
1100                with_suffix(&ring_prefix, &format!(".mpmc.{i}.bin")),
1101            ).ok();
1102        }
1103    }
1104}
1105
1106fn with_suffix(base: &Path, suffix: &str) -> PathBuf {
1107    let mut s = base.as_os_str().to_owned();
1108    s.push(suffix);
1109    PathBuf::from(s)
1110}
1111
1112fn map_ring_err(e: crate::shared_ring::RingError) -> ApiError {
1113    match e {
1114        crate::shared_ring::RingError::Full => {
1115            ApiError::Transport(TransportError::Full)
1116        }
1117        crate::shared_ring::RingError::Empty => {
1118            ApiError::Transport(TransportError::Empty)
1119        }
1120        crate::shared_ring::RingError::PayloadTooLarge => {
1121            ApiError::Transport(TransportError::PayloadTooLarge)
1122        }
1123        _ => ApiError::Transport(TransportError::Other),
1124    }
1125}
1126
1127/// Future from [`AdaptiveIpc::recv_async`].
1128pub struct AdaptiveRecvFut<'a, T: Marshal + Copy + 'static> {
1129    ipc: &'a AdaptiveIpc<T>,
1130}
1131
1132impl<'a, T: Marshal + Copy + 'static> Future for AdaptiveRecvFut<'a, T> {
1133    type Output = Result<T, ApiError>;
1134
1135    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1136        let ipc = self.ipc;
1137        match ipc.recv() {
1138            Ok(v) => return Poll::Ready(Ok(v)),
1139            Err(ApiError::Transport(TransportError::Empty)) => {}
1140            Err(e) => return Poll::Ready(Err(e)),
1141        }
1142        *ipc.recv_slot.lock() = Some(cx.waker().clone());
1143        match ipc.recv() {
1144            Ok(v) => Poll::Ready(Ok(v)),
1145            Err(ApiError::Transport(TransportError::Empty)) => Poll::Pending,
1146            Err(e) => Poll::Ready(Err(e)),
1147        }
1148    }
1149}
1150
1151/// Future from [`AdaptiveIpc::send_async`].
1152pub struct AdaptiveSendFut<'a, T: Marshal + Copy + 'static> {
1153    ipc: &'a AdaptiveIpc<T>,
1154    item: T,
1155}
1156
1157impl<'a, T: Marshal + Copy + 'static> Future for AdaptiveSendFut<'a, T> {
1158    type Output = Result<(), ApiError>;
1159
1160    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1161        // Read-only access through the pin's Deref; the future never
1162        // moves its fields, so no `Unpin` bound on `T` is needed.
1163        match self.ipc.send(&self.item) {
1164            Ok(()) => return Poll::Ready(Ok(())),
1165            Err(ApiError::Transport(TransportError::Full)) => {}
1166            Err(e) => return Poll::Ready(Err(e)),
1167        }
1168        *self.ipc.send_slot.lock() = Some(cx.waker().clone());
1169        match self.ipc.send(&self.item) {
1170            Ok(()) => Poll::Ready(Ok(())),
1171            Err(ApiError::Transport(TransportError::Full)) => Poll::Pending,
1172            Err(e) => Poll::Ready(Err(e)),
1173        }
1174    }
1175}
1176
1177fn control_path_for(base: &Path) -> PathBuf {
1178    let mut p = base.to_path_buf();
1179    let stem = p.file_stem().map(|s| s.to_owned()).unwrap_or_default();
1180    p.set_file_name(format!("{}.ctl.bin", stem.to_string_lossy()));
1181    p
1182}
1183
1184fn ring_path_prefix_for(base: &Path) -> PathBuf {
1185    // Returns the path PREFIX (no `.bin` suffix) that AdaptiveRing's
1186    // file-backed constructor appends its per-shape suffixes to.
1187    // The resulting files are
1188    // `{stem}.ring.spsc.bin` / `{stem}.ring.mpsc.{i}.bin` / etc.
1189    let mut p = base.to_path_buf();
1190    let stem = p.file_stem().map(|s| s.to_owned()).unwrap_or_default();
1191    p.set_file_name(format!("{}.ring", stem.to_string_lossy()));
1192    p
1193}
1194
1195fn khl_path_for(base: &Path) -> PathBuf {
1196    let stem = base.file_stem().map(|s| s.to_owned()).unwrap_or_default();
1197    base.with_file_name(format!("{}.khl.bin", stem.to_string_lossy()))
1198}
1199
1200fn deque_path_for(base: &Path) -> PathBuf {
1201    let mut p = base.to_path_buf();
1202    let stem = p.file_stem().map(|s| s.to_owned()).unwrap_or_default();
1203    p.set_file_name(format!("{}.deque.bin", stem.to_string_lossy()));
1204    p
1205}
1206
1207fn pingen_path_for(base: &Path) -> PathBuf {
1208    let mut p = base.to_path_buf();
1209    let stem = p.file_stem().map(|s| s.to_owned()).unwrap_or_default();
1210    p.set_file_name(format!("{}.pingen.bin", stem.to_string_lossy()));
1211    p
1212}
1213
1214// ===================================================================
1215// PinnedIpc: typed handle pinned to one family of the parent
1216// AdaptiveIpc. Hot path bypasses the runtime dispatch on send() and
1217// exposes the full SharedRing / SharedDeque API surface directly.
1218// ===================================================================
1219
1220/// Handle pinned to one family of the parent [`AdaptiveIpc`].
1221///
1222/// Captures the active family + pin_generation at pin time. Holders
1223/// drive ops through [`as_ring`](Self::as_ring) or
1224/// [`as_deque`](Self::as_deque) for as long as
1225/// [`is_still_valid`](Self::is_still_valid) returns `true`. On
1226/// `false` (a migration has happened) the holder releases this pin
1227/// and calls [`AdaptiveIpc::pin_current_family`] to capture the new
1228/// family.
1229///
1230/// `!Send + !Sync` (via [`PhantomData<Cell<()>>`]): the pin captures
1231/// the active family at pin time and cannot safely cross a thread
1232/// boundary because the parent may migrate concurrently. Each thread
1233/// that wants pinned access acquires its own pin.
1234pub struct PinnedIpc<'a, T: Marshal + Copy + 'static> {
1235    parent: &'a AdaptiveIpc<T>,
1236    pinned_generation: u64,
1237    family: MmfFamily,
1238    _not_sync: PhantomData<Cell<()>>,
1239}
1240
1241impl<'a, T: Marshal + Copy + 'static> PinnedIpc<'a, T> {
1242    /// Family this pin was captured at.
1243    pub fn family(&self) -> MmfFamily { self.family }
1244
1245    /// Pin generation captured at pin time.
1246    pub fn pinned_generation(&self) -> u64 { self.pinned_generation }
1247
1248    /// One Acquire load on the parent's pin_generation. Returns
1249    /// `true` while the pin is current; `false` if a migration has
1250    /// happened and the caller should release + re-acquire.
1251    pub fn is_still_valid(&self) -> bool {
1252        self.parent.pin_generation.load(Ordering::Acquire)
1253            == self.pinned_generation
1254    }
1255
1256    /// Typed handle to the active `AdaptiveRing` backing.
1257    ///
1258    /// Returns `Some(&AdaptiveRing)` when the pinned family is
1259    /// `SharedRing`, `None` otherwise. The composition pattern:
1260    /// chain into `pin_current_shape()` on the returned handle to
1261    /// drop one more axis level and reach the shape-pinned native
1262    /// primitive (`PinnedRing<'_>`), then call e.g.
1263    /// `.spsc_try_push()` for the SPSC fast path. Two Acquire loads
1264    /// total per validity check (one per axis), each at the
1265    /// caller's chosen cadence.
1266    pub fn as_ring(&self) -> Option<&AdaptiveRing> {
1267        match self.family {
1268            MmfFamily::SharedRing => Some(&self.parent.ring),
1269            _ => None,
1270        }
1271    }
1272
1273    /// Typed handle to the active `SharedDeque<PassSlot>` backing.
1274    pub fn as_deque(&self) -> Option<&SharedDeque<PassSlot>> {
1275        match self.family {
1276            MmfFamily::SharedDeque(_) => Some(&self.parent.deque),
1277            _ => None,
1278        }
1279    }
1280}
1281
1282// ===================================================================
1283// AdaptiveIpcSidecar: background thread that drives maybe_promote()
1284// on a timer. Mirrors AdaptiveRingSidecar.
1285// ===================================================================
1286
1287/// Background scanner thread that drives family promotions on an
1288/// [`AdaptiveIpc`] by polling [`AdaptiveIpc::maybe_promote`] on a
1289/// timer.
1290///
1291/// `spawn` starts the thread; `shutdown` stops it cleanly. The
1292/// thread polls every `scan_interval`, calls `maybe_promote()`, and
1293/// counts a promotion when the call returns `Ok(Some(_))`.
1294pub struct AdaptiveIpcSidecar {
1295    handle: Option<std::thread::JoinHandle<()>>,
1296    stop: Arc<std::sync::atomic::AtomicBool>,
1297    promotions_triggered: Arc<AtomicU64>,
1298}
1299
1300impl AdaptiveIpcSidecar {
1301    /// Spawn a sidecar thread that polls `ipc.maybe_promote()` every
1302    /// `scan_interval`. Each successful promotion is counted in
1303    /// `promotions_triggered`.
1304    pub fn spawn<T: Marshal + Copy + Send + Sync + 'static>(
1305        ipc: Arc<AdaptiveIpc<T>>,
1306        scan_interval: std::time::Duration,
1307    ) -> Self {
1308        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
1309        let promotions_triggered = Arc::new(AtomicU64::new(0));
1310
1311        let stop_c = stop.clone();
1312        let promotions_c = promotions_triggered.clone();
1313        let handle = std::thread::spawn(move || {
1314            while !stop_c.load(Ordering::Acquire) {
1315                if let Ok(Some(_)) = ipc.maybe_promote() {
1316                    promotions_c.fetch_add(1, Ordering::Relaxed);
1317                }
1318                std::thread::sleep(scan_interval);
1319            }
1320        });
1321
1322        Self {
1323            handle: Some(handle),
1324            stop,
1325            promotions_triggered,
1326        }
1327    }
1328
1329    /// Number of successful promotions the sidecar has issued since
1330    /// spawn.
1331    pub fn promotions_triggered(&self) -> u64 {
1332        self.promotions_triggered.load(Ordering::Acquire)
1333    }
1334
1335    /// Stop the scanner thread and join it.
1336    pub fn shutdown(mut self) {
1337        self.stop.store(true, Ordering::Release);
1338        if let Some(h) = self.handle.take() {
1339            h.join().expect("sidecar thread panicked");
1340        }
1341    }
1342}
1343
1344impl Drop for AdaptiveIpcSidecar {
1345    fn drop(&mut self) {
1346        self.stop.store(true, Ordering::Release);
1347        if let Some(h) = self.handle.take() {
1348            h.join().ok();
1349        }
1350    }
1351}
1352
1353
1354#[cfg(test)]
1355mod tests {
1356    use super::*;
1357    use crate::dispatch_deque::DequeVariant;
1358
1359    fn tmp(name: &str) -> PathBuf {
1360        let mut p = std::env::temp_dir();
1361        let pid = std::process::id();
1362        let nonce = std::time::SystemTime::now()
1363            .duration_since(std::time::UNIX_EPOCH)
1364            .map(|d| d.as_nanos())
1365            .unwrap_or(0);
1366        p.push(format!("subetha_adaptive_{pid}_{nonce}_{name}"));
1367        p
1368    }
1369
1370    #[test]
1371    fn create_and_send_round_trip_in_initial_family() {
1372        let path = tmp("init");
1373        let shape = MmfWorkloadShape::StreamingMpmc {
1374            n_producers: 1,
1375            n_consumers: 1,
1376        };
1377        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1378            .expect("create");
1379        assert_eq!(ipc.active_family(), MmfFamily::SharedRing);
1380        ipc.send(&111).expect("send");
1381        ipc.send(&222).expect("send");
1382        let a = ipc.recv().expect("recv");
1383        let b = ipc.recv().expect("recv");
1384        assert_eq!(a, 111);
1385        assert_eq!(b, 222);
1386    }
1387
1388    #[test]
1389    fn migrate_to_changes_active_family_kernel_bypass() {
1390        let path = tmp("migrate");
1391        let shape = MmfWorkloadShape::StreamingMpmc {
1392            n_producers: 1,
1393            n_consumers: 1,
1394        };
1395        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1396            .expect("create");
1397        assert_eq!(ipc.active_family(), MmfFamily::SharedRing);
1398        ipc.migrate_to(MmfFamily::SharedDeque(DequeVariant::Khl))
1399            .expect("migrate");
1400        assert_eq!(
1401            ipc.active_family(),
1402            MmfFamily::SharedDeque(DequeVariant::Khl)
1403        );
1404        ipc.send(&333).expect("send post-migrate");
1405        let v = ipc.recv().expect("recv post-migrate");
1406        assert_eq!(v, 333);
1407    }
1408
1409    #[test]
1410    fn maybe_promote_observes_batches_and_migrates_to_work_stealing() {
1411        let path = tmp("auto_promote");
1412        let shape = MmfWorkloadShape::StreamingMpmc {
1413            n_producers: 1,
1414            n_consumers: 1,
1415        };
1416        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 256, 1)
1417            .expect("create");
1418        assert_eq!(ipc.active_family(), MmfFamily::SharedRing);
1419        for _ in 0..10 {
1420            let batch: Vec<u64> = (0..16).collect();
1421            ipc.send_batch(&batch).expect("batch");
1422        }
1423        let snap = ipc.profile_snapshot();
1424        assert!(snap.batch_ratio() > 0.5);
1425        let promoted = ipc.maybe_promote().expect("promote");
1426        assert!(promoted.is_some());
1427        assert!(matches!(
1428            ipc.active_family(),
1429            MmfFamily::SharedDeque(_)
1430        ));
1431    }
1432
1433    #[test]
1434    fn drain_after_migration_reads_from_both_backings() {
1435        let path = tmp("drain");
1436        let shape = MmfWorkloadShape::StreamingMpmc {
1437            n_producers: 1,
1438            n_consumers: 1,
1439        };
1440        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1441            .expect("create");
1442        for i in 0..3u64 {
1443            ipc.send(&i).expect("send pre");
1444        }
1445        ipc.migrate_to(MmfFamily::SharedDeque(DequeVariant::Khl))
1446            .expect("migrate");
1447        for i in 100..103u64 {
1448            ipc.send(&i).expect("send post");
1449        }
1450        let mut seen = Vec::new();
1451        for _ in 0..6 {
1452            let v = ipc.recv().expect("recv");
1453            seen.push(v);
1454        }
1455        assert_eq!(seen.iter().sum::<u64>(), 306);
1456    }
1457
1458    #[test]
1459    fn profile_snapshot_tracks_single_and_batch_sends() {
1460        let path = tmp("profile");
1461        let shape = MmfWorkloadShape::StreamingMpmc {
1462            n_producers: 1,
1463            n_consumers: 1,
1464        };
1465        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 256, 1)
1466            .expect("create");
1467        ipc.send(&1).expect("send");
1468        ipc.send(&2).expect("send");
1469        let batch: Vec<u64> = (0..8).collect();
1470        ipc.send_batch(&batch).expect("batch");
1471        let snap = ipc.profile_snapshot();
1472        assert_eq!(snap.total_sends, 2);
1473        assert_eq!(snap.batch_sends, 1);
1474        assert_eq!(snap.batch_size_sum, 8);
1475        assert_eq!(snap.max_batch_size, 8);
1476    }
1477
1478    // A >16-byte Marshal type: forces `khl = None` (payload exceeds
1479    // KHL's 16-byte LineItem), exercising the payload gate.
1480    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1481    struct Big32([u8; 32]);
1482    unsafe impl Marshal for Big32 {
1483        const PAYLOAD_BYTES: usize = 32;
1484        fn marshal(&self, dst: &mut [u8]) {
1485            dst[..32].copy_from_slice(&self.0);
1486        }
1487        fn unmarshal(src: &[u8]) -> Result<Self, subetha_core::MarshalError> {
1488            if src.len() < 32 {
1489                return Err(subetha_core::MarshalError::ShortBuffer {
1490                    expected: 32,
1491                    got: src.len(),
1492                });
1493            }
1494            let mut b = [0u8; 32];
1495            b.copy_from_slice(&src[..32]);
1496            Ok(Big32(b))
1497        }
1498    }
1499
1500    fn drain_all_u64(ipc: &AdaptiveIpc<u64>, n: usize) -> Vec<u64> {
1501        let mut got = Vec::with_capacity(n);
1502        let mut spins = 0u64;
1503        while got.len() < n {
1504            match ipc.recv() {
1505                Ok(v) => got.push(v),
1506                Err(_) => {
1507                    spins += 1;
1508                    assert!(spins < 200_000_000, "recv stalled before draining all items");
1509                    std::hint::spin_loop();
1510                }
1511            }
1512        }
1513        got
1514    }
1515
1516    #[test]
1517    fn khl_batch_send_round_trips_through_side_backing() {
1518        let path = tmp("khl_rt");
1519        let shape = MmfWorkloadShape::StreamingMpmc { n_producers: 1, n_consumers: 1 };
1520        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 256, 1).expect("create");
1521        assert!(ipc.khl.is_some(), "u64 (8 bytes) fits KHL's 16-byte slot");
1522        let batch: Vec<u64> = (0..6).collect();
1523        ipc.send_batch(&batch).expect("batch");
1524        let mut got = drain_all_u64(&ipc, 6);
1525        got.sort_unstable();
1526        assert_eq!(got, batch, "every batched item received exactly once");
1527    }
1528
1529    #[test]
1530    fn khl_surplus_buffers_partial_slots() {
1531        // 7 items pack into KHL slots of 3 + 3 + 1, so recv drains the
1532        // 0..=2 surplus from the shared buffer across calls.
1533        let path = tmp("khl_surplus");
1534        let shape = MmfWorkloadShape::StreamingMpmc { n_producers: 1, n_consumers: 1 };
1535        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1).expect("create");
1536        let batch: Vec<u64> = (10..17).collect();
1537        ipc.send_batch(&batch).expect("batch");
1538        let mut got = drain_all_u64(&ipc, 7);
1539        got.sort_unstable();
1540        assert_eq!(got, batch);
1541    }
1542
1543    #[test]
1544    fn khl_mixed_single_and_batch_all_received() {
1545        // Singles route to ring/deque; the batch routes to khl. recv
1546        // drains all sources.
1547        let path = tmp("khl_mixed");
1548        let shape = MmfWorkloadShape::StreamingMpmc { n_producers: 1, n_consumers: 1 };
1549        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 256, 1).expect("create");
1550        ipc.send(&1).expect("single");
1551        ipc.send(&2).expect("single");
1552        let batch: Vec<u64> = (100..108).collect();
1553        ipc.send_batch(&batch).expect("batch");
1554        let mut expected: Vec<u64> = vec![1, 2];
1555        expected.extend(&batch);
1556        expected.sort_unstable();
1557        let mut got = drain_all_u64(&ipc, expected.len());
1558        got.sort_unstable();
1559        assert_eq!(got, expected);
1560    }
1561
1562    #[test]
1563    fn khl_large_batch_integrity() {
1564        let path = tmp("khl_large");
1565        let shape = MmfWorkloadShape::StreamingMpmc { n_producers: 1, n_consumers: 1 };
1566        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 512, 1).expect("create");
1567        let batch: Vec<u64> = (0..300).collect();
1568        ipc.send_batch(&batch).expect("batch");
1569        let mut got = drain_all_u64(&ipc, 300);
1570        got.sort_unstable();
1571        assert_eq!(got, batch);
1572    }
1573
1574    #[test]
1575    fn khl_payload_gate_large_type_uses_no_khl() {
1576        let path = tmp("khl_gate");
1577        let shape = MmfWorkloadShape::StreamingMpmc { n_producers: 1, n_consumers: 1 };
1578        let ipc: AdaptiveIpc<Big32> = AdaptiveIpc::create(&path, shape, 64, 1).expect("create");
1579        assert!(ipc.khl.is_none(), "32-byte payload exceeds KHL's 16-byte slot");
1580        let batch: Vec<Big32> = (0..5u8).map(|i| Big32([i; 32])).collect();
1581        ipc.send_batch(&batch).expect("batch via existing per-item path");
1582        let mut got = Vec::new();
1583        let mut spins = 0u64;
1584        while got.len() < 5 {
1585            match ipc.recv() {
1586                Ok(v) => got.push(v),
1587                Err(_) => {
1588                    spins += 1;
1589                    assert!(spins < 200_000_000, "recv stalled");
1590                    std::hint::spin_loop();
1591                }
1592            }
1593        }
1594        got.sort_by_key(|b| b.0[0]);
1595        assert_eq!(got, batch, ">16-byte batch round-trips via the per-item path");
1596    }
1597
1598    #[test]
1599    fn khl_multi_consumer_no_loss_or_dup() {
1600        use std::sync::atomic::{AtomicU64, Ordering as AOrd};
1601        let path = tmp("khl_multi");
1602        let shape = MmfWorkloadShape::StreamingMpmc { n_producers: 1, n_consumers: 4 };
1603        let ipc = Arc::new(AdaptiveIpc::<u64>::create(&path, shape, 512, 4).expect("create"));
1604        const N: u64 = 600;
1605        let batch: Vec<u64> = (0..N).collect();
1606        ipc.send_batch(&batch).expect("batch");
1607        let received = Arc::new(AtomicU64::new(0));
1608        let checksum = Arc::new(AtomicU64::new(0));
1609        let mut handles = Vec::new();
1610        for _ in 0..4 {
1611            let ipc = Arc::clone(&ipc);
1612            let received = Arc::clone(&received);
1613            let checksum = Arc::clone(&checksum);
1614            handles.push(std::thread::spawn(move || loop {
1615                if received.load(AOrd::Acquire) >= N {
1616                    break;
1617                }
1618                match ipc.recv() {
1619                    Ok(v) => {
1620                        checksum.fetch_add(v, AOrd::AcqRel);
1621                        received.fetch_add(1, AOrd::AcqRel);
1622                    }
1623                    Err(_) => std::hint::spin_loop(),
1624                }
1625            }));
1626        }
1627        for h in handles {
1628            h.join().unwrap();
1629        }
1630        assert_eq!(received.load(AOrd::Acquire), N, "exactly N items received");
1631        assert_eq!(
1632            checksum.load(AOrd::Acquire),
1633            (0..N).sum::<u64>(),
1634            "every item exactly once, none lost or duplicated"
1635        );
1636    }
1637
1638    #[test]
1639    fn rejects_kv_map_family_at_construction() {
1640        let path = tmp("reject_kv");
1641        let shape = MmfWorkloadShape::KeyValueLookup {
1642            n_readers: 1,
1643            n_writers: 1,
1644        };
1645        let result = AdaptiveIpc::<u64>::create(&path, shape, 64, 1);
1646        match result {
1647            Err(ApiError::WrongFamily { .. }) => {}
1648            Err(other) => panic!("expected WrongFamily, got {other:?}"),
1649            Ok(_) => panic!("expected error, got Ok"),
1650        }
1651    }
1652
1653    #[test]
1654    fn pin_captures_family_and_generation() {
1655        let path = tmp("pin_capture");
1656        let shape = MmfWorkloadShape::StreamingMpmc {
1657            n_producers: 1,
1658            n_consumers: 1,
1659        };
1660        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1661            .expect("create");
1662        let gen_before = ipc.pin_generation();
1663        let pin = ipc.pin_current_family();
1664        assert_eq!(pin.family(), MmfFamily::SharedRing);
1665        assert_eq!(pin.pinned_generation(), gen_before);
1666        assert!(pin.is_still_valid());
1667    }
1668
1669    #[test]
1670    fn migration_invalidates_outstanding_pin() {
1671        let path = tmp("pin_invalidate");
1672        let shape = MmfWorkloadShape::StreamingMpmc {
1673            n_producers: 1,
1674            n_consumers: 1,
1675        };
1676        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1677            .expect("create");
1678        let pin = ipc.pin_current_family();
1679        assert!(pin.is_still_valid());
1680        ipc.migrate_to(MmfFamily::SharedDeque(DequeVariant::Khl))
1681            .expect("migrate");
1682        assert!(!pin.is_still_valid(),
1683                "pin must invalidate on migration");
1684        let pin2 = ipc.pin_current_family();
1685        assert_eq!(pin2.family(), MmfFamily::SharedDeque(DequeVariant::Khl));
1686        assert!(pin2.is_still_valid());
1687    }
1688
1689    #[test]
1690    fn migrate_to_same_family_does_not_bump_generation() {
1691        let path = tmp("pin_noop");
1692        let shape = MmfWorkloadShape::StreamingMpmc {
1693            n_producers: 1,
1694            n_consumers: 1,
1695        };
1696        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1697            .expect("create");
1698        let pin = ipc.pin_current_family();
1699        let gen_before = pin.pinned_generation();
1700        ipc.migrate_to(MmfFamily::SharedRing).expect("noop migrate");
1701        assert_eq!(ipc.pin_generation(), gen_before,
1702                   "no-op migrate must not bump generation");
1703        assert!(pin.is_still_valid(),
1704                "no-op migrate must not invalidate pin");
1705    }
1706
1707    #[test]
1708    fn pinned_as_ring_round_trip() {
1709        let path = tmp("pin_ring_rt");
1710        let shape = MmfWorkloadShape::StreamingMpmc {
1711            n_producers: 1,
1712            n_consumers: 1,
1713        };
1714        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1715            .expect("create");
1716        let pin = ipc.pin_current_family();
1717        let ring = pin.as_ring().expect("pinned at ring family");
1718        assert!(pin.as_deque().is_none(),
1719                "as_deque must return None when pinned at ring family");
1720
1721        // Exercise the composition pattern: chain protocol-axis pin
1722        // (PinnedIpc::as_ring) into the shape-axis pin
1723        // (AdaptiveRing::pin_current_shape) and reach the native
1724        // SPSC primitive through the shape-pinned handle. The
1725        // initial shape of a 1P/1C registration is SPSC.
1726        let shape_pin = ring.pin_current_shape();
1727        assert_eq!(shape_pin.shape(), crate::RingShape::Spsc);
1728        assert!(shape_pin.is_still_valid());
1729
1730        let payload = 0xDEADBEEFu64.to_le_bytes();
1731        shape_pin.spsc_try_push(&payload).expect("native SPSC push");
1732        let mut buf = [0u8; crate::adaptive_ring::ADAPTIVE_SPSC_PAYLOAD_BYTES];
1733        let n = shape_pin.spsc_try_pop(&mut buf).expect("native SPSC pop");
1734        assert!(n >= 8);
1735        assert_eq!(&buf[..8], &payload);
1736    }
1737
1738    #[test]
1739    fn pinned_as_deque_round_trip() {
1740        let path = tmp("pin_deque_rt");
1741        let shape = MmfWorkloadShape::WorkStealing(
1742            crate::dispatch_deque::WorkloadShape {
1743                n_thieves: 1,
1744                batch_size: Some(4),
1745                wait_idle: false,
1746            },
1747        );
1748        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1749            .expect("create");
1750        let pin = ipc.pin_current_family();
1751        let deque = pin.as_deque().expect("pinned at deque family");
1752        assert!(pin.as_ring().is_none(),
1753                "as_ring must return None when pinned at deque family");
1754
1755        let mut slot = PassSlot([0u8; PAYLOAD_BYTES]);
1756        slot.0[..8].copy_from_slice(&7777u64.to_le_bytes());
1757        deque.push(&slot).expect("native push");
1758        let popped = deque.steal().expect("native steal");
1759        let val = u64::from_le_bytes(popped.0[..8].try_into().unwrap());
1760        assert_eq!(val, 7777);
1761    }
1762
1763    #[test]
1764    fn create_with_ordering_applies_declaration_and_round_trips() {
1765        let path = tmp("ordering_create");
1766        let shape = MmfWorkloadShape::StreamingMpmc {
1767            n_producers: 1,
1768            n_consumers: 1,
1769        };
1770        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create_with_ordering(
1771            &path, shape, 64, 1, QosOrdering::GlobalFifo, None,
1772        ).expect("create");
1773        assert!(ipc.ring_handle().is_stamped());
1774        assert_eq!(ipc.ordering(), QosOrdering::GlobalFifo);
1775        assert_eq!(ipc.ring_handle().ordering_mode(),
1776                   Some(OrderingMode::MergeByStamp));
1777
1778        // The stamp layer is transparent at the typed API surface.
1779        ipc.send(&777).expect("send");
1780        ipc.send(&888).expect("send");
1781        assert_eq!(ipc.recv().expect("recv"), 777);
1782        assert_eq!(ipc.recv().expect("recv"), 888);
1783
1784        // Runtime withdrawal flips the merge flag off.
1785        ipc.set_ordering(QosOrdering::PerProducer).expect("withdraw");
1786        assert_eq!(ipc.ordering(), QosOrdering::PerProducer);
1787        assert_eq!(ipc.ring_handle().ordering_mode(),
1788                   Some(OrderingMode::Unordered));
1789    }
1790
1791    #[test]
1792    fn set_ordering_on_unstamped_ring_routes_through_vyukov_morph() {
1793        let path = tmp("ordering_unstamped");
1794        let shape = MmfWorkloadShape::StreamingMpmc {
1795            n_producers: 1,
1796            n_consumers: 1,
1797        };
1798        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1799            .expect("create");
1800        assert!(!ipc.ring_handle().is_stamped());
1801        assert_eq!(ipc.ordering(), QosOrdering::PerProducer);
1802
1803        ipc.set_ordering(QosOrdering::GlobalFifo).expect("declare");
1804        assert_eq!(ipc.ring_handle().current_shape(), RingShape::Vyukov,
1805                   "unstamped GlobalFifo declaration must morph to Vyukov");
1806        assert_eq!(ipc.ordering(), QosOrdering::GlobalFifo);
1807        ipc.send(&5).expect("send through Vyukov");
1808        assert_eq!(ipc.recv().expect("recv"), 5);
1809
1810        ipc.set_ordering(QosOrdering::PerProducer).expect("withdraw");
1811        assert_ne!(ipc.ring_handle().current_shape(), RingShape::Vyukov,
1812                   "withdrawal must walk the Vyukov morph back");
1813    }
1814
1815    #[test]
1816    fn auto_order_arms_merge_on_observed_inversion_rate() {
1817        let path = tmp("auto_order");
1818        let shape = MmfWorkloadShape::StreamingMpmc {
1819            n_producers: 1,
1820            n_consumers: 2,
1821        };
1822        let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create_with_ordering(
1823            &path, shape, 64, 2, QosOrdering::PerProducer, Some(1.0),
1824        ).expect("create");
1825        let ring = ipc.ring_handle();
1826        assert_eq!(ring.ordering_mode(), Some(OrderingMode::Unordered));
1827
1828        // Manufacture a cross-producer inversion: a second producer
1829        // pushes first (older stamp in ring 1), then producer 0, and
1830        // the round-robin drain pops newer-then-older.
1831        ring.morph_to(crate::RingShape::Mpsc).expect("morph");
1832        ring.register_producer().expect("p1");
1833        ring.try_send(1, &1u64.to_le_bytes()).expect("send p1");
1834        ring.try_send(0, &2u64.to_le_bytes()).expect("send p0");
1835        let mut out = [0u8; crate::ordering::STAMPED_PAYLOAD_BYTES];
1836        ring.try_recv(0, &mut out).expect("pop 1");
1837        ring.try_recv(0, &mut out).expect("pop 2");
1838        assert!(ring.inversions() >= 1, "interleave must register an inversion");
1839
1840        // The pre-authorized response: maybe_promote's auto-order
1841        // check sees the rate spike and arms the merge.
1842        ipc.maybe_promote().expect("promote poll");
1843        assert_eq!(ring.ordering_mode(), Some(OrderingMode::MergeByStamp),
1844                   "auto_order threshold crossing must arm MergeByStamp");
1845        assert_eq!(ipc.ordering(), QosOrdering::GlobalFifo);
1846    }
1847
1848    #[test]
1849    fn sidecar_auto_promotes_on_observed_batches() {
1850        let path = tmp("sidecar_promote");
1851        let shape = MmfWorkloadShape::StreamingMpmc {
1852            n_producers: 1,
1853            n_consumers: 1,
1854        };
1855        let ipc = Arc::new(
1856            AdaptiveIpc::<u64>::create(&path, shape, 256, 1)
1857                .expect("create"),
1858        );
1859        assert_eq!(ipc.active_family(), MmfFamily::SharedRing);
1860        let sidecar = AdaptiveIpcSidecar::spawn(
1861            ipc.clone(),
1862            std::time::Duration::from_millis(5),
1863        );
1864
1865        // Drive a batched workload pattern the policy promotes on.
1866        for _ in 0..10 {
1867            let batch: Vec<u64> = (0..16).collect();
1868            ipc.send_batch(&batch).expect("batch");
1869        }
1870        // Drain a few items so the deque path stays usable.
1871        for _ in 0..16 { ipc.recv().ok(); }
1872
1873        // Give the sidecar a window to scan + promote.
1874        let deadline = std::time::Instant::now()
1875            + std::time::Duration::from_secs(2);
1876        while std::time::Instant::now() < deadline
1877            && !matches!(ipc.active_family(), MmfFamily::SharedDeque(_))
1878        {
1879            std::thread::sleep(std::time::Duration::from_millis(10));
1880        }
1881
1882        assert!(matches!(ipc.active_family(), MmfFamily::SharedDeque(_)),
1883                "sidecar should have promoted to SharedDeque");
1884        assert!(sidecar.promotions_triggered() >= 1,
1885                "sidecar should have recorded at least one promotion");
1886        sidecar.shutdown();
1887    }
1888}