subetha_cxc/shared_deque_urd.rs
1//! `SharedDequeUrd` - UMWAIT Rendezvous Deque, MMF-backed.
2//!
3//! Per-thief mailbox cache lines instead of a shared deque. Each
4//! mailbox is one 64-byte line carrying state (8 B) +
5//! `MAILBOX_ITEMS = 3` line items (48 B) + 8 B trailing padding.
6//! The owner picks a mailbox by round-robin (or by an explicit
7//! target index) and writes the items in; the addressed thief
8//! observes the cache-line transition and reads its items. There is
9//! **no shared head/tail counter on the steal path** - each thief
10//! has its own state byte and never CASes a contended atomic.
11//!
12//! # Wait strategy: runtime dispatch on WAITPKG
13//!
14//! Thieves idle on their mailbox's state byte. The wait primitive
15//! is chosen at runtime by [`subetha_core::has_waitpkg`]:
16//!
17//! - **WAITPKG available** (Intel Tremont / Tiger Lake+ and
18//! AMD Zen 5+): thief uses `UMONITOR` + `UMWAIT` to halt until
19//! the cache line transitions OR a TSC deadline fires.
20//! Power-efficient; the thief does not burn pipeline slots
21//! polling.
22//! - **WAITPKG not available** (most pre-2020 silicon including
23//! AMD Zen+/2/3/4): thief uses [`std::hint::spin_loop`] (`PAUSE`
24//! on x86) in a tight Acquire-load loop on the state byte.
25//!
26//! Both branches end the wait when `state` carries the ready bit
27//! for the expected epoch.
28//!
29//! # Why this shape vs `SharedDeque` / `SharedDequeKhpd`
30//!
31//! `SharedDeque` (Chase-Lev) and `SharedDequeKhpd` are *pull-based*:
32//! thieves CAS the deque to discover work. URD is *push-based*: the
33//! owner picks the target thief by writing its mailbox. Two
34//! architectural consequences:
35//!
36//! 1. **No CAS contention at the steal site.** N thieves on a
37//! shared deque CAS the same head; under contention each push
38//! racing N thieves can take O(N) failed CASes. URD's per-thief
39//! mailbox has zero contention because the owner is the only
40//! writer and the thief is the only reader.
41//! 2. **Owner-controlled distribution.** Round-robin /
42//! locality-aware / variance-driven targeting is the owner's
43//! choice, not a thief's victim-pick. The orchestrator gets
44//! explicit say in which thief does what.
45//!
46//! Trade-offs: the thief is bound to one mailbox (no work-stealing
47//! between mailboxes), the owner pays one mailbox-spin per publish
48//! if the previous batch has not been consumed yet, and the
49//! per-mailbox slot count is fixed at [`MAILBOX_ITEMS`].
50
51#![allow(clippy::missing_errors_doc)]
52
53use std::fs::{File, OpenOptions};
54use std::io;
55use std::path::Path;
56use std::sync::atomic::{AtomicU64, Ordering};
57
58use memmap2::{MmapMut, MmapOptions};
59
60use crate::shared_deque_khpd::LineItem;
61use subetha_core::{has_movdir64b, has_waitpkg};
62
63/// Magic byte sequence marking a valid URD file. ASCII 'WURD' + ver.
64pub const URD_MAGIC: u64 = 0x5755_5244_0000_0001;
65
66/// Cache-line size; one mailbox per cache line.
67pub const URD_MAILBOX_SIZE: usize = 64;
68
69/// Items per mailbox: state (8 B) + 3 * 16 = 56 B; 8 B trailing pad.
70pub const MAILBOX_ITEMS: usize = 3;
71
72/// Wait strategy chosen at runtime per CPUID.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum WaitStrategy {
75 /// `std::hint::spin_loop` (`PAUSE` on x86). Universally
76 /// available fallback.
77 PauseSpin,
78 /// `UMONITOR` + `UMWAIT`. Available on Intel Tremont /
79 /// Tiger Lake+ and AMD Zen 5+; detected via CPUID leaf 7 ECX
80 /// bit 5.
81 Waitpkg,
82}
83
84impl WaitStrategy {
85 /// Returns the best wait strategy for this host.
86 pub fn pick() -> Self {
87 if has_waitpkg() {
88 Self::Waitpkg
89 } else {
90 Self::PauseSpin
91 }
92 }
93}
94
95/// Publish strategy chosen at runtime per CPUID.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum PublishStrategy {
98 /// Byte-by-byte store path: the publisher writes items into the
99 /// mailbox slots through normal cached stores, then Release-
100 /// stores the state word to READY. Universally available.
101 Scalar,
102 /// `MOVDIR64B` path: the publisher builds a 64-byte source line
103 /// containing the new state plus all items, then issues one
104 /// `MOVDIR64B` instruction that atomically writes the entire
105 /// 64-byte mailbox cache line as a Write-Combining store. On
106 /// cross-CCX delivery the line is written directly to LLC,
107 /// eliminating the M-state coherence transfer the byte-by-byte
108 /// path pays. Available on Intel Tremont (2019) / Tiger Lake
109 /// (2020) and later Intel cores and AMD Zen 5 (2024) and later
110 /// AMD cores; detected via CPUID leaf 7 ECX bit 28.
111 Movdir64b,
112}
113
114impl PublishStrategy {
115 /// Returns the best publish strategy for this host.
116 pub fn pick() -> Self {
117 if has_movdir64b() {
118 Self::Movdir64b
119 } else {
120 Self::Scalar
121 }
122 }
123}
124
125/// State word packed-bit layout: top 32 bits = epoch, bits 16..32 =
126/// `n_items`, bits 0..16 = claim (0 = EMPTY, 1 = READY).
127const STATE_EMPTY: u64 = 0;
128const CLAIM_READY: u64 = 1;
129
130/// File header. Cache-line aligned.
131#[repr(C, align(64))]
132pub struct UrdHeader {
133 /// Magic constant.
134 pub magic: u64,
135 /// Number of mailboxes (one per thief).
136 pub n_mailboxes: u64,
137 /// Pid of the owner process; informational. Cleared on
138 /// `close_owner()`.
139 pub owner_pid: AtomicU64,
140 /// Shutdown epoch counter.
141 pub epoch: AtomicU64,
142 /// Padding to push `rr_cursor` to its own cache line.
143 pub _pad_meta: [u8; 24],
144 /// Round-robin cursor the owner uses to pick the next target
145 /// mailbox when no explicit target is supplied.
146 pub rr_cursor: AtomicU64,
147 /// Padding to round to two cache lines.
148 pub _pad_rr: [u8; 56],
149}
150
151/// One per-thief mailbox cache line.
152#[repr(C, align(64))]
153pub struct Mailbox {
154 /// State word: `(epoch:32) << 32 | (n_items:16) << 16 | claim:16`.
155 pub state: AtomicU64,
156 /// Inline items.
157 pub items: [LineItem; MAILBOX_ITEMS],
158 /// Trailing padding.
159 pub _pad: [u8; 8],
160}
161
162/// Total file size for a URD with `n_mailboxes` mailboxes.
163pub const fn urd_file_size(n_mailboxes: usize) -> usize {
164 std::mem::size_of::<UrdHeader>() + n_mailboxes * URD_MAILBOX_SIZE
165}
166
167/// Outcome of [`SharedDequeUrd::publish_to`].
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum PublishError {
170 /// Caller-supplied target mailbox index is out of range.
171 BadTarget(usize),
172 /// More items than [`MAILBOX_ITEMS`] passed in one publish call.
173 TooManyItems,
174 /// Payload exceeded [`super::shared_deque_khpd::KHPD_ITEM_BYTES`].
175 PayloadTooLarge,
176}
177
178/// Outcome of [`SharedDequeUrd::drain_mailbox`].
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum Drain {
181 /// Got items.
182 Success(DrainResult),
183 /// Mailbox empty (no published items past this thief's last
184 /// consume).
185 Empty,
186}
187
188/// Items pulled from a mailbox.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub struct DrainResult {
191 /// How many of the `items` slots are filled.
192 pub n_items: usize,
193 /// The items.
194 pub items: [LineItem; MAILBOX_ITEMS],
195}
196
197/// MMF-backed UMWAIT Rendezvous Deque. Single owner, N
198/// pre-configured thieves.
199pub struct SharedDequeUrd {
200 _file: File,
201 mmap: MmapMut,
202 n_mailboxes: usize,
203 wait_strategy: WaitStrategy,
204 publish_strategy: PublishStrategy,
205}
206
207// SAFETY: all fields are Send. The mmap handle is Send + Sync per
208// memmap2. Every mailbox access goes through the per-mailbox
209// state-atomic protocol.
210unsafe impl Send for SharedDequeUrd {}
211// SAFETY: same justification as the Send impl directly above.
212unsafe impl Sync for SharedDequeUrd {}
213
214impl SharedDequeUrd {
215 /// Create a fresh URD file with `n_mailboxes` mailboxes (one
216 /// per intended thief). Minimum 1. The round-robin cursor
217 /// reduces modulo `n_mailboxes` (no pow2 requirement so a
218 /// single-thief bench can use n = 1).
219 pub fn create<P: AsRef<Path>>(path: P, n_mailboxes: usize) -> io::Result<Self> {
220 let n_mailboxes = n_mailboxes.max(1);
221 let size = urd_file_size(n_mailboxes);
222
223 let file = OpenOptions::new()
224 .read(true)
225 .write(true)
226 .create(true)
227 .truncate(true)
228 .open(path.as_ref())?;
229 file.set_len(size as u64)?;
230
231 // SAFETY: `map_mut` is unsafe because the kernel cannot
232 // prevent another process from truncating the file. This
233 // call site upholds the soundness contract by writing only
234 // through the per-mailbox state-atomic protocol.
235 let mut mmap = unsafe { MmapOptions::new().len(size).map_mut(&file)? };
236
237 let header_ptr = mmap.as_mut_ptr() as *mut UrdHeader;
238 // SAFETY: mmap is page-aligned (>= 64-byte alignment); the
239 // map covers `urd_file_size(n_mailboxes)` bytes by
240 // construction.
241 unsafe {
242 (*header_ptr).magic = URD_MAGIC;
243 (*header_ptr).n_mailboxes = n_mailboxes as u64;
244 (*header_ptr).owner_pid = AtomicU64::new(std::process::id() as u64);
245 (*header_ptr).epoch = AtomicU64::new(0);
246 std::ptr::write_bytes((*header_ptr)._pad_meta.as_mut_ptr(), 0, 24);
247 (*header_ptr).rr_cursor = AtomicU64::new(0);
248 std::ptr::write_bytes((*header_ptr)._pad_rr.as_mut_ptr(), 0, 56);
249 }
250
251 // Zero all mailboxes (state == STATE_EMPTY).
252 let mailboxes_start = std::mem::size_of::<UrdHeader>();
253 // SAFETY: `write_bytes` covers the unwritten tail of the
254 // map.
255 unsafe {
256 std::ptr::write_bytes(
257 mmap.as_mut_ptr().add(mailboxes_start),
258 0,
259 n_mailboxes * URD_MAILBOX_SIZE,
260 );
261 }
262
263 mmap.flush()?;
264 let wait_strategy = WaitStrategy::pick();
265 let publish_strategy = PublishStrategy::pick();
266 Ok(Self {
267 _file: file,
268 mmap,
269 n_mailboxes,
270 wait_strategy,
271 publish_strategy,
272 })
273 }
274
275 /// Open an existing URD file.
276 pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
277 let file = OpenOptions::new()
278 .read(true)
279 .write(true)
280 .open(path.as_ref())?;
281 let size = file.metadata()?.len() as usize;
282 if size < std::mem::size_of::<UrdHeader>() {
283 return Err(io::Error::new(
284 io::ErrorKind::InvalidData,
285 "urd file too small",
286 ));
287 }
288 // SAFETY: same protocol-only-access justification as
289 // `create`.
290 let mmap = unsafe { MmapOptions::new().len(size).map_mut(&file)? };
291 let header_ptr = mmap.as_ptr() as *const UrdHeader;
292 // SAFETY: map size verified to cover header.
293 let (magic, n_mailboxes) =
294 unsafe { ((*header_ptr).magic, (*header_ptr).n_mailboxes as usize) };
295 if magic != URD_MAGIC {
296 return Err(io::Error::new(
297 io::ErrorKind::InvalidData,
298 format!("urd magic mismatch {magic:#x}"),
299 ));
300 }
301 if n_mailboxes == 0 {
302 return Err(io::Error::new(
303 io::ErrorKind::InvalidData,
304 "urd n_mailboxes must be >= 1",
305 ));
306 }
307 if size < urd_file_size(n_mailboxes) {
308 return Err(io::Error::new(
309 io::ErrorKind::InvalidData,
310 "urd file size below header expected",
311 ));
312 }
313 let wait_strategy = WaitStrategy::pick();
314 let publish_strategy = PublishStrategy::pick();
315 Ok(Self {
316 _file: file,
317 mmap,
318 n_mailboxes,
319 wait_strategy,
320 publish_strategy,
321 })
322 }
323
324 /// Number of configured mailboxes.
325 pub fn n_mailboxes(&self) -> usize {
326 self.n_mailboxes
327 }
328
329 /// Wait strategy this URD instance picked (per CPUID).
330 pub fn wait_strategy(&self) -> WaitStrategy {
331 self.wait_strategy
332 }
333
334 /// Publish strategy this URD instance picked (per CPUID).
335 pub fn publish_strategy(&self) -> PublishStrategy {
336 self.publish_strategy
337 }
338
339 /// Owner pid at create time, or 0 after `close_owner()`.
340 pub fn owner_pid(&self) -> u64 {
341 self.header().owner_pid.load(Ordering::Acquire)
342 }
343
344 /// Owner shutdown: zero pid + advance epoch.
345 pub fn close_owner(&self) {
346 self.header().owner_pid.store(0, Ordering::Release);
347 self.header().epoch.fetch_add(1, Ordering::Release);
348 }
349
350 fn header(&self) -> &UrdHeader {
351 // SAFETY: header is at the start of the map.
352 unsafe { &*(self.mmap.as_ptr() as *const UrdHeader) }
353 }
354
355 fn mailbox_ptr(&self, idx: usize) -> *mut Mailbox {
356 let off = std::mem::size_of::<UrdHeader>() + idx * URD_MAILBOX_SIZE;
357 // SAFETY: `idx < n_mailboxes` by caller contract; `off` is
358 // in-bounds + 64-byte aligned.
359 unsafe { self.mmap.as_ptr().add(off) as *mut Mailbox }
360 }
361
362 fn mailbox(&self, idx: usize) -> &Mailbox {
363 // SAFETY: same as `mailbox_ptr`.
364 unsafe { &*self.mailbox_ptr(idx) }
365 }
366
367 /// Owner-side: publish `items` to mailbox `target`. Spins until
368 /// the mailbox is EMPTY (the previous batch has been consumed),
369 /// then publishes via the per-host
370 /// [`PublishStrategy`](Self::publish_strategy):
371 ///
372 /// - [`PublishStrategy::Movdir64b`]: builds a 64-byte source
373 /// line and atomically writes the whole mailbox cache line
374 /// via the `MOVDIR64B` instruction (one Write-Combining
375 /// store, no RFO).
376 /// - [`PublishStrategy::Scalar`]: writes items via cached
377 /// stores, then Release-stores the state word to READY (two-
378 /// step protocol).
379 ///
380 /// Returns the number of items published.
381 pub fn publish_to(
382 &self,
383 target: usize,
384 items: &[LineItem],
385 ) -> Result<usize, PublishError> {
386 if target >= self.n_mailboxes {
387 return Err(PublishError::BadTarget(target));
388 }
389 if items.len() > MAILBOX_ITEMS {
390 return Err(PublishError::TooManyItems);
391 }
392 if items.is_empty() {
393 return Ok(0);
394 }
395 let mb = self.mailbox(target);
396 // Spin-wait for the mailbox to be EMPTY (previous batch
397 // consumed). The owner is on the WRITE side so a brief
398 // PAUSE-spin is the right primitive here regardless of the
399 // thief's WAITPKG availability.
400 loop {
401 let s = mb.state.load(Ordering::Acquire);
402 if s == STATE_EMPTY {
403 break;
404 }
405 std::hint::spin_loop();
406 }
407 let epoch = self.header().epoch.load(Ordering::Relaxed);
408 let new_state = (epoch << 32) | ((items.len() as u64) << 16) | CLAIM_READY;
409
410 match self.publish_strategy {
411 PublishStrategy::Movdir64b => {
412 // SAFETY: target index is bounds-checked above;
413 // mailbox_ptr returns an in-bounds aligned pointer.
414 // The strategy is `Movdir64b` only when
415 // `has_movdir64b()` returned true, so emitting the
416 // instruction is safe.
417 unsafe {
418 self.publish_movdir64b(target, items, new_state);
419 }
420 }
421 PublishStrategy::Scalar => {
422 // SAFETY: mailbox is in-bounds + aligned; we have
423 // exclusive access until the Release-store below
424 // transitions state to READY.
425 unsafe {
426 let mb_ptr = self.mailbox_ptr(target);
427 for (i, item) in items.iter().enumerate() {
428 (*mb_ptr).items[i] = *item;
429 }
430 }
431 mb.state.store(new_state, Ordering::Release);
432 }
433 }
434 Ok(items.len())
435 }
436
437 /// Build a 64-byte source line on the stack carrying the new
438 /// `state` plus the items, then atomically write it to the
439 /// destination mailbox via `MOVDIR64B`. `SFENCE` drains the WC
440 /// store buffer so the publish is globally observable before the
441 /// function returns.
442 ///
443 /// # Safety
444 ///
445 /// Caller must have validated that the per-host
446 /// [`PublishStrategy`] is `Movdir64b` (i.e. `has_movdir64b()`
447 /// returned true), that `target < self.n_mailboxes`, and that
448 /// `items.len() <= MAILBOX_ITEMS`.
449 #[inline(always)]
450 unsafe fn publish_movdir64b(
451 &self,
452 target: usize,
453 items: &[LineItem],
454 new_state: u64,
455 ) {
456 // Source line: layout-compatible with `Mailbox`. Built on
457 // the stack so the MOVDIR64B source is L1d-warm.
458 #[repr(C, align(64))]
459 struct SrcLine {
460 state: u64,
461 items: [LineItem; MAILBOX_ITEMS],
462 _pad: [u8; 8],
463 }
464 let mut src = SrcLine {
465 state: new_state,
466 items: [LineItem::default(); MAILBOX_ITEMS],
467 _pad: [0u8; 8],
468 };
469 for (i, item) in items.iter().enumerate() {
470 src.items[i] = *item;
471 }
472
473 let dst_ptr = self.mailbox_ptr(target) as *mut u8;
474 let src_ptr = &src as *const SrcLine as *const u8;
475
476 #[cfg(target_arch = "x86_64")]
477 {
478 // SAFETY: `MOVDIR64B` writes 64 bytes from `[src_ptr]`
479 // to `[dst_ptr]`. Both pointers are 64-byte aligned (the
480 // `Mailbox` is `#[repr(C, align(64))]` and `SrcLine` is
481 // `#[repr(C, align(64))]`). `nostack` + `preserves_flags`
482 // lets the optimizer schedule freely. `SFENCE` drains the
483 // WC store buffer.
484 unsafe {
485 core::arch::asm!(
486 "movdir64b {dst}, [{src}]",
487 "sfence",
488 dst = in(reg) dst_ptr,
489 src = in(reg) src_ptr,
490 options(nostack, preserves_flags),
491 );
492 }
493 }
494 #[cfg(not(target_arch = "x86_64"))]
495 {
496 _ = dst_ptr;
497 _ = src_ptr;
498 unreachable!(
499 "publish_movdir64b reached on non-x86_64 host; \
500 PublishStrategy::pick() returns Scalar there"
501 );
502 }
503 }
504
505 /// Owner-side: publish `items` to the next round-robin mailbox.
506 /// Returns `(target, n_published)`.
507 pub fn publish_round_robin(
508 &self,
509 items: &[LineItem],
510 ) -> Result<(usize, usize), PublishError> {
511 let cursor = self.header().rr_cursor.fetch_add(1, Ordering::Relaxed) as usize;
512 // Use modulo (not bit-mask) so non-pow2 mailbox counts
513 // work; the common case is `n_mailboxes` being a small
514 // constant the compiler reduces to a strength-reduced
515 // multiply.
516 let target = cursor % self.n_mailboxes;
517 let n = self.publish_to(target, items)?;
518 Ok((target, n))
519 }
520
521 /// Thief-side: drain own mailbox if it has READY items.
522 /// `mailbox_idx` is the thief's pre-assigned mailbox. Returns
523 /// [`Drain::Empty`] when the state byte is EMPTY (no work
524 /// published yet).
525 pub fn drain_mailbox(&self, mailbox_idx: usize) -> Drain {
526 if mailbox_idx >= self.n_mailboxes {
527 return Drain::Empty;
528 }
529 let mb = self.mailbox(mailbox_idx);
530 let s = mb.state.load(Ordering::Acquire);
531 if s & 0xFFFF != CLAIM_READY {
532 return Drain::Empty;
533 }
534 let n_items = ((s >> 16) & 0xFFFF) as usize;
535 let n_items = n_items.min(MAILBOX_ITEMS);
536 // SAFETY: state's READY bit is set; the publisher's
537 // Release-store synchronizes-with our Acquire-load above so
538 // item bytes are visible.
539 let result = unsafe {
540 DrainResult {
541 n_items,
542 items: (*self.mailbox_ptr(mailbox_idx)).items,
543 }
544 };
545 // Release the mailbox: state -> EMPTY. The owner's next
546 // `publish_to(target)` spin sees EMPTY and writes.
547 mb.state.store(STATE_EMPTY, Ordering::Release);
548 Drain::Success(result)
549 }
550
551 /// Thief-side: block (per the host's [`WaitStrategy`]) until
552 /// the mailbox transitions to READY, then drain it.
553 ///
554 /// On WAITPKG-capable hardware the thief uses `UMONITOR` +
555 /// `UMWAIT` to halt; otherwise it uses `PAUSE`-spin. The
556 /// deadline is expressed as the absolute TSC value at which
557 /// `UMWAIT` returns even if the line has not transitioned;
558 /// `u64::MAX` means "no deadline" (wait indefinitely - protocol
559 /// risk if the owner never publishes).
560 pub fn wait_and_drain(&self, mailbox_idx: usize, deadline_tsc: u64) -> Drain {
561 if mailbox_idx >= self.n_mailboxes {
562 return Drain::Empty;
563 }
564 let mb = self.mailbox(mailbox_idx);
565 let state_addr = (&raw const mb.state).cast::<u8>();
566 loop {
567 let s = mb.state.load(Ordering::Acquire);
568 if s & 0xFFFF == CLAIM_READY {
569 break;
570 }
571 match self.wait_strategy {
572 WaitStrategy::PauseSpin => std::hint::spin_loop(),
573 WaitStrategy::Waitpkg => {
574 // SAFETY: WAITPKG was confirmed available by
575 // CPUID at construction time. `state_addr` is a
576 // valid pointer into the mmap; `UMONITOR` arms
577 // the hardware monitor on its cache line.
578 // `UMWAIT` suspends until the monitor fires, an
579 // interrupt arrives, or the TSC deadline is
580 // reached. The double-check on the next loop
581 // iteration re-validates the state byte.
582 unsafe { wait_with_waitpkg(state_addr, deadline_tsc) };
583 }
584 }
585 }
586 self.drain_mailbox(mailbox_idx)
587 }
588
589 /// Force any dirty pages to disk.
590 pub fn flush_to_disk(&self) -> io::Result<()> {
591 self.mmap.flush()
592 }
593}
594
595/// `UMONITOR` + `UMWAIT` wait primitive. Halts the calling logical
596/// CPU until the monitored cache line transitions OR the TSC
597/// reaches `deadline_tsc` (whichever comes first). Pass
598/// `deadline_tsc = u64::MAX` for "no deadline".
599///
600/// # Safety
601///
602/// The caller MUST have confirmed WAITPKG is available via
603/// [`subetha_core::has_waitpkg`] - executing `UMONITOR` /
604/// `UMWAIT` on hardware without WAITPKG raises an illegal-
605/// instruction trap (`#UD`).
606///
607/// `state_addr` must be a valid pointer into accessible memory;
608/// `UMONITOR` reads no payload, only the address.
609#[cfg(target_arch = "x86_64")]
610#[inline(always)]
611unsafe fn wait_with_waitpkg(state_addr: *const u8, deadline_tsc: u64) {
612 use std::arch::asm;
613 let lo = deadline_tsc as u32;
614 let hi = (deadline_tsc >> 32) as u32;
615 // Two separate asm blocks: `UMONITOR` needs the address in RAX,
616 // `UMWAIT` needs EAX (low half of RAX) for the deadline low
617 // dword. We cannot bind RAX and EAX to different values in one
618 // `asm!` call, so we split. The monitor stays armed across the
619 // second asm block; `UMWAIT` in C0.1 (hint = 1) is the light
620 // wait state with low wake latency.
621 //
622 // SAFETY: caller-asserted WAITPKG availability + valid pointer.
623 unsafe {
624 asm!(
625 "umonitor rax",
626 in("rax") state_addr,
627 options(nostack, preserves_flags),
628 );
629 asm!(
630 "umwait {hint:e}",
631 hint = in(reg) 1u32,
632 in("eax") lo,
633 in("edx") hi,
634 options(nostack),
635 );
636 }
637}
638
639#[cfg(not(target_arch = "x86_64"))]
640#[inline(always)]
641unsafe fn wait_with_waitpkg(_state_addr: *const u8, _deadline_tsc: u64) {
642 // Non-x86_64: WAITPKG cannot be available; this function is
643 // never called on those targets (the `WaitStrategy::Waitpkg`
644 // branch is gated on `has_waitpkg()` which returns false).
645 std::hint::spin_loop();
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651 use std::sync::Arc;
652 use std::sync::atomic::{AtomicUsize, Ordering as O};
653 use std::thread;
654
655 fn temp_path(name: &str) -> std::path::PathBuf {
656 let mut p = std::env::temp_dir();
657 let pid = std::process::id();
658 let nonce = std::time::SystemTime::now()
659 .duration_since(std::time::UNIX_EPOCH)
660 .map(|d| d.as_nanos())
661 .unwrap_or(0);
662 p.push(format!("subetha_urd_{pid}_{nonce}_{name}.bin"));
663 p
664 }
665
666 fn u32_item(id: u32) -> LineItem {
667 LineItem::new(&id.to_le_bytes()).expect("item")
668 }
669
670 fn item_id(item: &LineItem) -> u32 {
671 u32::from_le_bytes(item.payload[..4].try_into().unwrap())
672 }
673
674 #[test]
675 fn create_open_round_trips_header() {
676 let path = temp_path("create_open");
677 let _u = SharedDequeUrd::create(&path, 4).expect("create");
678 let o = SharedDequeUrd::open(&path).expect("open");
679 assert_eq!(o.n_mailboxes(), 4);
680 assert_eq!(o.owner_pid(), std::process::id() as u64);
681 std::fs::remove_file(&path).ok();
682 }
683
684 #[test]
685 fn wait_strategy_matches_host() {
686 let path = temp_path("strategy");
687 let u = SharedDequeUrd::create(&path, 2).expect("create");
688 let s = u.wait_strategy();
689 if has_waitpkg() {
690 assert_eq!(s, WaitStrategy::Waitpkg);
691 } else {
692 assert_eq!(s, WaitStrategy::PauseSpin);
693 }
694 std::fs::remove_file(&path).ok();
695 }
696
697 #[test]
698 fn publish_strategy_matches_host() {
699 let path = temp_path("publish_strategy");
700 let u = SharedDequeUrd::create(&path, 2).expect("create");
701 let s = u.publish_strategy();
702 if subetha_core::has_movdir64b() {
703 assert_eq!(s, PublishStrategy::Movdir64b);
704 } else {
705 assert_eq!(s, PublishStrategy::Scalar);
706 }
707 std::fs::remove_file(&path).ok();
708 }
709
710 #[test]
711 fn publish_then_drain_works_through_strategy_dispatch() {
712 // Round-trip a publish + drain regardless of which strategy
713 // pick() chose for this host. Exercises the dispatch site
714 // in `publish_to` so the Movdir64b arm is in the binary on
715 // capable silicon (which is where it would actually run).
716 let path = temp_path("dispatch_round_trip");
717 let urd = SharedDequeUrd::create(&path, 1).expect("create");
718 let items = [
719 LineItem::new(&1u32.to_le_bytes()).expect("item"),
720 LineItem::new(&2u32.to_le_bytes()).expect("item"),
721 LineItem::new(&3u32.to_le_bytes()).expect("item"),
722 ];
723 let n = urd.publish_to(0, &items).expect("publish");
724 assert_eq!(n, 3);
725 match urd.drain_mailbox(0) {
726 Drain::Success(r) => {
727 assert_eq!(r.n_items, 3);
728 for (i, expected) in [1u32, 2, 3].iter().enumerate() {
729 let got = u32::from_le_bytes(
730 r.items[i].payload[..4].try_into().unwrap(),
731 );
732 assert_eq!(got, *expected, "item {i}");
733 }
734 }
735 Drain::Empty => panic!("expected items, got Empty"),
736 }
737 std::fs::remove_file(&path).ok();
738 }
739
740 #[test]
741 fn publish_then_drain_round_trips() {
742 let path = temp_path("publish_drain");
743 let u = SharedDequeUrd::create(&path, 2).expect("create");
744 let items = [u32_item(1), u32_item(2), u32_item(3)];
745 let n = u.publish_to(0, &items).expect("publish");
746 assert_eq!(n, 3);
747 match u.drain_mailbox(0) {
748 Drain::Success(r) => {
749 assert_eq!(r.n_items, 3);
750 assert_eq!(item_id(&r.items[0]), 1);
751 assert_eq!(item_id(&r.items[1]), 2);
752 assert_eq!(item_id(&r.items[2]), 3);
753 }
754 Drain::Empty => panic!("expected ready mailbox"),
755 }
756 // After drain, mailbox is EMPTY.
757 assert!(matches!(u.drain_mailbox(0), Drain::Empty));
758 std::fs::remove_file(&path).ok();
759 }
760
761 #[test]
762 fn publish_round_robin_cycles_targets() {
763 let path = temp_path("rr");
764 let u = SharedDequeUrd::create(&path, 4).expect("create");
765 let items = [u32_item(1)];
766 let (t0, _) = u.publish_round_robin(&items).expect("rr 0");
767 u.drain_mailbox(t0);
768 let (t1, _) = u.publish_round_robin(&items).expect("rr 1");
769 u.drain_mailbox(t1);
770 let (t2, _) = u.publish_round_robin(&items).expect("rr 2");
771 u.drain_mailbox(t2);
772 let (t3, _) = u.publish_round_robin(&items).expect("rr 3");
773 // The four picks must cover all mailboxes (mod
774 // `n_mailboxes`).
775 let mut targets = [t0, t1, t2, t3];
776 targets.sort();
777 assert_eq!(targets, [0, 1, 2, 3]);
778 std::fs::remove_file(&path).ok();
779 }
780
781 #[test]
782 fn too_many_items_rejected() {
783 let path = temp_path("too_many");
784 let u = SharedDequeUrd::create(&path, 2).expect("create");
785 let items: Vec<LineItem> = (0..(MAILBOX_ITEMS + 1) as u32).map(u32_item).collect();
786 let err = u.publish_to(0, &items).expect_err("too many");
787 assert_eq!(err, PublishError::TooManyItems);
788 std::fs::remove_file(&path).ok();
789 }
790
791 #[test]
792 fn bad_target_rejected() {
793 let path = temp_path("bad_target");
794 let u = SharedDequeUrd::create(&path, 2).expect("create");
795 let err = u.publish_to(99, &[u32_item(1)]).expect_err("bad target");
796 assert_eq!(err, PublishError::BadTarget(99));
797 std::fs::remove_file(&path).ok();
798 }
799
800 #[test]
801 fn empty_publish_is_noop() {
802 let path = temp_path("empty");
803 let u = SharedDequeUrd::create(&path, 2).expect("create");
804 assert_eq!(u.publish_to(0, &[]).expect("publish"), 0);
805 assert!(matches!(u.drain_mailbox(0), Drain::Empty));
806 std::fs::remove_file(&path).ok();
807 }
808
809 #[test]
810 fn close_owner_zeros_pid_and_advances_epoch() {
811 let path = temp_path("close");
812 let u = SharedDequeUrd::create(&path, 2).expect("create");
813 let before = u.header().epoch.load(O::Acquire);
814 u.close_owner();
815 assert_eq!(u.owner_pid(), 0);
816 assert_eq!(u.header().epoch.load(O::Acquire), before + 1);
817 std::fs::remove_file(&path).ok();
818 }
819
820 #[test]
821 fn four_thieves_no_double_take() {
822 // Stress: owner publishes round-robin to 4 mailboxes; 4
823 // thieves each drain their own mailbox. Sum invariant.
824 let path = temp_path("stress");
825 let u = Arc::new(SharedDequeUrd::create(&path, 4).expect("create"));
826 let n = 4_000usize;
827 let consumed = Arc::new(AtomicUsize::new(0));
828 let sum = Arc::new(AtomicUsize::new(0));
829
830 let mut thieves = Vec::new();
831 for tid in 0..4 {
832 let u = Arc::clone(&u);
833 let consumed = Arc::clone(&consumed);
834 let sum = Arc::clone(&sum);
835 thieves.push(thread::spawn(move || {
836 while consumed.load(O::Relaxed) < n {
837 match u.drain_mailbox(tid) {
838 Drain::Success(r) => {
839 for i in 0..r.n_items {
840 consumed.fetch_add(1, O::Relaxed);
841 sum.fetch_add(
842 item_id(&r.items[i]) as usize,
843 O::Relaxed,
844 );
845 }
846 }
847 Drain::Empty => std::hint::spin_loop(),
848 }
849 }
850 }));
851 }
852
853 // Publisher: round-robin batches of `MAILBOX_ITEMS` items.
854 let mut pushed = 0usize;
855 while pushed < n {
856 let want = MAILBOX_ITEMS.min(n - pushed);
857 let mut batch = [LineItem::default(); MAILBOX_ITEMS];
858 for slot in batch.iter_mut().take(want) {
859 *slot = u32_item(pushed as u32);
860 pushed += 1;
861 }
862 u.publish_round_robin(&batch[..want]).expect("rr publish");
863 }
864
865 for t in thieves {
866 t.join().expect("thief");
867 }
868 let expected: usize = (0..n).sum();
869 assert_eq!(
870 sum.load(O::Relaxed),
871 expected,
872 "stress sum mismatch (expected every item consumed exactly once)"
873 );
874 std::fs::remove_file(&path).ok();
875 }
876}