Skip to content

perf(import): memoize print() in processImport (O(n²) → O(n) on large schemas)#8258

Open
lemonmade wants to merge 1 commit into
ardatan:masterfrom
lemonmade:perf/import-memoize-print
Open

perf(import): memoize print() in processImport (O(n²) → O(n) on large schemas)#8258
lemonmade wants to merge 1 commit into
ardatan:masterfrom
lemonmade:perf/import-memoize-print

Conversation

@lemonmade

Copy link
Copy Markdown

Problem

processImport de-duplicates the collected definitions by printing each one to SDL and comparing the strings:

for (const defs of set.values()) {
  for (const def of defs) {
    const defStr = print(def);          // <-- here
    if (!definitionStrSet.has(defStr)) { ... }
  }
}

A single definition node appears in many of the dependency sets in set — any widely-referenced type (a root Query, a shared Node/User, etc.) is pulled into the dependency set of every definition that depends on it. So print() is called roughly O(n²) times for n unique nodes.

Because print(ast) in graphql-js is implemented as visit(ast, printDocASTReducer), this is a full AST walk each time, and on large, densely-connected schemas it dominates import time.

Profile (real-world schema)

A downstream codegen pass over a ~12,000-line schema imported across three projects spent ~133s, of which CPU profiling attributed ~63% to graphql-js visit + ~13% to print — i.e. almost all of it to this one print(def) line. The tool's own type generation was ~0%.

Fix

Memoize print by node identity, so each unique node is printed at most once:

const printCache = new Map<DefinitionNode, string>();
for (const defs of set.values()) {
  for (const def of defs) {
    let defStr = printCache.get(def);
    if (defStr === undefined) {
      defStr = print(def);
      printCache.set(def, defStr);
    }
    if (!definitionStrSet.has(defStr)) { ... }
  }
}

print is a pure function of its node, so the de-duplication is byte-for-byte identical — this is a pure performance change.

Result

  • Same real-world codegen pass: ~133s → ~17s (~7.6×). (The remainder is a separate O(n²) in the per-definition dependency-closure construction; I'll open a follow-up.)
  • All 80 existing @graphql-tools/import tests pass unchanged.

Notes

  • No public API change; patch changeset included.
  • The same node objects are shared across dependency sets (they reference the parsed AST), so Map identity keys are exact.

🤖 Generated with Claude Code

processImport de-duplicates collected definitions by printing each to SDL and
comparing strings. A single definition node appears in many dependency sets
(any widely-referenced type is pulled in by every dependent), so print() ran
~O(n^2) times for n unique nodes. For large, densely-connected schemas this
print/visit work dominates import time.

Memoize print() by node identity so each unique node prints at most once.
print() is pure, so output is identical (the existing 80 import tests pass).
On a ~12k-line schema imported across three projects, a downstream codegen
pass dropped from ~133s to ~17s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3627e4c

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

This PR includes changesets to release 3 packages
Name Type
@graphql-tools/import Patch
@graphql-tools/graphql-file-loader Patch
@graphql-tools/node-require 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

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6a70583-ea68-4e5c-95a0-e632f92932ac

📥 Commits

Reviewing files that changed from the base of the PR and between 21382c9 and 3627e4c.

📒 Files selected for processing (2)
  • .changeset/import-memoize-print.md
  • packages/import/src/index.ts

📝 Walkthrough

Summary by CodeRabbit

  • Performance
    • Improved import processing speed for GraphQL tool workflows, especially in larger schemas with overlapping dependencies.
    • Reduced repeated work during definition handling, which should make imports faster while keeping the same output.

Walkthrough

Adds a Map<DefinitionNode, string> cache (printCache) inside processImport to memoize print(def) results per node identity, replacing repeated print calls during de-duplication. A changeset entry documents the optimization.

Changes

Memoize print in processImport

Layer / File(s) Summary
printCache memoization
packages/import/src/index.ts, .changeset/import-memoize-print.md
Replaces direct print(def) calls in the nested de-duplication loop with a Map-based cache lookup, printing each node at most once. Changeset entry documents the patch.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐇 A map of prints, so neat and small,
No node gets printed twice at all!
The import loop now hops with glee,
Cached strings set every definition free.
Performance blooms like clover in spring! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the core change: memoizing print() in processImport for performance.
Description check ✅ Passed The description matches the code changes and explains the same performance-focused fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant