Repository navigation
perf(extract): resolve and parse each path once in the id remap and call tie-break - #4195
rohit-jsfreaky wants to merge 1 commit into
Conversation
…all 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 Graphify-Labs#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>
|
Thanks for the pull request, @rohit-jsfreaky. A maintainer will review it soon. Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions. A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic. |
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. PR-changed functions: 1/2 verified (0 proven, 1 may-equivalent, 0 distinguished) · 1 not verified (1 vacuous).
Not verified on this run: extract (vacuous: never exercised).
Graphify review — findings
Memoizes path resolution in extract's id-remap pass so each distinct file path is resolved once per call via a local _resolved helper, instead of once per input, stamped edge and node; paths that raise are never cached, so they keep falling through to the existing except branches. _path_proximity_winner now gets parent-directory segments from an lru_cached _parent_parts (backslashes normalized). That stops re-parsing the same candidate directories on every ambiguous call site, and the chosen winners stay the same.
No blocking issues surfaced.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3563 functions depend on the 313 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 787 callers, 51 callees - new:
_rebuild_code()— 151 callers, 56 callees - new:
build_from_json()— 233 callers, 20 callees - new:
detect()— 121 callers, 16 callees - new:
build_merge()— 85 callers, 14 callees - new:
save_semantic_cache()— 65 callers, 9 callees - new:
to_obsidian()— 41 callers, 14 callees - new:
save_manifest()— 44 callers, 13 callees - …and 98 more — each is listed as a finding
Verification — 3563 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 2939 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
247 of 338 test file(s) selected (73%) via static blast radius.
tests/test_affected_cli.py— impacttests/test_agents_platform.py— impacttests/test_analyze.py— impacttests/test_astro_extraction.py— impacttests/test_astro_import_ids.py— impacttests/test_atomic_canvas_export.py— impacttests/test_atomic_version_stamp.py— impacttests/test_atomic_writes.py— impacttests/test_benchmark.py— impacttests/test_benchmark_raw_graph.py— impacttests/test_blade_extractor.py— impacttests/test_build.py— impacttests/test_build_located_semantic_identity.py— impacttests/test_build_merge_dedup_scope.py— impacttests/test_build_merge_hyperedges_and_prune.py— impacttests/test_build_merge_shrink_guard.py— impacttests/test_builtin_global_type_refs.py— impacttests/test_cache.py— impacttests/test_callflow_html.py— impacttests/test_carried_hyperedge_remap.py— impacttests/test_case_sensitive_resolution.py— impacttests/test_charmap_encoding.py— impacttests/test_chunking.py— impacttests/test_cjs_module_extension.py— impacttests/test_cli_export.py— impacttests/test_cluster.py— impacttests/test_cobol_extractor.py— impacttests/test_codebuddy.py— impacttests/test_community_labels_skill.py— impacttests/test_confidence.py— impacttests/test_corrupt_graph_json.py— impacttests/test_cpp_method_declarations.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_cpp_objc_cross_file_calls.py— impacttests/test_cross_extension_reexport_self_cycle.py— impacttests/test_cross_language_call_resolution.py— impacttests/test_cross_repo_external_call_guards.py— impacttests/test_cross_repo_member_calls.py— impacttests/test_csharp_call_site_generic_args.py— impacttests/test_csharp_enum_members.py— impacttests/test_csharp_field_generic_args.py— impacttests/test_csharp_generic_callsites.py— impacttests/test_csharp_interface_dispatch.py— impacttests/test_csharp_member_calls.py— impacttests/test_csharp_member_nodes.py— impacttests/test_csharp_object_creation.py— impacttests/test_csharp_partial_classes.py— impacttests/test_csharp_tuple_type_refs.py— impacttests/test_csharp_type_resolution.py— impacttests/test_dedup.py— impact- … and 197 more
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Docs that may be stale (advisory)
graphify/skill-agents.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 564-665): references changed symbols_corpusgraphify/skill-aider.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 684-764): references changed symbols_corpusgraphify/skill-amp.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 564-665): references changed symbols_corpusgraphify/skill-claw.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 567-668): references changed symbols_corpusgraphify/skill-codex.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 564-665): references changed symbols_corpusgraphify/skill-copilot.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 567-668): references changed symbols_corpusgraphify/skill-devin.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 802-883): references changed symbols_corpusgraphify/skill-droid.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 564-665): references changed symbols_corpusgraphify/skill-kilo.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 567-668): references changed symbols_corpusgraphify/skill-kiro.md§ Step 9 - Save manifest, update cost tracker, clean up, and report (lines 567-668): references changed symbols_corpus
…and 10 more.
Formal verification
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 90 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in \_path\_proximity\_winner (not a proof).
The verifier ran both versions of \_path\_proximity\_winner on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
· 106 more finding(s) on lines outside this diff (see the check run).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* upstream/v8: (184 commits) release: 0.9.80 — include Graphify-Labs#4195, Graphify-Labs#4198 feat(python): resolve obj.method() through an annotated/constructor-bound local (Graphify-Labs#4198) perf(extract): resolve and parse each path once in the id remap and call tie-break release: 0.9.80 fix(deps): raise tree-sitter runtime floor to >=0.25 for ABI 15 grammars (Graphify-Labs#4148) fix(build): give --no-cluster graph.json the same endpoint rules as build_from_json fix(watch): keep extract()'s run-only keys out of --no-cluster graph.json fix(watch): drop an external import stub once no edge references it fix(detect): drop a previous checkout's absolute manifest keys (Graphify-Labs#4175) fix(python): keep TYPE_CHECKING-only imports out of import cycles (Graphify-Labs#3159) fix(python): resolve self.<attr>.<method>() calls through the attribute's constructor type (Graphify-Labs#2860) fix(cache): treat an AST hit whose import target is gone as a miss fix(extract): preserve cross-drive syntax warnings fix(extract): let the Windows console script use the extraction pool fix(resolution): keep re_exports from both files whose ids collide fix(markdown): resolve links to file names that contain a space fix(java): capture methods and calls inside anonymous class bodies test: skip optional-extra tests when the extra is not installed (Graphify-Labs#4190) fix(serve): show the graph's build commit in graph_stats perf(serve): build the MCP traversal view and shortest-path graphs once per graph ... # Conflicts: # CHANGELOG.md # graphify/extract.py # graphify/extractors/engine.py # graphify/serve.py # pyproject.toml # uv.lock
* upstream/v8: (184 commits) release: 0.9.80 — include Graphify-Labs#4195, Graphify-Labs#4198 feat(python): resolve obj.method() through an annotated/constructor-bound local (Graphify-Labs#4198) perf(extract): resolve and parse each path once in the id remap and call tie-break release: 0.9.80 fix(deps): raise tree-sitter runtime floor to >=0.25 for ABI 15 grammars (Graphify-Labs#4148) fix(build): give --no-cluster graph.json the same endpoint rules as build_from_json fix(watch): keep extract()'s run-only keys out of --no-cluster graph.json fix(watch): drop an external import stub once no edge references it fix(detect): drop a previous checkout's absolute manifest keys (Graphify-Labs#4175) fix(python): keep TYPE_CHECKING-only imports out of import cycles (Graphify-Labs#3159) fix(python): resolve self.<attr>.<method>() calls through the attribute's constructor type (Graphify-Labs#2860) fix(cache): treat an AST hit whose import target is gone as a miss fix(extract): preserve cross-drive syntax warnings fix(extract): let the Windows console script use the extraction pool fix(resolution): keep re_exports from both files whose ids collide fix(markdown): resolve links to file names that contain a space fix(java): capture methods and calls inside anonymous class bodies test: skip optional-extra tests when the extra is not installed (Graphify-Labs#4190) fix(serve): show the graph's build commit in graph_stats perf(serve): build the MCP traversal view and shortest-path graphs once per graph ... # Conflicts: # CHANGELOG.md # graphify/extract.py # graphify/extractors/engine.py # graphify/serve.py # pyproject.toml # uv.lock
What does this PR do?
Fixes #4194.
extract(): the id remap now resolves each distinct path once through a local memo (_resolved), instead of once per input, pertarget_file-stamped edge and per node. A hit returns the samePaththe call would; a path that raises is never stored, so every existingtry/exceptbehaves as before. The memo lives for oneextract()call only.paths._path_proximity_winner: the parent-directory parse of each candidate path is memoized (_parent_parts,functools.lru_cache(maxsize=65536)). Equal part-tuples are exactly equalPurePosixPath(...).parentvalues, so the same-file, same-directory and longest-common-prefix tiers decide as before.No other code changed; #3500 already memoized the symbol-resolution passes, this covers what it did not touch.
Type of change
(performance; no behaviour change)
Verification & Invariants
Invariant: graph.json is identical; only the wall time changes.
graphify's own repo (18,830 nodes),
python -m graphify update <repo> --force(clustered), v8 and this PR alternated:graph.json identical between v8 and this PR (all node and edge attributes), cold and warm.
Work removed:
Path.resolve()fromextract()'s remap 23,819 calls -> one per distinct path (~1k on graphify's repo);PurePosixPathobjects from the tie-break 226,809 per update -> one parse per distinct candidate path.Old vs new
_path_proximity_winneranddisambiguate_ambiguous_candidatescompared on 200,000 random cases (graphify's file list plus backslash, absolute, root-level,..,./and empty paths): 0 differences.How was this tested?
Graphify-specific checklist
uv run python -m tools.skillgen --bless) when changing their source fragments. (n/a, no skill sources touched)🤖 Generated with Claude Code