1use std::fs::{File, OpenOptions};
31use std::marker::PhantomData;
32use std::mem::{align_of, size_of};
33use std::path::Path;
34use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
35
36use memmap2::{MmapMut, MmapOptions};
37
38pub const TIME_POINT_MAGIC: u64 = 0x4150_4D46_5450_5054;
39pub const TILE_CAP: usize = 16;
40pub const SLOT_PAYLOAD: usize = 56;
41
42#[repr(C, align(64))]
43pub struct TileHeader {
44 pub magic: u64,
45 pub capacity: u32,
46 pub payload_size: u32,
47 pub occupied: AtomicU32,
48 _pad: [u8; 44],
49}
50
51#[repr(C, align(64))]
52pub struct VersionedSlot {
53 pub version: AtomicU64,
54 pub payload: [u8; SLOT_PAYLOAD],
55}
56
57pub const fn tile_file_size() -> usize {
58 size_of::<TileHeader>() + TILE_CAP * size_of::<VersionedSlot>()
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum TileError {
63 LayoutMismatch,
64 PayloadTooLarge,
65 Full,
66 IoError(std::io::ErrorKind),
67}
68
69impl From<std::io::Error> for TileError {
70 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
71}
72
73pub struct SharedTimePointTile<T: Copy + 'static> {
74 _file: File,
75 mmap: MmapMut,
76 _phantom: PhantomData<T>,
77 header_sidecar: subetha_core::HandshakeHeader,
78 ring_sidecar: Box<subetha_core::ObservationRing>,
79}
80
81unsafe impl<T: Copy + Send + 'static> Send for SharedTimePointTile<T> {}
82unsafe impl<T: Copy + Sync + 'static> Sync for SharedTimePointTile<T> {}
83
84impl<T: Copy + Send + Sync + 'static> subetha_sidecar::AdaptiveInstance for SharedTimePointTile<T> {
85 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
86 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
87 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
88 Box::new(subetha_sidecar::NoMigrationPolicy)
89 }
90}
91
92impl<T: Copy + 'static> SharedTimePointTile<T> {
93 pub fn create(path: impl AsRef<Path>) -> Result<Self, TileError> {
99 Self::check_layout()?;
100 let (file, mmap) = crate::mmf_attach::create_or_attach(
101 path.as_ref(),
102 tile_file_size(),
103 |ptr| unsafe { Self::init_region(ptr) },
104 |ptr| unsafe { (*(ptr as *const TileHeader)).magic == TIME_POINT_MAGIC },
105 )?;
106 Self::from_region(file, mmap)
107 }
108
109 pub fn reset(path: impl AsRef<Path>) -> Result<Self, TileError> {
113 Self::check_layout()?;
114 let (file, mmap) = crate::mmf_attach::reset(path.as_ref(), tile_file_size(), |ptr| unsafe {
115 Self::init_region(ptr)
116 })?;
117 Self::from_region(file, mmap)
118 }
119
120 unsafe fn init_region(ptr: *mut u8) {
129 let hdr = ptr as *mut TileHeader;
130 unsafe {
131 (*hdr).capacity = TILE_CAP as u32;
132 (*hdr).payload_size = size_of::<T>() as u32;
133 std::ptr::write_volatile(&raw mut (*hdr).magic, TIME_POINT_MAGIC);
134 }
135 }
136
137 fn from_region(file: File, mmap: MmapMut) -> Result<Self, TileError> {
140 let hdr = unsafe { &*(mmap.as_ptr() as *const TileHeader) };
141 if hdr.magic != TIME_POINT_MAGIC
142 || hdr.capacity != TILE_CAP as u32
143 || hdr.payload_size as usize != size_of::<T>()
144 {
145 return Err(TileError::LayoutMismatch);
146 }
147 Ok(Self {
148 _file: file, mmap, _phantom: PhantomData,
149 header_sidecar: subetha_core::HandshakeHeader::new(),
150 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
151 })
152 }
153
154 pub fn open(path: impl AsRef<Path>) -> Result<Self, TileError> {
155 Self::check_layout()?;
156 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
157 if file.metadata()?.len() < tile_file_size() as u64 {
158 return Err(TileError::LayoutMismatch);
159 }
160 let mmap = unsafe { MmapOptions::new().len(tile_file_size()).map_mut(&file)? };
161 Self::from_region(file, mmap)
162 }
163
164 fn check_layout() -> Result<(), TileError> {
165 if size_of::<T>() > SLOT_PAYLOAD {
166 return Err(TileError::PayloadTooLarge);
167 }
168 if align_of::<T>() > 8 {
169 return Err(TileError::PayloadTooLarge);
170 }
171 Ok(())
172 }
173
174 pub fn header(&self) -> &TileHeader {
175 unsafe { &*(self.mmap.as_ptr() as *const TileHeader) }
176 }
177
178 fn slot(&self, idx: usize) -> &VersionedSlot {
179 let base = unsafe { self.mmap.as_ptr().add(size_of::<TileHeader>()) };
180 unsafe {
181 &*(base.add(idx * size_of::<VersionedSlot>()) as *const VersionedSlot)
182 }
183 }
184
185 pub fn insert(&self, version: u64, value: T) -> Result<usize, TileError> {
188 let header = self.header();
189 loop {
190 let cur = header.occupied.load(Ordering::Acquire);
191 let free = !cur & ((1u32 << TILE_CAP) - 1);
192 if free == 0 {
193 self.ring_sidecar
194 .push_op(crate::sidecar_ops::versioned::OP_PUSH, 1);
195 return Err(TileError::Full);
196 }
197 let lane = free.trailing_zeros() as usize;
198 let new_occupied = cur | (1u32 << lane);
199 if header.occupied.compare_exchange_weak(
200 cur, new_occupied, Ordering::AcqRel, Ordering::Acquire,
201 ).is_ok() {
202 let slot = self.slot(lane);
203 unsafe {
205 let dst = slot.payload.as_ptr() as *mut T;
206 std::ptr::write_unaligned(dst, value);
207 }
208 slot.version.store(version, Ordering::Release);
209 self.ring_sidecar
210 .push_op(crate::sidecar_ops::versioned::OP_PUSH, 0);
211 return Ok(lane);
212 }
213 std::hint::spin_loop();
214 }
215 }
216
217 pub fn remove(&self, lane: usize) {
218 if lane < TILE_CAP {
219 self.header().occupied.fetch_and(!(1u32 << lane), Ordering::AcqRel);
220 }
221 }
222
223 pub fn len(&self) -> usize {
224 self.header().occupied.load(Ordering::Acquire).count_ones() as usize
225 }
226
227 pub fn is_empty(&self) -> bool { self.len() == 0 }
228 pub fn is_full(&self) -> bool {
229 self.header().occupied.load(Ordering::Acquire) == ((1u32 << TILE_CAP) - 1)
230 }
231
232 #[inline]
236 pub fn visible_mask(&self, snapshot: u64) -> u16 {
237 let header = self.header();
238 let occupied = header.occupied.load(Ordering::Acquire) as u16;
239 self.ring_sidecar.push_op(
240 crate::sidecar_ops::versioned::OP_VISIBLE_MASK,
241 if occupied == 0 { 2 } else { 0 },
242 );
243 if occupied == 0 { return 0; }
244 let versions_base = unsafe {
245 self.mmap.as_ptr().add(size_of::<TileHeader>())
246 };
247 let mut versions = [0u64; TILE_CAP];
254 for (i, v) in versions.iter_mut().enumerate() {
255 let slot = unsafe {
256 &*(versions_base.add(i * size_of::<VersionedSlot>()) as *const VersionedSlot)
257 };
258 *v = slot.version.load(Ordering::Acquire);
259 }
260 Self::simd_visible_mask(&versions, snapshot) & occupied
261 }
262
263 #[inline]
268 pub fn simd_visible_mask(versions: &[u64; TILE_CAP], snapshot: u64) -> u16 {
269 #[cfg(target_arch = "x86_64")]
270 {
271 if std::is_x86_feature_detected!("avx512f") {
272 return unsafe { Self::simd_visible_mask_avx512(versions, snapshot) };
274 }
275 if std::is_x86_feature_detected!("avx2") {
276 return unsafe { Self::simd_visible_mask_avx2(versions, snapshot) };
278 }
279 }
280 Self::simd_visible_mask_scalar(versions, snapshot)
281 }
282
283 #[cfg(target_arch = "x86_64")]
293 #[target_feature(enable = "avx512f")]
294 pub unsafe fn simd_visible_mask_avx512(
295 versions: &[u64; TILE_CAP],
296 snapshot: u64,
297 ) -> u16 {
298 use std::arch::x86_64::*;
299 let snap = _mm512_set1_epi64(snapshot as i64);
300 let v_lo = unsafe {
303 _mm512_loadu_si512(versions.as_ptr() as *const __m512i)
304 };
305 let v_hi = unsafe {
306 _mm512_loadu_si512(versions.as_ptr().add(8) as *const __m512i)
307 };
308 let mask_lo: u8 = _mm512_cmple_epu64_mask(v_lo, snap);
309 let mask_hi: u8 = _mm512_cmple_epu64_mask(v_hi, snap);
310 (mask_lo as u16) | ((mask_hi as u16) << 8)
311 }
312
313 #[cfg(target_arch = "x86_64")]
320 #[target_feature(enable = "avx2")]
321 pub unsafe fn simd_visible_mask_avx2(
322 versions: &[u64; TILE_CAP],
323 snapshot: u64,
324 ) -> u16 {
325 use std::arch::x86_64::*;
326 let sign_bit = _mm256_set1_epi64x(i64::MIN);
327 let snap_raw = _mm256_set1_epi64x(snapshot as i64);
328 let snap_s = _mm256_xor_si256(snap_raw, sign_bit);
329 let load_xord = |off: usize| -> __m256i {
332 let raw = unsafe {
333 _mm256_loadu_si256(versions.as_ptr().add(off) as *const __m256i)
334 };
335 _mm256_xor_si256(raw, sign_bit)
336 };
337 let load_raw = |off: usize| -> __m256i {
338 unsafe {
339 _mm256_loadu_si256(versions.as_ptr().add(off) as *const __m256i)
340 }
341 };
342 let v0 = load_xord(0);
343 let v1 = load_xord(4);
344 let v2 = load_xord(8);
345 let v3 = load_xord(12);
346 let raw0 = load_raw(0);
347 let raw1 = load_raw(4);
348 let raw2 = load_raw(8);
349 let raw3 = load_raw(12);
350 let gt0 = _mm256_cmpgt_epi64(snap_s, v0);
351 let gt1 = _mm256_cmpgt_epi64(snap_s, v1);
352 let gt2 = _mm256_cmpgt_epi64(snap_s, v2);
353 let gt3 = _mm256_cmpgt_epi64(snap_s, v3);
354 let eq0 = _mm256_cmpeq_epi64(snap_raw, raw0);
355 let eq1 = _mm256_cmpeq_epi64(snap_raw, raw1);
356 let eq2 = _mm256_cmpeq_epi64(snap_raw, raw2);
357 let eq3 = _mm256_cmpeq_epi64(snap_raw, raw3);
358 let m0 = _mm256_or_si256(gt0, eq0);
359 let m1 = _mm256_or_si256(gt1, eq1);
360 let m2 = _mm256_or_si256(gt2, eq2);
361 let m3 = _mm256_or_si256(gt3, eq3);
362 let bits0 = _mm256_movemask_pd(_mm256_castsi256_pd(m0)) as u16;
363 let bits1 = _mm256_movemask_pd(_mm256_castsi256_pd(m1)) as u16;
364 let bits2 = _mm256_movemask_pd(_mm256_castsi256_pd(m2)) as u16;
365 let bits3 = _mm256_movemask_pd(_mm256_castsi256_pd(m3)) as u16;
366 bits0 | (bits1 << 4) | (bits2 << 8) | (bits3 << 12)
367 }
368
369 #[inline]
372 pub fn simd_visible_mask_scalar(versions: &[u64; TILE_CAP], snapshot: u64) -> u16 {
373 let mut mask = 0u16;
374 for (i, v) in versions.iter().enumerate() {
375 if *v <= snapshot {
376 mask |= 1u16 << i;
377 }
378 }
379 mask
380 }
381
382 pub fn at(&self, lane: usize) -> Option<(u64, T)> {
384 if lane >= TILE_CAP {
385 self.ring_sidecar
386 .push_op(crate::sidecar_ops::versioned::OP_READ_AT, 2);
387 return None;
388 }
389 let occ = self.header().occupied.load(Ordering::Acquire);
390 if (occ >> lane) & 1 == 0 {
391 self.ring_sidecar
392 .push_op(crate::sidecar_ops::versioned::OP_READ_AT, 2);
393 return None;
394 }
395 let slot = self.slot(lane);
396 let v = slot.version.load(Ordering::Acquire);
397 let value: T = unsafe {
398 let src = slot.payload.as_ptr() as *const T;
399 std::ptr::read_unaligned(src)
400 };
401 self.ring_sidecar
402 .push_op(crate::sidecar_ops::versioned::OP_READ_AT, 0);
403 Some((v, value))
404 }
405
406 pub fn visible_count(&self, snapshot: u64) -> u32 {
408 self.visible_mask(snapshot).count_ones()
409 }
410
411 pub fn flush(&self) -> Result<(), TileError> {
412 self.mmap.flush()?;
413 Ok(())
414 }
415
416 pub fn flush_async(&self) -> Result<(), TileError> {
420 self.mmap.flush_async()?;
421 Ok(())
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 fn tmp(name: &str) -> std::path::PathBuf {
430 let mut p = std::env::temp_dir();
431 let pid = std::process::id();
432 p.push(format!("subetha-tile-{name}-{pid}.bin"));
433 p
434 }
435
436 #[test]
437 fn empty_tile_visible_mask_is_zero() {
438 let p = tmp("empty");
439 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
440 assert_eq!(t.visible_mask(u64::MAX), 0);
441 assert!(t.is_empty());
442 std::fs::remove_file(&p).ok();
443 }
444
445 #[test]
448 fn second_create_attaches_and_keeps_slots() {
449 let p = tmp("attach");
450 std::fs::remove_file(&p).ok();
451 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
452 t.insert(10, 777).unwrap();
453
454 let t2: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
455 assert_eq!(t2.visible_count(u64::MAX), 1, "attach dropped an occupied slot");
456 assert!(matches!(
457 SharedTimePointTile::<u32>::create(&p),
458 Err(TileError::LayoutMismatch),
459 ));
460
461 drop(t);
464 drop(t2);
465 let fresh: SharedTimePointTile<u64> = SharedTimePointTile::reset(&p).unwrap();
466 assert!(fresh.is_empty(), "reset left a slot occupied");
467 drop(fresh);
468 std::fs::remove_file(&p).ok();
469 }
470
471 #[test]
472 fn insert_then_visible_at_snapshot() {
473 let p = tmp("visible");
474 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
475 t.insert(10, 100).unwrap();
476 t.insert(20, 200).unwrap();
477 t.insert(30, 300).unwrap();
478 let m = t.visible_mask(25);
480 assert_eq!(m, 0b011);
481 assert_eq!(t.visible_count(25), 2);
482 let m_all = t.visible_mask(u64::MAX);
484 assert_eq!(m_all.count_ones(), 3);
485 std::fs::remove_file(&p).ok();
486 }
487
488 #[test]
489 fn fill_to_capacity_then_overflow() {
490 let p = tmp("fill");
491 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
492 for i in 0..TILE_CAP as u64 {
493 t.insert(i, i * 10).unwrap();
494 }
495 assert!(t.is_full());
496 assert_eq!(t.insert(99, 999).unwrap_err(), TileError::Full);
497 std::fs::remove_file(&p).ok();
498 }
499
500 #[test]
501 fn remove_frees_lane_for_reinsert() {
502 let p = tmp("remove");
503 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
504 let l0 = t.insert(1, 100).unwrap();
505 let l1 = t.insert(2, 200).unwrap();
506 t.remove(l0);
507 assert_eq!(t.len(), 1);
508 let l2 = t.insert(3, 300).unwrap();
509 assert_eq!(l2, l0, "freed lane reused");
510 assert_eq!(t.at(l1), Some((2, 200)));
511 assert_eq!(t.at(l2), Some((3, 300)));
512 std::fs::remove_file(&p).ok();
513 }
514
515 #[test]
516 fn empty_lanes_dont_match_zero_snapshot() {
517 let p = tmp("zero-snap");
518 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
519 t.insert(0, 100).unwrap();
520 let m = t.visible_mask(0);
522 assert_eq!(m, 0b1);
523 assert_eq!(m.count_ones(), 1);
524 std::fs::remove_file(&p).ok();
525 }
526
527 #[test]
528 fn simd_matches_scalar_for_boundary_snapshots() {
529 let p = tmp("simd-vs-scalar");
530 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
531 let versions = [5u64, 10, 15, 20, 25, 30, 35, 40,
532 45, 50, 55, 60, 65, 70, 75, 80];
533 for &v in versions.iter() {
534 t.insert(v, v * 100).unwrap();
535 }
536 for snap in [0u64, 10, 35, 80, 100, u64::MAX] {
537 let simd = t.visible_mask(snap);
538 let mut scalar = 0u16;
539 for (i, &v) in versions.iter().enumerate() {
540 if v <= snap { scalar |= 1 << i; }
541 }
542 assert_eq!(simd, scalar, "snap={snap}: simd={simd:#b} scalar={scalar:#b}");
543 }
544 std::fs::remove_file(&p).ok();
545 }
546
547 #[test]
548 fn cross_handle_inserts_visible() {
549 let p = tmp("cross-handle");
550 let writer: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
551 let reader: SharedTimePointTile<u64> = SharedTimePointTile::open(&p).unwrap();
552 writer.insert(10, 100).unwrap();
553 writer.insert(20, 200).unwrap();
554 assert_eq!(reader.visible_count(u64::MAX), 2);
555 let m = reader.visible_mask(15);
556 assert_eq!(m.count_ones(), 1);
557 std::fs::remove_file(&p).ok();
558 }
559
560 #[test]
561 fn disk_persistence_survives_reopen() {
562 let p = tmp("disk-persist");
563 {
564 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
565 t.insert(7, 70).unwrap();
566 t.insert(8, 80).unwrap();
567 t.flush().unwrap();
568 }
569 let t2: SharedTimePointTile<u64> = SharedTimePointTile::open(&p).unwrap();
570 assert_eq!(t2.len(), 2);
571 assert_eq!(t2.visible_count(u64::MAX), 2);
572 assert_eq!(t2.at(0), Some((7, 70)));
573 assert_eq!(t2.at(1), Some((8, 80)));
574 std::fs::remove_file(&p).ok();
575 }
576}