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}