perf(resource): skip duplicate archive metadata hashing
This commit is contained in:
@@ -453,17 +453,28 @@ impl CachedResourceRepository {
|
|||||||
|
|
||||||
impl ResourceRepository for CachedResourceRepository {
|
impl ResourceRepository for CachedResourceRepository {
|
||||||
fn open_archive(&self, path: &NormalizedPath) -> Result<ArchiveId, ResourceError> {
|
fn open_archive(&self, path: &NormalizedPath) -> Result<ArchiveId, ResourceError> {
|
||||||
let mut state = self.state.lock().map_err(|_| ResourceError::Poisoned)?;
|
|
||||||
let key = path.identity_bytes().to_vec();
|
let key = path.identity_bytes().to_vec();
|
||||||
loop {
|
loop {
|
||||||
// Decode outside the repository lock, then verify the VFS still points
|
// Read outside the repository lock. The content hash doubles as the
|
||||||
// at the same bytes before committing the slot under the lock.
|
// cache-validation value, so an unchanged open archive avoids both
|
||||||
drop(state);
|
// decode and a second whole-archive metadata read.
|
||||||
let bytes = self
|
let bytes = self
|
||||||
.vfs
|
.vfs
|
||||||
.read(path)
|
.read(path)
|
||||||
.map_err(|err| resource_error_from_vfs(path, err))?;
|
.map_err(|err| resource_error_from_vfs(path, err))?;
|
||||||
let observed_fingerprint = sha256(&bytes);
|
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 mut slot = decode_archive(path.clone(), bytes, observed_fingerprint)?;
|
||||||
let current_vfs_fingerprint = self
|
let current_vfs_fingerprint = self
|
||||||
.vfs
|
.vfs
|
||||||
@@ -907,6 +918,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use fparkan_vfs::{DirectoryVfs, MemoryVfs, Vfs, VfsEntry, VfsError, VfsMetadata};
|
use fparkan_vfs::{DirectoryVfs, MemoryVfs, Vfs, VfsEntry, VfsError, VfsMetadata};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::Condvar;
|
use std::sync::Condvar;
|
||||||
use std::thread;
|
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<VfsMetadata, VfsError> {
|
||||||
|
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<Arc<[u8]>, VfsError> {
|
||||||
|
self.reads.fetch_add(1, Ordering::Relaxed);
|
||||||
|
Ok(Arc::clone(&self.bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(&self, _prefix: &NormalizedPath) -> Result<Vec<VfsEntry>, VfsError> {
|
||||||
|
unreachable!("list is not used in these tests");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct CoordinatedReadState {
|
struct CoordinatedReadState {
|
||||||
current: Arc<[u8]>,
|
current: Arc<[u8]>,
|
||||||
first_read_started: bool,
|
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<dyn Vfs>);
|
||||||
|
|
||||||
|
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]
|
#[test]
|
||||||
fn concurrent_same_archive_open_reuses_archive_id() {
|
fn concurrent_same_archive_open_reuses_archive_id() {
|
||||||
let path = archive_path(b"archives/test.lib").expect("path");
|
let path = archive_path(b"archives/test.lib").expect("path");
|
||||||
|
|||||||
Reference in New Issue
Block a user