Out of memory generating types with `near-operation-file` + `typescript-operations`
Which packages are impacted by your issue?
@graphql-codegen/visitor-plugin-common
Describe the bug
We tried to upgrade visitor-plugin-common from 5.8.0 to 7.2.5. After the upgrade, codegen runs out of memory and never finishes.
Our config has four outputs in which three of them still work. The one that breaks is using near-operation-file and typescript-operations.
Increasing memory does not help:
- 4 GB -> crashes after 75s
- 8 GB -> crashes after 112s
It used all 8 GB, so it is not a matter of raising the limit. The garbage collector was busy about 90% of the time and could not free anything, so something is being held onto.
I looked into it and found two things
I did this investigation with an AI agent doing the profiling and the patching. The measurements below are real, taken from heap snapshots and allocation profiles of our own repo, and I verified each change by re-running codegen. But I do not know this codebase well myself, so if you ask follow-up questions I may need a little time to come back with a proper answer.
1. The cache key is the entire list of field paths
The cache uses a label to look things up, and that label is every field name in the selection set glued into one string. For our schema a single label is 168 MB, and the labels are kept until the run ends, so memory fills up with labels.
In transformSelectionSet, selection-set-to-object.ts#L1110C1-L1134C68:
const fieldSelections = [...getFieldNames({ selections, loadedFragments })].sort();
const cacheHashKey = `${fieldSelections.join(',')} @ ${possibleTypes.join('|')}`;
objMap.set(cacheHashKey, [result.mergedTypeString, fieldName]);The key is every field path in the selection set glued together with commas, and it stays in processor.typeCache until the run ends. In our project the three biggest keys are 168 MB, 166 MB and 154 MB. They are strings.
We changed the key to a hash and took heap snapshots before and after, at the same memory limit:
strings over 16KB: 517 MB in 261 strings -> 30 MB in 298 strings
So the key really was the problem for those 517 MB. The hash keeps the cache working:
const h = createHash('sha1');
for (const f of fieldSelections) { h.update(f); h.update(','); }
h.update(' @ ');
for (const p of possibleTypes) { h.update(p); h.update('|'); }
const cacheHashKey = h.digest('base64');2. getFieldNames walks the same fragments over and over
If a fragment is used in ten places, its subtree gets walked ten times instead of once and remembered. With fragments nested inside fragments, that multiplies.
In getFieldNames, utils.ts#L689-L699
case Kind.FRAGMENT_SPREAD: {
getFieldNames({
selections: loadedFragments
.filter(def => def.name === selection.name.value)
.flatMap(s => s.node.selectionSet.selections),
fieldNames, parentName, loadedFragments,
});Every time a fragment is used, its whole subtree is walked again, and loadedFragments.filter() scans all fragments once per use. This is the same M^N problem described in #752. A memory profile blames 91.7% of allocations on Set.prototype.add inside getFieldNames, calling itself dozens of levels deep.
The field paths inside a fragment do not depend on where the fragment is used, so they can be computed once per fragment and reused. We tried that and CPU time went from 98s to 60s, with exactly the same generated files.
What we could not figure out
With both changes applied it still runs out of memory. At that point the memory is not a few huge strings any more, it is about 3.2 million small strings. We could not tell what holds them, so we are reporting what we measured instead of guessing.
Things we tried that changed nothing: deduplicating fragment definitions, inlineFragmentTypes: 'inline' instead of 'combine', and skipping buildParentFieldName when extractAllFieldsToTypes is off. The fixes from #752 and #10895 are both already in the versions we use.
Setup
{
preset: 'near-operation-file',
plugins: ['typescript-operations'],
config: {
inlineFragmentTypes: 'combine',
declarationKind: 'interface',
nonOptionalTypename: true,
exportFragmentSpreadSubTypes: true,
immutableTypes: true,
},
}It is a React Native app with a lot of deeply nested fragments that are reused in many places. The schema is about 26 MB as an AST.
This is not about a community plugin. Both problems are in core visitor-plugin-common. near-operation-file-preset is only part of the config needed to see it.
We cannot share a public reproduction because the project is a private repo, but we are happy to run any patch or alpha build and report the numbers back.
Your Example Website or App
Private repo, cannot share. Happy to test any patch or alpha build and report numbers back.
Steps to Reproduce the Bug or Issue
- A project using
near-operation-filepreset withtypescript-operations, with deeply nested fragments reused across many operations. Ours has a ~26 MB schema AST. - With
[email protected](which pulls[email protected]), run codegen. It completes normally. - Upgrade to
[email protected](which pulls[email protected]), keepingclion 5.x. Run codegen again. - The output using
near-operation-file+typescript-operationsruns out of memory and never finishes. The other outputs in the same config still complete.
Expected behavior
Codegen generates the types without running out of memory.
Screenshots or Videos
No response
Platform
- OS: macOS (Apple Silicon)
- NodeJS: 24.18.0
graphqlversion: 15.8.0@graphql-codegen/*version(s):@graphql-codegen/[email protected]@graphql-codegen/[email protected]@graphql-codegen/[email protected]@graphql-codegen/[email protected]
Codegen Config File
{ schema: 'src/schema/schema.graphql', generates: { 'src/': { documents: ['src/**/*.tsx'], plugins: ['typescript-operations'], preset: 'near-operation-file', presetConfig: { baseTypesPath: 'graphql/globalTypes.ts', extension: '.ts', folder: 'graphql', }, config: { inlineFragmentTypes: 'combine', declarationKind: 'interface', nonOptionalTypename: true, exportFragmentSpreadSubTypes: true, immutableTypes: true, maybeValue: 'T | null', omitOperationSuffix: true, arrayInputCoercion: false, }, }, }, }
Additional context
No response
Source: dotansimha/graphql-code-generator