perf(mesh): memoise the whole-container decode
Filtering after the assignment made every subset query a full decode (~15s on a 50MB container). full_decode_cached memoises it per container -- fingerprint is length plus three sampled 4KB windows, keyed with min_consistency, last four kept. Decoding five ships from Stage_S02 in turn: 10.5s for the first, then 48us-1.4ms. A stage now costs one decode rather than one per ship. Ten suites green, viewer builds, and a spot-checked resource still lands on the same offset. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
22
crates/sylpheed-formats/examples/bench_ships.rs
Normal file
22
crates/sylpheed-formats/examples/bench_ships.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//! Time repeated per-ship decodes of one container, as the viewer does.
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::time::Instant;
|
||||||
|
use sylpheed_formats::mesh::Xbg7Model;
|
||||||
|
use sylpheed_formats::ship::{is_base_part, ship_id_of};
|
||||||
|
fn main() {
|
||||||
|
let a: Vec<String> = std::env::args().collect();
|
||||||
|
let bytes = std::fs::read(&a[1]).unwrap();
|
||||||
|
let names = sylpheed_formats::mesh::xbg7_resource_names(&bytes);
|
||||||
|
let ids: Vec<String> = {
|
||||||
|
let mut v: Vec<String> = names.iter().filter(|n| is_base_part(n))
|
||||||
|
.filter_map(|n| ship_id_of(n).map(|s| s.to_string())).collect();
|
||||||
|
v.sort(); v.dedup(); v.truncate(5); v
|
||||||
|
};
|
||||||
|
for id in &ids {
|
||||||
|
let want: HashSet<String> = names.iter()
|
||||||
|
.filter(|n| ship_id_of(n) == Some(id.as_str())).cloned().collect();
|
||||||
|
let t = Instant::now();
|
||||||
|
let got = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||||||
|
println!("{id}: {} models in {:?}", got.len(), t.elapsed());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -463,11 +463,32 @@ impl Xbg7Model {
|
|||||||
Self::anchor_models_filtered(bytes, min_consistency, should_cancel, None)
|
Self::anchor_models_filtered(bytes, min_consistency, should_cancel, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decode the whole container (cached), then hand back the requested subset.
|
||||||
|
///
|
||||||
|
/// The decode itself must always see every resource — distinct assignment
|
||||||
|
/// resolves collisions against the whole population, and pruning first made
|
||||||
|
/// the answer depend on the request (see the module history). That makes a
|
||||||
|
/// single-resource query as expensive as a full decode, so the full decode is
|
||||||
|
/// memoised per container: the viewer asks for one ship's parts at a time and
|
||||||
|
/// would otherwise re-anchor 6 000 resources per ship.
|
||||||
fn anchor_models_filtered(
|
fn anchor_models_filtered(
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
min_consistency: f32,
|
min_consistency: f32,
|
||||||
should_cancel: &(dyn Fn() -> bool + Sync),
|
should_cancel: &(dyn Fn() -> bool + Sync),
|
||||||
wanted: Option<&std::collections::HashSet<String>>,
|
wanted: Option<&std::collections::HashSet<String>>,
|
||||||
|
) -> Vec<Xbg7Model> {
|
||||||
|
let Some(w) = wanted else {
|
||||||
|
return Self::anchor_models_uncached(bytes, min_consistency, should_cancel, None);
|
||||||
|
};
|
||||||
|
let full = full_decode_cached(bytes, min_consistency, should_cancel);
|
||||||
|
full.iter().filter(|m| w.contains(&m.name)).cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn anchor_models_uncached(
|
||||||
|
bytes: &[u8],
|
||||||
|
min_consistency: f32,
|
||||||
|
should_cancel: &(dyn Fn() -> bool + Sync),
|
||||||
|
wanted: Option<&std::collections::HashSet<String>>,
|
||||||
) -> Vec<Xbg7Model> {
|
) -> Vec<Xbg7Model> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
if bytes.len() < 16 || &bytes[..4] != b"XPR2" {
|
if bytes.len() < 16 || &bytes[..4] != b"XPR2" {
|
||||||
@@ -1138,6 +1159,49 @@ pub fn debug_resource_params(bytes: &[u8], name: &str) -> Option<(Vec<(usize, us
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Memoised whole-container decode, keyed by a cheap fingerprint of the bytes
|
||||||
|
/// plus the consistency setting. Holds the last few containers; a stage decode is
|
||||||
|
/// a handful of MB, and the alternative is re-anchoring every resource for every
|
||||||
|
/// ship the viewer shows.
|
||||||
|
fn full_decode_cached(
|
||||||
|
bytes: &[u8],
|
||||||
|
min_consistency: f32,
|
||||||
|
should_cancel: &(dyn Fn() -> bool + Sync),
|
||||||
|
) -> std::sync::Arc<Vec<Xbg7Model>> {
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
// Fingerprint: length plus three sampled 4 KB windows. Two different
|
||||||
|
// containers agreeing on all of that is not a case this format produces.
|
||||||
|
let mut fp: u64 = 0xcbf2_9ce4_8422_2325 ^ bytes.len() as u64;
|
||||||
|
let windows = [0usize, bytes.len() / 2, bytes.len().saturating_sub(4096)];
|
||||||
|
for w in windows {
|
||||||
|
for b in bytes.iter().skip(w).take(4096) {
|
||||||
|
fp = (fp ^ *b as u64).wrapping_mul(0x100_0000_01b3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let key = (fp, min_consistency.to_bits());
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
static CACHE: OnceLock<Mutex<Vec<((u64, u32), Arc<Vec<Xbg7Model>>)>>> = OnceLock::new();
|
||||||
|
let cache = CACHE.get_or_init(|| Mutex::new(Vec::new()));
|
||||||
|
if let Ok(c) = cache.lock() {
|
||||||
|
if let Some((_, v)) = c.iter().find(|(k, _)| *k == key) {
|
||||||
|
return Arc::clone(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let models = Arc::new(Xbg7Model::anchor_models_uncached(
|
||||||
|
bytes,
|
||||||
|
min_consistency,
|
||||||
|
should_cancel,
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
if let Ok(mut c) = cache.lock() {
|
||||||
|
c.push((key, Arc::clone(&models)));
|
||||||
|
if c.len() > 4 {
|
||||||
|
c.remove(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
models
|
||||||
|
}
|
||||||
|
|
||||||
/// Diagnostic: the candidate vertex-buffer starts the stage anchor scan will
|
/// Diagnostic: the candidate vertex-buffer starts the stage anchor scan will
|
||||||
/// consider for a given `stride`, for one container. A runtime capture names the
|
/// consider for a given `stride`, for one container. A runtime capture names the
|
||||||
/// offsets the engine really drew from (see `examples/shared_vbase_check.rs`), so
|
/// offsets the engine really drew from (see `examples/shared_vbase_check.rs`), so
|
||||||
|
|||||||
@@ -1113,11 +1113,24 @@ always runs over the whole container, and a subset is now a subset of the
|
|||||||
container's own answer. Verified: one-name, three-name and full decodes now
|
container's own answer. Verified: one-name, three-name and full decodes now
|
||||||
return the identical offset for the same resource.
|
return the identical offset for the same resource.
|
||||||
|
|
||||||
**Cost, stated plainly:** a single-resource query on a 50 MB container went from
|
**Cost, and the cache that pays it back.** Because the assignment must see every
|
||||||
near-instant to **~15 s**, because it now anchors every resource. The viewer
|
resource, a single-resource query became as expensive as a full decode (~15 s on
|
||||||
calls this once per ship view (with cancellation), so it is a slow prepare step
|
a 50 MB container). `full_decode_cached` memoises the whole-container decode,
|
||||||
rather than a per-frame cost — but caching the per-container decode is the
|
keyed by a fingerprint of the bytes (length + three sampled 4 KB windows) and the
|
||||||
obvious follow-up and is not done here.
|
consistency setting, holding the last four containers. Decoding five ships from
|
||||||
|
`Stage_S02` in turn, as the viewer does:
|
||||||
|
|
||||||
|
```
|
||||||
|
e007: 2 models in 10.494 s ← the one full decode
|
||||||
|
e010: 2 models in 48.6 µs
|
||||||
|
e105: 37 models in 1.42 ms
|
||||||
|
e106: 32 models in 92.7 µs
|
||||||
|
e108: 13 models in 73.7 µs
|
||||||
|
```
|
||||||
|
|
||||||
|
So a stage costs one decode, not one per ship. The whole-container path
|
||||||
|
(`anchor_models_cancellable`, what the sweeps use) stays uncached — it is already
|
||||||
|
the thing being measured.
|
||||||
|
|
||||||
### The consistency figure is mostly bounding boxes — real disagreement is ONE resource
|
### The consistency figure is mostly bounding boxes — real disagreement is ONE resource
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user