Skip to main content

plugmem_arena/
arena.rs

1//! Sharded sorted arena over one flat byte pool.
2//!
3//! Full v2 mechanics of: each shard owns a **chain of
4//! range-partitioned pages** — every page holds a sorted run of keys, pages
5//! in a chain follow each other in ascending key ranges (a B+-tree leaf
6//! level). A full page **splits** in half instead of failing, and
7//! a page emptied by removals is unlinked into a **free-list** for reuse, so
8//! capacity is bounded only by [`ArenaCfg::max_bytes`].
9//!
10//! Chains are *not* assumed to be short. Shard hashing spreads uniform keys,
11//! but an [`ShardMode::Ordered`] arena whose keys share their leading bytes
12//! concentrates everything in one shard — every edge of a hub entity, every
13//! timestamp of a real clock — and that chain grows with the record count. So
14//! the interior level is a flat runtime **page directory** ([`Arena::dir`]):
15//! locating a page is a binary search over it, O(log pages), never a walk. The
16//! directory is derived state, rebuilt by the load walk and absent from the
17//! on-disk image.
18//!
19//! The structure is tuned for **wasm environments first**: `no_std + alloc`,
20//! a single linear byte pool (snapshot = memcpy), no threads, and the one
21//! measured `unsafe` (uninitialized page allocation) whose entire benefit is
22//! on the wasm allocation path — see [`Arena::insert`] internals and the
23//! crate-level documentation for the numbers.
24//!
25//! # Overlay opens
26//!
27//! [`Arena::load_overlay`] opens an arena whose existing pages are **borrowed**
28//! from a longer-lived buffer — typically a memory-mapped file — while the
29//! arena stays fully mutable. Because an arena mutates pages *in place* (slot
30//! shifts, page splits), it cannot use the append-only tail of
31//! [`BlobHeap`](crate::BlobHeap): instead the first write to a borrowed page
32//! copies just *that* page into an owned overlay, and pages grown after open
33//! live in an owned tail (per-page copy-on-write). The borrowed base is never
34//! mutated and never cloned as a whole, so a multi-gigabyte arena can be
35//! *written to* while resident only in the pages it actually touches. Staying
36//! true to the crate's flat philosophy, the overlay is two flat `Vec`s (a
37//! dense redirect and one contiguous copy pool) — no `Box`, no map. See
38//! `examples/overlay.rs`.
39
40use alloc::vec::Vec;
41use core::cmp::Ordering;
42use core::fmt;
43use core::marker::PhantomData;
44
45#[cfg(feature = "counters")]
46use core::cell::Cell;
47
48use crate::error::Error;
49use crate::paged::Paged;
50use crate::slot::Slot;
51
52/// Size of one arena page in bytes.
53///
54/// Fixed, not per-slot-count: whatever the slot size, a page is one
55/// L1-friendly unit of work, and every operation touches O(1) pages.
56pub const PAGE_BYTES: usize = 4096;
57
58/// Sentinel for "no page": an empty chain head, the end of a chain, or an
59/// empty free-list.
60const NONE: u32 = u32::MAX;
61
62/// Fibonacci hashing multiplier (2^64 / phi), used by [`ShardMode::Uniform`].
63const FIB: u64 = 0x9E37_79B9_7F4A_7C15;
64
65/// Fixed prefix of the serialized metadata section (see
66/// [`Arena::dump_meta`]).
67const IMAGE_HEADER: usize = 24;
68
69/// Serialized width of one page index entry (`heads`, `next` — little-endian
70/// `u32`), used to size and offset those metadata arrays.
71const PAGE_IDX_BYTES: usize = core::mem::size_of::<u32>();
72
73/// Serialized width of one page's slot count (`counts` — little-endian `u16`).
74const COUNT_BYTES: usize = core::mem::size_of::<u16>();
75
76/// Serialized bytes of per-page metadata: one `next` index plus one `counts`
77/// entry.
78const PAGE_META_BYTES: usize = PAGE_IDX_BYTES + COUNT_BYTES;
79
80/// `true` when the `counters` feature is enabled; lets hot loops keep their
81/// counting code in one expression that constant-folds away otherwise.
82const COUNT: bool = cfg!(feature = "counters");
83
84/// Adds `$n` to counter field `$field` when the `counters` feature is on;
85/// expands to nothing otherwise (zero cost, and the counter expression is
86/// not even evaluated).
87macro_rules! bump {
88    ($self:ident, $field:ident, $n:expr) => {
89        #[cfg(feature = "counters")]
90        {
91            let mut c = $self.counters.get();
92            c.$field += $n as u64;
93            $self.counters.set(c);
94        }
95    };
96}
97
98/// How keys are mapped to shards.
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub enum ShardMode {
102    /// Shard = Fibonacci hash of the key's leading bytes. Spreads any key
103    /// distribution (including sequential ids) evenly across shards. Global
104    /// iteration order is *not* the key order. Use for lookup tables.
105    Uniform,
106    /// Shard = top bits of the key's leading bytes. Shard index order equals
107    /// key order, so [`Arena::iter`] yields globally ascending keys and
108    /// [`Arena::range`] scans work. Keys arriving in a narrow value range
109    /// concentrate in few shards — commonly in exactly one, since the top bits
110    /// of a real timestamp or of a repeated id prefix are constant. Their
111    /// chains then grow with the record count; the page directory keeps
112    /// locating a page logarithmic anyway, so this costs ordering, not
113    /// asymptotics.
114    Ordered,
115}
116
117/// Arena configuration.
118///
119/// Stored verbatim in snapshots later, so everything that
120/// affects byte interpretation lives here.
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
123pub struct ArenaCfg {
124    /// Number of shards; must be a non-zero power of two.
125    pub shards: usize,
126    /// Hard ceiling for the byte pool; exceeding it fails with
127    /// [`Error::CapacityExceeded`] instead of growing.
128    pub max_bytes: usize,
129    /// Key-to-shard mapping mode.
130    pub mode: ShardMode,
131}
132
133impl ArenaCfg {
134    /// Creates a config with the given shard count and mode, and no byte
135    /// limit (`max_bytes = usize::MAX`).
136    pub const fn new(shards: usize, mode: ShardMode) -> Self {
137        Self {
138            shards,
139            max_bytes: usize::MAX,
140            mode,
141        }
142    }
143
144    /// Returns the config with `max_bytes` replaced.
145    pub const fn with_max_bytes(mut self, max_bytes: usize) -> Self {
146        self.max_bytes = max_bytes;
147        self
148    }
149}
150
151impl Default for ArenaCfg {
152    /// 1024 shards, [`ShardMode::Uniform`], unlimited bytes.
153    fn default() -> Self {
154        Self::new(1024, ShardMode::Uniform)
155    }
156}
157
158/// Deterministic work counters (feature `counters`).
159///
160/// These are the basis of the project's CI performance gates: unlike
161/// wall-clock time they are identical on every machine, so a complexity
162/// regression fails the same way everywhere.
163#[cfg(feature = "counters")]
164#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
165pub struct Counters {
166    /// Key comparisons performed by binary searches.
167    pub cmp_ops: u64,
168    /// Bytes moved by insert/remove shifts and page splits.
169    pub bytes_shifted: u64,
170    /// Pages taken from the pool or the free-list.
171    pub pages_allocated: u64,
172    /// First-key peeks spent locating a page (one cache line each). With the
173    /// page directory this is the binary search's probe count, so it grows
174    /// with the *logarithm* of a shard's page count; a value that grows
175    /// linearly with the record count means the directory stopped being used.
176    pub chain_steps: u64,
177    /// Page splits performed by inserts into full pages.
178    pub splits: u64,
179}
180
181/// A sharded collection of fixed-size records in one contiguous byte pool,
182/// sorted by key prefix within each shard.
183///
184/// See the [crate-level documentation](crate) for the philosophy and the
185/// module documentation for the chain/split mechanics. Highlights:
186///
187/// - all state is `pool` + small metadata arrays — persisting the arena is a
188///   `memcpy` of defined bytes;
189/// - every operation touches O(1) pages: a binary search over the page
190///   directory, then binary search and shifts inside one 4 KiB page — so a
191///   skewed key distribution costs a logarithm, not a scan;
192/// - capacity is bounded only by [`ArenaCfg::max_bytes`] — full pages split,
193///   emptied pages are recycled through a free-list;
194/// - `Arena` deliberately implements neither `Clone` nor `PartialEq`: pages
195///   are allocated **uninitialized** (the measured wasm optimization), and a
196///   byte-wise clone/compare would read the uninitialized tails.
197pub struct Arena<'a, T: Slot> {
198    /// Page pool; length is always a multiple of [`PAGE_BYTES`]. Bytes of a
199    /// page beyond `counts[page] * T::SIZE` are uninitialized and must
200    /// never be read.
201    ///
202    /// A [`Paged`] backing so the arena can either own the pool (`new`/`load` —
203    /// the writable default, and the only shape on wasm32) or borrow a base
204    /// from a longer-lived buffer such as a memory-mapped snapshot
205    /// (`load_borrowed`/`load_overlay` —). The overlay open borrows
206    /// the base read-only and copies just the touched pages up on first write
207    /// (per-page copy-on-write), so a mapped database is mutated without
208    /// cloning it. The backing is `alloc`-only (no `std`/`Arc`), so the crate
209    /// stays `no_std`, and the lifetime `'a` lets the compiler prove the arena
210    /// never outlives its base. The uninitialized-tail invariant is untouched:
211    /// base pages are a fully-initialized dumped image, overlay copies are
212    /// taken from them, and freshly grown pages keep the same
213    /// read-only-up-to-`count` contract as before.
214    pool: Paged<'a, PAGE_BYTES>,
215    /// Shard -> first page of its chain (`NONE` = empty shard).
216    heads: Vec<u32>,
217    /// Page -> successor: the next page in a shard chain, or the next free
218    /// page when the page sits in the free-list.
219    next: Vec<u32>,
220    /// Page -> predecessor in a shard chain. Rebuilt from `next` on load and
221    /// kept in memory so ordered arenas can scan a range newest-first without
222    /// changing the on-disk metadata format.
223    prev: Vec<u32>,
224    /// Page -> number of occupied slots.
225    counts: Vec<u16>,
226    /// Shard -> last page of its chain (`NONE` when empty). Runtime-only
227    /// metadata paired with `heads` for reverse ordered scans.
228    tails: Vec<u32>,
229    /// Every chain page, shard after shard, each shard's run in chain (=
230    /// ascending key) order — the **page directory** that turns locating a
231    /// page into a binary search instead of a walk. See [`Arena::find_page`].
232    ///
233    /// Runtime-only, like `prev`/`tails`: rebuilt by the load walk, absent
234    /// from the on-disk image. Flat by design — one [`PageEntry`] per 4 KiB
235    /// page (≈0.5% overhead), no `Box`, no map.
236    dir: Vec<PageEntry>,
237    /// Shard -> start of that shard's run in `dir`; `shards + 1` entries, the
238    /// last one being `dir.len()`, so shard `s` owns `dir[dir_at[s]..
239    /// dir_at[s + 1]]`.
240    dir_at: Vec<u32>,
241    /// Head of the free-list of recycled pages (linked through `next`).
242    free_head: u32,
243    /// Total records across all shards.
244    total: usize,
245    /// Reusable serialization buffer for [`Arena::insert`] (`T::SIZE` bytes,
246    /// allocated once) — inserts do not allocate after the first call except
247    /// when growing the pool itself.
248    scratch: Vec<u8>,
249    /// Reusable buffer for the cross-page slot moves in [`Arena::split`]. A
250    /// split reads a run of slots out of one page and writes it into another;
251    /// under the overlay backing those may be distinct owned pages, so the
252    /// bytes route through this buffer instead of a single in-pool
253    /// `copy_within`. Grown once; splits are amortized and off the recall path.
254    split_buf: Vec<u8>,
255    cfg: ArenaCfg,
256    #[cfg(feature = "counters")]
257    counters: Cell<Counters>,
258    /// `fn() -> T` keeps the arena `Send`/`Sync`-neutral and avoids bounding
259    /// auto-traits on `T` itself.
260    _marker: PhantomData<fn() -> T>,
261}
262
263/// Bytes of a page's first key cached in the directory. Every key this crate
264/// is used with fits; a wider one is compared on its prefix and disambiguated
265/// against the pool (see [`Arena::dir_cmp`]).
266const DIR_KEY_BYTES: usize = 16;
267
268/// One entry of the page directory: a chain page and a copy of its first key.
269///
270/// The copy is the whole point. A binary search over the directory compares
271/// first keys, and reading each one from the pool means a random access into
272/// a structure sized like the data — several cache misses per lookup, growing
273/// with the arena. Carrying the key here turns the search into a walk over a
274/// compact array: 20 bytes per 4 KiB page, so the directory of a 64 MB arena
275/// is 320 KB and stays in L2 while the pool does not.
276///
277/// The key rides *inside* the directory rather than in a second parallel
278/// vector on purpose: one collection means the same number of allocator calls
279/// the directory already made, and the key sits in the same cache line as the
280/// page id that follows it.
281#[derive(Clone, Copy, Debug)]
282struct PageEntry {
283    /// The page's first key, zero-padded to [`DIR_KEY_BYTES`].
284    key: [u8; DIR_KEY_BYTES],
285    /// The chain page.
286    page: u32,
287}
288
289impl PageEntry {
290    /// An entry whose cached key is not yet known — the caller must follow up
291    /// with [`Arena::refresh_dir_key`] once the page has its first record.
292    fn pending(page: u32) -> Self {
293        Self {
294            key: [0; DIR_KEY_BYTES],
295            page,
296        }
297    }
298}
299
300/// Where a key's record lives (or would live).
301struct Target {
302    /// Page preceding `page` in its chain, `NONE` when `page` is the head.
303    prev: u32,
304    /// The chain page whose key range covers the key.
305    page: u32,
306    /// Index of `page` in [`Arena::dir`] — the mutation paths (split, page
307    /// recycling) update the directory there without searching for it again.
308    at: usize,
309}
310
311impl<'a, T: Slot> Arena<'a, T> {
312    /// Creates an empty arena.
313    ///
314    /// # Errors
315    ///
316    /// - [`Error::BadSlot`] unless `1 <= T::KEY_LEN <= T::SIZE <= PAGE_BYTES`;
317    /// - [`Error::BadShardCount`] unless `cfg.shards` is a non-zero power of
318    ///   two.
319    pub fn new(cfg: ArenaCfg) -> Result<Self, Error> {
320        if T::SIZE == 0 || T::SIZE > PAGE_BYTES || T::KEY_LEN == 0 || T::KEY_LEN > T::SIZE {
321            return Err(Error::BadSlot {
322                size: T::SIZE,
323                key_len: T::KEY_LEN,
324            });
325        }
326        if cfg.shards == 0 || !cfg.shards.is_power_of_two() {
327            return Err(Error::BadShardCount { got: cfg.shards });
328        }
329        Ok(Self {
330            pool: Paged::owned_empty(),
331            heads: alloc::vec![NONE; cfg.shards],
332            next: Vec::new(),
333            prev: Vec::new(),
334            counts: Vec::new(),
335            tails: alloc::vec![NONE; cfg.shards],
336            dir: Vec::new(),
337            dir_at: alloc::vec![0; cfg.shards + 1],
338            free_head: NONE,
339            total: 0,
340            scratch: Vec::new(),
341            split_buf: Vec::new(),
342            cfg,
343            #[cfg(feature = "counters")]
344            counters: Cell::new(Counters::default()),
345            _marker: PhantomData,
346        })
347    }
348
349    /// Number of slots one page holds for this slot size (at least 1).
350    pub const fn slots_per_page() -> usize {
351        PAGE_BYTES / T::SIZE
352    }
353
354    /// Total number of records.
355    pub fn len(&self) -> usize {
356        self.total
357    }
358
359    /// `true` when the arena holds no records.
360    pub fn is_empty(&self) -> bool {
361        self.total == 0
362    }
363
364    /// Bytes currently allocated in the page pool (including recycled
365    /// free-list pages — the pool never shrinks; emptied pages are reused).
366    pub fn pool_bytes(&self) -> usize {
367        self.pool.len()
368    }
369
370    /// The configuration this arena was created with.
371    pub fn cfg(&self) -> &ArenaCfg {
372        &self.cfg
373    }
374
375    /// Inserts a record, keeping its shard's chain sorted by key prefix.
376    ///
377    /// Returns `Ok(false)` if a record with the same key prefix already
378    /// exists (the arena is a map keyed by prefix; existing payload is left
379    /// untouched — use [`Arena::payload_mut`] to update in place). A full
380    /// target page splits in half — inserts never fail on page capacity.
381    ///
382    /// # Errors
383    ///
384    /// [`Error::CapacityExceeded`] if a needed page allocation would cross
385    /// [`ArenaCfg::max_bytes`].
386    pub fn insert(&mut self, value: &T) -> Result<bool, Error> {
387        // Take the scratch buffer out of `self` to sidestep aliasing between
388        // `&self.scratch` and `&mut self.pool` below.
389        let mut buf = core::mem::take(&mut self.scratch);
390        buf.resize(T::SIZE, 0);
391        value.write(&mut buf);
392        let result = self.insert_bytes(&buf);
393        self.scratch = buf;
394        result
395    }
396
397    /// Insert path operating on the serialized slot.
398    fn insert_bytes(&mut self, slot: &[u8]) -> Result<bool, Error> {
399        let key = &slot[..T::KEY_LEN];
400        let shard = self.shard_of(key);
401
402        let target = match self.find_page(shard, key) {
403            Some(t) => t,
404            None => {
405                // Empty shard: allocate its first chain page.
406                let page = self.alloc_page()?;
407                self.heads[shard] = page;
408                self.tails[shard] = page;
409                let at = self.dir_at[shard] as usize;
410                self.dir_insert(shard, at, page);
411                Target {
412                    prev: NONE,
413                    page,
414                    at,
415                }
416            }
417        };
418
419        let mut page = target.page;
420        let mut count = self.counts[page as usize] as usize;
421        let mut pos = {
422            let mut cmps = 0u64;
423            let found = self.search_in(page, count, key, &mut cmps);
424            bump!(self, cmp_ops, cmps);
425            match found {
426                Ok(_) => return Ok(false),
427                Err(pos) => pos,
428            }
429        };
430
431        let mut at = target.at;
432        if count == Self::slots_per_page() {
433            // Split the full page; afterwards (page, pos, count) address the
434            // half that must receive the new record, which is either the page
435            // that split or the fresh one directly after it in the directory.
436            (page, pos, count) = self.split(shard, at, pos)?;
437            if page != self.dir[at].page {
438                at += 1;
439            }
440        }
441
442        let slot_start = pos * T::SIZE;
443        let used_end = count * T::SIZE;
444        let shifted = used_end - slot_start;
445        let bytes = self.pool.page_mut(page);
446        if shifted > 0 {
447            // Shift the sorted tail right by one slot; stays within the page.
448            bytes.copy_within(slot_start..used_end, slot_start + T::SIZE);
449        }
450        bytes[slot_start..slot_start + T::SIZE].copy_from_slice(slot);
451        bump!(self, bytes_shifted, shifted);
452
453        self.counts[page as usize] += 1;
454        self.total += 1;
455        if pos == 0 {
456            // The record now sorts first on its page, so the directory's copy
457            // of that key is stale — including the case of a page that had no
458            // records at all until now.
459            debug_assert_eq!(self.dir[at].page, page);
460            self.refresh_dir_key(at);
461        }
462        Ok(true)
463    }
464
465    /// Splits full `page` — the chain page at directory index `at` of `shard`
466    /// — given the insert position `pos` inside it. Returns
467    /// `(target_page, target_pos, target_count)` for the record that triggered
468    /// the split.
469    fn split(&mut self, shard: usize, at: usize, pos: usize) -> Result<(u32, usize, usize), Error> {
470        let spp = Self::slots_per_page();
471        let page = self.dir[at].page;
472        let fresh = self.alloc_page()?;
473        bump!(self, splits, 1);
474
475        // Link the fresh page right after the split one, in the chain and in
476        // the directory alike.
477        let successor = self.next[page as usize];
478        self.next[fresh as usize] = successor;
479        self.prev[fresh as usize] = page;
480        self.next[page as usize] = fresh;
481        if successor == NONE {
482            self.tails[shard] = fresh;
483        } else {
484            self.prev[successor as usize] = fresh;
485        }
486        self.dir_insert(shard, at + 1, fresh);
487
488        if pos == spp {
489            // Appending past the last key: splitting in half would leave both
490            // pages half empty forever, because nothing will ever sort into
491            // the lower one again. Every id and timestamp this crate stores
492            // arrives ascending, so this is the common case — hand the record
493            // a fresh page and leave the full one full. Pages stay 100% packed
494            // under a monotonic load instead of 50%.
495            return Ok((fresh, 0, 0));
496        }
497
498        if spp == 1 {
499            // Degenerate single-slot pages (T::SIZE > PAGE_BYTES / 2): `pos`
500            // can only be 0 here (`pos == spp` returned above), so the record
501            // sorts before the existing one, which moves to the fresh page.
502            self.move_slots(page, 0, fresh, T::SIZE);
503            bump!(self, bytes_shifted, T::SIZE);
504            self.counts[page as usize] = 0;
505            self.counts[fresh as usize] = 1;
506            self.refresh_dir_key(at + 1);
507            return Ok((page, 0, 0));
508        }
509
510        // Move the upper half [half..spp) into the fresh page.
511        let half = spp / 2;
512        let moved = spp - half;
513        self.move_slots(page, half * T::SIZE, fresh, moved * T::SIZE);
514        bump!(self, bytes_shifted, moved * T::SIZE);
515        self.counts[page as usize] = half as u16;
516        self.counts[fresh as usize] = moved as u16;
517        // The split page keeps its first record, so only the fresh page's
518        // cached key is new.
519        self.refresh_dir_key(at + 1);
520
521        // `pos` was computed against the full page; place the new record in
522        // whichever half now owns that position (pos == half belongs at the
523        // end of the lower page: the key sorts before the fresh page's first).
524        Ok(if pos <= half {
525            (page, pos, half)
526        } else {
527            (fresh, pos - half, moved)
528        })
529    }
530
531    /// Copies `len` bytes from `src_page` (starting at in-page offset
532    /// `src_off`) to the start of `dst_page`. The two pages may be distinct
533    /// owned pages under the overlay backing, so the bytes route through the
534    /// reusable [`Arena::split_buf`] rather than a single in-pool
535    /// `copy_within` — one immutable read of the source, then one write of the
536    /// destination (which copies-up if it is still a borrowed base page).
537    fn move_slots(&mut self, src_page: u32, src_off: usize, dst_page: u32, len: usize) {
538        let mut buf = core::mem::take(&mut self.split_buf);
539        buf.clear();
540        buf.extend_from_slice(&self.pool.page(src_page)[src_off..src_off + len]);
541        self.pool.page_mut(dst_page)[..len].copy_from_slice(&buf);
542        self.split_buf = buf;
543    }
544
545    /// Returns the record with the given key prefix, if present.
546    ///
547    /// # Panics
548    ///
549    /// Panics if `key.len() != T::KEY_LEN` (a caller bug, not a data
550    /// condition — mirrors slice indexing).
551    pub fn get(&self, key: &[u8]) -> Option<T> {
552        self.locate(key).map(|off| {
553            let (page, rel) = (off / PAGE_BYTES, off % PAGE_BYTES);
554            T::read(&self.pool.page(page as u32)[rel..rel + T::SIZE])
555        })
556    }
557
558    /// `true` if a record with the given key prefix exists.
559    ///
560    /// # Panics
561    ///
562    /// Panics if `key.len() != T::KEY_LEN`.
563    pub fn contains(&self, key: &[u8]) -> bool {
564        self.locate(key).is_some()
565    }
566
567    /// Returns the raw bytes of the record with the given key prefix.
568    ///
569    /// # Panics
570    ///
571    /// Panics if `key.len() != T::KEY_LEN`.
572    pub fn get_slot(&self, key: &[u8]) -> Option<&[u8]> {
573        self.locate(key).map(|off| {
574            let (page, rel) = (off / PAGE_BYTES, off % PAGE_BYTES);
575            &self.pool.page(page as u32)[rel..rel + T::SIZE]
576        })
577    }
578
579    /// Returns a mutable view of the record's **payload** (the bytes after
580    /// the key prefix), for in-place updates.
581    ///
582    /// The key prefix itself is not reachable through this method — sorted
583    /// order cannot be corrupted by construction, which is why this is safe
584    /// to expose at all.
585    ///
586    /// # Panics
587    ///
588    /// Panics if `key.len() != T::KEY_LEN`.
589    pub fn payload_mut(&mut self, key: &[u8]) -> Option<&mut [u8]> {
590        let off = self.locate(key)?;
591        let (page, rel) = (off / PAGE_BYTES, off % PAGE_BYTES);
592        Some(&mut self.pool.page_mut(page as u32)[rel + T::KEY_LEN..rel + T::SIZE])
593    }
594
595    /// Removes the record with the given key prefix. Returns `true` if it
596    /// existed. A page emptied by the removal is unlinked from its chain and
597    /// recycled through the free-list.
598    ///
599    /// # Panics
600    ///
601    /// Panics if `key.len() != T::KEY_LEN`.
602    pub fn remove(&mut self, key: &[u8]) -> bool {
603        assert_eq!(key.len(), T::KEY_LEN, "key length must equal Slot::KEY_LEN");
604        let shard = self.shard_of(key);
605        let Some(Target { prev, page, at }) = self.find_page(shard, key) else {
606            return false;
607        };
608        let count = self.counts[page as usize] as usize;
609        let pos = {
610            let mut cmps = 0u64;
611            let found = self.search_in(page, count, key, &mut cmps);
612            bump!(self, cmp_ops, cmps);
613            match found {
614                Ok(pos) => pos,
615                Err(_) => return false,
616            }
617        };
618
619        let slot_start = pos * T::SIZE;
620        let used_end = count * T::SIZE;
621        let tail = used_end - (slot_start + T::SIZE);
622        if tail > 0 {
623            // Shift the tail left over the removed slot; stays within the page.
624            self.pool
625                .page_mut(page)
626                .copy_within(slot_start + T::SIZE..used_end, slot_start);
627        }
628        bump!(self, bytes_shifted, tail);
629        self.counts[page as usize] -= 1;
630        self.total -= 1;
631
632        if self.counts[page as usize] == 0 {
633            // Unlink the emptied page and recycle it.
634            let successor = self.next[page as usize];
635            if prev == NONE {
636                self.heads[shard] = successor;
637            } else {
638                self.next[prev as usize] = successor;
639            }
640            if successor == NONE {
641                self.tails[shard] = prev;
642            } else {
643                self.prev[successor as usize] = prev;
644            }
645            self.next[page as usize] = self.free_head;
646            self.prev[page as usize] = NONE;
647            self.free_head = page;
648            self.dir_remove(shard, at);
649        } else if pos == 0 {
650            // The page survives but its first record is gone, so the
651            // directory's copy of that key no longer names it.
652            self.refresh_dir_key(at);
653        }
654        true
655    }
656
657    /// Iterates all records shard by shard, following each shard's chain.
658    ///
659    /// In [`ShardMode::Ordered`] the shard index order equals key order and
660    /// chains are range-partitioned, so this yields **globally ascending
661    /// keys**. In [`ShardMode::Uniform`] the global order is unspecified
662    /// (each shard is still internally sorted).
663    pub fn iter(&self) -> Iter<'_, T> {
664        Iter {
665            arena: self,
666            shard: 0,
667            page: NONE,
668            idx: 0,
669            remaining: self.total,
670        }
671    }
672
673    /// Iterates records whose key prefix lies in `[from, to)` — `from`
674    /// inclusive, `to` exclusive — in ascending key order.
675    ///
676    /// Only meaningful when shard order equals key order, hence restricted
677    /// to [`ShardMode::Ordered`].
678    ///
679    /// # Panics
680    ///
681    /// Panics if the arena is in [`ShardMode::Uniform`], or if either bound's
682    /// length differs from `T::KEY_LEN`.
683    pub fn range<'s>(&'s self, from: &[u8], to: &'s [u8]) -> Range<'s, T> {
684        assert_eq!(
685            self.cfg.mode,
686            ShardMode::Ordered,
687            "range scans require ShardMode::Ordered"
688        );
689        assert_eq!(
690            from.len(),
691            T::KEY_LEN,
692            "key length must equal Slot::KEY_LEN"
693        );
694        assert_eq!(to.len(), T::KEY_LEN, "key length must equal Slot::KEY_LEN");
695
696        // Position on the first record with key >= `from`.
697        let shard = self.shard_of(from);
698        let mut page = NONE;
699        let mut idx = 0usize;
700        if let Some(t) = self.find_page(shard, from) {
701            let count = self.counts[t.page as usize] as usize;
702            let mut cmps = 0u64;
703            let pos = match self.search_in(t.page, count, from, &mut cmps) {
704                Ok(p) | Err(p) => p,
705            };
706            bump!(self, cmp_ops, cmps);
707            if pos < count {
708                page = t.page;
709                idx = pos;
710            } else {
711                // Past the last record of the covering page: continue with
712                // the next page in this chain, or fall through to the next
713                // shard.
714                page = self.next[t.page as usize];
715            }
716        }
717        Range {
718            arena: self,
719            // The shard the iterator moves to once the current chain (if
720            // any) is exhausted.
721            shard: shard + 1,
722            page,
723            idx,
724            to,
725        }
726    }
727
728    /// Iterates records whose key prefix lies in `[from, to)` — `from`
729    /// inclusive, `to` exclusive — in descending key order.
730    ///
731    /// This is the bounded newest-first counterpart to [`Arena::range`]. It
732    /// is restricted to ordered arenas and keeps the predecessor links in
733    /// runtime metadata, so the on-disk arena format remains unchanged.
734    pub fn range_rev<'s>(&'s self, from: &'s [u8], to: &'s [u8]) -> RangeRev<'s, T> {
735        assert_eq!(
736            self.cfg.mode,
737            ShardMode::Ordered,
738            "reverse range scans require ShardMode::Ordered"
739        );
740        assert_eq!(
741            from.len(),
742            T::KEY_LEN,
743            "key length must equal Slot::KEY_LEN"
744        );
745        assert_eq!(to.len(), T::KEY_LEN, "key length must equal Slot::KEY_LEN");
746
747        if from >= to {
748            return RangeRev {
749                arena: self,
750                shard: 0,
751                page: NONE,
752                idx: 0,
753                from,
754            };
755        }
756
757        // Position immediately before `to`: an exact key is excluded, while
758        // an insertion point after the page's last key starts at that last
759        // key. If the covering page has no key below `to`, walk to its
760        // predecessor.
761        let shard = self.shard_of(to);
762        let mut page = NONE;
763        let mut idx = 0usize;
764        if let Some(t) = self.find_page(shard, to) {
765            let count = self.counts[t.page as usize] as usize;
766            let mut cmps = 0u64;
767            let pos = match self.search_in(t.page, count, to, &mut cmps) {
768                Ok(p) | Err(p) => p,
769            };
770            bump!(self, cmp_ops, cmps);
771            if pos > 0 {
772                page = t.page;
773                idx = pos;
774            } else {
775                page = self.prev[t.page as usize];
776                if page != NONE {
777                    idx = self.counts[page as usize] as usize;
778                }
779            }
780        }
781
782        RangeRev {
783            arena: self,
784            // The shard the iterator moves to once the current chain (if
785            // any) is exhausted.
786            shard,
787            page,
788            idx,
789            from,
790        }
791    }
792
793    /// A snapshot of the work counters (feature `counters`).
794    #[cfg(feature = "counters")]
795    pub fn counters(&self) -> Counters {
796        self.counters.get()
797    }
798
799    /// Resets all work counters to zero (feature `counters`).
800    #[cfg(feature = "counters")]
801    pub fn reset_counters(&self) {
802        self.counters.set(Counters::default());
803    }
804
805    /// Appends the arena's metadata section to `out`.
806    ///
807    /// Layout (all little-endian): `[shards u32][pages u32][free_head u32]
808    /// [total u64][mode u8][reserved 3]`, then `heads` (`shards × u32`),
809    /// `next` (`pages × u32`), `counts` (`pages × u16`). Together with
810    /// [`Arena::dump_pool`] this is the complete state; both dumps are
811    /// canonical — dump → [`Arena::load`] → dump reproduces identical
812    /// bytes.
813    pub fn dump_meta(&self, out: &mut Vec<u8>) {
814        let pages = self.pool.len() / PAGE_BYTES;
815        debug_assert!(self.cfg.shards <= u32::MAX as usize && pages < u32::MAX as usize);
816        out.reserve(IMAGE_HEADER + self.heads.len() * PAGE_IDX_BYTES + pages * PAGE_META_BYTES);
817        out.extend_from_slice(&(self.cfg.shards as u32).to_le_bytes());
818        out.extend_from_slice(&(pages as u32).to_le_bytes());
819        out.extend_from_slice(&self.free_head.to_le_bytes());
820        out.extend_from_slice(&(self.total as u64).to_le_bytes());
821        out.push(match self.cfg.mode {
822            ShardMode::Uniform => 0,
823            ShardMode::Ordered => 1,
824        });
825        out.extend_from_slice(&[0u8; 3]);
826        for &head in &self.heads {
827            out.extend_from_slice(&head.to_le_bytes());
828        }
829        for &next in &self.next {
830            out.extend_from_slice(&next.to_le_bytes());
831        }
832        for &count in &self.counts {
833            out.extend_from_slice(&count.to_le_bytes());
834        }
835    }
836
837    /// Appends the arena's pool section to `out`.
838    ///
839    /// Each page contributes its initialized prefix (`counts[page] ×
840    /// SIZE` bytes) followed by zero padding to [`PAGE_BYTES`]: the
841    /// uninitialized page tails (see [`Arena::insert`] internals) are
842    /// never read, and zero-filling them makes the image canonical and
843    /// leak-free.
844    pub fn dump_pool(&self, out: &mut Vec<u8>) {
845        out.reserve(self.pool.len());
846        for (page, &count) in self.counts.iter().enumerate() {
847            let used = count as usize * T::SIZE;
848            out.extend_from_slice(&self.pool.page(page as u32)[..used]);
849            out.resize(out.len() + (PAGE_BYTES - used), 0);
850        }
851    }
852
853    /// Rebuilds an arena from its two dumped sections.
854    ///
855    /// The input is **untrusted**: every metadata invariant is checked
856    /// before adoption and any inconsistency returns
857    /// [`Error::Corrupt`] — this method never panics on arbitrary bytes.
858    /// Cost is O(pages) on top of the pool copy:
859    ///
860    /// - exact section lengths; `cfg` agreement (shards, mode);
861    /// - per-page slot counts within `PAGE_BYTES / SIZE`;
862    /// - every page reachable exactly once — shard chains and the
863    ///   free-list are walked with a visited bitmap (no cycles, no shared
864    ///   or orphan pages); chain pages are non-empty, free pages empty;
865    /// - chain pages ascend by key range and sit in the shard their first
866    ///   key maps to; the record total matches the page counts.
867    ///
868    /// Slot *content* is not validated beyond the per-page first/last key
869    /// reads — record payloads are semantically validated lazily by the
870    /// owning engine.
871    ///
872    /// # Errors
873    ///
874    /// [`Error::BadSlot`] / [`Error::BadShardCount`] for an invalid `cfg`
875    /// (same gates as [`Arena::new`]); [`Error::Corrupt`] for any image
876    /// inconsistency.
877    pub fn load(cfg: ArenaCfg, meta: &[u8], pool: &[u8]) -> Result<Self, Error> {
878        Self::load_impl(cfg, meta, pool, Paged::owned_from(pool.to_vec()))
879    }
880
881    /// Rebuilds an arena that **borrows** its page pool from a longer-lived
882    /// buffer (a memory-mapped snapshot) instead of copying it.
883    /// Validation is identical to [`Arena::load`] — it reads each page's
884    /// first and last keys, so it touches one OS page per arena page (the
885    /// key-order check needs them), but no pool byte is copied.
886    ///
887    /// # Errors
888    ///
889    /// Same as [`Arena::load`].
890    pub fn load_borrowed(cfg: ArenaCfg, meta: &[u8], pool: &'a [u8]) -> Result<Self, Error> {
891        Self::load_impl(cfg, meta, pool, Paged::borrowed(pool))
892    }
893
894    /// Opens an arena over a borrowed base for the **overlay** write path: the
895    /// base pages are mapped read-only, and the first write to any base page
896    /// copies just that page into owned storage (per-page copy-on-write, see
897    /// `Paged`), while pages grown after open live in an owned
898    /// tail. Unlike [`Arena::load_borrowed`] the returned arena is fully
899    /// mutable — inserts, removals and splits work — yet the borrowed base is
900    /// never cloned as a whole and never mutated, so a memory-mapped database
901    /// can be written to while resident only in the pages it actually touches.
902    /// Validation is identical to [`Arena::load`].
903    ///
904    /// # Errors
905    ///
906    /// Same as [`Arena::load`].
907    pub fn load_overlay(cfg: ArenaCfg, meta: &[u8], pool: &'a [u8]) -> Result<Self, Error> {
908        Self::load_impl(cfg, meta, pool, Paged::borrowed(pool))
909    }
910
911    /// Shared body of [`Arena::load`] / [`Arena::load_borrowed`] /
912    /// [`Arena::load_overlay`]: validates the untrusted `meta`/`pool` image and
913    /// adopts `backing` as the pool.
914    fn load_impl(
915        cfg: ArenaCfg,
916        meta: &[u8],
917        pool: &[u8],
918        backing: Paged<'a, PAGE_BYTES>,
919    ) -> Result<Self, Error> {
920        let mut arena = Self::new(cfg)?;
921        if meta.len() < IMAGE_HEADER {
922            return Err(Error::Corrupt("arena meta shorter than its header"));
923        }
924        let shards = u32::from_le_bytes(meta[0..4].try_into().unwrap()) as usize;
925        let pages = u32::from_le_bytes(meta[4..8].try_into().unwrap()) as usize;
926        let free_head = u32::from_le_bytes(meta[8..12].try_into().unwrap());
927        let total = u64::from_le_bytes(meta[12..20].try_into().unwrap());
928        let mode = meta[20];
929        if meta[21..24] != [0u8; 3] {
930            return Err(Error::Corrupt("arena meta reserved bytes must be zero"));
931        }
932        if shards != cfg.shards {
933            return Err(Error::Corrupt("arena meta shard count disagrees with cfg"));
934        }
935        let want_mode = match cfg.mode {
936            ShardMode::Uniform => 0u8,
937            ShardMode::Ordered => 1,
938        };
939        if mode != want_mode {
940            return Err(Error::Corrupt("arena meta shard mode disagrees with cfg"));
941        }
942        if pages as u64 >= u64::from(NONE) {
943            return Err(Error::Corrupt("arena page count overflows the index space"));
944        }
945        let want_meta = IMAGE_HEADER as u64
946            + shards as u64 * PAGE_IDX_BYTES as u64
947            + pages as u64 * PAGE_META_BYTES as u64;
948        if meta.len() as u64 != want_meta {
949            return Err(Error::Corrupt("arena meta length mismatch"));
950        }
951        if pool.len() as u64 != pages as u64 * PAGE_BYTES as u64 {
952            return Err(Error::Corrupt("arena pool length mismatch"));
953        }
954
955        let mut heads = Vec::with_capacity(shards);
956        for i in 0..shards {
957            let at = IMAGE_HEADER + i * PAGE_IDX_BYTES;
958            heads.push(u32::from_le_bytes(
959                meta[at..at + PAGE_IDX_BYTES].try_into().unwrap(),
960            ));
961        }
962        let next_base = IMAGE_HEADER + shards * PAGE_IDX_BYTES;
963        let mut next = Vec::with_capacity(pages);
964        for i in 0..pages {
965            let at = next_base + i * PAGE_IDX_BYTES;
966            next.push(u32::from_le_bytes(
967                meta[at..at + PAGE_IDX_BYTES].try_into().unwrap(),
968            ));
969        }
970        let counts_base = next_base + pages * PAGE_IDX_BYTES;
971        let mut counts = Vec::with_capacity(pages);
972        for i in 0..pages {
973            let at = counts_base + i * COUNT_BYTES;
974            counts.push(u16::from_le_bytes(
975                meta[at..at + COUNT_BYTES].try_into().unwrap(),
976            ));
977        }
978        let mut prev_links = alloc::vec![NONE; pages];
979        let mut tails = alloc::vec![NONE; shards];
980
981        let spp = Self::slots_per_page();
982        if counts.iter().any(|&c| c as usize > spp) {
983            return Err(Error::Corrupt("arena page count exceeds slots per page"));
984        }
985
986        // Reachability walk: every page must appear exactly once across all
987        // shard chains and the free-list; the bitmap catches cycles, shared
988        // pages and (after the walks) orphans.
989        let mut seen = alloc::vec![false; pages];
990        let mut live = 0u64;
991        // The walk already visits every chain page in shard-then-key order —
992        // exactly the page directory's contents, so it is built here for free.
993        let mut dir = Vec::with_capacity(pages);
994        let mut dir_at = alloc::vec![0u32; shards + 1];
995        for (shard, &head) in heads.iter().enumerate() {
996            dir_at[shard] = dir.len() as u32;
997            let mut page = head;
998            let mut predecessor = NONE;
999            while page != NONE {
1000                let p = page as usize;
1001                if p >= pages {
1002                    return Err(Error::Corrupt("arena chain page out of bounds"));
1003                }
1004                if core::mem::replace(&mut seen[p], true) {
1005                    return Err(Error::Corrupt("arena page linked more than once"));
1006                }
1007                let count = counts[p] as usize;
1008                if count == 0 {
1009                    return Err(Error::Corrupt("arena chain contains an empty page"));
1010                }
1011                live += count as u64;
1012                let first = &pool[p * PAGE_BYTES..p * PAGE_BYTES + T::KEY_LEN];
1013                if arena.shard_of(first) != shard {
1014                    return Err(Error::Corrupt("arena page sits in the wrong shard"));
1015                }
1016                if predecessor != NONE {
1017                    let pr = predecessor as usize;
1018                    let last_at = pr * PAGE_BYTES + (counts[pr] as usize - 1) * T::SIZE;
1019                    if pool[last_at..last_at + T::KEY_LEN] >= *first {
1020                        return Err(Error::Corrupt("arena chain pages out of key order"));
1021                    }
1022                }
1023                prev_links[p] = predecessor;
1024                if next[p] == NONE {
1025                    tails[shard] = page;
1026                }
1027                // `first` is this page's first key, already read and validated
1028                // above — the directory's cached copy costs nothing here.
1029                let mut cached = [0u8; DIR_KEY_BYTES];
1030                let n = T::KEY_LEN.min(DIR_KEY_BYTES);
1031                cached[..n].copy_from_slice(&first[..n]);
1032                dir.push(PageEntry { key: cached, page });
1033                predecessor = page;
1034                page = next[p];
1035            }
1036        }
1037        dir_at[shards] = dir.len() as u32;
1038        let mut page = free_head;
1039        while page != NONE {
1040            let p = page as usize;
1041            if p >= pages {
1042                return Err(Error::Corrupt("arena free page out of bounds"));
1043            }
1044            if core::mem::replace(&mut seen[p], true) {
1045                return Err(Error::Corrupt("arena page linked more than once"));
1046            }
1047            if counts[p] != 0 {
1048                return Err(Error::Corrupt("arena free page has a nonzero count"));
1049            }
1050            page = next[p];
1051        }
1052        if seen.iter().any(|&s| !s) {
1053            return Err(Error::Corrupt("arena has an orphan page"));
1054        }
1055        if live != total {
1056            return Err(Error::Corrupt(
1057                "arena record total disagrees with page counts",
1058            ));
1059        }
1060
1061        arena.pool = backing;
1062        arena.heads = heads;
1063        arena.next = next;
1064        arena.prev = prev_links;
1065        arena.counts = counts;
1066        arena.tails = tails;
1067        arena.dir = dir;
1068        arena.dir_at = dir_at;
1069        arena.free_head = free_head;
1070        arena.total = total as usize;
1071        Ok(arena)
1072    }
1073
1074    /// Locates the chain page whose key range covers `key`. `None` when the
1075    /// shard is empty.
1076    ///
1077    /// A shard's pages are range-partitioned and ascending, so the covering
1078    /// page is the **last** one whose first key is `<= key` (or the head, when
1079    /// `key` sorts below every page). The directory holds exactly that
1080    /// sequence, so this is a binary search over `dir`: O(log pages) first-key
1081    /// peeks instead of one per page.
1082    ///
1083    /// That difference is the whole point of the directory. A chain is short
1084    /// only while the shard hash spreads the load; an `Ordered` arena whose
1085    /// keys share their leading 8 bytes (every edge of one hub entity, every
1086    /// timestamp of a real clock) concentrates in a single shard, and a walk
1087    /// there costs O(records) per operation — quadratic over a load.
1088    fn find_page(&self, shard: usize, key: &[u8]) -> Option<Target> {
1089        let (lo, hi) = self.dir_run(shard);
1090        if lo == hi {
1091            debug_assert_eq!(self.heads[shard], NONE, "empty directory run, live chain");
1092            return None;
1093        }
1094        // Partition point of `first_key(page) <= key` over the run *after* the
1095        // head: the head covers everything below the second page's first key,
1096        // so it never needs a probe — and a single-page shard costs nothing.
1097        let mut steps = 0u64;
1098        let (mut left, mut right) = (lo + 1, hi);
1099        while left < right {
1100            let mid = left + (right - left) / 2;
1101            steps += COUNT as u64;
1102            if self.dir_cmp(&self.dir[mid], key).is_le() {
1103                left = mid + 1;
1104            } else {
1105                right = mid;
1106            }
1107        }
1108        bump!(self, chain_steps, steps);
1109        let _ = steps; // read only by the counters feature
1110        let at = left - 1;
1111        self.debug_assert_dir_key(at);
1112        Some(Target {
1113            prev: if at > lo { self.dir[at - 1].page } else { NONE },
1114            page: self.dir[at].page,
1115            at,
1116        })
1117    }
1118
1119    /// Orders a directory entry's first key against `key`.
1120    ///
1121    /// The cached copy answers this outright for every key that fits in
1122    /// [`DIR_KEY_BYTES`], which is every key this crate is used with. A wider
1123    /// `Slot` still works: its entries are ordered on the cached prefix, and
1124    /// only a tie there — pages whose first keys share their leading 16 bytes
1125    /// — costs the pool read the cache exists to avoid.
1126    fn dir_cmp(&self, entry: &PageEntry, key: &[u8]) -> Ordering {
1127        let n = T::KEY_LEN.min(DIR_KEY_BYTES);
1128        match entry.key[..n].cmp(&key[..n]) {
1129            Ordering::Equal if T::KEY_LEN > DIR_KEY_BYTES => self.first_key(entry.page).cmp(key),
1130            other => other,
1131        }
1132    }
1133
1134    /// Re-reads the cached first key of directory index `at` from the pool.
1135    /// Called wherever a mutation can change which record sits first on a
1136    /// page: a fresh page receiving its first record, an insert at position
1137    /// zero, a removal of position zero, or a split moving records across.
1138    fn refresh_dir_key(&mut self, at: usize) {
1139        let page = self.dir[at].page;
1140        let mut key = [0u8; DIR_KEY_BYTES];
1141        let n = T::KEY_LEN.min(DIR_KEY_BYTES);
1142        key[..n].copy_from_slice(&self.first_key(page)[..n]);
1143        self.dir[at].key = key;
1144    }
1145
1146    /// Asserts that one directory entry's cached key still matches its page.
1147    ///
1148    /// Debug builds only: the directory is derived state, and a stale entry
1149    /// misdirects a lookup instead of failing loudly. Checking the single
1150    /// entry a search just landed on — rather than sweeping the directory
1151    /// after every mutation — catches the same staleness, because a stale
1152    /// entry has to be *used* to do harm, and it keeps the check O(1) instead
1153    /// of making a debug-build load quadratic.
1154    fn debug_assert_dir_key(&self, at: usize) {
1155        #[cfg(debug_assertions)]
1156        {
1157            let n = T::KEY_LEN.min(DIR_KEY_BYTES);
1158            let entry = &self.dir[at];
1159            debug_assert_eq!(
1160                &entry.key[..n],
1161                &self.first_key(entry.page)[..n],
1162                "page directory key is stale for page {}",
1163                entry.page
1164            );
1165        }
1166        let _ = at;
1167    }
1168
1169    /// The half-open `dir` range owned by `shard`.
1170    fn dir_run(&self, shard: usize) -> (usize, usize) {
1171        (self.dir_at[shard] as usize, self.dir_at[shard + 1] as usize)
1172    }
1173
1174    /// Records `page` at directory index `at`, which must be inside (or at the
1175    /// end of) `shard`'s run. Shifts the following runs by one.
1176    fn dir_insert(&mut self, shard: usize, at: usize, page: u32) {
1177        debug_assert!(self.dir_at[shard] as usize <= at && at <= self.dir_at[shard + 1] as usize);
1178        self.dir.insert(at, PageEntry::pending(page));
1179        for start in &mut self.dir_at[shard + 1..] {
1180            *start += 1;
1181        }
1182    }
1183
1184    /// Drops directory index `at` from `shard`'s run.
1185    fn dir_remove(&mut self, shard: usize, at: usize) {
1186        debug_assert!(self.dir_at[shard] as usize <= at && at < self.dir_at[shard + 1] as usize);
1187        self.dir.remove(at);
1188        for start in &mut self.dir_at[shard + 1..] {
1189            *start -= 1;
1190        }
1191    }
1192
1193    /// First key of a page. Caller guarantees the page is non-empty (every
1194    /// page in a chain holds at least one record — emptied pages are
1195    /// unlinked immediately).
1196    fn first_key(&self, page: u32) -> &[u8] {
1197        &self.pool.page(page)[..T::KEY_LEN]
1198    }
1199
1200    /// Binary search inside a page; wraps the free-function search with the
1201    /// page slice resolution.
1202    fn search_in(
1203        &self,
1204        page: u32,
1205        count: usize,
1206        key: &[u8],
1207        cmps: &mut u64,
1208    ) -> Result<usize, usize> {
1209        search::<T>(self.pool.page(page), count, key, cmps)
1210    }
1211
1212    /// Byte offset of the slot with the given key, if present.
1213    fn locate(&self, key: &[u8]) -> Option<usize> {
1214        assert_eq!(key.len(), T::KEY_LEN, "key length must equal Slot::KEY_LEN");
1215        let shard = self.shard_of(key);
1216        let Target { page, .. } = self.find_page(shard, key)?;
1217        let count = self.counts[page as usize] as usize;
1218        let mut cmps = 0u64;
1219        let found = self.search_in(page, count, key, &mut cmps).ok();
1220        bump!(self, cmp_ops, cmps);
1221        found.map(|pos| page as usize * PAGE_BYTES + pos * T::SIZE)
1222    }
1223
1224    /// Maps a key to its shard. Only the first 8 key bytes participate;
1225    /// longer keys sharing an 8-byte prefix land in the same shard (harmless
1226    /// for `Ordered`: order across shards is still by prefix).
1227    fn shard_of(&self, key: &[u8]) -> usize {
1228        let mut pad = [0u8; 8];
1229        let n = key.len().min(8);
1230        pad[..n].copy_from_slice(&key[..n]);
1231        let v = u64::from_be_bytes(pad);
1232        let bits = self.cfg.shards.trailing_zeros();
1233        if bits == 0 {
1234            return 0; // single shard; also avoids the undefined `v >> 64`
1235        }
1236        let h = match self.cfg.mode {
1237            ShardMode::Ordered => v,
1238            ShardMode::Uniform => v.wrapping_mul(FIB),
1239        };
1240        (h >> (64 - bits)) as usize
1241    }
1242
1243    /// Takes a page from the free-list, or grows the pool by one page.
1244    fn alloc_page(&mut self) -> Result<u32, Error> {
1245        if self.free_head != NONE {
1246            let page = self.free_head;
1247            self.free_head = self.next[page as usize];
1248            self.next[page as usize] = NONE;
1249            self.prev[page as usize] = NONE;
1250            self.counts[page as usize] = 0;
1251            bump!(self, pages_allocated, 1);
1252            return Ok(page);
1253        }
1254
1255        let old_len = self.pool.len();
1256        let new_len = old_len + PAGE_BYTES;
1257        if new_len > self.cfg.max_bytes {
1258            return Err(Error::CapacityExceeded {
1259                max_bytes: self.cfg.max_bytes,
1260            });
1261        }
1262        let page = (old_len / PAGE_BYTES) as u32;
1263        // Grow the owned tail by one page: the whole vector when owned, the
1264        // overlay's grown tail when borrowing (the borrowed base is never
1265        // resized). `grown_tail_mut` hands back that owned `Vec` directly — no
1266        // clone of the base — and the new page's global index is `old_len /
1267        // PAGE_BYTES` regardless of which tail holds it.
1268        let tail = self.pool.grown_tail_mut();
1269        let tail_len = tail.len() + PAGE_BYTES;
1270        tail.reserve(PAGE_BYTES);
1271        // SAFETY: the new page is left uninitialized on purpose — this is the
1272        // one measured unsafe of the crate. Zeroing fresh pages was benched
1273        // at 12x slower on the wasm allocation path (wasmtime, 32k pages:
1274        // 3889 us zeroed vs 316 us uninit; native: noise) — and wasm is this
1275        // structure's primary environment. The invariant making it sound:
1276        // *no byte of a page beyond `counts[page] * T::SIZE` is ever read*.
1277        // Every read (search, get, iter, range, first_key, shifts) is
1278        // bounded by the page count, and a slot's bytes are fully written
1279        // before the count is incremented. Consequently `Arena` exposes no
1280        // whole-pool reads (no `Clone`/`PartialEq`/`as_bytes`); the snapshot
1281        // writer emits only the initialized prefixes of pages.
1282        // Routing the same reserve+set_len at the grown tail (not the base)
1283        // keeps this the crate's sole `unsafe` — the overlay adds none.
1284        #[allow(clippy::uninit_vec)]
1285        unsafe {
1286            tail.set_len(tail_len);
1287        }
1288        self.next.push(NONE);
1289        self.prev.push(NONE);
1290        self.counts.push(0);
1291        bump!(self, pages_allocated, 1);
1292        Ok(page)
1293    }
1294}
1295
1296/// Binary search over a page's occupied slots, comparing key prefixes.
1297///
1298/// Free function (not a method) so the borrow of the page slice stays
1299/// independent from `&mut self` at call sites. Counting compiles away when
1300/// the `counters` feature is off (`COUNT` is a const `false`).
1301fn search<T: Slot>(page: &[u8], count: usize, key: &[u8], cmps: &mut u64) -> Result<usize, usize> {
1302    let mut lo = 0usize;
1303    let mut hi = count;
1304    while lo < hi {
1305        let mid = lo + (hi - lo) / 2;
1306        let off = mid * T::SIZE;
1307        // Branchless: adds 0 when the `counters` feature is off, which the
1308        // compiler folds away entirely.
1309        *cmps += COUNT as u64;
1310        match page[off..off + T::KEY_LEN].cmp(key) {
1311            core::cmp::Ordering::Less => lo = mid + 1,
1312            core::cmp::Ordering::Greater => hi = mid,
1313            core::cmp::Ordering::Equal => return Ok(mid),
1314        }
1315    }
1316    Err(lo)
1317}
1318
1319impl<T: Slot> fmt::Debug for Arena<'_, T> {
1320    /// Summary only — dumping the pool would both flood output and read
1321    /// uninitialized page tails.
1322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1323        f.debug_struct("Arena")
1324            .field("len", &self.total)
1325            .field("shards", &self.cfg.shards)
1326            .field("mode", &self.cfg.mode)
1327            .field("pages", &(self.pool.len() / PAGE_BYTES))
1328            .field("slot_size", &T::SIZE)
1329            .finish()
1330    }
1331}
1332
1333/// Iterator over all records of an [`Arena`]; see [`Arena::iter`] for
1334/// ordering guarantees.
1335pub struct Iter<'a, T: Slot> {
1336    arena: &'a Arena<'a, T>,
1337    shard: usize,
1338    /// Current chain page; `NONE` means "advance to the next shard's head".
1339    page: u32,
1340    idx: usize,
1341    remaining: usize,
1342}
1343
1344impl<T: Slot> Iterator for Iter<'_, T> {
1345    type Item = T;
1346
1347    fn next(&mut self) -> Option<T> {
1348        loop {
1349            if self.page == NONE {
1350                if self.shard >= self.arena.cfg.shards {
1351                    return None;
1352                }
1353                self.page = self.arena.heads[self.shard];
1354                self.idx = 0;
1355                self.shard += 1;
1356                continue;
1357            }
1358            if self.idx < self.arena.counts[self.page as usize] as usize {
1359                let rel = self.idx * T::SIZE;
1360                self.idx += 1;
1361                self.remaining -= 1;
1362                return Some(T::read(
1363                    &self.arena.pool.page(self.page)[rel..rel + T::SIZE],
1364                ));
1365            }
1366            self.page = self.arena.next[self.page as usize];
1367            self.idx = 0;
1368        }
1369    }
1370
1371    fn size_hint(&self) -> (usize, Option<usize>) {
1372        (self.remaining, Some(self.remaining))
1373    }
1374}
1375
1376impl<T: Slot> ExactSizeIterator for Iter<'_, T> {}
1377
1378impl<'a, T: Slot> IntoIterator for &'a Arena<'a, T> {
1379    type Item = T;
1380    type IntoIter = Iter<'a, T>;
1381
1382    fn into_iter(self) -> Self::IntoIter {
1383        self.iter()
1384    }
1385}
1386
1387/// Iterator over a key range of an [`Arena`]; see [`Arena::range`].
1388pub struct Range<'a, T: Slot> {
1389    arena: &'a Arena<'a, T>,
1390    shard: usize,
1391    /// Current chain page; `NONE` means "advance to the next shard's head".
1392    page: u32,
1393    idx: usize,
1394    /// Exclusive upper bound on key prefixes.
1395    to: &'a [u8],
1396}
1397
1398impl<T: Slot> Iterator for Range<'_, T> {
1399    type Item = T;
1400
1401    fn next(&mut self) -> Option<T> {
1402        loop {
1403            if self.page == NONE {
1404                if self.shard >= self.arena.cfg.shards {
1405                    return None;
1406                }
1407                self.page = self.arena.heads[self.shard];
1408                self.idx = 0;
1409                self.shard += 1;
1410                continue;
1411            }
1412            if self.idx < self.arena.counts[self.page as usize] as usize {
1413                let rel = self.idx * T::SIZE;
1414                let page = self.arena.pool.page(self.page);
1415                if page[rel..rel + T::KEY_LEN] >= *self.to {
1416                    // Keys only grow from here on — the scan is complete.
1417                    return None;
1418                }
1419                self.idx += 1;
1420                return Some(T::read(&page[rel..rel + T::SIZE]));
1421            }
1422            self.page = self.arena.next[self.page as usize];
1423            self.idx = 0;
1424        }
1425    }
1426}
1427
1428/// Iterator over a key range of an [`Arena`] in descending order; see
1429/// [`Arena::range_rev`].
1430pub struct RangeRev<'a, T: Slot> {
1431    arena: &'a Arena<'a, T>,
1432    /// The current shard. Lower shards are visited after the current chain.
1433    shard: usize,
1434    /// Current chain page; `NONE` means "advance to the previous shard's
1435    /// tail".
1436    page: u32,
1437    /// Exclusive upper slot index in the current page.
1438    idx: usize,
1439    /// Inclusive lower key bound.
1440    from: &'a [u8],
1441}
1442
1443impl<T: Slot> Iterator for RangeRev<'_, T> {
1444    type Item = T;
1445
1446    fn next(&mut self) -> Option<T> {
1447        loop {
1448            if self.page == NONE {
1449                if self.shard == 0 {
1450                    return None;
1451                }
1452                self.shard -= 1;
1453                self.page = self.arena.tails[self.shard];
1454                self.idx = if self.page == NONE {
1455                    0
1456                } else {
1457                    self.arena.counts[self.page as usize] as usize
1458                };
1459                continue;
1460            }
1461
1462            if self.idx == 0 {
1463                self.page = self.arena.prev[self.page as usize];
1464                self.idx = if self.page == NONE {
1465                    0
1466                } else {
1467                    self.arena.counts[self.page as usize] as usize
1468                };
1469                continue;
1470            }
1471
1472            self.idx -= 1;
1473            let rel = self.idx * T::SIZE;
1474            let page = self.arena.pool.page(self.page);
1475            if page[rel..rel + T::KEY_LEN] < *self.from {
1476                return None;
1477            }
1478            return Some(T::read(&page[rel..rel + T::SIZE]));
1479        }
1480    }
1481}