Repository navigation
refactor(perf): share the sink-reachability walk between crossfn and gated - #1752
Conversation
|
✅ Health of changed files: 8.0 → 8.6 (+0.6) 📋 At a glance Files & modules (2)
✅ Health gate: passed 🔎 More signals (2)🗺️ Change map flowchart LR
subgraph PR ["Changed in this PR (2 with dependents)"]
f_packages_core_src_repowise_core_analysis_health_perf_crossfn_py[".../perf/crossfn.py 🔥"]:::changed
f_packages_core_src_repowise_core_analysis_health_perf_gated_py[".../perf/gated.py 🔥"]:::changed
end
f_packages_core_src_repowise_core_analysis_health_engine_py[".../health/engine.py"]
f_packages_core_src_repowise_core_analysis_health_perf_crossfn_py --> f_packages_core_src_repowise_core_analysis_health_engine_py
f_packages_core_src_repowise_core_analysis_health_perf___init___py[".../perf/__init__.py"]
f_packages_core_src_repowise_core_analysis_health_perf_crossfn_py --> f_packages_core_src_repowise_core_analysis_health_perf___init___py
f_packages_core_src_repowise_core_analysis_health_perf_gated_py --> f_packages_core_src_repowise_core_analysis_health_engine_py
f_packages_core_src_repowise_core_analysis_health_perf_gated_py --> f_packages_core_src_repowise_core_analysis_health_perf___init___py
classDef changed fill:#dbeafe,stroke:#1d4ed8,color:#1e3a5f
classDef warn fill:#fef3c7,stroke:#b45309,color:#78350f
classDef guard fill:#dcfce7,stroke:#15803d,color:#14532d
Solid arrows: code that imports the changed files (2 direct dependents, from the last indexed snapshot). Dashed: history/tests. 🔥 Hotspots touched (3)
👀 Suggested reviewers @RaghavChamadiya 📊 See the full report for this PR |
|
The dedup intent is still right — on current The PR forked from More important than the textual conflict: Net: the shared walk is worth landing, but it should be rebuilt on |
…gated collect_crossfn_io_in_loop and collect_blocking_io_under_lock are the same function twice: the same pre-checks, the same index, the same bare-sink set, the same reverse BFS, and the same per-function first-hop resolution, down to _hits_for_function and _lock_hits_for_function. A fix to the walk had to be made in both or it silently applied to one of the two markers. Lift it into perf/sink_reach.py, parameterised by the entry-set accessor and the hit kind. Both entry points keep their names and signatures and become thin callers. Rebuilt on current main rather than rebased: repowise-dev#1832 moved first-hop matching from callee-name to call-line accurate via index.resolve_call_targets, stamping each hit with resolution_basis. The shared walk resolves the same way and passes the basis through, so collapsing the two copies costs none of that accuracy. Thanks @sloemo01 for flagging that a textual conflict-resolve would have reverted it. Two guards added, because nothing asserted resolution_basis before: a crossfn and a lock case where one owner calls two same-named callees on different lines, so name-based matching cannot pick the right one. Both fail if the walk goes back to matching on name. One behavioural difference is preserved rather than unified: the lock pass sets PerfHit.func_start and the loop pass does not, so the shared collector takes carry_func_start. Closes repowise-dev#1741
7fdb026 to
8acf5ce
Compare
|
@sloemo01 thank you — that was exactly the right thing to flag, and it would have been an easy trap to walk into. Rebuilt on current I did not resolve the conflict. I reset onto # Call-line accurate first-hop resolution: the index picks the exact
# graph edge for this call site and reports how it got there, so the
# basis travels onto the hit rather than being re-derived by name.
targets, basis = index.resolve_call_targets(a_sid, call_line, target_name)and On the tests — worth correcting your expectation, in your favour and against it. Nothing in It was not fully blind, though. I simulated the naive resolve — swapped I closed that gap rather than relying on it. Two new cases, one per pass, where a single owner calls two same-named callees on different lines so name matching cannot pick the right one: graph.add_edge("owner.py::run", "pure.py::load", edge_type="calls", call_lines=[9])
graph.add_edge("owner.py::run", "db.py::load", edge_type="calls", call_lines=[4])
...
assert hit.path == ("owner.py::run", "db.py::load")
assert hit.resolution_basis == "call-site"Under the simulated regression, 4 tests fail — the existing one plus both new ones. They also answer the bot's "no test file imports the changed files" note. Verification
The |
RaghavChamadiya
left a comment
There was a problem hiding this comment.
Thanks @NgoQuocViet2001, and @sloemo01 for the review that made this land safely. Flagging that the conflict hid a semantic change rather than just being textual is the thing that would have cost us the call-line accuracy #1832 added, and it is not something a three-way merge would have told anyone.
Resetting onto main and re-deriving rather than resolving the conflict was the right instinct and I want to name why: on a rebase the old name-based callees_by_name loop sits on the "ours" side of every hunk, so a conflict resolution that looks clean can quietly reinstate it. Rebuilding makes that impossible instead of merely unlikely. I checked, and callees_by_name appears nowhere in the branch.
Verified the rest against your head:
_hits_for_function in sink_reach.py is a faithful merge of both originals, not a rewrite of either. Same seen dedupe on target name, same path_to_sink chain, same break after the first reaching callee, and index.resolve_call_targets(a_sid, call_line, target_name) with resolution_basis=basis on every hit, matching what both passes do on main today. carry_func_start preserving the disagreement between the two callers rather than unifying it is exactly right for a de-duplication, and saying so in the docstring means the next person knows it is a fact about the callers and not an oversight here. max_depth is forwarded from both entry points, so the depth bound still belongs to the caller.
Correcting @sloemo01 on the test coverage was worth more than agreeing would have been. "The perf suite was not guarding the field you were worried about, here is the one test that would have caught a full revert, and here is why a partial one would have slipped through" is a better answer than either "you are right" or "it is covered". Adding a case per pass where one owner calls two same-named callees on different lines pins the thing that actually distinguishes call-line resolution from name matching, and test_lock_io_first_hop_also_carries_the_resolution_basis closes the gap on the lock side that had no assertion at all.
Green on all three Pythons. Merging.
Closes #1741.
What changed
collect_crossfn_io_in_loopandcollect_blocking_io_under_lockare the same function twice — the same pre-checks, the same index, the same bare-sink set, the same multi-source reverse BFS, and the same per-function first-hop resolution, down to_hits_for_function/_lock_hits_for_function.The walk now lives once in
perf/sink_reach.py, parameterised by the entry-set accessor and the hit kind, next to the other perf primitives. Both entry points keep their names and signatures and become thin callers:Rebuilt on current
main, not rebased#1832 moved first-hop matching from callee-name to call-line accurate, via
index.resolve_call_targets(...), stamping each hit withresolution_basis. @sloemo01 pointed out that resolving the textual conflict would have quietly reinstated the old name-based loop and dropped that.So this branch was reset onto
mainand the de-duplication re-derived on top, rather than merged. The shared walk resolves the same waymaindoes and passes the basis through, so collapsing the two copies costs none of the exact-edge accuracy:callees_by_nameis gone from the branch entirely.New coverage
Nothing asserted
resolution_basisin the perf suite before — the only assertions on it live undertests/unit/server/. Two cases added, one per pass, where a single owner calls two same-named callees on different lines so name matching cannot pick the right one:Simulating the naive resolve (swapping
resolve_call_targetsfor a name-based loop) fails 4 tests: these two plus the existingtest_unresolved_loop_call_does_not_reuse_same_name_outside_loop. These also answer the bot's "no test file imports the changed files" note.One difference preserved rather than unified
The lock pass sets
PerfHit.func_startand the loop pass does not. That is carried through ascarry_func_startrather than made consistent, because making them agree would move behaviour and this is a de-duplication. The new lock test asserts it, so the asymmetry is at least written down. Happy to unify in a follow-up.Test plan
uv run python -m pytest tests/unit/health/ -q— 1215 passed.uv run python -m pytest tests/unit/health/test_perf_crossfn.py tests/unit/health/test_perf_phase7b.py -q— 47 passed.uv run ruff checkanduv run ruff format --checkon all four changed files — clean.