Skip to content

[visitor-plugin-common] fix: memoize fragment field names in getFieldNames to stop exponential blow-up (#10940) - #10944

Draft
eddeee888 wants to merge 3 commits into
masterfrom
claude/beautiful-fermi-7ybluj
Draft

[visitor-plugin-common] fix: memoize fragment field names in getFieldNames to stop exponential blow-up (#10940)#10944
eddeee888 wants to merge 3 commits into
masterfrom
claude/beautiful-fermi-7ybluj

Conversation

@eddeee888

@eddeee888 eddeee888 commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR fixes one of the two root causes reported in #10940 (out-of-memory generating types with near-operation-file + typescript-operations on schemas with deeply nested, widely reused fragments): getFieldNames() in visitor-plugin-common recomputed a fragment's field names from scratch on every FRAGMENT_SPREAD, instead of computing them once per fragment and reusing them. When fragments are nested inside other reused fragments (a "diamond" shape), that work doubles at every level, turning a document with N fragments into O(2^N) work instead of O(N) — the same class of bug as graphql-code-generator-community#752.

A fragment's own field names don't depend on where it's spread from, only on its own body, so they can be computed once and reused. This is scoped to getFieldNames() itself in visitor-plugin-common — it doesn't touch the run-wide typeCache or the giant joined-string cache key in transformSelectionSet (the other root cause described in #10940), which is left for a follow-up.

Related # (issue): #10940

What's changed

  • packages/plugins/other/visitor-plugin-common/src/utils.ts: getFieldNames() now takes an optional fragmentFieldNamesCache (a Map<string, ReadonlySet<string>>), threaded through the recursion. On a FRAGMENT_SPREAD, it computes that fragment's field names relative to its own root once, caches them by fragment name, and re-prefixes them with the current parentName on every reuse — so a fragment spread at different nesting depths within the same call still gets the correct fully-qualified paths.
  • The checkpoint test from the previous commit (get-field-names-perf.spec.ts) is unchanged — it's the acceptance criterion for this fix.
  • Added a changeset (patch on @graphql-codegen/visitor-plugin-common).

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

  • The checkpoint test (getFieldNames (issue #10940) > does not re-walk a fragment subtree once per spread) now passes: 42 Set.add calls instead of 1,048,576 for a 20-level diamond fragment chain.
  • Full visitor-plugin-common test suite (51 tests) passes.
  • Downstream consumers that exercise this code path — typescript-operations, typescript, near-operation-file-preset, client-preset (440 tests total) — all still pass.
  • tsc --noEmit on the whole repo is clean.

Test Environment:

  • OS: Linux
  • @graphql-codegen/visitor-plugin-common: current master
  • NodeJS: 22

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added tests that prove my fix is effective or that my feature works — the checkpoint test from the previous commit now passes
  • New and existing unit tests pass locally with my changes

Further comments

Root cause #1 from #10940 (the selection-set-to-object.ts cache key built from the full joined field-path list, measured at up to 168MB per key on the reporter's schema) is intentionally out of scope here — it's a separate, additive concern from the exponential re-walk fixed in this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RmAsddNmmJrhGmTSa1euU4

@changeset-bot

changeset-bot Bot commented Sep 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 23bcbc7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@graphql-codegen/visitor-plugin-common Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@eddeee888
eddeee888 force-pushed the claude/beautiful-fermi-7ybluj branch from f8332e3 to 35a6b5c Compare September 12, 2026 14:52
@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

🚀 Snapshot Release (alpha)

The latest changes of this PR are available as alpha on npm (based on the declared changesets):

Package Version Info
@graphql-codegen/visitor-plugin-common 7.2.6-alpha-20260913024757-23bcbc729bb8c044054d0958237feeea7a45bb2d npm ↗︎ unpkg ↗︎
@graphql-codegen/client-preset 6.2.0-alpha-20260913024757-23bcbc729bb8c044054d0958237feeea7a45bb2d npm ↗︎ unpkg ↗︎
@graphql-codegen/testing 5.0.2-alpha-20260913024757-23bcbc729bb8c044054d0958237feeea7a45bb2d npm ↗︎ unpkg ↗︎

@eddeee888 eddeee888 changed the title [visitor-plugin-common] test: reproduce #10940 — getFieldNames re-walks fragment subtrees (failing) [visitor-plugin-common] fix: memoize fragment field names in getFieldNames to stop exponential blow-up (#10940) Sep 12, 2026
…onentially

visitor-plugin-common's getFieldNames() recomputes a fragment's field
names from scratch on every FRAGMENT_SPREAD instead of computing them
once per fragment and reusing the result. When fragments are nested and
reused (a "diamond" shape), the work doubles at every level, turning a
document with N fragments into O(2^N) work instead of O(N).

This is the second root cause reported in #10940 (out-of-memory
generating types with near-operation-file + typescript-operations on a
schema with deeply nested, widely reused fragments), matching the
current source at selection-set-to-object.ts and utils.ts exactly.

The added test builds a chain of 20 fragments, each spreading the
previous one twice, with a single real field at the bottom. It spies on
Set.prototype.add to count how many times that field actually gets
walked: today it is 2^20 (1,048,576) instead of staying close to linear
in the fragment count.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmAsddNmmJrhGmTSa1euU4

eddeee888:oss:verify
…l blow-up (#10940)

getFieldNames() recomputed a fragment's field names from scratch on
every FRAGMENT_SPREAD, even when the same fragment appeared multiple
times within the same walk (e.g. fragments nested inside other reused
fragments). Since the work doubles at every level of such nesting, a
document with N fragments could cost O(2^N) instead of O(N), which is
the second root cause reported in #10940.

A fragment's own field names don't depend on where it's spread from —
only on its own body — so they're now computed once per fragment name
(scoped to the lifetime of one top-level getFieldNames() call, via an
optional fragmentFieldNamesCache Map threaded through the recursion)
and reused, re-prefixing with the current parentName on each reuse.
This keeps correctness for a fragment spread at different nesting
depths within the same call, while eliminating the redundant re-walk.

Fix is scoped to this one function in visitor-plugin-common; it does
not touch the run-wide type cache or the cache-key string described as
the first root cause in #10940 (left for a follow-up).

Verified against the checkpoint test added in the previous commit:
`getFieldNames (issue #10940) > does not re-walk a fragment subtree
once per spread` now passes (42 Set.add calls instead of 1,048,576 for
a 20-level diamond fragment chain). Full visitor-plugin-common suite
(51 tests) and downstream consumers - typescript-operations, typescript,
near-operation-file-preset, client-preset (440 tests) - all still pass,
and `tsc --noEmit` on the whole repo is clean.

Related #10940

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmAsddNmmJrhGmTSa1euU4

eddeee888:oss:fix
@eddeee888
eddeee888 force-pushed the claude/beautiful-fermi-7ybluj branch from fe8cb51 to 23bcbc7 Compare September 13, 2026 02:47
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