perf(dedup): remove O(nodes x components) scan from remap construction - #2328
perf(dedup): remove O(nodes x components) scan from remap construction#2328stupidprogrammer4 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
This pull request reverts the 0.9.31 changelog entry and appears to roll back several features/fixes that shipped with it. In cli.py, it removes the per-edge _src/_tgt direction-recovery logic (#2309) from path, query, and explain, restoring behavior that derives direction from stored source/target order, and it removes the ambiguity check that made explain refuse and list candidates when a name matches nodes in multiple files (#2233). It also drops the Go predeclared-function filter import (_GO_PREDECLARED_FUNCS) from extract.py. Separately, dedup.py adds a nodes_by_id index to build the dedup remap in a single pass instead of re-scanning the node list per component, with ordering preserved to keep tie-breaking stable. The diff is truncated, so additional changes across the touched extractor/serve symbols aren't fully visible here.
Worth a look
- explain no longer refuses ambiguous names across multiple files —
graphify/cli.py:1316· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2367 functions depend on the 1036 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
_find_node()— 13 callers, 3 callees
Verification — 2367 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: 2345 function(s) in the blast radius were not formally verified this run
· 1 more finding(s) on lines outside this diff (see the check run).
deduplicate_entities() rebuilt its remap table by re-scanning the whole
unique_nodes list once per merged union-find component, costing
O(nodes x components) -- roughly 137M membership checks on a 50k-node
corpus with 2.5k merged components, about a third of dedup wall-clock.
Build an id -> (position, node) index once and slice each component out of
it instead: O(members log members) per component.
The index carries the enumeration position, not just the node, because
_pick_winner() selects via min(), which returns the first minimum -- so
ties (equal chunk-suffix status and equal id length) are resolved by list
order. Sorting the group by id instead changed 179 of 2358 survivors on a
tie-heavy corpus. Sorting by position reproduces unique_nodes order
exactly, leaving survivors and edges byte-identical.
Benchmark, median of 5 runs:
nodes before after speedup
5000 2.37s 2.29s 1.03x
20000 10.58s 9.70s 1.09x
50000 31.72s 21.92s 1.45x
Isolated remap loop at 50k nodes / 2.5k components: 6.838s -> 0.019s (370x).
Merge counts and output node counts are identical at every scale.
This does not address the pre-existing arrival-order dependence of the
component structure itself, which originates in the fuzzy-merge loop's
uf.find() short-circuit and is out of scope here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
This PR refactors the remap-construction loop in deduplicate_entities (in graphify/dedup.py). Instead of re-scanning the full unique_nodes list for each connected component, it builds a single nodes_by_id lookup mapping each id to its position and node, then constructs each group_nodes by looking up members and sorting them by original position. The stated intent is a performance optimization for large corpora, while preserving unique_nodes ordering so that _pick_winner's tie-breaking behavior remains unchanged. The many graphify_dedup_* symbols listed appear to be the surrounding module contents; the actual code change is localized to this loop.
No blocking issues surfaced. 3 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 258 functions depend on the 43 functions this change touches.
Health — grade A; 7 existing hotspot(s) in the area this change touches (pre-existing, not introduced here):
deduplicate_entities()— 49 callers, 21 callees (high)dispatch_command()— 2 callers, 110 callees (high)main()— 81 callers, 2 callees (high)build_merge()— 23 callers, 7 callees (high)build()— 19 callers, 3 callees (high)_run()— 12 callers, 1 callees (medium)_llm_tiebreak()— 1 callers, 11 callees (medium)
Verification — 258 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: 116 function(s) in the blast radius were not formally verified this run
|
Shipped in v0.9.32: the O(nodes x components) scan is gone from remap construction, with identical results (#2328). Credited in the release notes. Thanks @stupidprogrammer4! |
deduplicate_entities() rebuilt its remap table by re-scanning the whole unique_nodes list once per merged union-find component, costing O(nodes x components) -- roughly 137M membership checks on a 50k-node corpus with 2.5k merged components, about a quarter of dedup wall-clock.
Build an id -> (position, node) index once and slice each component out of it instead: O(members log members) per component.
The index carries the enumeration position, not just the node, because _pick_winner() selects via min(), which returns the first minimum -- so ties (equal chunk-suffix status and equal id length) are resolved by list order. Sorting the group by id instead changed 179 of 2358 survivors on a tie-heavy corpus. Sorting by position reproduces unique_nodes order exactly, leaving survivors and edges byte-identical.
Benchmark, median of 5 runs:
Isolated remap loop at 50k nodes / 2.5k components: 8.671s -> 0.030s (289x). Merge counts and output node counts are identical at every scale.
This does not address the pre-existing arrival-order dependence of the component structure itself, which originates in the fuzzy-merge loop's uf.find() short-circuit and is out of scope here.