perf(resource): avoid copying nres payloads
This commit is contained in:
@@ -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]
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user