Files
graphify/tests/test_extract_path_memo.py
T
rohit-jsfreaky fedf15d4f9 perf(extract): resolve and parse each path once in the id remap and call tie-break
extract()'s id remap called Path.resolve() on the same file paths once
per input, once per target_file-stamped edge and once per node: ~24k
calls for ~1k distinct paths on graphify's own repo. Resolve each
distinct path once per extract() call through a local memo; a hit
returns the same Path the call would, and a path that raises is never
stored.

_path_proximity_winner (the #1553 god-node tie-break) re-parsed every
candidate's parent directory with PurePosixPath on every ambiguous call
site (~227k Path objects per update on graphify's repo), although the
same candidate files come back for every site of a common name. Memoize
the parent-directory parts per path string; equal tuples are exactly
equal PurePosixPath parents, so every tier decides as before.

graphify's own repo (18,830 nodes), `graphify update --force`,
alternated: cold 28.1 -> 26.3 s, no-change update 24.8 -> 22.5 s and
20.9 -> 18.0 s. graph.json identical (all attributes) cold and warm;
old vs new tie-break identical on 200,000 random cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d5966eda8a)
2026-10-07 17:52:47 +01:00

68 lines
3.0 KiB
Python

"""extract()'s id remap and the call tie-break parse each distinct path once.
The remap resolved the same file paths once per input, once per stamped edge
and once per node, and `_path_proximity_winner` re-parsed every candidate's
directory on every ambiguous call site. Both now memoize; answers are unchanged.
"""
from __future__ import annotations
import sys
from pathlib import Path, PurePosixPath
import pytest
from graphify import paths as paths_mod
from graphify.extract import extract
def _corpus(root: Path) -> list[Path]:
(root / "pkg").mkdir(parents=True)
files = []
for i in range(4):
body = "".join(f"def f{i}_{j}():\n return f{i}_{j + 1}()\n\n\n" for j in range(40))
body += "".join(f"from pkg.m{k} import f{k}_0\n" for k in range(4) if k != i)
p = root / "pkg" / f"m{i}.py"
p.write_text(body, encoding="utf-8")
files.append(p)
(root / "pkg" / "__init__.py").write_text("", encoding="utf-8")
return files + [root / "pkg" / "__init__.py"]
def test_extract_resolves_each_distinct_path_once(tmp_path: Path, monkeypatch):
files = _corpus(tmp_path / "proj")
calls: list[str] = []
real_resolve = Path.resolve
remap_frames = {"extract", "_decompose", "_resolved"} # the id-remap code in extract()
def counting_resolve(self, *args, **kwargs):
if sys._getframe(1).f_code.co_name in remap_frames:
calls.append(str(self))
return real_resolve(self, *args, **kwargs)
monkeypatch.setattr(Path, "resolve", counting_resolve)
result = extract(files, root=tmp_path / "proj", cache_root=tmp_path / "cache", parallel=False)
assert len(result["nodes"]) > 160 # every function became a node, so the per-node remap ran
# Without the memo the remap resolved once per node and per stamped edge
# (~200 calls here); with it, once per distinct path.
assert len(calls) == len(set(calls)), f"{len(calls)} remap resolve calls for {len(set(calls))} paths"
@pytest.mark.parametrize(
"site, candidates, expected",
[
("pkg/a/x.py", {"1": "pkg/a/y.py", "2": "pkg/b/y.py"}, "1"), # same dir
("pkg\\a\\x.py", {"1": "pkg/a/y.py", "2": "pkg/b/y.py"}, "1"), # backslashes
("pkg/a/b/x.py", {"1": "pkg/a/y.py", "2": "other/y.py"}, "1"), # longest prefix
("pkg/a/x.py", {"1": "pkg/b/y.py", "2": "pkg/c/y.py"}, None), # tied prefix
("x.py", {"1": "y.py", "2": "z/y.py"}, "1"), # root-level file
("pkg/a/x.py", {"1": "pkg/a/x.py", "2": "pkg/a/y.py"}, "1"), # same file
],
)
def test_proximity_winner_matches_a_fresh_parse(site, candidates, expected):
paths_mod._parent_parts.cache_clear()
assert paths_mod._path_proximity_winner(site, candidates) == expected
# second call is served from the memo and must agree
assert paths_mod._path_proximity_winner(site, candidates) == expected
for f in candidates.values():
assert paths_mod._parent_parts(f) == PurePosixPath(f.replace("\\", "/")).parent.parts