From a4c7e4b0ba64007ff8d4b42703ea4a3967b9b7f9 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 06:52:54 +0400 Subject: [PATCH] perf(resource): skip duplicate archive metadata hashing --- crates/fparkan-resource/src/lib.rs | 70 ++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/crates/fparkan-resource/src/lib.rs b/crates/fparkan-resource/src/lib.rs index 9697e8b..96be4d8 100644 --- a/crates/fparkan-resource/src/lib.rs +++ b/crates/fparkan-resource/src/lib.rs @@ -453,17 +453,28 @@ impl CachedResourceRepository { impl ResourceRepository for CachedResourceRepository { fn open_archive(&self, path: &NormalizedPath) -> Result { - let mut state = self.state.lock().map_err(|_| ResourceError::Poisoned)?; let key = path.identity_bytes().to_vec(); loop { - // Decode outside the repository lock, then verify the VFS still points - // at the same bytes before committing the slot under the lock. - drop(state); + // Read outside the repository lock. The content hash doubles as the + // cache-validation value, so an unchanged open archive avoids both + // decode and a second whole-archive metadata read. let bytes = self .vfs .read(path) .map_err(|err| resource_error_from_vfs(path, err))?; let observed_fingerprint = sha256(&bytes); + let mut state = self.state.lock().map_err(|_| ResourceError::Poisoned)?; + if let Some(id) = state.paths.get(&key).copied() { + let current = state.archive(id)?; + if current.document.is_some() && current.fingerprint == observed_fingerprint { + state.touch_archive(id)?; + return Ok(id); + } + } + + // A new or changed archive still receives the full decode and a + // post-decode VFS fingerprint check before it commits to the cache. + drop(state); let mut slot = decode_archive(path.clone(), bytes, observed_fingerprint)?; let current_vfs_fingerprint = self .vfs @@ -907,6 +918,7 @@ mod tests { use super::*; use fparkan_vfs::{DirectoryVfs, MemoryVfs, Vfs, VfsEntry, VfsError, VfsMetadata}; use std::path::PathBuf; + use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Condvar; use std::thread; @@ -938,6 +950,41 @@ mod tests { } } + struct CountingVfs { + bytes: Arc<[u8]>, + reads: AtomicUsize, + metadata_reads: AtomicUsize, + } + + impl CountingVfs { + fn new(bytes: Arc<[u8]>) -> Self { + Self { + bytes, + reads: AtomicUsize::new(0), + metadata_reads: AtomicUsize::new(0), + } + } + } + + impl Vfs for CountingVfs { + fn metadata(&self, _path: &NormalizedPath) -> Result { + self.metadata_reads.fetch_add(1, Ordering::Relaxed); + Ok(VfsMetadata { + len: self.bytes.len() as u64, + fingerprint: sha256(&self.bytes), + }) + } + + fn read(&self, _path: &NormalizedPath) -> Result, VfsError> { + self.reads.fetch_add(1, Ordering::Relaxed); + Ok(Arc::clone(&self.bytes)) + } + + fn list(&self, _prefix: &NormalizedPath) -> Result, VfsError> { + unreachable!("list is not used in these tests"); + } + } + struct CoordinatedReadState { current: Arc<[u8]>, first_read_started: bool, @@ -1037,6 +1084,21 @@ mod tests { )); } + #[test] + fn cached_archive_fast_path_skips_second_metadata_fingerprint() { + let path = archive_path(b"archives/test.lib").expect("path"); + let bytes = Arc::from(build_nres(&[("one", b"payload")]).into_boxed_slice()); + let vfs = Arc::new(CountingVfs::new(bytes)); + let repo = CachedResourceRepository::new(Arc::clone(&vfs) as Arc); + + let first = repo.open_archive(&path).expect("first open"); + let second = repo.open_archive(&path).expect("cached open"); + + assert_eq!(first, second); + assert_eq!(vfs.reads.load(Ordering::Relaxed), 2); + assert_eq!(vfs.metadata_reads.load(Ordering::Relaxed), 1); + } + #[test] fn concurrent_same_archive_open_reuses_archive_id() { let path = archive_path(b"archives/test.lib").expect("path");