Skip to main content

plugmem_arena/
chunk.rs

1//! Many small growable lists over one flat chunk pool.
2//!
3//! Inverted-index posting lists and graph adjacency lists share a shape:
4//! *lots* of independent lists, most tiny, a few huge, all append-mostly.
5//! Giving each a `Vec` means a heap allocation per list and pointer-chasing
6//! on iteration. [`ChunkPool`] instead carves one byte pool into fixed
7//! 64-byte chunks and threads each list through a chain of them; the pool
8//! never allocates per list, freed chains recycle through a free-list, and
9//! the whole state is two flat sections (pool + per-chunk fill counts) —
10//! the same snapshot-is-memcpy contract as the other structures here.
11//!
12//! A list is addressed by a [`ListHandle`] that the *owner* stores (it is
13//! 12 bytes — designed to sit inside a fixed-size arena
14//! [`Slot`](crate::Slot)); the pool itself keeps no list directory.
15//!
16//! Values are opaque byte runs. The pool guarantees one property the
17//! consumers rely on: **a value never straddles a chunk boundary** — a value
18//! that does not fit in the current tail chunk starts a fresh chunk instead
19//! (the skipped tail bytes are excluded from iteration, so the concatenated
20//! stream stays exactly the pushed bytes). Self-delimiting encodings
21//! (varints) can therefore decode chunk by chunk without a reassembly
22//! buffer.
23//!
24//! # Overlay opens
25//!
26//! [`ChunkPool::load_overlay`] opens a pool whose chunks are **borrowed** from
27//! a longer-lived buffer — typically a memory-mapped file — while the pool
28//! stays fully mutable. A chunk carries its chain link (and, when free, its
29//! free-list link) in its own bytes, so a pool mutates chunks in place; the
30//! first write to a borrowed chunk copies just *that* chunk into an owned
31//! overlay, and chunks grown after open live in an owned tail (per-page
32//! copy-on-write). The borrowed base is never mutated or cloned as a whole, so
33//! a large mapped pool can be *written to* while resident only in the chunks it
34//! actually touches. The overlay keeps the crate's flat philosophy — two flat
35//! `Vec`s, no `Box`, no map. See `examples/overlay.rs`.
36
37use alloc::vec::Vec;
38use core::fmt;
39
40use crate::error::Error;
41use crate::paged::Paged;
42
43/// Size of one chunk in bytes: a chain link ([`LINK_BYTES`]) plus the payload.
44pub const CHUNK_BYTES: usize = 64;
45
46/// Bytes of a chunk's chain link — a little-endian `u32` at the chunk's start,
47/// holding either the next chunk in a list chain or the next free chunk.
48const LINK_BYTES: usize = core::mem::size_of::<u32>();
49
50/// Payload bytes per chunk ([`CHUNK_BYTES`] minus the [`LINK_BYTES`] link).
51/// Also the maximum length of a single pushed value.
52pub const CHUNK_PAYLOAD: usize = CHUNK_BYTES - LINK_BYTES;
53
54/// Bytes of the dumped metadata header: `[chunks u32][free_head u32]`
55/// (see [`ChunkPool::dump_meta`]).
56const META_HEADER: usize = 2 * core::mem::size_of::<u32>();
57
58/// Sentinel for "no chunk": an empty list, the end of a chain, or an empty
59/// free-list.
60const NONE: u32 = u32::MAX;
61
62/// [`ChunkPool`] configuration.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65pub struct ChunkPoolCfg {
66    /// Hard ceiling for the chunk pool; allocating a chunk past it fails
67    /// with [`Error::CapacityExceeded`].
68    pub max_bytes: usize,
69}
70
71impl ChunkPoolCfg {
72    /// Creates a config with no byte limit.
73    pub const fn new() -> Self {
74        Self {
75            max_bytes: usize::MAX,
76        }
77    }
78
79    /// Returns the config with `max_bytes` replaced.
80    pub const fn with_max_bytes(mut self, max_bytes: usize) -> Self {
81        self.max_bytes = max_bytes;
82        self
83    }
84}
85
86impl Default for ChunkPoolCfg {
87    /// Same as [`ChunkPoolCfg::new`].
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93/// Owner-held handle to one list inside a [`ChunkPool`].
94///
95/// 12 bytes, `Copy` — meant to be embedded in a fixed-size record. The
96/// handle *is* the list: the pool keeps no directory, so losing the handle
97/// leaks the chain until the owner's compaction pass rebuilds the pool.
98/// A handle is only meaningful with the pool that filled it; passing it to
99/// another pool reads unrelated (but initialized) chunks.
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102pub struct ListHandle {
103    /// First chunk of the chain (`NONE` = empty list).
104    head: u32,
105    /// Last chunk of the chain — O(1) appends and O(1) frees.
106    tail: u32,
107    /// Number of values pushed (bookkeeping for the owner; iteration yields
108    /// chunk slices, not values).
109    len: u32,
110}
111
112impl ListHandle {
113    /// An empty list.
114    pub const EMPTY: Self = Self {
115        head: NONE,
116        tail: NONE,
117        len: 0,
118    };
119
120    /// Number of values pushed into the list.
121    pub fn len(&self) -> u32 {
122        self.len
123    }
124
125    /// `true` when nothing has been pushed.
126    pub fn is_empty(&self) -> bool {
127        self.len == 0
128    }
129
130    /// Serializes the handle into 12 bytes: `[head BE | tail BE | len BE]`.
131    ///
132    /// This is the *stable* wire form — owners embed handles inside their
133    /// own fixed-size records (and, later, snapshots), so the encoding is
134    /// part of the crate's format contract and is fixed by tests.
135    pub fn to_bytes(self) -> [u8; 12] {
136        let mut out = [0u8; 12];
137        out[0..4].copy_from_slice(&self.head.to_be_bytes());
138        out[4..8].copy_from_slice(&self.tail.to_be_bytes());
139        out[8..12].copy_from_slice(&self.len.to_be_bytes());
140        out
141    }
142
143    /// Inverse of [`ListHandle::to_bytes`].
144    ///
145    /// The bytes are trusted bookkeeping, not validated content: a handle
146    /// is only meaningful with the pool that produced it (same rule as the
147    /// in-memory value — see the type docs).
148    pub fn from_bytes(bytes: [u8; 12]) -> Self {
149        Self {
150            head: u32::from_be_bytes(bytes[0..4].try_into().unwrap()),
151            tail: u32::from_be_bytes(bytes[4..8].try_into().unwrap()),
152            len: u32::from_be_bytes(bytes[8..12].try_into().unwrap()),
153        }
154    }
155}
156
157impl Default for ListHandle {
158    /// Same as [`ListHandle::EMPTY`].
159    fn default() -> Self {
160        Self::EMPTY
161    }
162}
163
164/// A pool of 64-byte chunks backing many independent append-only lists.
165///
166/// ```
167/// use plugmem_arena::{ChunkPool, ChunkPoolCfg, ListHandle};
168///
169/// let mut pool = ChunkPool::new(ChunkPoolCfg::new());
170/// let mut evens = ListHandle::EMPTY;
171/// let mut odds = ListHandle::EMPTY;
172/// for n in 0u32..10 {
173///     let list = if n % 2 == 0 { &mut evens } else { &mut odds };
174///     pool.push(list, &n.to_be_bytes()).unwrap();
175/// }
176/// let bytes: Vec<u8> = pool.iter(&evens).flatten().copied().collect();
177/// let evens_back: Vec<u32> = bytes
178///     .chunks_exact(4)
179///     .map(|b| u32::from_be_bytes(b.try_into().unwrap()))
180///     .collect();
181/// assert_eq!(evens_back, [0, 2, 4, 6, 8]);
182/// pool.free(&mut odds); // chunks recycle; `evens` is untouched
183/// ```
184pub struct ChunkPool<'a> {
185    /// Chunk storage; length is always a multiple of [`CHUNK_BYTES`]. The
186    /// first 4 bytes of a chunk are its chain link (little-endian `u32`,
187    /// internal metadata — not a sort key, so no big-endian mandate), the
188    /// remaining [`CHUNK_PAYLOAD`] bytes hold values.
189    ///
190    /// A [`Paged`] backing (chunk-sized pages) so the pool can either own its
191    /// bytes or borrow a base from a mapped snapshot and overlay writes
192    /// (`load_borrowed`/`load_overlay` —). Both the chain link and the
193    /// free-list link live in a chunk's bytes, so writing either copies just
194    /// that chunk up (per-page copy-on-write); the borrowed base is never
195    /// mutated or cloned. `alloc`-only, so `no_std` holds.
196    pool: Paged<'a, CHUNK_BYTES>,
197    /// Chunk -> payload bytes occupied. Payload bytes beyond `used[chunk]`
198    /// are zero-filled, never exposed. (Chunks are grown zeroed on purpose:
199    /// the arena's measured uninit-page `unsafe` pays off when zeroing
200    /// megabytes of pages at once; here growth is one 64-byte chunk at a
201    /// time and zeroing it is noise, so the safe path costs nothing.)
202    used: Vec<u8>,
203    /// Head of the free-list of recycled chunks (linked through the chunks'
204    /// own chain links).
205    free_head: u32,
206    cfg: ChunkPoolCfg,
207}
208
209impl<'a> ChunkPool<'a> {
210    /// Creates an empty pool. Allocates nothing until the first push.
211    pub const fn new(cfg: ChunkPoolCfg) -> Self {
212        Self {
213            pool: Paged::owned_empty(),
214            used: Vec::new(),
215            free_head: NONE,
216            cfg,
217        }
218    }
219
220    /// Appends one value to a list. Zero-length values only bump the
221    /// handle's [`len`](ListHandle::len).
222    ///
223    /// # Errors
224    ///
225    /// - [`Error::ValueTooLarge`] if `value` is longer than
226    ///   [`CHUNK_PAYLOAD`] bytes (it could never fit in one chunk);
227    /// - [`Error::CapacityExceeded`] if a needed fresh chunk would grow the
228    ///   pool past [`ChunkPoolCfg::max_bytes`].
229    pub fn push(&mut self, list: &mut ListHandle, value: &[u8]) -> Result<(), Error> {
230        if value.len() > CHUNK_PAYLOAD {
231            return Err(Error::ValueTooLarge { len: value.len() });
232        }
233        if !value.is_empty() {
234            let tail_fits = list.tail != NONE
235                && self.used[list.tail as usize] as usize + value.len() <= CHUNK_PAYLOAD;
236            if !tail_fits {
237                let chunk = self.alloc_chunk()?;
238                if list.tail == NONE {
239                    list.head = chunk;
240                } else {
241                    self.set_link(list.tail, chunk);
242                }
243                list.tail = chunk;
244            }
245            let tail = list.tail as usize;
246            let rel = LINK_BYTES + self.used[tail] as usize;
247            let len = value.len();
248            self.pool.page_mut(tail as u32)[rel..rel + len].copy_from_slice(value);
249            self.used[tail] += len as u8;
250        }
251        list.len += 1;
252        Ok(())
253    }
254
255    /// Returns the whole chain of a list to the free-list and resets the
256    /// handle to [`ListHandle::EMPTY`]. O(1): the chain is spliced onto the
257    /// free-list as-is, chunk by chunk bookkeeping happens on reuse.
258    pub fn free(&mut self, list: &mut ListHandle) {
259        if list.head != NONE {
260            self.set_link(list.tail, self.free_head);
261            self.free_head = list.head;
262        }
263        *list = ListHandle::EMPTY;
264    }
265
266    /// Iterates a list's bytes as one `&[u8]` slice per chunk, in push
267    /// order. Slices contain exactly the pushed bytes (skipped tail bytes of
268    /// earlier chunks are excluded), and no pushed value ever spans two
269    /// slices.
270    pub fn iter<'s>(&'s self, list: &ListHandle) -> ChunkIter<'s> {
271        ChunkIter {
272            pool: self,
273            chunk: list.head,
274        }
275    }
276
277    /// Total bytes held by the pool (allocated chunks, including free ones).
278    pub fn pool_bytes(&self) -> usize {
279        self.pool.len()
280    }
281
282    /// Pops a free chunk or grows the pool by one chunk; returns it reset
283    /// (`used = 0`, link = `NONE`).
284    fn alloc_chunk(&mut self) -> Result<u32, Error> {
285        let chunk = if self.free_head != NONE {
286            let chunk = self.free_head;
287            self.free_head = self.link(chunk);
288            self.used[chunk as usize] = 0;
289            chunk
290        } else {
291            let capacity_exceeded = Error::CapacityExceeded {
292                max_bytes: self.cfg.max_bytes,
293            };
294            let new_len = self
295                .pool
296                .len()
297                .checked_add(CHUNK_BYTES)
298                .ok_or(capacity_exceeded)?;
299            if new_len > self.cfg.max_bytes {
300                return Err(capacity_exceeded);
301            }
302            let chunk = self.used.len();
303            // Chunk indices are u32 with NONE reserved; unreachable before
304            // max_bytes in any real configuration (would need a 256 GiB pool).
305            let chunk = u32::try_from(chunk)
306                .ok()
307                .filter(|&c| c != NONE)
308                .ok_or(capacity_exceeded)?;
309            // Grow the owned tail by one zeroed chunk (the whole vector when
310            // owned, the overlay tail when borrowing — the base is never
311            // resized). Zeroing a 64-byte chunk is noise, so no unsafe here.
312            let tail = self.pool.grown_tail_mut();
313            let tail_len = tail.len() + CHUNK_BYTES;
314            tail.resize(tail_len, 0);
315            self.used.push(0);
316            chunk
317        };
318        self.set_link(chunk, NONE);
319        Ok(chunk)
320    }
321
322    /// Reads a chunk's chain link.
323    fn link(&self, chunk: u32) -> u32 {
324        u32::from_le_bytes(self.pool.page(chunk)[..LINK_BYTES].try_into().unwrap())
325    }
326
327    /// Writes a chunk's chain link (copying the chunk up first when it is a
328    /// still-borrowed base chunk).
329    fn set_link(&mut self, chunk: u32, to: u32) {
330        self.pool.page_mut(chunk)[..LINK_BYTES].copy_from_slice(&to.to_le_bytes());
331    }
332
333    /// Number of chunks the pool holds (used and free alike).
334    pub fn chunks(&self) -> usize {
335        self.used.len()
336    }
337
338    /// Walks one list's chain, marking every chunk in `visited`
339    /// (`visited.len()` must equal [`ChunkPool::chunks`]).
340    ///
341    /// This is the owner's load-time validation hook: the
342    /// pool cannot see the owners' handles, so cycle-freedom and
343    /// exclusive ownership of chains are checked here — a shared bitmap
344    /// across all of an owner's handles catches a chunk claimed twice,
345    /// a cycle (the chain would revisit), and a chain that leaks past
346    /// its tail. Never panics on untrusted handle bytes.
347    ///
348    /// # Errors
349    ///
350    /// [`Error::Corrupt`] naming the violated invariant.
351    pub fn validate_chain(&self, list: &ListHandle, visited: &mut [bool]) -> Result<(), Error> {
352        debug_assert_eq!(visited.len(), self.chunks());
353        if list.head == NONE || list.tail == NONE {
354            if list.head != list.tail {
355                return Err(Error::Corrupt("chunk chain head/tail disagree"));
356            }
357            return Ok(());
358        }
359        let mut chunk = list.head;
360        loop {
361            let c = chunk as usize;
362            if c >= visited.len() {
363                return Err(Error::Corrupt("chunk chain reaches out of bounds"));
364            }
365            if core::mem::replace(&mut visited[c], true) {
366                return Err(Error::Corrupt("chunk claimed by two chains"));
367            }
368            if chunk == list.tail {
369                // Live tails never link onward (only freed chains do, and
370                // those are reachable through the free-list instead).
371                if self.link(chunk) != NONE {
372                    return Err(Error::Corrupt("chunk chain tail links onward"));
373                }
374                return Ok(());
375            }
376            let next = self.link(chunk);
377            if next == NONE {
378                return Err(Error::Corrupt("chunk chain ends before its tail"));
379            }
380            chunk = next;
381        }
382    }
383
384    /// Counts chunks that are neither claimed (per the owner's `visited`
385    /// map from [`ChunkPool::validate_chain`] walks) nor in the
386    /// free-list — leaked chunks that no snapshot writer produces, so a
387    /// loader treats any as corruption.
388    pub fn orphan_count(&self, claimed: &[bool]) -> usize {
389        let free = self.free_map();
390        (0..self.used.len())
391            .filter(|&c| !claimed[c] && !free[c])
392            .count()
393    }
394
395    /// Marks every chunk currently sitting in the free-list. Free chunks
396    /// carry stale `used` values and payload bytes ([`ChunkPool::free`] is
397    /// an O(1) splice, cleanup happens on reuse), so dumps consult this map
398    /// to canonicalize them.
399    fn free_map(&self) -> Vec<bool> {
400        let mut free = alloc::vec![false; self.used.len()];
401        let mut chunk = self.free_head;
402        while chunk != NONE {
403            free[chunk as usize] = true;
404            chunk = self.link(chunk);
405        }
406        free
407    }
408
409    /// Appends the pool's metadata section to `out`.
410    ///
411    /// Layout (little-endian): `[chunks u32][free_head u32]` then one
412    /// `used u8` per chunk. Free chunks are written with `used = 0`
413    /// regardless of their stale in-memory value, making the dump
414    /// canonical. Together with [`ChunkPool::dump_pool`] this is the
415    /// complete pool-side state; the list handles live with their owners.
416    pub fn dump_meta(&self, out: &mut Vec<u8>) {
417        let free = self.free_map();
418        out.reserve(META_HEADER + self.used.len());
419        out.extend_from_slice(&(self.used.len() as u32).to_le_bytes());
420        out.extend_from_slice(&self.free_head.to_le_bytes());
421        for (chunk, &used) in self.used.iter().enumerate() {
422            out.push(if free[chunk] { 0 } else { used });
423        }
424    }
425
426    /// Appends the pool section to `out`.
427    ///
428    /// Each chunk contributes its [`LINK_BYTES`] link, its used payload prefix
429    /// and zero padding to [`CHUNK_BYTES`]; free chunks contribute link plus
430    /// zeros. This canonicalizes the stale bytes recycling leaves behind
431    /// (see [`ChunkPool::free`]) — identical logical state, identical
432    /// bytes.
433    pub fn dump_pool(&self, out: &mut Vec<u8>) {
434        let free = self.free_map();
435        out.reserve(self.pool.len());
436        for (chunk, &used) in self.used.iter().enumerate() {
437            let used = if free[chunk] { 0 } else { used as usize };
438            out.extend_from_slice(&self.pool.page(chunk as u32)[..LINK_BYTES + used]);
439            out.resize(out.len() + (CHUNK_PAYLOAD - used), 0);
440        }
441    }
442
443    /// Rebuilds a pool from its two dumped sections.
444    ///
445    /// The input is **untrusted** — validation is O(chunks), never panics
446    /// on arbitrary bytes: exact section lengths (within `cfg.max_bytes`),
447    /// per-chunk `used` within [`CHUNK_PAYLOAD`], every chain link in
448    /// bounds (or the `NONE` sentinel — this is what keeps a later
449    /// [`ChunkPool::iter`] from indexing out of bounds), and a free-list
450    /// that is acyclic (visited bitmap) with `used = 0` on every member.
451    ///
452    /// What this method *cannot* check: cycles among chunks referenced by
453    /// the owners' [`ListHandle`]s — the handles live in the owner's
454    /// records, so the owning engine walks its handles with a shared
455    /// visited bitmap as part of its own load.
456    ///
457    /// # Errors
458    ///
459    /// [`Error::Corrupt`] for any inconsistency.
460    pub fn load(cfg: ChunkPoolCfg, meta: &[u8], pool: &[u8]) -> Result<Self, Error> {
461        let (used, free_head) = validate_chunks(cfg, meta, pool)?;
462        Ok(Self {
463            pool: Paged::owned_from(pool.to_vec()),
464            used,
465            free_head,
466            cfg,
467        })
468    }
469
470    /// Rebuilds a pool that **borrows** its chunk bytes from a longer-lived
471    /// buffer (a memory-mapped snapshot) instead of copying them.
472    /// Validation is identical to [`ChunkPool::load`]; the free-list walk
473    /// and per-chunk link check touch the chunk bytes (needed for safety),
474    /// but no byte is copied.
475    ///
476    /// # Errors
477    ///
478    /// [`Error::Corrupt`] for any inconsistency (same gates as `load`).
479    pub fn load_borrowed(cfg: ChunkPoolCfg, meta: &[u8], pool: &'a [u8]) -> Result<Self, Error> {
480        let (used, free_head) = validate_chunks(cfg, meta, pool)?;
481        Ok(Self {
482            pool: Paged::borrowed(pool),
483            used,
484            free_head,
485            cfg,
486        })
487    }
488
489    /// Opens a pool over a borrowed base for the **overlay** write path: the
490    /// chunk bytes are mapped read-only, and the first write to any base chunk
491    /// (a value push, or a chain/free-list link update) copies just that chunk
492    /// into owned storage (per-page copy-on-write, see [`Paged`](crate::paged)),
493    /// while chunks grown after open live in an owned tail. Unlike
494    /// [`ChunkPool::load_borrowed`] the returned pool is fully mutable — pushes
495    /// and frees work — yet the borrowed base is never cloned as a whole or
496    /// mutated, so a memory-mapped pool can be written to while resident only in
497    /// the chunks it actually touches. Validation is identical to
498    /// [`ChunkPool::load`].
499    ///
500    /// # Errors
501    ///
502    /// [`Error::Corrupt`] for any inconsistency (same gates as `load`).
503    pub fn load_overlay(cfg: ChunkPoolCfg, meta: &[u8], pool: &'a [u8]) -> Result<Self, Error> {
504        let (used, free_head) = validate_chunks(cfg, meta, pool)?;
505        Ok(Self {
506            pool: Paged::borrowed(pool),
507            used,
508            free_head,
509            cfg,
510        })
511    }
512}
513
514/// Validates a dumped chunk pool and returns its `used` table and free-list
515/// head. Shared by [`ChunkPool::load`] and [`ChunkPool::load_borrowed`];
516/// input is untrusted (see `load`).
517fn validate_chunks(cfg: ChunkPoolCfg, meta: &[u8], pool: &[u8]) -> Result<(Vec<u8>, u32), Error> {
518    if meta.len() < META_HEADER {
519        return Err(Error::Corrupt("chunk meta shorter than its header"));
520    }
521    let chunks = u32::from_le_bytes(meta[0..4].try_into().unwrap());
522    let free_head = u32::from_le_bytes(meta[4..META_HEADER].try_into().unwrap());
523    if chunks == NONE {
524        return Err(Error::Corrupt("chunk count overflows the index space"));
525    }
526    if meta.len() as u64 != META_HEADER as u64 + u64::from(chunks) {
527        return Err(Error::Corrupt("chunk meta length mismatch"));
528    }
529    if pool.len() as u64 != u64::from(chunks) * CHUNK_BYTES as u64 {
530        return Err(Error::Corrupt("chunk pool length mismatch"));
531    }
532    if pool.len() > cfg.max_bytes {
533        return Err(Error::Corrupt("chunk pool exceeds the configured ceiling"));
534    }
535    let used: Vec<u8> = meta[META_HEADER..].to_vec();
536    if used.iter().any(|&u| u as usize > CHUNK_PAYLOAD) {
537        return Err(Error::Corrupt("chunk used bytes exceed the payload size"));
538    }
539    let link_of = |chunk: usize| {
540        let at = chunk * CHUNK_BYTES;
541        u32::from_le_bytes(pool[at..at + LINK_BYTES].try_into().unwrap())
542    };
543    for chunk in 0..chunks as usize {
544        let link = link_of(chunk);
545        if link != NONE && link >= chunks {
546            return Err(Error::Corrupt("chunk link out of bounds"));
547        }
548    }
549    let mut seen = alloc::vec![false; chunks as usize];
550    let mut chunk = free_head;
551    while chunk != NONE {
552        let c = chunk as usize;
553        if c >= chunks as usize {
554            return Err(Error::Corrupt("chunk free-list head out of bounds"));
555        }
556        if core::mem::replace(&mut seen[c], true) {
557            return Err(Error::Corrupt("chunk free-list contains a cycle"));
558        }
559        if used[c] != 0 {
560            return Err(Error::Corrupt("free chunk has nonzero used bytes"));
561        }
562        chunk = link_of(c);
563    }
564    Ok((used, free_head))
565}
566
567impl fmt::Debug for ChunkPool<'_> {
568    /// Summary only — chunk contents are the owners' data, not ours to dump.
569    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
570        f.debug_struct("ChunkPool")
571            .field("chunks", &self.used.len())
572            .field("pool_bytes", &self.pool.len())
573            .finish()
574    }
575}
576
577/// Iterator over one list's chunks; see [`ChunkPool::iter`].
578pub struct ChunkIter<'a> {
579    pool: &'a ChunkPool<'a>,
580    chunk: u32,
581}
582
583impl<'a> Iterator for ChunkIter<'a> {
584    type Item = &'a [u8];
585
586    fn next(&mut self) -> Option<&'a [u8]> {
587        if self.chunk == NONE {
588            return None;
589        }
590        let chunk = self.chunk;
591        self.chunk = self.pool.link(chunk);
592        let used = self.pool.used[chunk as usize] as usize;
593        Some(&self.pool.pool.page(chunk)[LINK_BYTES..LINK_BYTES + used])
594    }
595}