Skip to content

fix(resolution): skip oversized files when chasing imports/re-exports — a single large asset import OOMs the whole index - #1288

Closed
colinli007 wants to merge 1 commit into
colbymchenry:mainfrom
colinli007:fix/resolution-oversized-file-oom
Closed

colinli007 wants to merge 1 commit into
colbymchenry:mainfrom
colinli007:fix/resolution-oversized-file-oom

Conversation

@colinli007

Copy link
Copy Markdown

fix(resolution): skip oversized files when chasing imports/re-exports — a single large asset import OOMs the whole index

Problem

codegraph init dies with FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory during "Resolving refs", reproducibly at the same ref, on a mid-size React/TS project (1,161 files, ~54k refs). Raising the heap doesn't help — an 8 GB heap (NODE_OPTIONS=--max-old-space-size=8192, honored by the vendored runtime) dies at the same ~65%. The background daemon OOMs the same way on every session, so the project can't be indexed at all. Affects 1.3.1 and 1.4.1; 0.9.9 was fine.

Minimal repro — two files:

mkdir -p repro/src && cd repro && git init
dd if=/dev/urandom of=src/big.mp4 bs=1m count=240
printf 'import video from "./big.mp4";\nexport function play(): string {\n  return video;\n}\n' > src/app.ts
codegraph init   # OOM at "Resolving refs"

Root cause

Reference resolution follows import targets verbatim. An asset import — import Emilia from "@/assets/Emilia.mp4" in the real project (a 243 MB video), ./big.mp4 above — hands the resolver a path that extraction never parsed. Extraction has a MAX_FILE_SIZE guard (1 MB) precisely so vendored blobs and bundles stay out of the graph, but resolution's readFileCached() has no such guard: the import/re-export chase (getReExports, getImportMappings) reads the target whole, decodes hundreds of MB of binary as UTF-8, and regex-scans the resulting string for re-exports (for a non-JS extension the language re-key falls back to the consumer's language, so the mp4 bytes are scanned as TypeScript). The transient strings and match arrays blow through any heap limit. Since the file was never parsed, the read can never produce a resolution — the cost buys nothing.

Instrumentation pinpointing it (per-ref log, 2 GB heap): heap is at 110 MB entering the Emilia import ref, then the process dies inside that single resolveOne call.

Fix

Mirror extraction's guard in readFileCached(): stat before reading, and treat anything over 1 MB as unreadable (cached null). Oversized files become exactly as invisible to resolution as they already are to extraction — no behavior change for any file the graph can actually contain.

Tests

Two regression tests in __tests__/resolution-oversized-file.test.ts:

  • an oversized (>1 MB) but syntactically valid barrel must not have its re-export chain chased — pre-fix this produced a calls edge through a file the graph doesn't contain (that's the read the OOM rides in on); post-fix the chase stops at the barrel
  • a project importing a >1 MB binary asset indexes cleanly

Both fail before the fix and pass after. Full suite: 139 files, 2,374 passed / 4 skipped — green.

Verified end-to-end: the 240 MB two-file repro and the original 1,161-file project both index completely with a 2 GB heap post-fix (previously OOM'd 8 GB); index integrity confirmed (13,633 nodes / 31,397 edges — 232 more resolved edges than 0.9.9 produced on the same repo).

🤖 Generated with Claude Code

@colinli007
colinli007 force-pushed the fix/resolution-oversized-file-oom branch from 7d4f1a8 to 9d38b80 Compare July 15, 2026 03:38
Reference resolution follows import targets verbatim, so an asset import
(import video from "./intro.mp4") hands readFileCached() a path
extraction never parsed. The content can't answer an import/re-export
chase — extraction skips files over MAX_FILE_SIZE, so they have no
nodes — but resolution read them whole anyway, decoded them as UTF-8,
and regex-scanned them. On a real project a single ~240 MB video import
OOM'd an 8 GB heap at "Resolving refs" ~65%, killing every codegraph
init (reproducible with two files: one .ts importing one 240 MB binary).

Mirror extraction's guard in readFileCached(): stat first, and treat
anything over 1 MB as unreadable (cached null), making oversized files
as invisible to resolution as they already are to extraction.

Regression tests: an oversized barrel's re-export chain must not be
chased (pre-fix it produced a calls edge through a file the graph
doesn't contain), and a project importing a >1 MB binary must index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@colinli007
colinli007 force-pushed the fix/resolution-oversized-file-oom branch from 9d38b80 to ecccb95 Compare July 15, 2026 03:45
colbymchenry

This comment was marked as outdated.

colbymchenry

This comment was marked as outdated.

@colinli007

Copy link
Copy Markdown
Author

Friendly ping — this has been open since mid-July, and I just re-verified that the bug is still live on main.

Checked against 3ed73bc (v1.6.0, the current npm release):

  • readFileCached() in src/resolution/index.ts still does a bare fs.readFileSync(fullPath, 'utf-8') with no size check, and there is no size guard anywhere else in src/resolution/ — so nothing fixed this by another route in the meantime.
  • Dropping this PR's regression test onto a clean main worktree, the re-export case still fails: a calls edge is resolved through an oversized barrel that extraction never put in the graph.
× does not chase re-exports through a barrel extraction skipped for size
  → expected [ { …(7) } ] to have a length of +0 but got 1
✓ indexes a project importing a >1MB binary asset without dying

(The second one passes on main only because it uses a 2 MB fixture — the real-world trigger was a ~240 MB .mp4 import, which OOM's an 8 GB heap at "Resolving refs" ~65% and kills every codegraph init on the project. The threshold is what the test pins; the scale is what bites.)

The branch is still conflict-free against main (GitHub reports MERGEABLE), and the change is 22 lines in one function plus the test file. Happy to rebase, adjust the constant to import MAX_FILE_SIZE from extraction directly instead of re-declaring it, or split the test out — whatever makes it easier to take. Thanks!

@colinli007
colinli007 force-pushed the fix/resolution-oversized-file-oom branch from ecccb95 to 7992f42 Compare September 15, 2026 08:35
@colbymchenry

Copy link
Copy Markdown
Owner

Thank you for the precise root cause and the two-file repro, @colinli007. The same guard landed as #2006, which shipped in 1.6.1: the resolver checks a file's size before reading it and treats anything over the 1 MB source limit as unreadable, so an imported video or an oversized barrel is never decoded or chased. Your test's cases behave correctly on current main, so we're closing this as fixed.

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