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}