dotansimha / dotansimha/graphql-code-generator

Out of memory generating types with `near-operation-file` + `typescript-operations`

Open
#10,940 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
11.3k
Forks
1.4k
Avg merge
1d 1h
Merged PRs (30d)
23

Description

### 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](https://github.com/dotansimha/graphql-code-generator/blob/cf205e01ac5fe320f01e3cab00987d65620f1a4b/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts#L1110C1-L1134C68):

```typescript
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:

```typescript
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](https://github.com/dotansimha/graphql-code-generator/blob/cf205e01ac5fe320f01e3cab00987d65620f1a4b/packages/plugins/other/visitor-plugin-common/src/utils.ts#L689-L699)

```typescript
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](https://github.com/dotansimha/graphql-code-generator-community/issues/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](https://github.com/dotansimha/graphql-code-generator-community/issues/752) and #10895 are both already in the versions we use.

## Setup

```typescript
{
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

1. A project using `near-operation-file` preset with `typescript-operations`, with
deeply nested fragments reused across many operations. Ours has a ~26 MB schema AST.
2. With `typescript-operations@4.6.0` (which pulls `visitor-plugin-common@5.8.0`),
run codegen. It completes normally.
3. Upgrade to `typescript-operations@6.1.6` (which pulls `visitor-plugin-common@7.2.5`),
keeping `cli` on 5.x. Run codegen again.
4. The output using `near-operation-file` + `typescript-operations` runs 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
- `graphql` version: 15.8.0
- `@graphql-codegen/*` version(s):
- `@graphql-codegen/cli@5.0.7`
- `@graphql-codegen/typescript-operations@6.1.6`
- `@graphql-codegen/near-operation-file-preset@5.2.2`
- `@graphql-codegen/visitor-plugin-common@7.2.5`

### 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_

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts at transformSelectionSet and in src/utils.ts at getFieldNames. Run code generation against a comparable near-operation-file/typescript-operations workload and profile memory and allocations. Done means deeply nested, reused fragments complete without out-of-memory failure while generated files remain unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
performance, tooling
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.