Skip to main content

ahrightos_fs/
block_cache.rs

1use crate::block_dev::{BlockDevice, BlockDeviceHandle, BLOCK_SIZE};
2use crate::file::FileError;
3use alloc::{boxed::Box, collections::VecDeque};
4use spin::Mutex;
5
6const DEFAULT_CACHE_BLOCKS: usize = 64;
7
8struct CachedBlock {
9    block_no: usize,
10    data: Box<[u8; BLOCK_SIZE]>,
11    dirty: bool,
12}
13
14struct CacheInner {
15    entries: VecDeque<CachedBlock>,
16    capacity: usize,
17    hits: u64,
18    misses: u64,
19}
20
21impl CacheInner {
22    fn new(capacity: usize) -> Self {
23        Self {
24            entries: VecDeque::with_capacity(capacity.max(1)),
25            capacity: capacity.max(1),
26            hits: 0,
27            misses: 0,
28        }
29    }
30
31    fn find(&self, block_no: usize) -> Option<usize> {
32        self.entries
33            .iter()
34            .position(|entry| entry.block_no == block_no)
35    }
36
37    fn touch(&mut self, idx: usize) {
38        if idx + 1 == self.entries.len() {
39            return;
40        }
41        if let Some(entry) = self.entries.remove(idx) {
42            self.entries.push_back(entry);
43        }
44    }
45
46    fn evict_lru(&mut self, dev: &dyn BlockDevice) -> Result<(), FileError> {
47        if let Some(victim) = self.entries.pop_front() {
48            if victim.dirty {
49                dev.write_block(victim.block_no, victim.data.as_ref())?;
50            }
51        }
52        Ok(())
53    }
54
55    fn dirty_count(&self) -> usize {
56        self.entries.iter().filter(|entry| entry.dirty).count()
57    }
58
59    fn flush_block(&mut self, dev: &dyn BlockDevice, block_no: usize) -> Result<bool, FileError> {
60        let Some(idx) = self.find(block_no) else {
61            return Ok(false);
62        };
63        if self.entries[idx].dirty {
64            let victim = &self.entries[idx];
65            dev.write_block(victim.block_no, victim.data.as_ref())?;
66            self.entries[idx].dirty = false;
67        }
68        Ok(true)
69    }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct BlockCacheStats {
74    pub hits: u64,
75    pub misses: u64,
76    pub cached_blocks: usize,
77    pub dirty_blocks: usize,
78    pub capacity: usize,
79}
80
81impl BlockCacheStats {
82    pub fn total_reads(self) -> u64 {
83        self.hits + self.misses
84    }
85
86    pub fn hit_rate_percent(self) -> u64 {
87        let total = self.total_reads();
88        if total == 0 {
89            0
90        } else {
91            self.hits * 100 / total
92        }
93    }
94}
95
96pub struct BlockCache {
97    dev: BlockDeviceHandle,
98    inner: Mutex<CacheInner>,
99}
100
101impl BlockCache {
102    pub fn new(dev: BlockDeviceHandle) -> Self {
103        Self {
104            dev,
105            inner: Mutex::new(CacheInner::new(DEFAULT_CACHE_BLOCKS)),
106        }
107    }
108
109    pub fn with_capacity(dev: BlockDeviceHandle, capacity: usize) -> Self {
110        Self {
111            dev,
112            inner: Mutex::new(CacheInner::new(capacity)),
113        }
114    }
115
116    pub fn flush(&self) -> Result<(), FileError> {
117        let mut inner = self.inner.lock();
118        for entry in &inner.entries {
119            if entry.dirty {
120                self.dev.write_block(entry.block_no, entry.data.as_ref())?;
121            }
122        }
123        for entry in &mut inner.entries {
124            entry.dirty = false;
125        }
126        Ok(())
127    }
128
129    pub fn stats(&self) -> (u64, u64) {
130        let inner = self.inner.lock();
131        (inner.hits, inner.misses)
132    }
133
134    pub fn stats_snapshot(&self) -> BlockCacheStats {
135        let inner = self.inner.lock();
136        BlockCacheStats {
137            hits: inner.hits,
138            misses: inner.misses,
139            cached_blocks: inner.entries.len(),
140            dirty_blocks: inner.dirty_count(),
141            capacity: inner.capacity,
142        }
143    }
144
145    pub fn reset_stats(&self) {
146        let mut inner = self.inner.lock();
147        inner.hits = 0;
148        inner.misses = 0;
149    }
150
151    pub fn flush_block(&self, block_no: usize) -> Result<bool, FileError> {
152        self.inner.lock().flush_block(&*self.dev, block_no)
153    }
154}
155
156impl BlockDevice for BlockCache {
157    fn read_block(&self, block_no: usize, buf: &mut [u8]) -> Result<(), FileError> {
158        if buf.len() != BLOCK_SIZE {
159            return Err(FileError::InvalidOperation);
160        }
161        let mut inner = self.inner.lock();
162
163        if let Some(idx) = inner.find(block_no) {
164            buf.copy_from_slice(inner.entries[idx].data.as_ref());
165            inner.touch(idx);
166            inner.hits += 1;
167            return Ok(());
168        }
169
170        inner.misses += 1;
171        if inner.entries.len() >= inner.capacity {
172            inner.evict_lru(&*self.dev)?;
173        }
174
175        let mut data = Box::new([0u8; BLOCK_SIZE]);
176        self.dev.read_block(block_no, data.as_mut())?;
177        buf.copy_from_slice(data.as_ref());
178        inner.entries.push_back(CachedBlock {
179            block_no,
180            data,
181            dirty: false,
182        });
183        Ok(())
184    }
185
186    fn write_block(&self, block_no: usize, buf: &[u8]) -> Result<(), FileError> {
187        if buf.len() != BLOCK_SIZE {
188            return Err(FileError::InvalidOperation);
189        }
190        let mut inner = self.inner.lock();
191
192        if let Some(idx) = inner.find(block_no) {
193            inner.entries[idx].data.copy_from_slice(buf);
194            inner.entries[idx].dirty = true;
195            inner.touch(idx);
196            return Ok(());
197        }
198
199        if inner.entries.len() >= inner.capacity {
200            inner.evict_lru(&*self.dev)?;
201        }
202
203        let mut data = Box::new([0u8; BLOCK_SIZE]);
204        data.copy_from_slice(buf);
205        inner.entries.push_back(CachedBlock {
206            block_no,
207            data,
208            dirty: true,
209        });
210        Ok(())
211    }
212
213    fn block_count(&self) -> usize {
214        self.dev.block_count()
215    }
216
217    fn flush(&self) -> Result<(), FileError> {
218        BlockCache::flush(self)
219    }
220}
221
222impl Drop for BlockCache {
223    fn drop(&mut self) {
224        let _ = self.flush();
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::DummyBlockDev;
232    use alloc::sync::Arc;
233
234    fn block(fill: u8) -> [u8; BLOCK_SIZE] {
235        [fill; BLOCK_SIZE]
236    }
237
238    #[test]
239    fn repeated_read_uses_cache_and_updates_stats() {
240        let raw = Arc::new(DummyBlockDev::new(4));
241        raw.write_block(1, &block(7)).unwrap();
242        let cache = BlockCache::with_capacity(raw, 2);
243
244        let mut buf = [0u8; BLOCK_SIZE];
245        cache.read_block(1, &mut buf).unwrap();
246        assert_eq!(buf, block(7));
247        cache.read_block(1, &mut buf).unwrap();
248
249        let stats = cache.stats_snapshot();
250        assert_eq!(stats.misses, 1);
251        assert_eq!(stats.hits, 1);
252        assert_eq!(stats.cached_blocks, 1);
253        assert_eq!(stats.hit_rate_percent(), 50);
254    }
255
256    #[test]
257    fn write_back_defers_device_write_until_flush() {
258        let raw = Arc::new(DummyBlockDev::new(4));
259        let cache = BlockCache::with_capacity(raw.clone(), 2);
260
261        cache.write_block(2, &block(9)).unwrap();
262        let mut raw_buf = [0u8; BLOCK_SIZE];
263        raw.read_block(2, &mut raw_buf).unwrap();
264        assert_eq!(raw_buf, block(0));
265
266        assert!(cache.flush_block(2).unwrap());
267        raw.read_block(2, &mut raw_buf).unwrap();
268        assert_eq!(raw_buf, block(9));
269    }
270}