From 77a39b2f4e220b18a87dbf20e6f2b83d2006ac0d Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:48:59 +0200 Subject: [PATCH] perf: reuse diffs and raise the file size cap --- emulators/fbneo_cps12.yml | 2 +- scripts/profile_sync.py | 45 +++++++++++++++++++++++++++++++++----- tests/test_profile_sync.py | 16 ++++++++++---- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/emulators/fbneo_cps12.yml b/emulators/fbneo_cps12.yml index 14ace3bb..be540f8b 100644 --- a/emulators/fbneo_cps12.yml +++ b/emulators/fbneo_cps12.yml @@ -5,7 +5,7 @@ source: "https://github.com/libretro/FBNeo" upstream: "https://github.com/finalburnneo/FBNeo" profiled_date: "2026-03-23" source_commit: "ef1f64b7023bc511858e96b8961f99c18a09f112" -core_version: "v1.0.0.03" +core_version: "v1.0.0.2" display_name: "Arcade (FinalBurn Neo) (CPS-1 / CPS-2)" cores: - fbneo_cps12 diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 073929ce..ac0a181f 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -40,7 +40,10 @@ REVIEW_STATUSES = ("CHANGED", "GONE", "AMBIGUOUS") REBASE_STATUSES = ("SHIFTED", "RENAMED", "MOVED") WIDEN_STEPS = (0, 3, 6, 12, 25, 50) -MAX_MATCH_LINES = 20000 +# A 30k-line driver diffs in about five seconds, and the biggest files the +# corpus cites sit just above that. The cap stays as a guard against +# pathological inputs, not as a limit on real source files. +MAX_MATCH_LINES = 40000 PIN = "pin" HEAD = "head" @@ -257,14 +260,32 @@ def _map_index(opcodes, index: int) -> int | None: return None +_OPCODE_CACHE: dict[tuple[int, int], list] = {} + + +def _opcodes(pin: list[str], head: list[str]) -> list: + """Opcodes for one pair of revisions, computed once per run. + + Every ref citing the same file would otherwise redo the same diff, and a + driver of thirty thousand lines takes seconds each time. + """ + key = (id(pin), id(head)) + cached = _OPCODE_CACHE.get(key) + if cached is None: + cached = difflib.SequenceMatcher( + None, pin, head, autojunk=False + ).get_opcodes() + _OPCODE_CACHE[key] = cached + return cached + + def _map_changed(pin: list[str], head: list[str], lo: int, hi: int) -> AnchorResult: """Map a pinned range onto HEAD once exact anchoring has failed.""" if len(pin) > MAX_MATCH_LINES or len(head) > MAX_MATCH_LINES: return AnchorResult( "CHANGED", None, None, [], f"file over {MAX_MATCH_LINES} lines" ) - matcher = difflib.SequenceMatcher(None, pin, head, autojunk=False) - opcodes = matcher.get_opcodes() + opcodes = _opcodes(pin, head) if not any(tag == "equal" for tag, *_ in opcodes): return AnchorResult("GONE", None, None, []) new_lo = _map_index(opcodes, lo) @@ -837,10 +858,22 @@ def build_report( candidates = near return None, candidates + lines_cache: dict[tuple[str, str], list[str] | None] = {} + def fetch(which: str, path: str): - view, actual = resolve_path(path) - sha = view.pin if which == PIN else view.head - return upstream.fetch_file(view.repo, sha, actual, cache_dir, offline) + """Lines for one path at one revision, read once per run. + + The same object is handed back every time so the opcode cache can key + on identity. + """ + key = (which, path) + if key not in lines_cache: + view, actual = resolve_path(path) + sha = view.pin if which == PIN else view.head + lines_cache[key] = upstream.fetch_file( + view.repo, sha, actual, cache_dir, offline + ) + return lines_cache[key] def describe(path: str) -> tuple[str | None, str | None, str]: view, actual = resolve_path(path) diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index fab0b484..d625df7a 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -283,14 +283,22 @@ class TestAnchorBlock(unittest.TestCase): result = anchor_block(["a", "", "b"], ["a", "", "b"], 2, 2) self.assertEqual(result.status, "CHANGED") - def test_large_file_skips_mapping_with_reason(self): - pin = [f"line {i}" for i in range(25000)] - head = [f"other {i}" for i in range(25000)] + def test_a_file_past_the_cap_skips_mapping_with_reason(self): + size = profile_sync.MAX_MATCH_LINES + 1 + pin = [f"line {i}" for i in range(size)] + head = [f"other {i}" for i in range(size)] result = anchor_block(pin, head, 10, 10) self.assertEqual(result.status, "CHANGED") - self.assertIsNotNone(result.reason) + self.assertIn(str(profile_sync.MAX_MATCH_LINES), result.reason) self.assertIsNone(result.start) + def test_a_thirty_thousand_line_file_is_still_mapped(self): + pin = [f"line {i}" for i in range(30000)] + head = ["pad"] + pin[:-1] + result = anchor_block(pin, head, 10, 10) + self.assertEqual(result.status, "SHIFTED") + self.assertEqual(result.start, 11) + def _part(path, old, new, status): return PartResult(RefPart(path, old, old), status, None, new, new, [])