Skip to content

perf(ids): memoize normalize_id - #3645

Open
abhay-codes07 wants to merge 1 commit into
Graphify-Labs:v8from
abhay-codes07:perf/normalize-id-memo
Open

abhay-codes07 wants to merge 1 commit into
Graphify-Labs:v8from
abhay-codes07:perf/normalize-id-memo

Conversation

@abhay-codes07

Copy link
Copy Markdown
Contributor

What

normalize_id is the canonical-id transform every node id flows through (via make_id / _make_id, called from the engine, extract and build — 64/92/20 call sites). Each call runs up to six casefold + NFKC iterations (they don't commute, so the code iterates to a fixpoint — see the function's own docstring) plus two regex passes: ~1.3µs each. And it is called on a small, heavily-repeating set of strings — a file's stem is normalized once per node it owns, and the same symbol names recur across files.

The change

Memoize normalize_id with a bounded functools.lru_cache. It is a pure, deterministic str -> str function of its input alone — nothing to invalidate — so caching is exact; the bound keeps a pathological corpus from growing it without limit.

Measured

Microbenchmark over the repeating-input pattern the pipeline exhibits (best of 5):

before after
normalize_id, 24k calls 31.6 ms 2.4 ms (~13×)

Correctness

Output is unchanged, and the three documented, test-enforced invariants still hold — idempotent (normalize_id(normalize_id(s)) == normalize_id(s)), caseless-stable (normalize_id(s) == normalize_id(s.casefold())), and word-only output — verified against the pre-memo implementation across ASCII, casing, punctuation, whitespace, and combining-mark/Unicode cases (İstanbul, résumé, straße, Greek ypogegrammeni). The test_id_normalization_contract suite (131 tests) passes unchanged.

Tests

tests/test_normalize_id_memo.py — equivalence with the unmemoized reference; the documented invariants; repeated inputs hit the cache (1 miss / 99 hits); and distinct inputs map distinctly through the cache. The full suite matches a fresh v8 (0.9.63) baseline (zero new failures).

🤖 Generated with Claude Code

https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q

normalize_id is a pure, deterministic str->str transform — up to six
casefold+NFKC iterations plus two regex passes (~1.3us each) — and every node
id in the pipeline flows through it via make_id, almost always on a repeating
handful of file stems and symbol names (a file's stem is normalized once per
node it owns; the same identifiers recur across files). lru_cache collapses
those repeats: ~13x on the repeated-input pattern (31.6ms -> 2.4ms per 24k
calls). The result depends only on the input string, so there is nothing to
invalidate; the cache is bounded so a pathological corpus cannot grow it
without limit. Output and the documented invariants (idempotent,
caseless-stable, word-only) are unchanged, verified against the pre-memo
implementation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
Copilot AI lite review requested due to automatic review settings September 17, 2026 17:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. 1 change(s) tested, no difference found (not proven).


Graphify review — findings

Memoizes normalize_id with a bounded lru_cache (262144 entries) so the repeated stems and identifiers every node id runs through via make_id are computed once instead of re-running six casefold+NFKC passes and two regex substitutions each time. Adds tests pinning cache behaviour against a verbatim pre-memo reference oracle and confirming the documented invariants (idempotent, caseless-stable, \w-only output) and hit/miss counts still hold.

No blocking issues surfaced. 3 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 3029 functions depend on the 14 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 645 callers, 45 callees
  • new: _rebuild_code() — 129 callers, 54 callees
  • new: build_from_json() — 214 callers, 20 callees
  • new: deduplicate_entities() — 77 callers, 24 callees
  • new: build_merge() — 76 callers, 14 callees
  • new: save_semantic_cache() — 63 callers, 9 callees
  • new: _extract_generic() — 18 callers, 29 callees
  • new: extract_bash() — 45 callers, 10 callees
  • …and 106 more — each is listed as a finding

Verification — 3029 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: 704 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

113 of 287 test file(s) selected (39%) via static blast radius.

  • tests/test_analyze.py — impact
  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_build.py — impact
  • tests/test_build_merge_dedup_scope.py — impact
  • tests/test_build_merge_hyperedges_and_prune.py — impact
  • tests/test_build_merge_shrink_guard.py — impact
  • tests/test_cache.py — impact
  • tests/test_carried_hyperedge_remap.py — impact
  • tests/test_charmap_encoding.py — impact
  • tests/test_chunking.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cli_export.py — impact
  • tests/test_cluster.py — impact
  • tests/test_confidence.py — impact
  • tests/test_corrupt_graph_json.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_dart.py — impact
  • tests/test_dedup.py — impact
  • tests/test_dedup_remaps_hyperedges.py — impact
  • tests/test_dedup_survivor_richness.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_evidence_binding.py — impact
  • tests/test_export.py — impact
  • tests/test_export_control_characters.py — impact
  • tests/test_external_stub_endpoints.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cache_location.py — impact
  • tests/test_extract_cli.py — impact
  • tests/test_falkordb_integration.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_forwarding_review_findings.py — impact
  • tests/test_global_graph.py — impact
  • tests/test_go_qualified_resolution.py — impact
  • tests/test_hyperedge_member_shapes.py — impact
  • tests/test_hyperedge_roundtrip.py — impact
  • tests/test_hypergraph.py — impact
  • tests/test_id_normalization_contract.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_import_self_loops.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_issue_3472_source_file_collision.py — impact
  • … and 63 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.

Formal verification

No difference found (not proven): No behavior difference found in normalize\_id (not a proof).

The verifier ran both versions of normalize\_id 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.

· 114 more finding(s) on lines outside this diff (see the check run).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants