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> {
94 Self::check_layout()?;
95 let total = tile_file_size();
96 let file = OpenOptions::new()
97 .read(true).write(true).create(true).truncate(true)
98 .open(path.as_ref())?;
99 file.set_len(total as u64)?;
100 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
101 let hdr = mmap.as_mut_ptr() as *mut TileHeader;
102 unsafe {
103 std::ptr::write(hdr, TileHeader {
104 magic: TIME_POINT_MAGIC,
105 capacity: TILE_CAP as u32,
106 payload_size: size_of::<T>() as u32,
107 occupied: AtomicU32::new(0),
108 _pad: [0; 44],
109 });
110 }
111 let slots_base = unsafe { mmap.as_mut_ptr().add(size_of::<TileHeader>()) };
112 for i in 0..TILE_CAP {
113 let slot_ptr = unsafe {
114 slots_base.add(i * size_of::<VersionedSlot>()) as *mut VersionedSlot
115 };
116 unsafe {
117 std::ptr::write(slot_ptr, VersionedSlot {
118 version: AtomicU64::new(0),
119 payload: [0; SLOT_PAYLOAD],
120 });
121 }
122 }
123 Ok(Self {
124 _file: file, mmap, _phantom: PhantomData,
125 header_sidecar: subetha_core::HandshakeHeader::new(),
126 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
127 })
128 }
129
130 pub fn open(path: impl AsRef<Path>) -> Result<Self, TileError> {
131 Self::check_layout()?;
132 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
133 if file.metadata()?.len() < tile_file_size() as u64 {
134 return Err(TileError::LayoutMismatch);
135 }
136 let mmap = unsafe { MmapOptions::new().len(tile_file_size()).map_mut(&file)? };
137 let hdr = unsafe { &*(mmap.as_ptr() as *const TileHeader) };
138 if hdr.magic != TIME_POINT_MAGIC
139 || hdr.capacity != TILE_CAP as u32
140 || hdr.payload_size as usize != size_of::<T>()
141 {
142 return Err(TileError::LayoutMismatch);
143 }
144 Ok(Self {
145 _file: file, mmap, _phantom: PhantomData,
146 header_sidecar: subetha_core::HandshakeHeader::new(),
147 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
148 })
149 }
150
151 fn check_layout() -> Result<(), TileError> {
152 if size_of::<T>() > SLOT_PAYLOAD {
153 return Err(TileError::PayloadTooLarge);
154 }
155 if align_of::<T>() > 8 {
156 return Err(TileError::PayloadTooLarge);
157 }
158 Ok(())
159 }
160
161 pub fn header(&self) -> &TileHeader {
162 unsafe { &*(self.mmap.as_ptr() as *const TileHeader) }
163 }
164
165 fn slot(&self, idx: usize) -> &VersionedSlot {
166 let base = unsafe { self.mmap.as_ptr().add(size_of::<TileHeader>()) };
167 unsafe {
168 &*(base.add(idx * size_of::<VersionedSlot>()) as *const VersionedSlot)
169 }
170 }
171
172 pub fn insert(&self, version: u64, value: T) -> Result<usize, TileError> {
175 let header = self.header();
176 loop {
177 let cur = header.occupied.load(Ordering::Acquire);
178 let free = !cur & ((1u32 << TILE_CAP) - 1);
179 if free == 0 {
180 self.ring_sidecar
181 .push_op(crate::sidecar_ops::versioned::OP_PUSH, 1);
182 return Err(TileError::Full);
183 }
184 let lane = free.trailing_zeros() as usize;
185 let new_occupied = cur | (1u32 << lane);
186 if header.occupied.compare_exchange_weak(
187 cur, new_occupied, Ordering::AcqRel, Ordering::Acquire,
188 ).is_ok() {
189 let slot = self.slot(lane);
190 unsafe {
192 let dst = slot.payload.as_ptr() as *mut T;
193 std::ptr::write_unaligned(dst, value);
194 }
195 slot.version.store(version, Ordering::Release);
196 self.ring_sidecar
197 .push_op(crate::sidecar_ops::versioned::OP_PUSH, 0);
198 return Ok(lane);
199 }
200 std::hint::spin_loop();
201 }
202 }
203
204 pub fn remove(&self, lane: usize) {
205 if lane < TILE_CAP {
206 self.header().occupied.fetch_and(!(1u32 << lane), Ordering::AcqRel);
207 }
208 }
209
210 pub fn len(&self) -> usize {
211 self.header().occupied.load(Ordering::Acquire).count_ones() as usize
212 }
213
214 pub fn is_empty(&self) -> bool { self.len() == 0 }
215 pub fn is_full(&self) -> bool {
216 self.header().occupied.load(Ordering::Acquire) == ((1u32 << TILE_CAP) - 1)
217 }
218
219 #[inline]
223 pub fn visible_mask(&self, snapshot: u64) -> u16 {
224 let header = self.header();
225 let occupied = header.occupied.load(Ordering::Acquire) as u16;
226 self.ring_sidecar.push_op(
227 crate::sidecar_ops::versioned::OP_VISIBLE_MASK,
228 if occupied == 0 { 2 } else { 0 },
229 );
230 if occupied == 0 { return 0; }
231 let versions_base = unsafe {
232 self.mmap.as_ptr().add(size_of::<TileHeader>())
233 };
234 let mut versions = [0u64; TILE_CAP];
241 for (i, v) in versions.iter_mut().enumerate() {
242 let slot = unsafe {
243 &*(versions_base.add(i * size_of::<VersionedSlot>()) as *const VersionedSlot)
244 };
245 *v = slot.version.load(Ordering::Acquire);
246 }
247 Self::simd_visible_mask(&versions, snapshot) & occupied
248 }
249
250 #[inline]
255 pub fn simd_visible_mask(versions: &[u64; TILE_CAP], snapshot: u64) -> u16 {
256 #[cfg(target_arch = "x86_64")]
257 {
258 if std::is_x86_feature_detected!("avx512f") {
259 return unsafe { Self::simd_visible_mask_avx512(versions, snapshot) };
261 }
262 if std::is_x86_feature_detected!("avx2") {
263 return unsafe { Self::simd_visible_mask_avx2(versions, snapshot) };
265 }
266 }
267 Self::simd_visible_mask_scalar(versions, snapshot)
268 }
269
270 #[cfg(target_arch = "x86_64")]
280 #[target_feature(enable = "avx512f")]
281 pub unsafe fn simd_visible_mask_avx512(
282 versions: &[u64; TILE_CAP],
283 snapshot: u64,
284 ) -> u16 {
285 use std::arch::x86_64::*;
286 let snap = _mm512_set1_epi64(snapshot as i64);
287 let v_lo = unsafe {
290 _mm512_loadu_si512(versions.as_ptr() as *const __m512i)
291 };
292 let v_hi = unsafe {
293 _mm512_loadu_si512(versions.as_ptr().add(8) as *const __m512i)
294 };
295 let mask_lo: u8 = _mm512_cmple_epu64_mask(v_lo, snap);
296 let mask_hi: u8 = _mm512_cmple_epu64_mask(v_hi, snap);
297 (mask_lo as u16) | ((mask_hi as u16) << 8)
298 }
299
300 #[cfg(target_arch = "x86_64")]
307 #[target_feature(enable = "avx2")]
308 pub unsafe fn simd_visible_mask_avx2(
309 versions: &[u64; TILE_CAP],
310 snapshot: u64,
311 ) -> u16 {
312 use std::arch::x86_64::*;
313 let sign_bit = _mm256_set1_epi64x(i64::MIN);
314 let snap_raw = _mm256_set1_epi64x(snapshot as i64);
315 let snap_s = _mm256_xor_si256(snap_raw, sign_bit);
316 let load_xord = |off: usize| -> __m256i {
319 let raw = unsafe {
320 _mm256_loadu_si256(versions.as_ptr().add(off) as *const __m256i)
321 };
322 _mm256_xor_si256(raw, sign_bit)
323 };
324 let load_raw = |off: usize| -> __m256i {
325 unsafe {
326 _mm256_loadu_si256(versions.as_ptr().add(off) as *const __m256i)
327 }
328 };
329 let v0 = load_xord(0);
330 let v1 = load_xord(4);
331 let v2 = load_xord(8);
332 let v3 = load_xord(12);
333 let raw0 = load_raw(0);
334 let raw1 = load_raw(4);
335 let raw2 = load_raw(8);
336 let raw3 = load_raw(12);
337 let gt0 = _mm256_cmpgt_epi64(snap_s, v0);
338 let gt1 = _mm256_cmpgt_epi64(snap_s, v1);
339 let gt2 = _mm256_cmpgt_epi64(snap_s, v2);
340 let gt3 = _mm256_cmpgt_epi64(snap_s, v3);
341 let eq0 = _mm256_cmpeq_epi64(snap_raw, raw0);
342 let eq1 = _mm256_cmpeq_epi64(snap_raw, raw1);
343 let eq2 = _mm256_cmpeq_epi64(snap_raw, raw2);
344 let eq3 = _mm256_cmpeq_epi64(snap_raw, raw3);
345 let m0 = _mm256_or_si256(gt0, eq0);
346 let m1 = _mm256_or_si256(gt1, eq1);
347 let m2 = _mm256_or_si256(gt2, eq2);
348 let m3 = _mm256_or_si256(gt3, eq3);
349 let bits0 = _mm256_movemask_pd(_mm256_castsi256_pd(m0)) as u16;
350 let bits1 = _mm256_movemask_pd(_mm256_castsi256_pd(m1)) as u16;
351 let bits2 = _mm256_movemask_pd(_mm256_castsi256_pd(m2)) as u16;
352 let bits3 = _mm256_movemask_pd(_mm256_castsi256_pd(m3)) as u16;
353 bits0 | (bits1 << 4) | (bits2 << 8) | (bits3 << 12)
354 }
355
356 #[inline]
359 pub fn simd_visible_mask_scalar(versions: &[u64; TILE_CAP], snapshot: u64) -> u16 {
360 let mut mask = 0u16;
361 for (i, v) in versions.iter().enumerate() {
362 if *v <= snapshot {
363 mask |= 1u16 << i;
364 }
365 }
366 mask
367 }
368
369 pub fn at(&self, lane: usize) -> Option<(u64, T)> {
371 if lane >= TILE_CAP {
372 self.ring_sidecar
373 .push_op(crate::sidecar_ops::versioned::OP_READ_AT, 2);
374 return None;
375 }
376 let occ = self.header().occupied.load(Ordering::Acquire);
377 if (occ >> lane) & 1 == 0 {
378 self.ring_sidecar
379 .push_op(crate::sidecar_ops::versioned::OP_READ_AT, 2);
380 return None;
381 }
382 let slot = self.slot(lane);
383 let v = slot.version.load(Ordering::Acquire);
384 let value: T = unsafe {
385 let src = slot.payload.as_ptr() as *const T;
386 std::ptr::read_unaligned(src)
387 };
388 self.ring_sidecar
389 .push_op(crate::sidecar_ops::versioned::OP_READ_AT, 0);
390 Some((v, value))
391 }
392
393 pub fn visible_count(&self, snapshot: u64) -> u32 {
395 self.visible_mask(snapshot).count_ones()
396 }
397
398 pub fn flush(&self) -> Result<(), TileError> {
399 self.mmap.flush()?;
400 Ok(())
401 }
402
403 pub fn flush_async(&self) -> Result<(), TileError> {
407 self.mmap.flush_async()?;
408 Ok(())
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 fn tmp(name: &str) -> std::path::PathBuf {
417 let mut p = std::env::temp_dir();
418 let pid = std::process::id();
419 p.push(format!("subetha-tile-{name}-{pid}.bin"));
420 p
421 }
422
423 #[test]
424 fn empty_tile_visible_mask_is_zero() {
425 let p = tmp("empty");
426 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
427 assert_eq!(t.visible_mask(u64::MAX), 0);
428 assert!(t.is_empty());
429 std::fs::remove_file(&p).ok();
430 }
431
432 #[test]
433 fn insert_then_visible_at_snapshot() {
434 let p = tmp("visible");
435 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
436 t.insert(10, 100).unwrap();
437 t.insert(20, 200).unwrap();
438 t.insert(30, 300).unwrap();
439 let m = t.visible_mask(25);
441 assert_eq!(m, 0b011);
442 assert_eq!(t.visible_count(25), 2);
443 let m_all = t.visible_mask(u64::MAX);
445 assert_eq!(m_all.count_ones(), 3);
446 std::fs::remove_file(&p).ok();
447 }
448
449 #[test]
450 fn fill_to_capacity_then_overflow() {
451 let p = tmp("fill");
452 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
453 for i in 0..TILE_CAP as u64 {
454 t.insert(i, i * 10).unwrap();
455 }
456 assert!(t.is_full());
457 assert_eq!(t.insert(99, 999).unwrap_err(), TileError::Full);
458 std::fs::remove_file(&p).ok();
459 }
460
461 #[test]
462 fn remove_frees_lane_for_reinsert() {
463 let p = tmp("remove");
464 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
465 let l0 = t.insert(1, 100).unwrap();
466 let l1 = t.insert(2, 200).unwrap();
467 t.remove(l0);
468 assert_eq!(t.len(), 1);
469 let l2 = t.insert(3, 300).unwrap();
470 assert_eq!(l2, l0, "freed lane reused");
471 assert_eq!(t.at(l1), Some((2, 200)));
472 assert_eq!(t.at(l2), Some((3, 300)));
473 std::fs::remove_file(&p).ok();
474 }
475
476 #[test]
477 fn empty_lanes_dont_match_zero_snapshot() {
478 let p = tmp("zero-snap");
479 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
480 t.insert(0, 100).unwrap();
481 let m = t.visible_mask(0);
483 assert_eq!(m, 0b1);
484 assert_eq!(m.count_ones(), 1);
485 std::fs::remove_file(&p).ok();
486 }
487
488 #[test]
489 fn simd_matches_scalar_for_boundary_snapshots() {
490 let p = tmp("simd-vs-scalar");
491 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
492 let versions = [5u64, 10, 15, 20, 25, 30, 35, 40,
493 45, 50, 55, 60, 65, 70, 75, 80];
494 for &v in versions.iter() {
495 t.insert(v, v * 100).unwrap();
496 }
497 for snap in [0u64, 10, 35, 80, 100, u64::MAX] {
498 let simd = t.visible_mask(snap);
499 let mut scalar = 0u16;
500 for (i, &v) in versions.iter().enumerate() {
501 if v <= snap { scalar |= 1 << i; }
502 }
503 assert_eq!(simd, scalar, "snap={snap}: simd={simd:#b} scalar={scalar:#b}");
504 }
505 std::fs::remove_file(&p).ok();
506 }
507
508 #[test]
509 fn cross_handle_inserts_visible() {
510 let p = tmp("cross-handle");
511 let writer: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
512 let reader: SharedTimePointTile<u64> = SharedTimePointTile::open(&p).unwrap();
513 writer.insert(10, 100).unwrap();
514 writer.insert(20, 200).unwrap();
515 assert_eq!(reader.visible_count(u64::MAX), 2);
516 let m = reader.visible_mask(15);
517 assert_eq!(m.count_ones(), 1);
518 std::fs::remove_file(&p).ok();
519 }
520
521 #[test]
522 fn disk_persistence_survives_reopen() {
523 let p = tmp("disk-persist");
524 {
525 let t: SharedTimePointTile<u64> = SharedTimePointTile::create(&p).unwrap();
526 t.insert(7, 70).unwrap();
527 t.insert(8, 80).unwrap();
528 t.flush().unwrap();
529 }
530 let t2: SharedTimePointTile<u64> = SharedTimePointTile::open(&p).unwrap();
531 assert_eq!(t2.len(), 2);
532 assert_eq!(t2.visible_count(u64::MAX), 2);
533 assert_eq!(t2.at(0), Some((7, 70)));
534 assert_eq!(t2.at(1), Some((8, 80)));
535 std::fs::remove_file(&p).ok();
536 }
537}