aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 11:03:56 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 11:03:56 +0300
commit6b4fefc21f30300b07e6e6e99eedf3d81e855388 (patch)
treecdf822136c75f60a3ae6166fbab9575d88947382
parentf952593e27a2ea8222f2a0d3fa545654ce073c6b (diff)
downloadfparkan-6b4fefc21f30300b07e6e6e99eedf3d81e855388.tar.xz
fparkan-6b4fefc21f30300b07e6e6e99eedf3d81e855388.zip
perf(resource): avoid copying nres payloads
-rw-r--r--crates/fparkan-nres/src/lib.rs19
-rw-r--r--crates/fparkan-resource/src/lib.rs28
-rw-r--r--docs/tomes/03-resources.md6
3 files changed, 40 insertions, 13 deletions
diff --git a/crates/fparkan-nres/src/lib.rs b/crates/fparkan-nres/src/lib.rs
index 778b76b..e82ca08 100644
--- a/crates/fparkan-nres/src/lib.rs
+++ b/crates/fparkan-nres/src/lib.rs
@@ -479,6 +479,23 @@ impl NresDocument {
Ok(&self.bytes[entry.data_range.clone()])
}
+ /// Returns an owned view of an entry payload without copying its bytes.
+ ///
+ /// The returned owner is the immutable archive buffer. Consumers may retain
+ /// the view after dropping this document.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`NresError::EntryIdOutOfRange`] when `id` is not present in
+ /// this document.
+ pub fn payload_view(&self, id: EntryId) -> Result<(Arc<[u8]>, Range<usize>), NresError> {
+ let entry = self.entry(id).ok_or_else(|| NresError::EntryIdOutOfRange {
+ id: id.0,
+ entry_count: saturating_u32_len(self.entries.len()),
+ })?;
+ Ok((Arc::clone(&self.bytes), entry.data_range.clone()))
+ }
+
/// Encodes the document according to the selected write profile.
#[must_use]
pub fn encode(&self, profile: WriteProfile) -> Vec<u8> {
@@ -1213,6 +1230,8 @@ mod tests {
assert_eq!(entry.data_range().end, HEADER_LEN + 1);
assert_eq!(doc.header().directory_offset % 8, 0);
assert_eq!(doc.payload(EntryId(0)).expect("payload"), b"x");
+ let (owner, range) = doc.payload_view(EntryId(0)).expect("payload view");
+ assert_eq!(&owner[range], b"x");
}
#[test]
diff --git a/crates/fparkan-resource/src/lib.rs b/crates/fparkan-resource/src/lib.rs
index 96be4d8..8fbd62e 100644
--- a/crates/fparkan-resource/src/lib.rs
+++ b/crates/fparkan-resource/src/lib.rs
@@ -381,7 +381,7 @@ struct DecodedPayloadCache {
#[derive(Clone, Debug)]
struct PayloadCacheEntry {
- bytes: Arc<[u8]>,
+ bytes: ResourceBytes,
last_access: u64,
}
@@ -562,7 +562,7 @@ impl ResourceRepository for CachedResourceRepository {
let task = {
let mut state = self.state.lock().map_err(|_| ResourceError::Poisoned)?;
if let Some(bytes) = state.payload_cache.get(entry) {
- return Ok(ResourceBytes::Shared(bytes));
+ return Ok(bytes);
}
state.payload_decode_task(entry)?
};
@@ -573,15 +573,14 @@ impl ResourceRepository for CachedResourceRepository {
key: task.key,
source,
})?;
- let shared = Arc::from(payload.into_boxed_slice());
let mut state = self.state.lock().map_err(|_| ResourceError::Poisoned)?;
if let Some(bytes) = state.payload_cache.get(entry) {
- return Ok(ResourceBytes::Shared(bytes));
+ return Ok(bytes);
}
state.entry_archive(entry)?;
- state.payload_cache.insert(entry, Arc::clone(&shared));
- Ok(ResourceBytes::Shared(shared))
+ state.payload_cache.insert(entry, payload.clone());
+ Ok(payload)
}
fn entry_info(&self, entry: EntryHandle) -> Result<ResourceEntryInfo, ResourceError> {
@@ -638,14 +637,14 @@ impl DecodedPayloadCache {
}
}
- fn get(&mut self, handle: EntryHandle) -> Option<Arc<[u8]>> {
+ fn get(&mut self, handle: EntryHandle) -> Option<ResourceBytes> {
let entry = self.entries.get_mut(&handle)?;
self.generation = self.generation.saturating_add(1);
entry.last_access = self.generation;
- Some(Arc::clone(&entry.bytes))
+ Some(entry.bytes.clone())
}
- fn insert(&mut self, handle: EntryHandle, bytes: Arc<[u8]>) {
+ fn insert(&mut self, handle: EntryHandle, bytes: ResourceBytes) {
let len = bytes.len();
if self.max_entries == 0 || len > self.max_bytes {
return;
@@ -815,14 +814,15 @@ impl ArchiveSlot {
}
impl ArchiveDocument {
- fn read_payload(&self, local: u32) -> Result<Vec<u8>, String> {
+ fn read_payload(&self, local: u32) -> Result<ResourceBytes, String> {
match self {
ArchiveDocument::Nres(document) => document
- .payload(fparkan_nres::EntryId(local))
- .map(<[u8]>::to_vec)
+ .payload_view(fparkan_nres::EntryId(local))
+ .map(|(owner, range)| ResourceBytes::Slice { owner, range })
.map_err(|err| err.to_string()),
ArchiveDocument::Rsli(document) => document
.load(fparkan_rsli::EntryId(local))
+ .map(|payload| ResourceBytes::Shared(Arc::from(payload.into_boxed_slice())))
.map_err(|err| err.to_string()),
}
}
@@ -1070,7 +1070,9 @@ mod tests {
.find(first, &resource_name(b"alpha.txt"))
.expect("find")
.expect("entry");
- assert_eq!(repo.read(handle).expect("read").as_slice(), b"alpha");
+ let payload = repo.read(handle).expect("read");
+ assert_eq!(payload.as_slice(), b"alpha");
+ assert!(matches!(payload, ResourceBytes::Slice { .. }));
let info = repo.entry_info(handle).expect("entry info");
assert_eq!(info.key.archive, path);
assert!(info.key.name.0.eq_ignore_ascii_case(b"Alpha.TXT"));
diff --git a/docs/tomes/03-resources.md b/docs/tomes/03-resources.md
index acf97a8..7bce1a1 100644
--- a/docs/tomes/03-resources.md
+++ b/docs/tomes/03-resources.md
@@ -128,6 +128,12 @@ directory_offset = total_size - directory_size
Reader проверяет, что `directory_offset >= 16`, умножение не переполнено, а
каталог заканчивается точно на `total_size`.
+В repository NRes payload возвращается как slice с `Arc`-владельцем полного
+неизменяемого архива: валидация формата не создаёт промежуточную копию каждого
+payload. Такой view учитывается в том же bounded decoded-payload cache по
+длине диапазона и сохраняет deterministic eviction. Это применимо только к
+raw NRes; сжатый RsLi по-прежнему материализуется после декодирования.
+
### Запись каталога NRes
```c