fix: cap decoded payload cache bytes
This commit is contained in:
@@ -180,6 +180,24 @@ pub struct CachedResourceRepository {
|
|||||||
state: Mutex<RepositoryState>,
|
state: Mutex<RepositoryState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decoded payload cache limits.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct PayloadCacheLimits {
|
||||||
|
/// Maximum cached decoded payload entries.
|
||||||
|
pub max_entries: usize,
|
||||||
|
/// Maximum cached decoded payload bytes.
|
||||||
|
pub max_bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PayloadCacheLimits {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_entries: 64,
|
||||||
|
max_bytes: 64 * 1024 * 1024,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct RepositoryState {
|
struct RepositoryState {
|
||||||
paths: BTreeMap<String, ArchiveId>,
|
paths: BTreeMap<String, ArchiveId>,
|
||||||
@@ -203,6 +221,8 @@ enum ArchiveDocument {
|
|||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct DecodedPayloadCache {
|
struct DecodedPayloadCache {
|
||||||
max_entries: usize,
|
max_entries: usize,
|
||||||
|
max_bytes: usize,
|
||||||
|
current_bytes: usize,
|
||||||
generation: u64,
|
generation: u64,
|
||||||
entries: BTreeMap<EntryHandle, PayloadCacheEntry>,
|
entries: BTreeMap<EntryHandle, PayloadCacheEntry>,
|
||||||
}
|
}
|
||||||
@@ -217,16 +237,28 @@ impl CachedResourceRepository {
|
|||||||
/// Creates a cached repository.
|
/// Creates a cached repository.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(vfs: Arc<dyn Vfs>) -> Self {
|
pub fn new(vfs: Arc<dyn Vfs>) -> Self {
|
||||||
Self::with_payload_cache_budget(vfs, 64)
|
Self::with_payload_cache_limits(vfs, PayloadCacheLimits::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a cached repository with a decoded payload entry budget.
|
/// Creates a cached repository with a decoded payload entry budget.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_payload_cache_budget(vfs: Arc<dyn Vfs>, max_payload_entries: usize) -> Self {
|
pub fn with_payload_cache_budget(vfs: Arc<dyn Vfs>, max_payload_entries: usize) -> Self {
|
||||||
|
Self::with_payload_cache_limits(
|
||||||
|
vfs,
|
||||||
|
PayloadCacheLimits {
|
||||||
|
max_entries: max_payload_entries,
|
||||||
|
..PayloadCacheLimits::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a cached repository with decoded payload entry and byte budgets.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_payload_cache_limits(vfs: Arc<dyn Vfs>, limits: PayloadCacheLimits) -> Self {
|
||||||
Self {
|
Self {
|
||||||
vfs,
|
vfs,
|
||||||
state: Mutex::new(RepositoryState {
|
state: Mutex::new(RepositoryState {
|
||||||
payload_cache: DecodedPayloadCache::new(max_payload_entries),
|
payload_cache: DecodedPayloadCache::new(limits),
|
||||||
..RepositoryState::default()
|
..RepositoryState::default()
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -394,9 +426,11 @@ impl CachedResourceRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DecodedPayloadCache {
|
impl DecodedPayloadCache {
|
||||||
fn new(max_entries: usize) -> Self {
|
fn new(limits: PayloadCacheLimits) -> Self {
|
||||||
Self {
|
Self {
|
||||||
max_entries,
|
max_entries: limits.max_entries,
|
||||||
|
max_bytes: limits.max_bytes,
|
||||||
|
current_bytes: 0,
|
||||||
generation: 0,
|
generation: 0,
|
||||||
entries: BTreeMap::new(),
|
entries: BTreeMap::new(),
|
||||||
}
|
}
|
||||||
@@ -410,18 +444,39 @@ impl DecodedPayloadCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn insert(&mut self, handle: EntryHandle, bytes: Arc<[u8]>) {
|
fn insert(&mut self, handle: EntryHandle, bytes: Arc<[u8]>) {
|
||||||
if self.max_entries == 0 {
|
let len = bytes.len();
|
||||||
|
if self.max_entries == 0 || len > self.max_bytes {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.generation = self.generation.saturating_add(1);
|
self.generation = self.generation.saturating_add(1);
|
||||||
self.entries.insert(
|
if let Some(previous) = self.entries.insert(
|
||||||
handle,
|
handle,
|
||||||
PayloadCacheEntry {
|
PayloadCacheEntry {
|
||||||
bytes,
|
bytes,
|
||||||
last_access: self.generation,
|
last_access: self.generation,
|
||||||
},
|
},
|
||||||
);
|
) {
|
||||||
while self.entries.len() > self.max_entries {
|
self.current_bytes = self.current_bytes.saturating_sub(previous.bytes.len());
|
||||||
|
}
|
||||||
|
self.current_bytes = self.current_bytes.saturating_add(len);
|
||||||
|
self.evict_until_within_budget();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_archive(&mut self, archive: ArchiveId) {
|
||||||
|
let mut removed_bytes = 0usize;
|
||||||
|
self.entries.retain(|handle, entry| {
|
||||||
|
if handle.archive == archive {
|
||||||
|
removed_bytes = removed_bytes.saturating_add(entry.bytes.len());
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.current_bytes = self.current_bytes.saturating_sub(removed_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evict_until_within_budget(&mut self) {
|
||||||
|
while self.entries.len() > self.max_entries || self.current_bytes > self.max_bytes {
|
||||||
let Some(victim) = self
|
let Some(victim) = self
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
@@ -430,13 +485,11 @@ impl DecodedPayloadCache {
|
|||||||
else {
|
else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
self.entries.remove(&victim);
|
if let Some(removed) = self.entries.remove(&victim) {
|
||||||
|
self.current_bytes = self.current_bytes.saturating_sub(removed.bytes.len());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_archive(&mut self, archive: ArchiveId) {
|
|
||||||
self.entries.retain(|handle, _| handle.archive != archive);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepositoryState {
|
impl RepositoryState {
|
||||||
@@ -674,6 +727,77 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decoded_payload_cache_evicts_by_byte_budget() {
|
||||||
|
let path = archive_path(b"cache/bytes.lib").expect("path");
|
||||||
|
let bytes = build_nres(&[
|
||||||
|
("a.bin", b"1234".as_slice()),
|
||||||
|
("b.bin", b"5678".as_slice()),
|
||||||
|
("c.bin", b"90".as_slice()),
|
||||||
|
]);
|
||||||
|
let mut vfs = MemoryVfs::default();
|
||||||
|
vfs.insert(path.clone(), Arc::from(bytes.into_boxed_slice()));
|
||||||
|
let repo = CachedResourceRepository::with_payload_cache_limits(
|
||||||
|
Arc::new(vfs),
|
||||||
|
PayloadCacheLimits {
|
||||||
|
max_entries: 64,
|
||||||
|
max_bytes: 6,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let archive = repo.open_archive(&path).expect("open archive");
|
||||||
|
let first = repo
|
||||||
|
.find(archive, &resource_name(b"a.bin"))
|
||||||
|
.expect("find a")
|
||||||
|
.expect("a");
|
||||||
|
let second = repo
|
||||||
|
.find(archive, &resource_name(b"b.bin"))
|
||||||
|
.expect("find b")
|
||||||
|
.expect("b");
|
||||||
|
let third = repo
|
||||||
|
.find(archive, &resource_name(b"c.bin"))
|
||||||
|
.expect("find c")
|
||||||
|
.expect("c");
|
||||||
|
|
||||||
|
assert_eq!(repo.read(first).expect("read a").as_slice(), b"1234");
|
||||||
|
assert_eq!(repo.read(second).expect("read b").as_slice(), b"5678");
|
||||||
|
assert_eq!(repo.read(third).expect("read c").as_slice(), b"90");
|
||||||
|
|
||||||
|
let state = repo.state.lock().expect("state");
|
||||||
|
assert_eq!(state.payload_cache.current_bytes, 6);
|
||||||
|
assert_eq!(state.payload_cache.entries.len(), 2);
|
||||||
|
assert!(!state.payload_cache.entries.contains_key(&first));
|
||||||
|
assert!(state.payload_cache.entries.contains_key(&second));
|
||||||
|
assert!(state.payload_cache.entries.contains_key(&third));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decoded_payload_cache_does_not_store_payload_larger_than_budget() {
|
||||||
|
let path = archive_path(b"cache/oversized.lib").expect("path");
|
||||||
|
let bytes = build_nres(&[("big.bin", b"1234567".as_slice())]);
|
||||||
|
let mut vfs = MemoryVfs::default();
|
||||||
|
vfs.insert(path.clone(), Arc::from(bytes.into_boxed_slice()));
|
||||||
|
let repo = CachedResourceRepository::with_payload_cache_limits(
|
||||||
|
Arc::new(vfs),
|
||||||
|
PayloadCacheLimits {
|
||||||
|
max_entries: 64,
|
||||||
|
max_bytes: 6,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let archive = repo.open_archive(&path).expect("open archive");
|
||||||
|
let handle = repo
|
||||||
|
.find(archive, &resource_name(b"big.bin"))
|
||||||
|
.expect("find big")
|
||||||
|
.expect("big");
|
||||||
|
|
||||||
|
assert_eq!(repo.read(handle).expect("read big").as_slice(), b"1234567");
|
||||||
|
|
||||||
|
let state = repo.state.lock().expect("state");
|
||||||
|
assert_eq!(state.payload_cache.current_bytes, 0);
|
||||||
|
assert!(state.payload_cache.entries.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn archive_cache_invalidates_when_vfs_bytes_change() {
|
fn archive_cache_invalidates_when_vfs_bytes_change() {
|
||||||
let root = temp_dir("archive-invalidate");
|
let root = temp_dir("archive-invalidate");
|
||||||
|
|||||||
Reference in New Issue
Block a user