perf(resource): avoid copying nres payloads
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
Test / Lint (push) Failing after 2m50s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 12:03:56 +04:00
parent f952593e27
commit 6b4fefc21f
3 changed files with 40 additions and 13 deletions
+19
View File
@@ -479,6 +479,23 @@ impl NresDocument {
Ok(&self.bytes[entry.data_range.clone()]) 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. /// Encodes the document according to the selected write profile.
#[must_use] #[must_use]
pub fn encode(&self, profile: WriteProfile) -> Vec<u8> { 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!(entry.data_range().end, HEADER_LEN + 1);
assert_eq!(doc.header().directory_offset % 8, 0); assert_eq!(doc.header().directory_offset % 8, 0);
assert_eq!(doc.payload(EntryId(0)).expect("payload"), b"x"); 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] #[test]
+15 -13
View File
@@ -381,7 +381,7 @@ struct DecodedPayloadCache {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct PayloadCacheEntry { struct PayloadCacheEntry {
bytes: Arc<[u8]>, bytes: ResourceBytes,
last_access: u64, last_access: u64,
} }
@@ -562,7 +562,7 @@ impl ResourceRepository for CachedResourceRepository {
let task = { let task = {
let mut state = self.state.lock().map_err(|_| ResourceError::Poisoned)?; let mut state = self.state.lock().map_err(|_| ResourceError::Poisoned)?;
if let Some(bytes) = state.payload_cache.get(entry) { if let Some(bytes) = state.payload_cache.get(entry) {
return Ok(ResourceBytes::Shared(bytes)); return Ok(bytes);
} }
state.payload_decode_task(entry)? state.payload_decode_task(entry)?
}; };
@@ -573,15 +573,14 @@ impl ResourceRepository for CachedResourceRepository {
key: task.key, key: task.key,
source, source,
})?; })?;
let shared = Arc::from(payload.into_boxed_slice());
let mut state = self.state.lock().map_err(|_| ResourceError::Poisoned)?; let mut state = self.state.lock().map_err(|_| ResourceError::Poisoned)?;
if let Some(bytes) = state.payload_cache.get(entry) { if let Some(bytes) = state.payload_cache.get(entry) {
return Ok(ResourceBytes::Shared(bytes)); return Ok(bytes);
} }
state.entry_archive(entry)?; state.entry_archive(entry)?;
state.payload_cache.insert(entry, Arc::clone(&shared)); state.payload_cache.insert(entry, payload.clone());
Ok(ResourceBytes::Shared(shared)) Ok(payload)
} }
fn entry_info(&self, entry: EntryHandle) -> Result<ResourceEntryInfo, ResourceError> { 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)?; let entry = self.entries.get_mut(&handle)?;
self.generation = self.generation.saturating_add(1); self.generation = self.generation.saturating_add(1);
entry.last_access = self.generation; 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(); let len = bytes.len();
if self.max_entries == 0 || len > self.max_bytes { if self.max_entries == 0 || len > self.max_bytes {
return; return;
@@ -815,14 +814,15 @@ impl ArchiveSlot {
} }
impl ArchiveDocument { impl ArchiveDocument {
fn read_payload(&self, local: u32) -> Result<Vec<u8>, String> { fn read_payload(&self, local: u32) -> Result<ResourceBytes, String> {
match self { match self {
ArchiveDocument::Nres(document) => document ArchiveDocument::Nres(document) => document
.payload(fparkan_nres::EntryId(local)) .payload_view(fparkan_nres::EntryId(local))
.map(<[u8]>::to_vec) .map(|(owner, range)| ResourceBytes::Slice { owner, range })
.map_err(|err| err.to_string()), .map_err(|err| err.to_string()),
ArchiveDocument::Rsli(document) => document ArchiveDocument::Rsli(document) => document
.load(fparkan_rsli::EntryId(local)) .load(fparkan_rsli::EntryId(local))
.map(|payload| ResourceBytes::Shared(Arc::from(payload.into_boxed_slice())))
.map_err(|err| err.to_string()), .map_err(|err| err.to_string()),
} }
} }
@@ -1070,7 +1070,9 @@ mod tests {
.find(first, &resource_name(b"alpha.txt")) .find(first, &resource_name(b"alpha.txt"))
.expect("find") .expect("find")
.expect("entry"); .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"); let info = repo.entry_info(handle).expect("entry info");
assert_eq!(info.key.archive, path); assert_eq!(info.key.archive, path);
assert!(info.key.name.0.eq_ignore_ascii_case(b"Alpha.TXT")); assert!(info.key.name.0.eq_ignore_ascii_case(b"Alpha.TXT"));
+6
View File
@@ -128,6 +128,12 @@ directory_offset = total_size - directory_size
Reader проверяет, что `directory_offset >= 16`, умножение не переполнено, а Reader проверяет, что `directory_offset >= 16`, умножение не переполнено, а
каталог заканчивается точно на `total_size`. каталог заканчивается точно на `total_size`.
В repository NRes payload возвращается как slice с `Arc`-владельцем полного
неизменяемого архива: валидация формата не создаёт промежуточную копию каждого
payload. Такой view учитывается в том же bounded decoded-payload cache по
длине диапазона и сохраняет deterministic eviction. Это применимо только к
raw NRes; сжатый RsLi по-прежнему материализуется после декодирования.
### Запись каталога NRes ### Запись каталога NRes
```c ```c