ui: memoise the texture-existence probe, as its two neighbours already are
`set_texture_probe`'s closure answers every path form of `SetTexture`, and it was the only one of the three probes installed beside it without a cache — `set_texture_size_probe` and `set_font_probe` both memoise, and the font one's comment already gives the reason that applies here verbatim: a path's answer cannot change, and an addon may ask every frame. A miss is the expensive answer. An addon texture is never in the MPQ chain, so both candidates fall through to `loose_addon_file`, which walks the path one component at a time and, whenever a component's real spelling is not the lower-cased one it was handed, enumerates that whole directory to compare case-insensitively. `Interface\AddOns` holds 94 folders here and `pfquest` never matches `pfQuest` on a case-sensitive filesystem, so an uncached call read that directory twice over, on the main thread, between two systems. grom's 2026-10-21 17:08 session priced it: `[ui-verbs]` reports `texture:SetTexture` at **45-59 us a call across all 33 windows** — stable enough to be a fixed charge rather than a tail — against the ~0.2 us a cached existence answer costs. 1.75 ms a frame over 34 calls while the world map was up, because `WorldMapFrame_Update` re-textures every overlay tile and POI on each `WORLD_MAP_UPDATE`. Replaying the same walk over the real AddOns tree reproduces 26-42 us for a single candidate. Misses are cached too, as the size probe's are: an addon probing for optional art is the common case and it is the one that pays for the walk. This is the smaller half of the map's cost — `scrollframe.rs`' derivations were the larger — and it is a ~1-2% of frame fix, not the dip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -84,13 +84,40 @@ fn install_addon_asset_resolvers(world: &mut World, script: &mut UiScript) {
|
||||
let size_root = root.clone();
|
||||
let font_chain = chain.clone();
|
||||
let font_root = root.clone();
|
||||
// Memoised on the same grounds as the font probe below, and it matters more: this one answers
|
||||
// every path form of `SetTexture`, which is among the most-called verbs in the interface, and
|
||||
// a miss is the expensive answer. An addon texture is never in the MPQ chain, so both
|
||||
// candidates fall through to [`benilla_assets::loose_addon_file`], which walks the path one
|
||||
// component at a time and, whenever a component's real spelling is not the lower-cased one it
|
||||
// is handed, falls back to enumerating that whole directory to compare case-insensitively.
|
||||
// `Interface\AddOns` holds 94 folders here and `pfquest` never matches `pfQuest` on a
|
||||
// case-sensitive filesystem, so an uncached call read that directory twice over.
|
||||
//
|
||||
// grom's 2026-10-21 17:08 session priced it: `[ui-verbs]` reports `texture:SetTexture` at
|
||||
// **45-59 us a call across all 33 windows** — 1.75 ms a frame over 34 calls while the world
|
||||
// map was up, against the 0.2-0.3 us a cached existence answer costs. The map is the worst of
|
||||
// it because `WorldMapFrame_Update` re-textures every overlay tile and POI on each
|
||||
// `WORLD_MAP_UPDATE`, which is how one map click came to cost 57 ms.
|
||||
//
|
||||
// Misses are cached too, as the size probe's are: a path that does not resolve is the common
|
||||
// case for an addon probing for optional art, and it is the one that pays for the directory
|
||||
// walk.
|
||||
let seen_textures: std::cell::RefCell<std::collections::HashMap<String, bool>> =
|
||||
std::cell::RefCell::new(std::collections::HashMap::new());
|
||||
script.set_texture_probe(Box::new(move |path| {
|
||||
benilla_assets::sprite_candidates(path).iter().any(|c| {
|
||||
// Keyed by the path exactly as the region carries it, so a hit allocates nothing; two
|
||||
// spellings of one file get two entries, which is cheaper than normalising to find out.
|
||||
if let Some(&cached) = seen_textures.borrow().get(path) {
|
||||
return cached;
|
||||
}
|
||||
let found = benilla_assets::sprite_candidates(path).iter().any(|c| {
|
||||
chain.lock_recover().contains(c)
|
||||
|| root
|
||||
.as_deref()
|
||||
.is_some_and(|r| benilla_assets::loose_addon_file(r, c).is_some())
|
||||
})
|
||||
});
|
||||
seen_textures.borrow_mut().insert(path.to_string(), found);
|
||||
found
|
||||
}));
|
||||
// Keyed by the path exactly as the region carries it, so a hit allocates nothing (this is
|
||||
// asked per zero-size textured region per layout pass); misses are cached too.
|
||||
|
||||
Reference in New Issue
Block a user