Skip to content

Commit 05302f6

Browse files
sp00ktoberclaude
andcommitted
perf(studio): parse a built PAC's clips once: the findings re-check asked every few seconds
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 394f2a2 commit 05302f6

5 files changed

Lines changed: 68 additions & 7 deletions

File tree

‎apps/studio/src/mhfu_studio/monster/clips.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313

1414
from __future__ import annotations
1515

16+
import functools
1617
import hashlib
1718
import re
1819
from collections.abc import Callable, Iterable, Mapping
1920
from dataclasses import dataclass, field
21+
from types import MappingProxyType
2022

2123
from mhfu_port import layout as layouts
2224
from mhfu_port import motion, slots
@@ -77,8 +79,11 @@ def clip_table(anim: AnimPack) -> dict[int, Fingerprint]:
7779
return {s: (frames, bool(loop)) for s, (frames, loop) in slots.slot_prints(anim).items()}
7880

7981

80-
def pac_clip_table(pac: bytes) -> dict[int, Fingerprint]:
81-
return clip_table(slots.anim_of(pac))
82+
@functools.lru_cache(maxsize=2)
83+
def pac_clip_table(pac: bytes) -> Mapping[int, Fingerprint]:
84+
"""`clip_table` of a built PAC, parsed once per PAC: the findings ask every few seconds.
85+
Read-only, as the table is shared by every caller."""
86+
return MappingProxyType(clip_table(slots.anim_of(pac)))
8287

8388

8489
def source_clip_table(moveset: bytes) -> dict[int, Fingerprint]:

‎apps/studio/src/mhfu_studio/monster/validate.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def _pac(m: Manifest, pac: bytes | None, sources: Mapping[int, int]) -> list[Fin
208208
return []
209209
out = []
210210
try:
211-
table: dict[int, clips.Fingerprint] | None = clips.pac_clip_table(pac)
211+
table: Mapping[int, clips.Fingerprint] | None = clips.pac_clip_table(pac)
212212
except ValueError as e:
213213
out.append(_f("error", "PAC_UNREADABLE", "", f"no clip table in the PAC: {e}"))
214214
table = None
@@ -225,7 +225,7 @@ def _pac(m: Manifest, pac: bytes | None, sources: Mapping[int, int]) -> list[Fin
225225

226226

227227
def _clips(
228-
m: Manifest, table: dict[int, clips.Fingerprint], sources: Mapping[int, int]
228+
m: Manifest, table: Mapping[int, clips.Fingerprint], sources: Mapping[int, int]
229229
) -> list[Finding]:
230230
out = []
231231
moved = {

‎apps/studio/src/mhfu_studio/monster/workspace.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import dataclasses
1414
import tomllib
15-
from collections.abc import Callable, Hashable, Sequence
15+
from collections.abc import Callable, Hashable, Mapping, Sequence
1616
from concurrent.futures import Future, ThreadPoolExecutor
1717
from contextlib import AbstractContextManager
1818
from dataclasses import dataclass
@@ -924,7 +924,7 @@ def attack_session(self) -> AttackSession | None:
924924

925925
# clips
926926

927-
def clip_table(self) -> dict[int, clips.Fingerprint]:
927+
def clip_table(self) -> Mapping[int, clips.Fingerprint]:
928928
"""The open build's fingerprints."""
929929
sc = self.scene
930930
if sc is None:

‎apps/studio/src/mhfu_studio/shell/studio.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
from mhfu_studio.shell.viewport import Viewport
3434

3535

36+
SLOW_COST, SLOW_EVERY = 0.05, 60.0
37+
"""A timed re-check that cost more than this many seconds is not repeated for that many: a stall
38+
is the check's whole length, which the interval's share of the time does not bound. A change
39+
(`Findings.stale`) re-checks at once."""
40+
41+
3642
class Findings:
3743
"""The active document's findings, re-checked on demand and, when `auto`, now and then.
3844
@@ -55,7 +61,8 @@ def get(self, doc: Any, now: float | None = None) -> list[Finding]:
5561
if doc is None:
5662
self.found, self._doc = [], None
5763
return self.found
58-
due = self.auto and now - self._at >= max(1.0, 10 * self._cost)
64+
wait = SLOW_EVERY if self._cost > SLOW_COST else max(1.0, 10 * self._cost)
65+
due = self.auto and now - self._at >= wait
5966
if self._stale or doc is not self._doc or due:
6067
t0 = time.monotonic()
6168
try:

‎apps/studio/tests/monster/test_clips.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,52 @@ def test_zinogre(games, built, ports):
216216
def test_brute(built):
217217
table = C.pac_clip_table(built("brute_tigrex"))
218218
assert len(table) == 77 and table[61] == (382, False) and 24 not in table
219+
220+
221+
@pytest.fixture
222+
def parses(monkeypatch):
223+
"""The PACs `pac_clip_table` parsed, with a table that says which."""
224+
seen: list[bytes] = []
225+
monkeypatch.setattr(slots, "anim_of", lambda pac: seen.append(pac) or pac)
226+
monkeypatch.setattr(C, "clip_table", lambda pack: {1: (len(pack), True)})
227+
C.pac_clip_table.cache_clear()
228+
yield seen
229+
C.pac_clip_table.cache_clear()
230+
231+
232+
def test_a_pac_is_parsed_once_whoever_asks(parses):
233+
pac = b"a pac"
234+
first = C.pac_clip_table(pac)
235+
assert C.pac_clip_table(pac) is first and C.pac_clip_table(bytes(bytearray(pac))) is first
236+
assert parses == [pac]
237+
238+
239+
def test_other_bytes_are_another_table(parses):
240+
one, two = C.pac_clip_table(b"one"), C.pac_clip_table(b"three")
241+
assert one is not two and dict(one) == {1: (3, True)} and dict(two) == {1: (5, True)}
242+
assert parses == [b"one", b"three"]
243+
244+
245+
def test_the_table_is_shared_so_no_caller_can_change_it(parses):
246+
table = C.pac_clip_table(b"pac")
247+
with pytest.raises(TypeError):
248+
table[2] = (1, False)
249+
with pytest.raises(TypeError):
250+
del table[1]
251+
assert dict(C.pac_clip_table(b"pac")) == {1: (3, True)}
252+
253+
254+
def test_an_unreadable_pac_is_not_remembered(monkeypatch):
255+
tries = []
256+
257+
def broken(pac):
258+
tries.append(pac)
259+
raise ValueError("no clips")
260+
261+
monkeypatch.setattr(slots, "anim_of", broken)
262+
C.pac_clip_table.cache_clear()
263+
for _ in range(2):
264+
with pytest.raises(ValueError, match="no clips"):
265+
C.pac_clip_table(b"bad")
266+
assert len(tries) == 2
267+
C.pac_clip_table.cache_clear()

0 commit comments

Comments
 (0)