Skip to main content

subetha_cxc/
shared_time_point.rs

1//! `SharedTimePointTile<T>` - cross-process BSPA + Versioned tile
2//! with AVX2 SIMD snapshot-isolation scan.
3//!
4//! Direct lift of the in-process TimePointTile to MMF. The SIMD
5//! code is unchanged because AVX2 instructions operate on memory
6//! addresses identically whether the address is stack-local or
7//! memory-mapped. Cross-process safety comes from atomic insert
8//! (CAS on the occupied bitmap) and atomic version writes; the
9//! SIMD scan is a pure read (no synchronization needed since
10//! version writes are AtomicU64 with Release semantics).
11//!
12//! # Layout
13//!
14//! ```text
15//! +-----------------------------+
16//! | TileHeader (64B)            |
17//! |   - magic                   |
18//! |   - capacity (always 16)    |
19//! |   - payload_size            |
20//! |   - occupied: AtomicU32     |
21//! +-----------------------------+
22//! | VersionedSlot[0] (64B)      |
23//! |   - version: AtomicU64      |
24//! |   - payload: [u8; 56]       |
25//! +-----------------------------+
26//! | ... 15 more slots           |
27//! +-----------------------------+
28//! ```
29
30use 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    /// Obtain the tile at `path`, initializing an empty one if the path
94    /// does not yet exist and attaching to it if it does. Attaching
95    /// leaves occupied slots and their versions in place; a region
96    /// built for a different payload type is a `LayoutMismatch`.
97    /// [`reset`](Self::reset) reinitializes.
98    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    /// Truncate the tile at `path` and initialize an empty one,
110    /// discarding every slot a live peer holds. For a caller that knows
111    /// it owns the path.
112    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    /// Lay out an empty tile: the zeroed region is already the empty
121    /// slot array (version 0, zero payloads) and occupied 0, so only
122    /// the capacity, payload size and then the magic are written, magic
123    /// last, because attachers spin on it.
124    ///
125    /// # Safety
126    /// `ptr` addresses at least [`tile_file_size()`] writable zeroed
127    /// bytes.
128    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    /// Wrap an initialized region, refusing one built for a different
138    /// payload type.
139    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    /// Atomic insert via CAS on the occupied bitmap. Returns the
186    /// claimed lane index, or `Err(Full)`.
187    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                // SAFETY: lane is now exclusively ours (CAS won).
204                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    /// SIMD scan: return a 16-bit lane mask of entries with version
233    /// <= snapshot AND currently occupied. AVX2 uses the unsigned-
234    /// compare-via-sign-XOR trick because cmpgt_epi64 is signed.
235    #[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        // The versions are at offset 0 of each VersionedSlot. We
248        // need a contiguous u64 array of versions for the SIMD load;
249        // since slots are 64-byte aligned and versions are at slot
250        // offset 0, a naive gather is needed. For simplicity we
251        // copy into a stack buffer; the bench shows this is still
252        // very fast for 16 entries.
253        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    /// SIMD visibility scan dispatcher. Picks AVX-512F (one ZMM
264    /// per 8-lane half + mask-producing `_mm512_cmple_epu64_mask`)
265    /// when present, AVX2 (4 YMM compares with sign-bit XOR trick)
266    /// otherwise, scalar on non-x86 or feature-stripped builds.
267    #[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                // SAFETY: AVX-512F runtime-detected.
273                return unsafe { Self::simd_visible_mask_avx512(versions, snapshot) };
274            }
275            if std::is_x86_feature_detected!("avx2") {
276                // SAFETY: AVX2 runtime-detected.
277                return unsafe { Self::simd_visible_mask_avx2(versions, snapshot) };
278            }
279        }
280        Self::simd_visible_mask_scalar(versions, snapshot)
281    }
282
283    /// AVX-512F path: TILE_CAP=16 covered by two 8-u64 chunks. Each
284    /// chunk uses one `_mm512_loadu_si512` and one
285    /// `_mm512_cmple_epu64_mask` (returns `__mmask8` directly - no
286    /// sign-bit XOR trick needed because the instruction is unsigned
287    /// natively). Two 8-bit masks pack into the 16-bit result via
288    /// `low | (high << 8)`.
289    ///
290    /// # Safety
291    /// Caller must ensure AVX-512F is available.
292    #[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        // SAFETY: versions is 16 contiguous u64s; two 8-u64 loads at
301        // offsets 0 and 8 cover the full tile.
302        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    /// AVX2 path: 4 chunks of 4 u64. Signed `cmpgt_epi64` plus
314    /// sign-bit XOR delivers the unsigned `<=` predicate; `cmpeq`
315    /// handles the equality boundary.
316    ///
317    /// # Safety
318    /// Caller must ensure AVX2 is available.
319    #[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        // SAFETY: versions is 16 contiguous u64s; four 4-u64 loads at
330        // offsets 0, 4, 8, 12 cover the full tile.
331        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    /// Scalar reference: always available, used as the fallback for
370    /// non-x86 builds and as the ground-truth oracle in tests.
371    #[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    /// Read the payload at `lane` if occupied.
383    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    /// Count visible at `snapshot`.
407    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    /// Non-blocking flush: schedules a writeback via the OS.
417    /// Note: Windows is only partially async (sync to page cache,
418    /// not to disk).
419    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    /// A second create attaches with occupied slots in place; reset is
446    /// what strips them.
447    #[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        // Windows refuses to truncate a mapped file, so every handle goes
462        // before the reset.
463        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        // Snapshot 25 sees lanes 0+1 (versions 10, 20).
479        let m = t.visible_mask(25);
480        assert_eq!(m, 0b011);
481        assert_eq!(t.visible_count(25), 2);
482        // Snapshot u64::MAX sees all three.
483        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        // Snapshot 0: only the occupied lane with version 0 matches.
521        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}