Files
graphify/tests/test_resolve_memoization.py
T
Pini Vaknin 16378b4846 perf(resolution): skip os.getcwd() in the path-memo key for absolute paths
_source_key and _resolve_cached key their lru_cache on the path and the
cwd, and call os.getcwd() on every lookup, hit or miss. On a 900-file
repo an extract makes about 215k of those calls. os.getcwd() is a
syscall that costs 30 to 40us on macOS, so building the key cost more
than the resolution the memo saved.

An absolute path resolves the same from any cwd, so it now keys with an
empty cwd. A relative path still keys on the cwd, so a chdir still misses
the cache. Path.is_absolute() applies host rules, which is what local
resolution needs: a driveless path on Windows depends on the drive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2228f80804)
2026-10-10 22:11:03 +01:00

90 lines
3.2 KiB
Python

"""_resolve_cached: Path.resolve() with a per-(path, cwd) memo (#perf).
The symbol-resolution passes call Path.resolve() once per import/export/use
fact and per node — tens of thousands of calls over a few hundred distinct
corpus paths, each walking nt._getfinalpathname / readlink. Memoizing on the
(path, cwd) pair collapses that to one syscall per distinct path; on a
364-file self-corpus a sequential extract dropped from ~27s to ~14s, with a
byte-identical graph (verified separately).
"""
import os
from pathlib import Path
import pytest
try:
from graphify.extractors.resolution import _resolve_cached, _cached_realpath
except ImportError: # pre-fix tree
_resolve_cached = None
_cached_realpath = None
needs_cache = pytest.mark.skipif(
_resolve_cached is None, reason="resolve memo not present"
)
@needs_cache
def test_matches_path_resolve(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
(tmp_path / "pkg").mkdir()
(tmp_path / "pkg" / "mod.py").write_text("x = 1\n", encoding="utf-8")
for arg in ("pkg/mod.py", str(tmp_path / "pkg" / "mod.py"), "missing.py"):
assert _resolve_cached(arg) == Path(arg).resolve()
assert _resolve_cached(Path(arg)) == Path(arg).resolve()
@needs_cache
def test_resolves_once_per_distinct_path(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
(tmp_path / "a.py").write_text("x = 1\n", encoding="utf-8")
_cached_realpath.cache_clear()
for _ in range(50):
_resolve_cached("a.py")
_resolve_cached(Path("a.py")) # str/Path collapse to one key
info = _cached_realpath.cache_info()
assert info.misses == 1 and info.hits == 99, info
@needs_cache
def test_cache_is_cwd_sensitive(tmp_path, monkeypatch):
d1, d2 = tmp_path / "one", tmp_path / "two"
for d in (d1, d2):
d.mkdir()
(d / "m.py").write_text("x = 1\n", encoding="utf-8")
monkeypatch.chdir(d1)
r1 = _resolve_cached("m.py")
monkeypatch.chdir(d2)
r2 = _resolve_cached("m.py")
assert r1 == d1.resolve() / "m.py"
assert r2 == d2.resolve() / "m.py"
@needs_cache
def test_exception_paths_match_resolve(tmp_path, monkeypatch):
"""A path that resolve() can handle returns the same as a direct call;
the helper never swallows what resolve() would raise (callers keep their
own try/except, unchanged)."""
monkeypatch.chdir(tmp_path)
weird = "a/../b/./c.py"
assert _resolve_cached(weird) == Path(weird).resolve()
@needs_cache
def test_absolute_path_lookup_skips_getcwd(tmp_path, monkeypatch):
"""An absolute path resolves the same from any cwd, so its key leaves the
cwd out and a lookup never calls os.getcwd() (a syscall per call)."""
target = tmp_path / "a.py"
expected = target.resolve()
assert _resolve_cached is not None
calls = []
real_getcwd = os.getcwd
monkeypatch.chdir(tmp_path)
assert _resolve_cached("a.py") == expected # warm the memo
monkeypatch.setattr(os, "getcwd", lambda: calls.append(1) or real_getcwd())
assert _resolve_cached(target) == expected
assert _resolve_cached(str(target)) == expected
assert calls == []
assert _resolve_cached("a.py") == expected
assert calls, "a relative path keys on the cwd, even on a memo hit"