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}