use crate::error::{Error, Result};
use crate::platform::{DiskId, Host};
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, Default)]
pub struct CacheStats {
pub write_ops: u64,
pub write_bytes: u64,
pub bytes_coalesced: u64,
pub ops_saved: u64,
pub read_ops: u64,
pub read_bytes: u64,
pub read_hits: u64,
pub evictions: u64,
}
#[derive(Debug)]
pub struct DiskCache {
budget: u64,
used: u64,
dirty: BTreeMap<(DiskId, u64), Vec<u8>>,
clean: BTreeMap<(DiskId, u64), (Vec<u8>, u64)>,
clean_used: u64,
lru_clock: u64,
pub stats: CacheStats,
}
impl DiskCache {
const CLEAN_BUDGET: u64 = 1 << 20;
const CLEAN_MAX_ENTRIES: usize = 4096;
pub fn new(budget: u64) -> Self {
DiskCache {
budget: budget.max(1 << 20),
used: 0,
dirty: BTreeMap::new(),
clean: BTreeMap::new(),
clean_used: 0,
lru_clock: 0,
stats: CacheStats::default(),
}
}
pub fn used(&self) -> u64 {
self.used
}
pub fn budget(&self) -> u64 {
self.budget
}
pub fn dirty_bytes(&self, disk: DiskId) -> u64 {
self.dirty
.range((disk, 0)..=(disk, u64::MAX))
.map(|(_, v)| v.len() as u64)
.sum()
}
pub fn write<H: Host>(
&mut self,
host: &mut H,
disk: DiskId,
offset: u64,
data: &[u8],
) -> Result<()> {
if data.is_empty() {
return Ok(());
}
if data.len() as u64 > self.budget {
self.flush(host)?;
host.disk_write(disk, offset, data)?;
self.stats.write_ops += 1;
self.stats.write_bytes += data.len() as u64;
return Ok(());
}
while self.used + data.len() as u64 > self.budget && !self.dirty.is_empty() {
let freed = self.flush_oldest(host, data.len() as u64)?;
self.stats.evictions += 1;
if freed == 0 {
break;
}
}
self.insert_coalesced(disk, offset, data);
Ok(())
}
fn insert_coalesced(&mut self, disk: DiskId, offset: u64, data: &[u8]) {
if let Some(existing) = self.dirty.get_mut(&(disk, offset)) {
let old_len = existing.len();
self.used -= old_len as u64;
existing.clear();
existing.extend_from_slice(data);
self.used += data.len() as u64;
return;
}
let mut mergeable_pred: Option<(DiskId, u64)> = None;
if let Some((&k, v)) = self.dirty.range(..(disk, offset)).next_back() {
if k.0 == disk && k.1 + v.len() as u64 == offset {
mergeable_pred = Some(k);
}
}
if let Some(k) = mergeable_pred {
let end = k.1 + self.dirty.get(&k).map(|v| v.len() as u64).unwrap_or(0);
let succ_data = self.dirty.get(&(disk, end)).cloned();
let has_succ = succ_data.is_some();
if has_succ {
let sd = succ_data.clone().unwrap();
self.dirty.remove(&(disk, end));
self.used -= sd.len() as u64;
}
let v = self.dirty.get_mut(&k).unwrap();
v.extend_from_slice(data);
self.used += data.len() as u64;
self.stats.bytes_coalesced += data.len() as u64;
self.stats.ops_saved += 1;
if let Some(succ_data) = succ_data {
v.extend_from_slice(&succ_data);
self.used += succ_data.len() as u64;
}
return;
}
if let Some(succ) = self.dirty.get(&(disk, offset + data.len() as u64)) {
let mut merged = Vec::with_capacity(data.len() + succ.len());
merged.extend_from_slice(data);
merged.extend_from_slice(succ);
let succ_len = succ.len();
self.dirty.remove(&(disk, offset + data.len() as u64));
self.used -= succ_len as u64;
self.dirty.insert((disk, offset), merged);
self.used += data.len() as u64;
self.stats.bytes_coalesced += data.len() as u64;
self.stats.ops_saved += 1;
return;
}
self.dirty.insert((disk, offset), data.to_vec());
self.used += data.len() as u64;
}
pub fn read<H: Host>(
&mut self,
host: &mut H,
disk: DiskId,
offset: u64,
buf: &mut [u8],
) -> Result<usize> {
let mut got = 0usize;
let mut pos = offset;
while got < buf.len() {
let len = (buf.len() - got) as u64;
let dirty_hit = self
.dirty
.range(..=(disk, pos))
.next_back()
.filter(|((d, o), v)| *d == disk && *o <= pos && pos < *o + v.len() as u64);
if let Some(((_, page_off), page)) = dirty_hit {
let in_page = (pos - page_off) as usize;
let take = core::cmp::min(len as usize, page.len() - in_page);
buf[got..got + take].copy_from_slice(&page[in_page..in_page + take]);
got += take;
pos += take as u64;
self.stats.read_hits += take as u64;
continue;
}
let clean_hit = self
.clean
.range(..=(disk, pos))
.next_back()
.filter(|((d, o), (v, _))| *d == disk && *o <= pos && pos < *o + v.len() as u64);
if let Some(((_, page_off), (page, _))) = clean_hit {
let in_page = (pos - page_off) as usize;
let take = core::cmp::min(len as usize, page.len() - in_page);
buf[got..got + take].copy_from_slice(&page[in_page..in_page + take]);
got += take;
pos += take as u64;
self.stats.read_hits += take as u64;
if pos >= page_off + page.len() as u64 {
self.lru_clock = self.lru_clock.wrapping_add(1);
if let Some((_, last)) = self.clean.get_mut(&(disk, *page_off)) {
*last = self.lru_clock;
}
}
continue;
}
let n = host.disk_read(disk, pos, &mut buf[got..])?;
if n == 0 {
break;
}
self.stats.read_ops += 1;
self.stats.read_bytes += n as u64;
self.promote_clean(disk, pos, &buf[got..got + n]);
got += n;
pos += n as u64;
}
Ok(got)
}
fn promote_clean(&mut self, disk: DiskId, offset: u64, data: &[u8]) {
if data.is_empty() {
return;
}
self.lru_clock = self.lru_clock.wrapping_add(1);
let mut base = offset;
let mut merged: Vec<u8> = data.to_vec();
let at_capacity = self.clean.len() >= Self::CLEAN_MAX_ENTRIES;
if !at_capacity {
if let Some((&k, _)) = self
.clean
.range(..(disk, offset))
.next_back()
.filter(|((d, o), (v, _))| *d == disk && *o + v.len() as u64 == offset)
{
let v = self.clean.remove(&k).map(|(v, _)| v).unwrap();
self.clean_used -= v.len() as u64;
base = k.1;
let mut pred = v;
pred.extend_from_slice(&merged);
merged = pred;
}
if let Some((&k, _)) = self
.clean
.range((disk, base + merged.len() as u64)..=(disk, u64::MAX))
.next()
.filter(|((d, o), _)| *d == disk && *o == base + merged.len() as u64)
{
let v = self.clean.remove(&k).map(|(v, _)| v).unwrap();
self.clean_used -= v.len() as u64;
merged.extend_from_slice(&v);
}
}
while self.clean_used + merged.len() as u64 > Self::CLEAN_BUDGET && !self.clean.is_empty() {
let victim = self
.clean
.iter()
.min_by_key(|(_, (_, last))| *last)
.map(|(k, _)| *k);
match victim {
Some(k) => {
if let Some((v, _)) = self.clean.remove(&k) {
self.clean_used -= v.len() as u64;
}
}
None => break,
}
}
if self.clean.len() < Self::CLEAN_MAX_ENTRIES {
self.clean_used += merged.len() as u64;
self.clean.insert((disk, base), (merged, self.lru_clock));
}
}
pub fn flush<H: Host>(&mut self, host: &mut H) -> Result<()> {
self.flush_oldest(host, u64::MAX)?;
debug_assert!(self.dirty.is_empty());
Ok(())
}
pub fn flush_bounded<H: Host>(&mut self, host: &mut H, max_bytes: u64) -> Result<u64> {
self.flush_oldest(host, max_bytes)
}
pub fn flush_disk<H: Host>(&mut self, host: &mut H, disk: DiskId) -> Result<()> {
let keys: alloc::vec::Vec<(DiskId, u64)> = self
.dirty
.range((disk, 0)..=(disk, u64::MAX))
.map(|(k, _)| *k)
.collect();
for k in keys {
let data = self.dirty.remove(&k).ok_or(Error::Internal)?;
self.used -= data.len() as u64;
host.disk_write(k.0, k.1, &data)?;
self.stats.write_ops += 1;
self.stats.write_bytes += data.len() as u64;
self.promote_clean(k.0, k.1, &data);
}
Ok(())
}
fn flush_oldest<H: Host>(&mut self, host: &mut H, min_freed: u64) -> Result<u64> {
let mut freed = 0u64;
let mut batch: Vec<(DiskId, u64, Vec<u8>)> = Vec::new();
let keys: alloc::vec::Vec<(DiskId, u64)> = self.dirty.keys().copied().collect();
for k in keys {
if freed >= min_freed {
break;
}
if let Some(data) = self.dirty.remove(&k) {
self.used -= data.len() as u64;
freed += data.len() as u64;
if let Some(last) = batch.last_mut() {
if last.0 == k.0 && last.1 + last.2.len() as u64 == k.1 {
last.2.extend_from_slice(&data);
self.stats.ops_saved += 1;
continue;
}
}
batch.push((k.0, k.1, data));
}
}
for (d, o, data) in batch {
host.disk_write(d, o, &data)?;
self.promote_clean(d, o, &data);
self.stats.write_ops += 1;
self.stats.write_bytes += data.len() as u64;
}
Ok(freed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error as E;
use crate::platform::{ConnId, LogLevel, NetAddr};
use alloc::collections::BTreeMap;
struct MemHost {
files: BTreeMap<DiskId, Vec<u8>>,
next_disk: DiskId,
}
impl MemHost {
fn new() -> Self {
MemHost {
files: BTreeMap::new(),
next_disk: 1,
}
}
fn data(&self, id: DiskId) -> &[u8] {
self.files.get(&id).map(|v| v.as_slice()).unwrap_or(&[])
}
}
impl Host for MemHost {
fn now_ms(&self) -> u64 {
0
}
fn fill_random(&mut self, _buf: &mut [u8]) {}
fn log(&mut self, _l: LogLevel, _m: &str) {}
fn http_get(&mut self, _u: &str, _t: u64, _o: &mut alloc::vec::Vec<u8>) -> Result<()> {
Ok(())
}
fn tcp_connect(&mut self, _a: &NetAddr) -> Result<ConnId> {
Err(E::NotSupported)
}
fn tcp_connect_done(&mut self, _id: ConnId) -> Result<()> {
Err(E::NotSupported)
}
fn tcp_send(&mut self, _id: ConnId, _d: &[u8]) -> Result<usize> {
Err(E::NotSupported)
}
fn tcp_recv(&mut self, _id: ConnId, _b: &mut [u8]) -> Result<usize> {
Err(E::NotSupported)
}
fn tcp_close(&mut self, _id: ConnId) {}
fn udp_open(&mut self, _p: u16) -> Result<()> {
Ok(())
}
fn udp_send(&mut self, _a: &NetAddr, _d: &[u8]) -> Result<()> {
Ok(())
}
fn udp_recv(&mut self, _b: &mut [u8]) -> Result<(NetAddr, usize)> {
Err(E::WouldBlock)
}
fn disk_open(&mut self, _p: &str) -> Result<DiskId> {
let id = self.next_disk;
self.next_disk += 1;
self.files.insert(id, Vec::new());
Ok(id)
}
fn disk_read(&mut self, id: DiskId, offset: u64, buf: &mut [u8]) -> Result<usize> {
let f = self.files.get(&id).ok_or(E::NotFound)?;
let start = offset as usize;
if start >= f.len() {
return Ok(0);
}
let n = core::cmp::min(buf.len(), f.len() - start);
buf[..n].copy_from_slice(&f[start..start + n]);
Ok(n)
}
fn disk_write(&mut self, id: DiskId, offset: u64, data: &[u8]) -> Result<()> {
let f = self.files.get_mut(&id).ok_or(E::NotFound)?;
let start = offset as usize;
if start + data.len() > f.len() {
f.resize(start + data.len(), 0);
}
f[start..start + data.len()].copy_from_slice(data);
Ok(())
}
fn disk_prealloc(&mut self, id: DiskId, size: u64) -> Result<()> {
let f = self.files.get_mut(&id).ok_or(E::NotFound)?;
f.resize(size as usize, 0);
Ok(())
}
fn disk_flush(&mut self, _id: DiskId) -> Result<()> {
Ok(())
}
fn disk_close(&mut self, _id: DiskId) {}
}
fn file_of(host: &MemHost, id: DiskId) -> Vec<u8> {
host.data(id).to_vec()
}
#[test]
fn coalesces_contiguous_writes() {
let mut host = MemHost::new();
let d = host.disk_open("f").unwrap();
let mut cache = DiskCache::new(1 << 20);
for i in 0..4u64 {
let data = vec![i as u8; 16384];
cache.write(&mut host, d, i * 16384, &data).unwrap();
}
assert_eq!(cache.used(), 4 * 16384);
assert_eq!(cache.dirty.len(), 1); cache.flush(&mut host).unwrap();
assert_eq!(cache.stats.write_ops, 1); let f = file_of(&host, d);
assert_eq!(f.len(), 4 * 16384);
for i in 0..4u64 {
let start = (i * 16384) as usize;
assert_eq!(f[start], i as u8);
}
}
#[test]
fn read_serves_from_cache() {
let mut host = MemHost::new();
let d = host.disk_open("f").unwrap();
let mut cache = DiskCache::new(1 << 20);
cache.write(&mut host, d, 0, &[9u8; 100]).unwrap();
assert_eq!(host.data(d).len(), 0);
let mut buf = [0u8; 50];
let n = cache.read(&mut host, d, 25, &mut buf).unwrap();
assert_eq!(n, 50);
assert!(buf.iter().all(|&b| b == 9));
assert_eq!(cache.stats.read_ops, 0);
}
#[test]
fn flushes_under_pressure() {
let mut host = MemHost::new();
let d = host.disk_open("f").unwrap();
let budget = 1 << 20;
let mut cache = DiskCache::new(budget);
for i in 0..512u64 {
cache
.write(&mut host, d, i * 16384, &vec![(i % 251) as u8; 16384])
.unwrap();
}
assert!(
cache.used() <= budget,
"used={} budget={} dirty_entries={}",
cache.used(),
budget,
cache.dirty.len()
);
cache.flush(&mut host).unwrap();
let f = file_of(&host, d);
assert_eq!(f.len(), 8 << 20);
assert_eq!(f[0], 0);
assert_eq!(f[(8 << 20) - 1], 9);
}
#[test]
fn flush_disk_only_one_file() {
let mut host = MemHost::new();
let a = host.disk_open("a").unwrap();
let b = host.disk_open("b").unwrap();
let mut cache = DiskCache::new(1 << 20);
cache.write(&mut host, a, 0, &[1u8; 10]).unwrap();
cache.write(&mut host, b, 0, &[2u8; 10]).unwrap();
cache.flush_disk(&mut host, a).unwrap();
assert_eq!(host.data(a).len(), 10);
assert_eq!(host.data(b).len(), 0);
cache.flush(&mut host).unwrap();
assert_eq!(host.data(b).len(), 10);
}
}