Skip to main content

btrfs_fs/
cache.rs

1//! Cache layer for the filesystem.
2//!
3//! Three caches sit on the read path:
4//!
5//! - [`LruTreeBlockCache`] — a [`btrfs_disk::reader::TreeBlockCache`] impl
6//!   keyed by logical address. Wired into the `BlockReader` so every
7//!   tree walk benefits transparently.
8//! - [`InodeCache`] — `Inode` → parsed `InodeItem`. Hot path for
9//!   `getattr` and the inode lookups inside `lookup` / `readlink` /
10//!   `read`.
11//! - [`ExtentMapCache`] — `Inode` → sorted list of `EXTENT_DATA`
12//!   records for that file. Built lazily on first read and reused for
13//!   subsequent reads of the same file.
14//!
15//! All three use interior mutability behind a `Mutex` so they're
16//! `Send + Sync` and shareable through the `Arc<Inner>` filesystem
17//! handle. `Mutex` rather than `RwLock` because LRU mutation happens
18//! on every access (touching MRU order), so even a "read" needs
19//! exclusive access to the cache structure.
20
21use crate::Inode;
22use btrfs_disk::{
23    items::{FileExtentItem, InodeItem},
24    reader::TreeBlockCache,
25    tree::TreeBlock,
26};
27use lru::LruCache;
28use std::{
29    num::NonZeroUsize,
30    sync::{
31        Arc, Mutex,
32        atomic::{AtomicU64, Ordering},
33    },
34};
35
36/// Default tree-block cache capacity in entries (~16 KiB each, so 4096
37/// entries ≈ 64 MiB).
38pub(crate) const TREE_BLOCK_CACHE_DEFAULT_ENTRIES: usize = 4096;
39
40/// Default inode cache capacity in entries.
41pub(crate) const INODE_CACHE_DEFAULT_ENTRIES: usize = 4096;
42
43/// Default extent-map cache capacity in entries.
44pub(crate) const EXTENT_MAP_CACHE_DEFAULT_ENTRIES: usize = 1024;
45
46/// Capacities for the three caches a [`crate::Filesystem`] holds.
47/// Pass to [`crate::Filesystem::open_with_caches`] /
48/// [`crate::Filesystem::open_subvol_with_caches`] to override the
49/// defaults — useful for memory-constrained embedders or
50/// benchmarking the cold path.
51///
52/// All three values must be `> 0`; the underlying LRU caches reject
53/// zero-capacity construction. Use [`CacheConfig::no_cache`] for the
54/// minimum-viable single-entry caches.
55#[derive(Debug, Clone, Copy)]
56pub struct CacheConfig {
57    /// Number of tree blocks to cache (~16 KiB each on default
58    /// nodesize). Default: 4096 (~64 MiB).
59    pub tree_blocks: usize,
60    /// Number of parsed inode items to cache. Default: 4096.
61    pub inodes: usize,
62    /// Number of per-inode extent maps to cache. Default: 1024.
63    pub extent_maps: usize,
64}
65
66impl Default for CacheConfig {
67    fn default() -> Self {
68        Self {
69            tree_blocks: TREE_BLOCK_CACHE_DEFAULT_ENTRIES,
70            inodes: INODE_CACHE_DEFAULT_ENTRIES,
71            extent_maps: EXTENT_MAP_CACHE_DEFAULT_ENTRIES,
72        }
73    }
74}
75
76impl CacheConfig {
77    /// Smallest viable configuration: each cache holds a single
78    /// entry. Useful for benchmarking the cold path or when the
79    /// embedder wants to disable caching entirely. The LRU
80    /// implementations reject `0`, so this is the actual floor.
81    #[must_use]
82    pub fn no_cache() -> Self {
83        Self {
84            tree_blocks: 1,
85            inodes: 1,
86            extent_maps: 1,
87        }
88    }
89}
90
91/// Live counters for an [`LruTreeBlockCache`]. Useful for tests,
92/// benchmarks, and embedders who want to expose cache hit ratios via
93/// metrics.
94#[derive(Debug, Default, Clone, Copy)]
95pub struct CacheStats {
96    pub hits: u64,
97    pub misses: u64,
98    pub insertions: u64,
99    pub invalidations: u64,
100}
101
102/// LRU-evicting [`TreeBlockCache`] keyed by logical address.
103///
104/// Hit/miss/insertion counters are tracked via atomics so reading
105/// `stats()` doesn't contend with cache traffic.
106pub struct LruTreeBlockCache {
107    inner: Mutex<LruCache<u64, Arc<TreeBlock>>>,
108    hits: AtomicU64,
109    misses: AtomicU64,
110    insertions: AtomicU64,
111    invalidations: AtomicU64,
112}
113
114impl LruTreeBlockCache {
115    /// Create a cache with `capacity` entries. `capacity` must be > 0.
116    #[must_use]
117    pub fn new(capacity: usize) -> Self {
118        let cap = NonZeroUsize::new(capacity)
119            .expect("LruTreeBlockCache capacity must be > 0");
120        Self {
121            inner: Mutex::new(LruCache::new(cap)),
122            hits: AtomicU64::new(0),
123            misses: AtomicU64::new(0),
124            insertions: AtomicU64::new(0),
125            invalidations: AtomicU64::new(0),
126        }
127    }
128
129    /// Snapshot of the current hit/miss/insertion counters.
130    #[must_use]
131    pub fn stats(&self) -> CacheStats {
132        CacheStats {
133            hits: self.hits.load(Ordering::Relaxed),
134            misses: self.misses.load(Ordering::Relaxed),
135            insertions: self.insertions.load(Ordering::Relaxed),
136            invalidations: self.invalidations.load(Ordering::Relaxed),
137        }
138    }
139}
140
141impl TreeBlockCache for LruTreeBlockCache {
142    fn get(&self, addr: u64) -> Option<Arc<TreeBlock>> {
143        // `LruCache::get` mutates MRU order, so we need exclusive
144        // access. `&self` is fine because we use interior mutability.
145        let hit = self.inner.lock().unwrap().get(&addr).map(Arc::clone);
146        if hit.is_some() {
147            self.hits.fetch_add(1, Ordering::Relaxed);
148        } else {
149            self.misses.fetch_add(1, Ordering::Relaxed);
150        }
151        hit
152    }
153
154    fn put(&self, addr: u64, block: Arc<TreeBlock>) {
155        self.inner.lock().unwrap().put(addr, block);
156        self.insertions.fetch_add(1, Ordering::Relaxed);
157    }
158
159    fn invalidate(&self, addr: u64) {
160        self.inner.lock().unwrap().pop(&addr);
161        self.invalidations.fetch_add(1, Ordering::Relaxed);
162    }
163}
164
165/// LRU cache mapping `Inode` → parsed `InodeItem`. Stored as `Arc<...>`
166/// so cache lookups don't have to copy the 160-byte struct.
167pub(crate) struct InodeCache {
168    inner: Mutex<LruCache<Inode, Arc<InodeItem>>>,
169}
170
171impl InodeCache {
172    pub(crate) fn new(capacity: usize) -> Self {
173        let cap = NonZeroUsize::new(capacity)
174            .expect("InodeCache capacity must be > 0");
175        Self {
176            inner: Mutex::new(LruCache::new(cap)),
177        }
178    }
179
180    pub(crate) fn get(&self, ino: Inode) -> Option<Arc<InodeItem>> {
181        self.inner.lock().unwrap().get(&ino).map(Arc::clone)
182    }
183
184    pub(crate) fn put(&self, ino: Inode, item: Arc<InodeItem>) {
185        self.inner.lock().unwrap().put(ino, item);
186    }
187
188    /// Drop a single entry. Wired to FUSE `forget` so the kernel's
189    /// reference-count signal frees memory ahead of LRU eviction;
190    /// also called by upcoming write ops (F9+) for invalidation.
191    pub(crate) fn invalidate(&self, ino: Inode) {
192        self.inner.lock().unwrap().pop(&ino);
193    }
194}
195
196/// A single `EXTENT_DATA` item collected for a file. Mirrors the
197/// shape used by the read path so cache hits can feed straight into
198/// `read::read_file` without re-parsing.
199#[derive(Clone)]
200pub(crate) struct ExtentRecord {
201    pub file_pos: u64,
202    pub item: FileExtentItem,
203    /// Raw on-disk payload of the `EXTENT_DATA` item, used to extract
204    /// inline extent bytes.
205    pub raw: Vec<u8>,
206}
207
208/// Sorted (by `file_pos`) list of `EXTENT_DATA` items for one inode.
209#[derive(Default)]
210pub(crate) struct ExtentMap {
211    pub records: Vec<ExtentRecord>,
212}
213
214/// LRU cache mapping `Inode` → its full extent map. Built lazily on
215/// the first `read` of a file; reused for all subsequent reads.
216pub(crate) struct ExtentMapCache {
217    inner: Mutex<LruCache<Inode, Arc<ExtentMap>>>,
218}
219
220impl ExtentMapCache {
221    pub(crate) fn new(capacity: usize) -> Self {
222        let cap = NonZeroUsize::new(capacity)
223            .expect("ExtentMapCache capacity must be > 0");
224        Self {
225            inner: Mutex::new(LruCache::new(cap)),
226        }
227    }
228
229    pub(crate) fn get(&self, ino: Inode) -> Option<Arc<ExtentMap>> {
230        self.inner.lock().unwrap().get(&ino).map(Arc::clone)
231    }
232
233    pub(crate) fn put(&self, ino: Inode, map: Arc<ExtentMap>) {
234        self.inner.lock().unwrap().put(ino, map);
235    }
236
237    /// Drop a single entry. Wired to FUSE `forget` so the kernel's
238    /// reference-count signal frees memory ahead of LRU eviction;
239    /// also called by upcoming write ops (F9+) for invalidation.
240    pub(crate) fn invalidate(&self, ino: Inode) {
241        self.inner.lock().unwrap().pop(&ino);
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    //! Cache mechanics are exercised end-to-end by `fs/tests/basic.rs`
248    //! and `fs/tests/cache.rs`. The unit-test surface here would have
249    //! to construct an `InodeItem` from scratch (16+ fields including
250    //! a `Timespec` quartet and bitflags) just to prove that an
251    //! `lru::LruCache` evicts correctly, which would test the upstream
252    //! crate more than our wrapper. Skipping in favour of integration
253    //! coverage.
254}