Skip to content

Make --build --watch iterations reuse programs (30s → ~2s per file change)#1200

Open
NullVoxPopuli wants to merge 2 commits into
mainfrom
nvp/fix-build-watch-program-reuse
Open

Make --build --watch iterations reuse programs (30s → ~2s per file change)#1200
NullVoxPopuli wants to merge 2 commits into
mainfrom
nvp/fix-build-watch-program-reuse

Conversation

@NullVoxPopuli

Copy link
Copy Markdown
Contributor

Important

made with claude

I'm not an expert in this codebase, and could be way off base, and I apologize if this is a waste of time / completely wrong


Fixes #1199 (upstream: volarjs/volar.js#314).

In a TS project-references monorepo (59 composite projects, 19,286-file app), a one-line change under ember-tsc --build --watch took ~30s to report — the same cost as the project's initial cold build. Plain --watch on the same project: ~2s.

phase stock -b --watch this PR plain --watch
Parse 7.29s 0.00s 0.00s
ResolveModule 7.22s 0.00s 0.03s
Program 27.57s 1.74s 1.80s
Check 0.14s 0.13s 0.15s
Total 29.98s 2.08s 2.14s

Why it was slow (three compounding causes, proven by instrumentation)

  1. TS's solution builder unconditionally releases each project's Program after building (afterProgramDonereleaseProgram()), even in watch mode — so oldProgram never reaches createProgram and SourceFile/resolution reuse is impossible.
  2. Its compiler host (unlike plain watch's) has no SourceFile cache across iterations — every file re-parses, and the new object identities also evict volar's parsedSourceFiles WeakMap, so every .gts re-transforms.
  3. volar's proxyCreateProgram keeps its language/snapshot/moduleResolutionCache state in a single slot keyed on the last options — any multi-project -b solution thrashes it every iteration.

The patches

Same playbook as the existing proxyCreateProgram/tryGetJSExtensionForFile source patches in run-volar-tsc.ts (exact-shape matching, loud error if the dep changes — verified matching on both TS 5.9.3 and 6.0.3):

  • afterProgramDone: keep programs alive in watch mode (~+1.4GB RSS on the 19k-file project).
  • setGetSourceFileAsHashVersioned: version-validated SourceFile cache, scoped per project — with project references the same physical file is a redirect in one program and a root in another, so SourceFile objects must never be shared across projects (sharing them crashes buildinfo emit).
  • tryAddRoot: with a partially reused program, source-of-project-reference redirect files can lose their fileIncludeReasons keying and buildinfo emit crashed (Cannot read properties of undefined (reading 'some')). A file without include reasons can't be a RootFile, so skipping it is the correct result.
  • proxyCreateProgram: key volar's caches per configFilePath.

Tests

New build-watch-mode.test.ts + a two-project fixture (composite emitter + noEmit/incremental consumer importing the emitter's source through a project-reference redirect — the exact shape that crashed). Spawns --build --watch, asserts diagnostics appear and clear across iterations against a reused program, no crash. Existing build-mode tests still pass (6/6 in ts-extensionless-app); the 5 failing language-server tests in package-test-core fail identically on main (pre-existing, CLI path untouched).

🤖 Generated with Claude Code

NullVoxPopuli and others added 2 commits July 8, 2026 19:42
tsc -b --watch reconstructs each project's Program from scratch on every
file change: the solution builder unconditionally releases built programs
(afterProgramDone -> releaseProgram()), its compiler host has no
SourceFile cache across iterations, and volar's proxyCreateProgram keeps
its language/snapshot/resolution caches in a single slot that any
multi-project solution thrashes. For a 19,286-file project a one-line
change cost 30s (Parse 7.3s, ResolveModule 7.2s, Check 0.14s); with these
patches the same iteration costs ~2s, identical to plain --watch.

Three source patches (same playbook as the existing proxyCreateProgram /
tryGetJSExtensionForFile patches, loud errors if dep shapes change):
- afterProgramDone: retain programs in watch mode so oldProgram reuse
  engages (~+1.4GB RSS on the 19k-file project).
- setGetSourceFileAsHashVersioned: version-validated SourceFile cache,
  scoped per project (redirect files must not be shared across programs).
- tryAddRoot: with a partially reused program, source-of-project-reference
  redirect files can lose their include-reasons keying during buildinfo
  emit; files without reasons cannot be roots, so skip them.
- proxyCreateProgram: key volar's caches per configFilePath.

Tracking: #1199, upstream volarjs/volar.js#314

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NullVoxPopuli NullVoxPopuli added the bug Something isn't working label Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ember-tsc --build --watch is ~15× slower per iteration than plain --watch: every file change re-parses and re-resolves the entire project

1 participant