Skip to main content

hdf5_pure/
source.rs

1//! Random-access byte sources for the reader: the [`Source`] trait and its backends.
2//!
3//! [`BytesSource`] reads from a buffer that holds the whole file, and [`ReadSeekSource`] from a
4//! reader that seeks. [`MetadataCachingSource`] wraps either with a bounded cache of the metadata
5//! reads and passes a dataset's payload through to the inner source.
6
7#[cfg(not(feature = "std"))]
8use alloc::{vec, vec::Vec};
9
10#[cfg(feature = "std")]
11use std::collections::BTreeMap;
12
13use crate::address::{BaseAddress, StoredAddress};
14use crate::convert::Narrow;
15use crate::error::FormatError;
16
17/// Default maximum size of one entry admitted to a streaming metadata cache.
18pub const DEFAULT_METADATA_CACHE_MAX_ENTRY_BYTES: usize = 64 * 1024;
19
20/// Initial metadata-cache settings for streaming file access.
21///
22/// This is the `hdf5-pure` counterpart to the memory-budget portion of HDF5's
23/// `H5Pset_mdc_config`: it bounds the bytes retained for parsed metadata reads
24/// while a file is opened through [`crate::File::open_streaming_with_options`].
25/// Raw dataset payload reads use `Source::read_exact_at` and are not
26/// admitted to this cache.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct MetadataCacheConfig {
29    max_bytes: usize,
30    max_entry_bytes: usize,
31}
32
33impl MetadataCacheConfig {
34    /// Create a metadata cache with the given total byte budget.
35    ///
36    /// Individual cached reads are capped at
37    /// `DEFAULT_METADATA_CACHE_MAX_ENTRY_BYTES` (64 KiB) by default so one large
38    /// heap or index block cannot monopolize the cache. Use
39    /// [`with_max_entry_bytes`](Self::with_max_entry_bytes) to change that.
40    pub const fn new(max_bytes: usize) -> Self {
41        let max_entry_bytes = if max_bytes < DEFAULT_METADATA_CACHE_MAX_ENTRY_BYTES {
42            max_bytes
43        } else {
44            DEFAULT_METADATA_CACHE_MAX_ENTRY_BYTES
45        };
46        Self {
47            max_bytes,
48            max_entry_bytes,
49        }
50    }
51
52    /// Disable metadata read caching.
53    pub const fn disabled() -> Self {
54        Self {
55            max_bytes: 0,
56            max_entry_bytes: 0,
57        }
58    }
59
60    /// Set the maximum size of a single metadata read admitted to the cache.
61    pub const fn with_max_entry_bytes(mut self, max_entry_bytes: usize) -> Self {
62        self.max_entry_bytes = max_entry_bytes;
63        self
64    }
65
66    /// Return the total metadata-cache byte budget.
67    pub const fn max_bytes(&self) -> usize {
68        self.max_bytes
69    }
70
71    /// Return the maximum size of one cached metadata entry.
72    pub const fn max_entry_bytes(&self) -> usize {
73        self.max_entry_bytes
74    }
75
76    /// Whether metadata read caching is enabled.
77    pub const fn is_enabled(&self) -> bool {
78        self.max_bytes > 0 && self.max_entry_bytes > 0
79    }
80}
81
82impl Default for MetadataCacheConfig {
83    fn default() -> Self {
84        Self::disabled()
85    }
86}
87
88/// What a file's metadata cache has done, and what it is holding.
89///
90/// Returned by [`crate::File::metadata_cache_stats`]. This is the `hdf5-pure`
91/// counterpart to HDF5's `H5Fget_mdc_hit_rate` and `H5Fget_mdc_size`:
92/// [`entries`](Self::entries) and [`bytes`](Self::bytes) are a point-in-time
93/// view of occupancy, and the counters are cumulative since the file was
94/// opened or since the last
95/// [`reset_metadata_cache_stats`](crate::File::reset_metadata_cache_stats).
96///
97/// The reason to look is that [`MetadataCacheConfig`] is a budget chosen before
98/// a single read has happened, and nothing else reports whether it was the
99/// right one:
100///
101/// - [`hit_rate`](Self::hit_rate) says whether the cache is earning its memory.
102/// - [`evictions`](Self::evictions) says whether the budget is the binding
103///   constraint. A hit rate below expectations with no evictions is not a
104///   budget problem, and raising it will not help.
105/// - [`oversize_reads`](Self::oversize_reads) says whether
106///   [`max_entry_bytes`](MetadataCacheConfig::max_entry_bytes) is turning reads
107///   away before they reach the cache at all.
108/// - [`invalidations`](Self::invalidations) says how much of the cache a
109///   read-write session is throwing away with its own writes.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
111pub struct MetadataCacheStats {
112    hits: u64,
113    misses: u64,
114    oversize_reads: u64,
115    evictions: u64,
116    invalidations: u64,
117    entries: usize,
118    bytes: usize,
119}
120
121impl MetadataCacheStats {
122    /// Metadata reads served from the cache.
123    pub const fn hits(&self) -> u64 {
124        self.hits
125    }
126
127    /// Metadata reads eligible for the cache that were not in it.
128    pub const fn misses(&self) -> u64 {
129        self.misses
130    }
131
132    /// Metadata reads that bypassed the cache because they exceed
133    /// [`MetadataCacheConfig::max_entry_bytes`] (or the whole budget).
134    ///
135    /// These are counted apart from [`misses`](Self::misses) rather than folded
136    /// into them: the cache was never offered the read, so charging it as a miss
137    /// would report a failure at work it could not have done. They still show up
138    /// in [`reads`](Self::reads).
139    pub const fn oversize_reads(&self) -> u64 {
140        self.oversize_reads
141    }
142
143    /// Entries dropped to stay inside [`MetadataCacheConfig::max_bytes`].
144    pub const fn evictions(&self) -> u64 {
145        self.evictions
146    }
147
148    /// Entries dropped because an in-place write overlapped them.
149    ///
150    /// Only a read-write session invalidates; this stays zero on a read-only
151    /// open. Invalidations approaching [`misses`](Self::misses) mean the session
152    /// is rewriting the metadata it is caching, and a larger budget will not
153    /// change that.
154    pub const fn invalidations(&self) -> u64 {
155        self.invalidations
156    }
157
158    /// Entries currently held.
159    ///
160    /// Against [`bytes`](Self::bytes) this is the mean entry size, which is what
161    /// says whether a few large reads are spending the budget; pair it with
162    /// [`oversize_reads`](Self::oversize_reads) to see the ones already refused.
163    pub const fn entries(&self) -> usize {
164        self.entries
165    }
166
167    /// Bytes currently held, to compare against
168    /// [`MetadataCacheConfig::max_bytes`].
169    pub const fn bytes(&self) -> usize {
170        self.bytes
171    }
172
173    /// Every metadata read through this source: hits, misses, and reads too
174    /// large to admit.
175    ///
176    /// The last of those three is not in [`hit_rate`](Self::hit_rate)'s
177    /// denominator, so `hits() / reads()` is a different figure and a lower one.
178    pub const fn reads(&self) -> u64 {
179        self.hits
180            .saturating_add(self.misses)
181            .saturating_add(self.oversize_reads)
182    }
183
184    /// The fraction of *eligible* metadata reads served from the cache, or
185    /// `None` before any eligible read has happened.
186    ///
187    /// `None` rather than C's `0.0`, which `H5Fget_mdc_hit_rate` also returns
188    /// for a cache that has missed every access: the two mean opposite things to
189    /// a caller deciding whether to raise the budget, and only one of them is a
190    /// reason to.
191    pub fn hit_rate(&self) -> Option<f64> {
192        let eligible = self.hits.saturating_add(self.misses);
193        if eligible == 0 {
194            return None;
195        }
196        #[expect(
197            clippy::cast_precision_loss,
198            reason = "a hit rate is a ratio; f64 holds these counts exactly far past any \
199                      read count a process will reach"
200        )]
201        Some(self.hits as f64 / eligible as f64)
202    }
203}
204
205/// A random-access, read-only source of the bytes of an HDF5 file.
206///
207/// Offsets are `u64` (HDF5's native address width); lengths of individual reads
208/// are `usize` (they must fit in a caller-provided buffer). Implementations must
209/// either fill the whole request or return an error — a short read is always an
210/// error, never silently truncated.
211///
212/// # Implementing one
213///
214/// Two methods carry the whole trait: [`len`](Source::len) and
215/// [`read_at`](Source::read_at). The rest are conveniences with default bodies,
216/// and every method added later will have one too, so an implementation written
217/// today keeps compiling.
218///
219/// The reader treats a source as an immutable file for as long as it holds one:
220/// [`len`](Source::len) is expected to stay put and to be true — the reader
221/// bounds allocations with it — and bytes already read are expected to read
222/// back the same. A source over something that grows underneath it — a file a
223/// writer is appending to — is what [`File::open_swmr`](crate::File::open_swmr)
224/// is for instead. `read_at` takes `&self` so a `File` can be shared, so an
225/// implementation over a mutable handle owns its own synchronisation, the way
226/// [`ReadSeekSource`] wraps its reader in a mutex.
227///
228/// Leave the last three defaulted unless you mean to cache.
229/// [`read_metadata_at`](Source::read_metadata_at),
230/// [`metadata_cache_stats`](Source::metadata_cache_stats) and
231/// [`reset_metadata_cache_stats`](Source::reset_metadata_cache_stats) are the
232/// seam this crate's own bounded metadata cache hangs on, and they come as a
233/// set: overriding the first without the other two reports *no* cache where
234/// there is a full one. To have metadata reads cached, ask for it with a
235/// [`MetadataCacheConfig`] through
236/// [`File::from_source_with_options`](crate::File::from_source_with_options),
237/// which wraps the source in an implementation of all three.
238#[expect(
239    clippy::len_without_is_empty,
240    reason = "`len` here is a file's byte length, not a container's count — the shape of \
241              `std::fs::Metadata::len`, which ships without an `is_empty` for the same \
242              reason. An HDF5 file is never empty (the signature alone is eight bytes), so \
243              an `is_empty` on this trait would be a public method with no caller, and one \
244              an implementation could contradict its own `len` with"
245)]
246pub trait Source {
247    /// Total number of bytes the source can supply.
248    ///
249    /// Report the true length: the reader bounds allocations with it. Metadata
250    /// lengths come out of the file being read, and
251    /// [`read_exact_at`](Source::read_exact_at) rejects one that runs past the
252    /// end *before* reserving a buffer for it, so a malformed file cannot name
253    /// a multi-gigabyte read and have it reserved. A `len` larger than what the
254    /// source can actually serve passes that check and the reservation happens,
255    /// which leaves the file choosing the size of an allocation. A length that
256    /// arrives over a channel the caller does not control — a `Content-Length`
257    /// header, a size a host reports — should be held to what the source can
258    /// really serve before it is reported here.
259    fn len(&self) -> u64;
260
261    /// Read exactly `buf.len()` bytes starting at absolute offset `offset`,
262    /// filling `buf`.
263    ///
264    /// Returns [`FormatError::UnexpectedEof`] if fewer than `buf.len()` bytes are
265    /// available at `offset`, [`FormatError::OffsetOverflow`] if
266    /// `offset + buf.len()` overflows, [`FormatError::ValueTooLargeForPlatform`]
267    /// if `offset` does not fit this platform's `usize` (for in-memory
268    /// backends), or [`FormatError::Source`] for a backend I/O failure.
269    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError>;
270
271    /// Read `len` bytes starting at `offset` into a freshly allocated `Vec`.
272    ///
273    /// Convenience wrapper over [`read_at`](Source::read_at) for callers that
274    /// want an owned buffer; the lazy backends keep no more than this resident.
275    ///
276    /// The request is bounds-checked against [`len`](Source::len) *before* the
277    /// buffer is allocated. The metadata parsers feed `len` values straight from
278    /// the file (a chunk-0 body size, a continuation-block length, a heap object
279    /// size), so a malformed file could otherwise name a multi-gigabyte length
280    /// and make this reserve `vec![0u8; len]` up front only for the read to fail
281    /// EOF anyway — a cheap denial of service. Rejecting an out-of-range request
282    /// before allocating avoids that; the error returned is identical to the one
283    /// the underlying [`read_at`](Source::read_at) would have produced.
284    fn read_exact_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
285        let end = offset
286            .checked_add(len as u64)
287            .ok_or(FormatError::OffsetOverflow {
288                offset,
289                length: len as u64,
290            })?;
291        if end > self.len() {
292            return Err(FormatError::UnexpectedEof {
293                expected: end.to_usize().unwrap_or(usize::MAX),
294                available: self.len().to_usize().unwrap_or(usize::MAX),
295            });
296        }
297        let mut buf = vec![0u8; len];
298        self.read_at(offset, &mut buf)?;
299        Ok(buf)
300    }
301
302    /// Read metadata bytes, allowing source implementations to apply a bounded
303    /// metadata cache.
304    ///
305    /// The default implementation performs an uncached exact read. Raw dataset
306    /// payload readers intentionally call [`read_exact_at`](Self::read_exact_at)
307    /// instead, so a metadata cache does not retain user data chunks.
308    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
309        self.read_exact_at(offset, len)
310    }
311
312    /// What the metadata cache in front of this source has done, or `None` when
313    /// it has none.
314    ///
315    /// The observation half of [`read_metadata_at`](Self::read_metadata_at):
316    /// that method exists so an implementation *may* cache a metadata read, and
317    /// this one reports whether doing so paid. The default is `None`, since most
318    /// sources cache nothing.
319    ///
320    /// A wrapper that forwards `read_metadata_at` to an inner source must
321    /// forward this too. Leaving it defaulted would have it report *no* cache
322    /// where there is a full one, which reads as "caching is off" rather than as
323    /// the missing forward it is.
324    fn metadata_cache_stats(&self) -> Option<MetadataCacheStats> {
325        None
326    }
327
328    /// Zero that cache's cumulative counters, leaving its contents alone.
329    ///
330    /// The counterpart of HDF5's `H5Freset_mdc_hit_rate_stats`, for measuring
331    /// one phase of a program rather than a whole run. A no-op where there is no
332    /// cache, and it evicts nothing: occupancy is not a counter.
333    fn reset_metadata_cache_stats(&self) {}
334}
335
336// Forward `Source` through references and boxes so `&S`, `&dyn Source`,
337// and `Box<dyn Source>` are all usable wherever an `S: Source` is.
338impl<S: Source + ?Sized> Source for &S {
339    fn len(&self) -> u64 {
340        (**self).len()
341    }
342    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
343        (**self).read_at(offset, buf)
344    }
345
346    fn read_exact_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
347        (**self).read_exact_at(offset, len)
348    }
349
350    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
351        (**self).read_metadata_at(offset, len)
352    }
353
354    fn metadata_cache_stats(&self) -> Option<MetadataCacheStats> {
355        (**self).metadata_cache_stats()
356    }
357
358    fn reset_metadata_cache_stats(&self) {
359        (**self).reset_metadata_cache_stats();
360    }
361}
362
363#[cfg(feature = "std")]
364impl<S: Source + ?Sized> Source for std::boxed::Box<S> {
365    fn len(&self) -> u64 {
366        (**self).len()
367    }
368    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
369        (**self).read_at(offset, buf)
370    }
371
372    fn read_exact_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
373        (**self).read_exact_at(offset, len)
374    }
375
376    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
377        (**self).read_metadata_at(offset, len)
378    }
379
380    fn metadata_cache_stats(&self) -> Option<MetadataCacheStats> {
381        (**self).metadata_cache_stats()
382    }
383
384    fn reset_metadata_cache_stats(&self) {
385        (**self).reset_metadata_cache_stats();
386    }
387}
388
389// ---------------------------------------------------------------------------
390// Caller-supplied sources
391// ---------------------------------------------------------------------------
392
393/// Holds a caller-supplied [`Source`] to the part of the trait's contract the
394/// parsers above it cannot check for themselves.
395///
396/// [`Source::read_exact_at`] and [`Source::read_metadata_at`] have default
397/// bodies that return exactly the bytes asked for, and every source this crate
398/// builds either uses those bodies or forwards to one that does. An
399/// implementation from outside may override them, and has a reason to — a
400/// remote source that batches or coalesces its reads is the case
401/// [`crate::File::from_source`] exists for. The parsers then index the returned
402/// buffer at offsets derived from the length they *requested*, so a buffer that
403/// comes back short is a slice panic inside a header parser, blaming a file
404/// format for what the source did.
405///
406/// One length comparison per read turns that into [`FormatError::Source`],
407/// which names the source instead. This wraps only what a caller hands in;
408/// the crate's own sources have no override to check.
409#[cfg(feature = "std")]
410pub(crate) struct ValidatedSource<S>(S);
411
412#[cfg(feature = "std")]
413impl<S> ValidatedSource<S> {
414    pub(crate) fn new(inner: S) -> Self {
415        Self(inner)
416    }
417
418    /// Refuse a buffer whose length is not the one that was asked for.
419    fn check(offset: u64, len: usize, bytes: Vec<u8>) -> Result<Vec<u8>, FormatError> {
420        if bytes.len() == len {
421            return Ok(bytes);
422        }
423        Err(FormatError::Source(std::format!(
424            "the source returned {} bytes for a {len}-byte read at offset {offset}",
425            bytes.len()
426        )))
427    }
428}
429
430#[cfg(feature = "std")]
431impl<S: Source> Source for ValidatedSource<S> {
432    fn len(&self) -> u64 {
433        self.0.len()
434    }
435
436    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
437        self.0.read_at(offset, buf)
438    }
439
440    fn read_exact_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
441        Self::check(offset, len, self.0.read_exact_at(offset, len)?)
442    }
443
444    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
445        Self::check(offset, len, self.0.read_metadata_at(offset, len)?)
446    }
447
448    fn metadata_cache_stats(&self) -> Option<MetadataCacheStats> {
449        self.0.metadata_cache_stats()
450    }
451
452    fn reset_metadata_cache_stats(&self) {
453        self.0.reset_metadata_cache_stats();
454    }
455}
456
457// ---------------------------------------------------------------------------
458// Base-relative view
459// ---------------------------------------------------------------------------
460
461/// A [`Source`] view shifted forward by a base address: every read at a
462/// base-relative `offset` is served from `inner` at `offset + base`.
463///
464/// Used wherever on-disk addresses are stored relative to the superblock's base
465/// address rather than absolutely — the data layout's contiguous-data, chunk-index,
466/// and chunk addresses on a file with a userblock, and the fractal-heap address in
467/// an Attribute Info message. Presenting this shifted view lets those relative
468/// addresses index it directly, exactly as an in-memory path slices the buffer at
469/// `base`. For a plain (base-0) file it is the identity.
470///
471/// `len`/`read_at` shift by the base; `read_metadata_at` forwards to the inner
472/// source at the *absolute* offset so the inner source's metadata cache is shared
473/// (a chunk-index walk on a streaming userblock file would otherwise re-read every
474/// node), while payload reads keep the default uncached `read_exact_at` so user
475/// data does not evict metadata.
476pub(crate) struct BaseOffsetSource<'a, S: Source + ?Sized> {
477    pub(crate) inner: &'a S,
478    pub(crate) base: BaseAddress,
479}
480
481/// A base-relative view of an in-memory file: `bytes` with its first `base` bytes
482/// (the userblock) cut off, so every address stored relative to the base address
483/// indexes it directly. The in-memory counterpart of [`BaseOffsetSource`], and the
484/// identity for a plain file.
485pub(crate) fn frame(bytes: &[u8], base: BaseAddress) -> Result<&[u8], FormatError> {
486    if base.is_zero() {
487        return Ok(bytes);
488    }
489    let start = base.get().to_usize()?;
490    bytes.get(start..).ok_or(FormatError::UnexpectedEof {
491        expected: start,
492        available: bytes.len(),
493    })
494}
495
496impl<S: Source + ?Sized> Source for BaseOffsetSource<'_, S> {
497    fn len(&self) -> u64 {
498        self.inner.len().saturating_sub(self.base.get())
499    }
500
501    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
502        self.inner
503            .read_at(self.base.absolute(StoredAddress::new(offset))?, buf)
504    }
505
506    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
507        self.inner
508            .read_metadata_at(self.base.absolute(StoredAddress::new(offset))?, len)
509    }
510
511    // The metadata reads above are the inner source's, so its cache is the one
512    // to report on. A base-relative view holds none of its own.
513    fn metadata_cache_stats(&self) -> Option<MetadataCacheStats> {
514        self.inner.metadata_cache_stats()
515    }
516
517    fn reset_metadata_cache_stats(&self) {
518        self.inner.reset_metadata_cache_stats();
519    }
520}
521
522// ---------------------------------------------------------------------------
523// In-memory backend
524// ---------------------------------------------------------------------------
525
526/// A [`Source`] over an in-memory byte buffer: anything that is
527/// `AsRef<[u8]>` (`Vec<u8>`, `&[u8]`, `Box<[u8]>`, `Arc<[u8]>`, …).
528///
529/// This is the always-available backend that mirrors the crate's current
530/// in-memory model, usable on WASM and `no_std`.
531#[derive(Debug, Clone, Copy)]
532pub struct BytesSource<T>(pub T);
533
534impl<T: AsRef<[u8]>> BytesSource<T> {
535    /// Wrap an in-memory byte buffer.
536    pub fn new(bytes: T) -> Self {
537        BytesSource(bytes)
538    }
539}
540
541impl<T: AsRef<[u8]>> Source for BytesSource<T> {
542    fn len(&self) -> u64 {
543        self.0.as_ref().len() as u64
544    }
545
546    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
547        let bytes = self.0.as_ref();
548        let start = offset.to_usize()?;
549        let end = start
550            .checked_add(buf.len())
551            .ok_or(FormatError::OffsetOverflow {
552                offset,
553                length: buf.len() as u64,
554            })?;
555        if end > bytes.len() {
556            return Err(FormatError::UnexpectedEof {
557                expected: end,
558                available: bytes.len(),
559            });
560        }
561        buf.copy_from_slice(&bytes[start..end]);
562        Ok(())
563    }
564}
565
566// ---------------------------------------------------------------------------
567// Metadata-caching wrapper (std)
568// ---------------------------------------------------------------------------
569
570#[cfg(feature = "std")]
571struct CachedMetadataRead {
572    bytes: Vec<u8>,
573    last_access: u64,
574}
575
576/// The bounded LRU store behind [`MetadataCachingSource`], also embedded
577/// directly by the mirrorless write image (`crate::image::HandleImage`), which
578/// must invalidate entries that overlap an in-place write.
579///
580/// # Why this is indexed rather than scanned (issue #367)
581///
582/// It held a `Vec` walked end to end by every operation, which made a *hit*
583/// cost O(entries) and put the budget's useful range at a few thousand of them.
584/// Measured against the positioned read a hit replaces: 9x faster at 64
585/// entries, 3.2x at 1,024, then 1.2x **slower** at 4,096 and 21.9x slower at
586/// 65,536. An 8 MiB budget, the figure `README.md` recommends, admits over
587/// 100,000 metadata-sized reads, so the knob documented as a way to make a file
588/// of many datasets read faster made one read about 30% slower.
589///
590/// Both maps below are therefore keyed, not searched, and the budget is a dial
591/// over its whole range rather than only below a cliff.
592#[cfg(feature = "std")]
593pub(crate) struct MetadataReadCache {
594    /// Entries by the `(offset, len)` the caller asked for. Two reads may share
595    /// an offset at different lengths, so the length is part of the key.
596    ///
597    /// Ordered by offset first, which is what lets
598    /// [`invalidate_overlapping`](Self::invalidate_overlapping) look at one
599    /// bounded key range instead of every entry.
600    entries: BTreeMap<(u64, usize), CachedMetadataRead>,
601    /// `last_access` -> the key stamped with it, one row per entry. Its first
602    /// row is the least recently used entry, which is what eviction wants.
603    by_access: BTreeMap<u64, (u64, usize)>,
604    current_bytes: usize,
605    tick: u64,
606    /// The longest `len` ever admitted, bounding how far *before* a given
607    /// offset an entry that overlaps it can start. It only grows, so the window
608    /// it gives can be wider than needed but never too narrow.
609    longest_entry: usize,
610    /// Cumulative counters, reported as [`MetadataCacheStats`].
611    hits: u64,
612    misses: u64,
613    oversize_reads: u64,
614    evictions: u64,
615    invalidations: u64,
616}
617
618#[cfg(feature = "std")]
619impl MetadataReadCache {
620    pub(crate) fn new() -> Self {
621        Self {
622            entries: BTreeMap::new(),
623            by_access: BTreeMap::new(),
624            current_bytes: 0,
625            tick: 0,
626            longest_entry: 0,
627            hits: 0,
628            misses: 0,
629            oversize_reads: 0,
630            evictions: 0,
631            invalidations: 0,
632        }
633    }
634
635    /// Take the cache's lock, treating a poisoned one as held rather than
636    /// panicking: a cache is a performance aid, and a reader that panicked
637    /// elsewhere leaves no invariant here for a later caller to trip over.
638    pub(crate) fn locked(lock: &std::sync::Mutex<Self>) -> std::sync::MutexGuard<'_, Self> {
639        lock.lock()
640            .unwrap_or_else(std::sync::PoisonError::into_inner)
641    }
642
643    /// Serve one [`Source::read_metadata_at`] through the cache behind `lock`,
644    /// falling back to `read` and recording what happened.
645    ///
646    /// Both call sites that have a metadata cache — [`MetadataCachingSource`]
647    /// and `crate::image::HandleImage` — go through here rather than each
648    /// repeating the admission rule and its five counters; they differ only in
649    /// what `read` does, which is why it is a closure.
650    ///
651    /// The lock is taken up to twice and never held across `read`. A metadata
652    /// read is file I/O, and serializing every one of them behind this mutex
653    /// would cost more than the cache saves.
654    pub(crate) fn read_through(
655        lock: &std::sync::Mutex<Self>,
656        config: MetadataCacheConfig,
657        offset: u64,
658        len: usize,
659        read: impl FnOnce() -> Result<Vec<u8>, FormatError>,
660    ) -> Result<Vec<u8>, FormatError> {
661        // A zero-length read is not a read of anything, and a disabled cache has
662        // no counters worth keeping; neither is worth a lock.
663        if len == 0 || !config.is_enabled() {
664            return read();
665        }
666        if len > config.max_entry_bytes() || len > config.max_bytes() {
667            Self::locked(lock).oversize_reads += 1;
668            return read();
669        }
670        if let Some(bytes) = Self::locked(lock).get(offset, len) {
671            return Ok(bytes);
672        }
673        let bytes = read()?;
674        Self::locked(lock).insert(offset, len, bytes.clone(), config.max_bytes());
675        Ok(bytes)
676    }
677
678    /// Snapshot the counters and the current occupancy.
679    pub(crate) fn stats(&self) -> MetadataCacheStats {
680        MetadataCacheStats {
681            hits: self.hits,
682            misses: self.misses,
683            oversize_reads: self.oversize_reads,
684            evictions: self.evictions,
685            invalidations: self.invalidations,
686            entries: self.entries.len(),
687            bytes: self.current_bytes,
688        }
689    }
690
691    /// Zero the counters, keeping every entry. Occupancy is a measurement of the
692    /// cache's contents rather than a tally of its history, so resetting the
693    /// history must not disturb it.
694    pub(crate) fn reset_stats(&mut self) {
695        self.hits = 0;
696        self.misses = 0;
697        self.oversize_reads = 0;
698        self.evictions = 0;
699        self.invalidations = 0;
700    }
701
702    /// Drop one entry and its access row together, keeping the two maps and the
703    /// byte total in step. Every removal goes through here for that reason.
704    fn remove(&mut self, key: (u64, usize)) {
705        if let Some(entry) = self.entries.remove(&key) {
706            self.by_access.remove(&entry.last_access);
707            self.current_bytes -= entry.bytes.len();
708        }
709        debug_assert_eq!(
710            self.entries.len(),
711            self.by_access.len(),
712            "every entry holds exactly one access row"
713        );
714    }
715
716    /// Drop every cached entry that overlaps `[offset, offset + len)`, so a
717    /// read after an in-place write never observes stale bytes.
718    pub(crate) fn invalidate_overlapping(&mut self, offset: u64, len: usize) {
719        if len == 0 {
720            return;
721        }
722        let end = offset.saturating_add(len as u64);
723        // An entry starting before this cannot reach `offset` at any length the
724        // cache has admitted, so the search starts here rather than at the map's
725        // first key. It is never above `end`, which is what `BTreeMap::range`
726        // requires of its bounds: it is at most `offset`, and `end` is at least
727        // `offset` even where the addition above saturates.
728        let first = offset.saturating_sub(self.longest_entry as u64);
729        let doomed: Vec<(u64, usize)> = self
730            .entries
731            .range((first, 0)..(end, 0))
732            // The range settles `entry_offset < end`; this settles the other
733            // half, that the entry reaches forward as far as `offset`.
734            .filter(|((entry_offset, entry_len), _)| {
735                entry_offset.saturating_add(*entry_len as u64) > offset
736            })
737            .map(|(key, _)| *key)
738            .collect();
739        self.invalidations += doomed.len() as u64;
740        for key in doomed {
741            self.remove(key);
742        }
743    }
744
745    pub(crate) fn get(&mut self, offset: u64, len: usize) -> Option<Vec<u8>> {
746        let key = (offset, len);
747        // One tick per cached read, so a `u64` outlasts any process that could
748        // run. The counter formerly wrapped, which would have inverted the very
749        // ordering it exists to record.
750        let tick = self.tick + 1;
751        let Some(entry) = self.entries.get_mut(&key) else {
752            self.misses += 1;
753            return None;
754        };
755        let previous = core::mem::replace(&mut entry.last_access, tick);
756        let bytes = entry.bytes.clone();
757        // Only past the lookup is this a hit, so only here does the clock move.
758        self.tick = tick;
759        self.hits += 1;
760        self.by_access.remove(&previous);
761        self.by_access.insert(tick, key);
762        Some(bytes)
763    }
764
765    pub(crate) fn insert(&mut self, offset: u64, len: usize, bytes: Vec<u8>, max_bytes: usize) {
766        if len == 0 || bytes.len() > max_bytes {
767            return;
768        }
769
770        let key = (offset, len);
771        // Re-reading a key replaces it. Removing first means the byte total and
772        // the access index never keep a row for the value being displaced.
773        self.remove(key);
774
775        self.tick += 1;
776        let tick = self.tick;
777        self.longest_entry = self.longest_entry.max(len);
778        self.current_bytes += bytes.len();
779        self.entries.insert(
780            key,
781            CachedMetadataRead {
782                bytes,
783                last_access: tick,
784            },
785        );
786        self.by_access.insert(tick, key);
787        debug_assert_eq!(
788            self.entries.len(),
789            self.by_access.len(),
790            "every entry holds exactly one access row"
791        );
792        self.evict_to_budget(max_bytes);
793    }
794
795    fn evict_to_budget(&mut self, max_bytes: usize) {
796        while self.current_bytes > max_bytes {
797            let Some((_, &key)) = self.by_access.first_key_value() else {
798                break;
799            };
800            // Counted here rather than in `remove`, which also serves
801            // invalidation and replacement. Only a drop the *budget* forced is
802            // an eviction, and that is the one that says to raise it.
803            self.evictions += 1;
804            self.remove(key);
805        }
806    }
807}
808
809/// A [`Source`] wrapper with a bounded cache for metadata reads.
810///
811/// The wrapper only caches calls to [`Source::read_metadata_at`]. Plain
812/// [`Source::read_exact_at`] calls still go directly to the inner source,
813/// which keeps raw dataset payloads out of the metadata cache.
814#[cfg(feature = "std")]
815pub struct MetadataCachingSource<S> {
816    inner: S,
817    config: MetadataCacheConfig,
818    cache: std::sync::Mutex<MetadataReadCache>,
819}
820
821#[cfg(feature = "std")]
822impl<S> MetadataCachingSource<S> {
823    /// Wrap a source with the supplied metadata-cache configuration.
824    pub fn new(inner: S, config: MetadataCacheConfig) -> Self {
825        Self {
826            inner,
827            config,
828            cache: std::sync::Mutex::new(MetadataReadCache::new()),
829        }
830    }
831}
832
833#[cfg(feature = "std")]
834impl<S: Source> Source for MetadataCachingSource<S> {
835    fn len(&self) -> u64 {
836        self.inner.len()
837    }
838
839    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
840        self.inner.read_at(offset, buf)
841    }
842
843    fn read_exact_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
844        self.inner.read_exact_at(offset, len)
845    }
846
847    fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
848        MetadataReadCache::read_through(&self.cache, self.config, offset, len, || {
849            self.inner.read_metadata_at(offset, len)
850        })
851    }
852
853    /// `None` when the configuration disabled the cache, which is what the
854    /// wrapper being present but inert means to a caller.
855    fn metadata_cache_stats(&self) -> Option<MetadataCacheStats> {
856        self.config
857            .is_enabled()
858            .then(|| MetadataReadCache::locked(&self.cache).stats())
859    }
860
861    fn reset_metadata_cache_stats(&self) {
862        MetadataReadCache::locked(&self.cache).reset_stats();
863    }
864}
865
866// ---------------------------------------------------------------------------
867// Read + Seek backend (std)
868// ---------------------------------------------------------------------------
869
870/// A lazy [`Source`] over any [`std::io::Read`] + [`std::io::Seek`] (a
871/// [`std::fs::File`], an in-memory `Cursor`, etc.).
872///
873/// Each [`read_at`](Source::read_at) performs a `seek` + `read_exact`, so no
874/// more than the requested bytes are ever held in memory. This is the backend
875/// that lets a 32-bit host read a file larger than its address space: the
876/// metadata and one working chunk fit even when the whole file does not.
877///
878/// The reader is wrapped in a [`std::sync::Mutex`] so the source is `Sync` and
879/// `read_at` can take `&self` (seeking needs `&mut` access). This serializes
880/// concurrent reads, which is correct though not maximally parallel; a future
881/// backend can use positioned reads (`pread`/`seek_read`) to avoid the lock.
882#[cfg(feature = "std")]
883pub struct ReadSeekSource<R> {
884    inner: std::sync::Mutex<R>,
885    len: u64,
886}
887
888#[cfg(feature = "std")]
889impl<R: std::io::Read + std::io::Seek> ReadSeekSource<R> {
890    /// Wrap a `Read + Seek`, measuring its length by seeking to the end (then
891    /// restoring nothing — every `read_at` seeks absolutely anyway).
892    pub fn new(mut reader: R) -> Result<Self, FormatError> {
893        let len = reader
894            .seek(std::io::SeekFrom::End(0))
895            .map_err(|e| FormatError::Source(format_io(&e)))?;
896        Ok(ReadSeekSource {
897            inner: std::sync::Mutex::new(reader),
898            len,
899        })
900    }
901}
902
903#[cfg(feature = "std")]
904impl<R: std::io::Read + std::io::Seek> Source for ReadSeekSource<R> {
905    fn len(&self) -> u64 {
906        self.len
907    }
908
909    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
910        // Bound-check up front so a request past EOF is a clean error rather
911        // than a backend-specific short read.
912        let end = offset
913            .checked_add(buf.len() as u64)
914            .ok_or(FormatError::OffsetOverflow {
915                offset,
916                length: buf.len() as u64,
917            })?;
918        if end > self.len {
919            return Err(FormatError::UnexpectedEof {
920                // `expected`/`available` are byte counts; report them as the
921                // best `usize` we can without truncating on a 32-bit host.
922                expected: end.to_usize().unwrap_or(usize::MAX),
923                available: self.len.to_usize().unwrap_or(usize::MAX),
924            });
925        }
926        let mut guard = self
927            .inner
928            .lock()
929            .unwrap_or_else(std::sync::PoisonError::into_inner);
930        guard
931            .seek(std::io::SeekFrom::Start(offset))
932            .map_err(|e| FormatError::Source(format_io(&e)))?;
933        guard
934            .read_exact(buf)
935            .map_err(|e| FormatError::Source(format_io(&e)))?;
936        Ok(())
937    }
938}
939
940/// Render an `std::io::Error` to a short owned string for [`FormatError::Source`]
941/// (which is `no_std`-friendly and cannot hold the error itself).
942#[cfg(feature = "std")]
943fn format_io(e: &std::io::Error) -> std::string::String {
944    std::format!("{e}")
945}
946
947#[cfg(test)]
948mod tests {
949    use super::*;
950
951    #[cfg(not(feature = "std"))]
952    use alloc::vec;
953
954    #[test]
955    fn bytes_source_reads_and_reports_len() {
956        let data = (0u8..=255).collect::<Vec<u8>>();
957        let src = BytesSource::new(data.clone());
958        assert_eq!(src.len(), 256);
959
960        let mut buf = [0u8; 4];
961        src.read_at(10, &mut buf).unwrap();
962        assert_eq!(buf, [10, 11, 12, 13]);
963
964        let owned = src.read_exact_at(250, 6).unwrap();
965        assert_eq!(owned, vec![250, 251, 252, 253, 254, 255]);
966    }
967
968    #[test]
969    fn bytes_source_short_read_is_eof() {
970        let src = BytesSource::new(vec![1u8, 2, 3]);
971        let mut buf = [0u8; 4];
972        let err = src.read_at(0, &mut buf).unwrap_err();
973        assert!(matches!(err, FormatError::UnexpectedEof { .. }));
974        // Reading exactly to the end is fine.
975        let mut ok = [0u8; 3];
976        src.read_at(0, &mut ok).unwrap();
977        assert_eq!(ok, [1, 2, 3]);
978    }
979
980    #[test]
981    fn bytes_source_offset_past_end_is_eof() {
982        let src = BytesSource::new(vec![0u8; 8]);
983        let mut buf = [0u8; 1];
984        assert!(matches!(
985            src.read_at(8, &mut buf).unwrap_err(),
986            FormatError::UnexpectedEof { .. }
987        ));
988        // Zero-length read at EOF succeeds.
989        src.read_at(8, &mut []).unwrap();
990    }
991
992    #[test]
993    fn read_exact_at_rejects_oversized_len_without_allocating() {
994        // A length far larger than the source must error cleanly rather than
995        // attempt to reserve the buffer first. Before the pre-allocation bounds
996        // check, this called `vec![0u8; usize::MAX]` and aborted the process.
997        let src = BytesSource::new(vec![1u8, 2, 3, 4]);
998        assert!(matches!(
999            src.read_exact_at(0, usize::MAX).unwrap_err(),
1000            FormatError::UnexpectedEof { .. }
1001        ));
1002        // A read that fits is unaffected.
1003        assert_eq!(src.read_exact_at(1, 3).unwrap(), vec![2, 3, 4]);
1004    }
1005
1006    #[test]
1007    fn empty_source() {
1008        let src = BytesSource::new(Vec::<u8>::new());
1009        assert_eq!(src.len(), 0);
1010    }
1011
1012    #[test]
1013    fn forwarding_through_reference() {
1014        let src = BytesSource::new(vec![9u8, 8, 7]);
1015        let r: &dyn Source = &src;
1016        let mut buf = [0u8; 2];
1017        r.read_at(1, &mut buf).unwrap();
1018        assert_eq!(buf, [8, 7]);
1019    }
1020
1021    #[test]
1022    fn forwarding_through_reference_preserves_metadata_reads() {
1023        use core::cell::Cell;
1024
1025        struct MetadataSource {
1026            metadata_reads: Cell<usize>,
1027        }
1028
1029        impl Source for MetadataSource {
1030            fn len(&self) -> u64 {
1031                16
1032            }
1033
1034            fn read_at(&self, _offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
1035                buf.fill(0);
1036                Ok(())
1037            }
1038
1039            fn read_metadata_at(&self, _offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
1040                self.metadata_reads.set(self.metadata_reads.get() + 1);
1041                Ok(vec![0xAB; len])
1042            }
1043        }
1044
1045        fn read_metadata_via_trait<T: Source>(source: T) -> Vec<u8> {
1046            source.read_metadata_at(4, 3).unwrap()
1047        }
1048
1049        let source = MetadataSource {
1050            metadata_reads: Cell::new(0),
1051        };
1052
1053        assert_eq!(read_metadata_via_trait(&source), vec![0xAB; 3]);
1054        assert_eq!(source.metadata_reads.get(), 1);
1055    }
1056
1057    #[cfg(feature = "std")]
1058    #[test]
1059    fn metadata_cache_caches_only_metadata_reads() {
1060        use std::sync::{
1061            Arc,
1062            atomic::{AtomicUsize, Ordering},
1063        };
1064
1065        struct CountingSource {
1066            data: Vec<u8>,
1067            reads: Arc<AtomicUsize>,
1068        }
1069
1070        impl Source for CountingSource {
1071            fn len(&self) -> u64 {
1072                self.data.len() as u64
1073            }
1074
1075            fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
1076                self.reads.fetch_add(1, Ordering::SeqCst);
1077                BytesSource::new(&self.data).read_at(offset, buf)
1078            }
1079        }
1080
1081        let reads = Arc::new(AtomicUsize::new(0));
1082        let source = MetadataCachingSource::new(
1083            CountingSource {
1084                data: (0u8..16).collect(),
1085                reads: Arc::clone(&reads),
1086            },
1087            MetadataCacheConfig::new(16),
1088        );
1089
1090        assert_eq!(source.read_metadata_at(4, 4).unwrap(), vec![4, 5, 6, 7]);
1091        assert_eq!(source.read_metadata_at(4, 4).unwrap(), vec![4, 5, 6, 7]);
1092        assert_eq!(reads.load(Ordering::SeqCst), 1);
1093
1094        assert_eq!(source.read_exact_at(4, 4).unwrap(), vec![4, 5, 6, 7]);
1095        assert_eq!(source.read_exact_at(4, 4).unwrap(), vec![4, 5, 6, 7]);
1096        assert_eq!(reads.load(Ordering::SeqCst), 3);
1097    }
1098
1099    #[cfg(feature = "std")]
1100    #[test]
1101    fn read_seek_source_matches_in_memory() {
1102        use std::io::Cursor;
1103        let data = (0u8..200).collect::<Vec<u8>>();
1104        let mem = BytesSource::new(data.clone());
1105        let seek = ReadSeekSource::new(Cursor::new(data.clone())).unwrap();
1106        assert_eq!(seek.len(), mem.len());
1107
1108        // Every read_at against the lazy source matches the in-memory source.
1109        for &(off, len) in &[(0u64, 1usize), (5, 10), (199, 1), (100, 50)] {
1110            let a = mem.read_exact_at(off, len).unwrap();
1111            let b = seek.read_exact_at(off, len).unwrap();
1112            assert_eq!(a, b, "mismatch at offset {off} len {len}");
1113        }
1114    }
1115
1116    #[cfg(feature = "std")]
1117    #[test]
1118    fn read_seek_source_past_end_is_error() {
1119        use std::io::Cursor;
1120        let seek = ReadSeekSource::new(Cursor::new(vec![1u8, 2, 3, 4])).unwrap();
1121        let mut buf = [0u8; 3];
1122        assert!(matches!(
1123            seek.read_at(2, &mut buf).unwrap_err(),
1124            FormatError::UnexpectedEof { .. }
1125        ));
1126    }
1127
1128    #[cfg(feature = "std")]
1129    #[test]
1130    fn read_seek_source_is_sync() {
1131        // Compile-time assertion that the std backend is Send + Sync so it can
1132        // back a parallel reader.
1133        fn assert_send_sync<T: Send + Sync>() {}
1134        assert_send_sync::<ReadSeekSource<std::io::Cursor<Vec<u8>>>>();
1135    }
1136
1137    // -----------------------------------------------------------------------
1138    // The bounded metadata store (issue #367)
1139    // -----------------------------------------------------------------------
1140
1141    #[test]
1142    fn eviction_drops_the_least_recently_used_entry_not_the_oldest() {
1143        // Room for three ten-byte entries, so the fourth displaces exactly one.
1144        let budget = 30;
1145        let mut cache = MetadataReadCache::new();
1146        cache.insert(0, 10, vec![0u8; 10], budget);
1147        cache.insert(100, 10, vec![1u8; 10], budget);
1148        cache.insert(200, 10, vec![2u8; 10], budget);
1149
1150        // Reading the first entry makes the *second* the least recently used,
1151        // which is what separates an LRU from a queue.
1152        assert!(cache.get(0, 10).is_some());
1153        cache.insert(300, 10, vec![3u8; 10], budget);
1154
1155        assert!(
1156            cache.get(0, 10).is_some(),
1157            "read most recently, must survive"
1158        );
1159        assert!(cache.get(100, 10).is_none(), "least recently used, must go");
1160        assert!(cache.get(200, 10).is_some());
1161        assert!(cache.get(300, 10).is_some());
1162    }
1163
1164    #[test]
1165    fn invalidation_takes_every_overlap_and_spares_the_neighbours() {
1166        let budget = 1024;
1167        let mut cache = MetadataReadCache::new();
1168        for offset in [0u64, 10, 20, 30] {
1169            cache.insert(offset, 10, vec![offset as u8; 10], budget);
1170        }
1171        // One long entry starting well before the write below and reaching well
1172        // past it. Nothing but its own length says it can be reached from there.
1173        cache.insert(5, 40, vec![9u8; 40], budget);
1174
1175        cache.invalidate_overlapping(20, 5);
1176
1177        assert!(cache.get(0, 10).is_some(), "ends at 10, short of the write");
1178        assert!(
1179            cache.get(10, 10).is_some(),
1180            "ends exactly where the write starts, so it shares no byte with it"
1181        );
1182        assert!(cache.get(20, 10).is_none(), "the write lands inside it");
1183        assert!(cache.get(30, 10).is_some(), "starts after the write ends");
1184        assert!(
1185            cache.get(5, 40).is_none(),
1186            "starts before the write and spans it, so a search beginning at the \
1187             write's own offset would walk straight past it"
1188        );
1189    }
1190
1191    #[test]
1192    fn re_inserting_a_key_replaces_it_rather_than_counting_it_twice() {
1193        // Exactly two ten-byte entries fit.
1194        let budget = 20;
1195        let mut cache = MetadataReadCache::new();
1196        cache.insert(0, 10, vec![0u8; 10], budget);
1197        cache.insert(0, 10, vec![1u8; 10], budget);
1198        cache.insert(100, 10, vec![2u8; 10], budget);
1199
1200        assert_eq!(
1201            cache.get(0, 10).as_deref(),
1202            Some(&[1u8; 10][..]),
1203            "the later value replaces the earlier one"
1204        );
1205        assert!(
1206            cache.get(100, 10).is_some(),
1207            "a replacement that was counted twice would have evicted to make room"
1208        );
1209    }
1210
1211    #[test]
1212    fn one_offset_at_two_lengths_holds_two_entries() {
1213        let budget = 1024;
1214        let mut cache = MetadataReadCache::new();
1215        cache.insert(64, 4, vec![1u8; 4], budget);
1216        cache.insert(64, 8, vec![2u8; 8], budget);
1217
1218        assert_eq!(cache.get(64, 4).as_deref(), Some(&[1u8; 4][..]));
1219        assert_eq!(cache.get(64, 8).as_deref(), Some(&[2u8; 8][..]));
1220    }
1221
1222    /// A hit must not get slower as the cache gets bigger (issue #367).
1223    ///
1224    /// The store this replaced walked a `Vec`, so a hit cost O(entries), which
1225    /// put it above the cost of the positioned read it exists to avoid from
1226    /// about 3,000 entries on. Measured in release against that read: 9x faster
1227    /// at 64 entries, 3.2x at 1,024, then 1.2x *slower* at 4,096 and 21.9x
1228    /// slower at 65,536.
1229    ///
1230    /// Across the pair below, the scanning store measured 16.6 in release
1231    /// (369 ns to 6,134) and 36.8 unoptimized. Indexed, the same pair measures
1232    /// 1.10 and 1.30. The allowance sits between those two groups with room on
1233    /// either side: six times what an unoptimized build measures here, and a
1234    /// fifth of what a return to scanning would.
1235    #[test]
1236    fn a_hit_does_not_get_slower_as_the_cache_grows() {
1237        /// Entry counts either side of the growth, and the factor the cost is
1238        /// allowed to move across it. Named so the failure message cannot drift
1239        /// from what was measured.
1240        const SMALL: usize = 1_024;
1241        const LARGE: usize = 16_384;
1242        const ALLOWED_GROWTH: f64 = 8.0;
1243
1244        fn nanos_per_hit(entries: usize) -> f64 {
1245            const ENTRY_LEN: usize = 64;
1246            let budget = entries * ENTRY_LEN * 2;
1247            let mut cache = MetadataReadCache::new();
1248            for i in 0..entries {
1249                cache.insert(
1250                    (i * ENTRY_LEN) as u64,
1251                    ENTRY_LEN,
1252                    vec![7u8; ENTRY_LEN],
1253                    budget,
1254                );
1255            }
1256            // Warm the caches the machine has, then time a pass that is all hits.
1257            for i in 0..entries {
1258                assert!(cache.get((i * ENTRY_LEN) as u64, ENTRY_LEN).is_some());
1259            }
1260            let started = std::time::Instant::now();
1261            for i in 0..entries {
1262                assert!(cache.get((i * ENTRY_LEN) as u64, ENTRY_LEN).is_some());
1263            }
1264            started.elapsed().as_secs_f64() * 1e9 / entries as f64
1265        }
1266
1267        let small = nanos_per_hit(SMALL);
1268        let large = nanos_per_hit(LARGE);
1269        assert!(
1270            large < small * ALLOWED_GROWTH,
1271            "a hit cost {large:.0} ns with {LARGE} entries against {small:.0} ns with \
1272             {SMALL} -- growing the cache should not move it, and a cost that tracks \
1273             its size is the shape of a store being searched rather than indexed"
1274        );
1275    }
1276
1277    // -----------------------------------------------------------------------
1278    // What the cache reports about itself (issue #353)
1279    // -----------------------------------------------------------------------
1280
1281    /// A source of `len` bytes that serves every metadata read, so a cache in
1282    /// front of it is the only thing that can make a read not happen.
1283    #[cfg(feature = "std")]
1284    fn ramp(len: usize) -> BytesSource<Vec<u8>> {
1285        BytesSource::new((0..len).map(|i| i as u8).collect::<Vec<u8>>())
1286    }
1287
1288    #[cfg(feature = "std")]
1289    #[test]
1290    fn a_read_too_large_to_admit_is_not_charged_as_a_miss() {
1291        // 64-byte entries are eligible; anything above that is turned away
1292        // before it reaches the cache.
1293        let config = MetadataCacheConfig::new(4096).with_max_entry_bytes(64);
1294        let source = MetadataCachingSource::new(ramp(4096), config);
1295
1296        source.read_metadata_at(0, 64).unwrap(); // miss, then admitted
1297        source.read_metadata_at(0, 64).unwrap(); // hit
1298        source.read_metadata_at(128, 256).unwrap(); // too large to admit
1299        source.read_metadata_at(128, 256).unwrap(); // and so, still too large
1300
1301        let stats = source.metadata_cache_stats().unwrap();
1302        assert_eq!(stats.hits(), 1);
1303        assert_eq!(stats.misses(), 1);
1304        assert_eq!(stats.oversize_reads(), 2);
1305        assert_eq!(stats.reads(), 4);
1306        // The two oversize reads are the caller's to fix by raising
1307        // `max_entry_bytes`, and folding them in would report the cache at 25%
1308        // rather than saying which knob is turning them away.
1309        assert_eq!(stats.hit_rate(), Some(0.5));
1310
1311        // The other half of the same rule. `with_max_entry_bytes` can name a cap
1312        // above the whole budget, and a read between the two would pass the entry
1313        // check only for `insert` to refuse it every time -- a permanent miss
1314        // reported as an ordinary one. The budget turns it away up front instead.
1315        let lopsided = MetadataCacheConfig::new(128).with_max_entry_bytes(512);
1316        let source = MetadataCachingSource::new(ramp(4096), lopsided);
1317        source.read_metadata_at(0, 256).unwrap();
1318        source.read_metadata_at(0, 256).unwrap();
1319        let stats = source.metadata_cache_stats().unwrap();
1320        assert_eq!(stats.oversize_reads(), 2);
1321        assert_eq!(stats.misses(), 0);
1322        assert_eq!(stats.hit_rate(), None);
1323    }
1324
1325    #[cfg(feature = "std")]
1326    #[test]
1327    fn no_eligible_read_yet_is_not_a_hit_rate_of_zero() {
1328        let config = MetadataCacheConfig::new(4096).with_max_entry_bytes(64);
1329        let source = MetadataCachingSource::new(ramp(4096), config);
1330
1331        let fresh = source.metadata_cache_stats().unwrap();
1332        assert_eq!(fresh.hit_rate(), None, "nothing has been read");
1333
1334        source.read_metadata_at(0, 256).unwrap();
1335        assert_eq!(
1336            source.metadata_cache_stats().unwrap().hit_rate(),
1337            None,
1338            "a read the cache never saw does not make a rate out of it"
1339        );
1340
1341        source.read_metadata_at(0, 64).unwrap();
1342        assert_eq!(
1343            source.metadata_cache_stats().unwrap().hit_rate(),
1344            Some(0.0),
1345            "one eligible read that missed *is* a rate, and the opposite reading"
1346        );
1347    }
1348
1349    #[cfg(feature = "std")]
1350    #[test]
1351    fn the_budget_and_a_write_drop_entries_for_different_reasons() {
1352        // Two 64-byte entries fit; a third forces one out.
1353        const BUDGET: usize = 128;
1354        let mut cache = MetadataReadCache::new();
1355        cache.insert(0, 64, vec![1u8; 64], BUDGET);
1356        cache.insert(64, 64, vec![2u8; 64], BUDGET);
1357
1358        // Re-reading a key replaces it. Nothing was dropped for want of room or
1359        // because the bytes changed, so neither counter moves.
1360        cache.insert(0, 64, vec![1u8; 64], BUDGET);
1361        let replaced = cache.stats();
1362        assert_eq!(replaced.entries(), 2);
1363        assert_eq!(replaced.evictions(), 0);
1364        assert_eq!(replaced.invalidations(), 0);
1365
1366        cache.insert(128, 64, vec![3u8; 64], BUDGET);
1367        let evicted = cache.stats();
1368        assert_eq!(evicted.evictions(), 1, "the budget forced this one");
1369        assert_eq!(evicted.invalidations(), 0);
1370        // The least recently used of the three went, leaving [0, 64) and
1371        // [128, 192).
1372        assert_eq!(evicted.entries(), 2);
1373
1374        // A write across [32, 160) reaches into both survivors: one starts
1375        // before it, the other after.
1376        cache.invalidate_overlapping(32, 128);
1377        let invalidated = cache.stats();
1378        assert_eq!(invalidated.invalidations(), 2, "the write overlapped both");
1379        assert_eq!(
1380            invalidated.evictions(),
1381            1,
1382            "a write is not the budget, and a caller told to raise the budget \
1383             because of one would be raising it for nothing"
1384        );
1385        assert_eq!(invalidated.entries(), 0);
1386        assert_eq!(invalidated.bytes(), 0);
1387    }
1388
1389    #[cfg(feature = "std")]
1390    #[test]
1391    fn resetting_the_counters_keeps_the_entries() {
1392        const BUDGET: usize = 4096;
1393        let mut cache = MetadataReadCache::new();
1394        cache.insert(0, 64, vec![1u8; 64], BUDGET);
1395        assert!(cache.get(0, 64).is_some());
1396        assert!(cache.get(512, 64).is_none());
1397
1398        cache.reset_stats();
1399
1400        let stats = cache.stats();
1401        assert_eq!(stats.hits(), 0);
1402        assert_eq!(stats.misses(), 0);
1403        assert_eq!(stats.hit_rate(), None);
1404        // Occupancy measures the cache rather than tallying its history, so a
1405        // reset that emptied it would answer a different question than the one
1406        // `H5Freset_mdc_hit_rate_stats` asks.
1407        assert_eq!(stats.entries(), 1);
1408        assert_eq!(stats.bytes(), 64);
1409        assert!(cache.get(0, 64).is_some(), "the entry is still servable");
1410    }
1411
1412    #[cfg(feature = "std")]
1413    #[test]
1414    fn a_disabled_cache_reports_nothing_rather_than_zeroes() {
1415        let source = MetadataCachingSource::new(ramp(4096), MetadataCacheConfig::disabled());
1416        assert_eq!(
1417            source.read_metadata_at(0, 64).unwrap(),
1418            (0..64u8).collect::<Vec<u8>>()
1419        );
1420        assert_eq!(
1421            source.metadata_cache_stats(),
1422            None,
1423            "an all-zero snapshot would read as a cache that is on and idle"
1424        );
1425        source.reset_metadata_cache_stats();
1426        assert_eq!(
1427            source.metadata_cache_stats(),
1428            None,
1429            "and resetting one there is nothing to reset does not conjure one"
1430        );
1431    }
1432
1433    #[cfg(feature = "std")]
1434    #[test]
1435    fn a_wrapper_reports_the_cache_it_reads_through() {
1436        let config = MetadataCacheConfig::new(4096);
1437        let source = MetadataCachingSource::new(ramp(4096), config);
1438        // The base-relative view a userblock file reads through forwards its
1439        // metadata reads to the inner source, so it must forward the account of
1440        // them too.
1441        let framed = BaseOffsetSource {
1442            inner: &source,
1443            base: BaseAddress::new(512),
1444        };
1445        framed.read_metadata_at(0, 64).unwrap();
1446        framed.read_metadata_at(0, 64).unwrap();
1447
1448        let stats = framed.metadata_cache_stats().expect("forwarded");
1449        assert_eq!((stats.hits(), stats.misses()), (1, 1));
1450        assert_eq!(stats, source.metadata_cache_stats().unwrap());
1451
1452        framed.reset_metadata_cache_stats();
1453        assert_eq!(source.metadata_cache_stats().unwrap().hits(), 0);
1454        assert_eq!(
1455            source.metadata_cache_stats().unwrap().entries(),
1456            1,
1457            "reset through the view is a reset of counters, not a flush"
1458        );
1459    }
1460}