You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Keep extracted auto-import packages across cancelled builds - #65412
While typing, the auto-import warm-up gets cancelled on every keystroke and used to throw away every package it had already extracted, so it kept re-reading and re-parsing node_modules from scratch and never finished if you type faster than one full build. This keeps per-package extraction results on the registry, shared from one clone to the next, so a cancelled build leaves its finished packages for the next one:
a package extraction that has started runs to completion even if the build gets cancelled, and concurrent builds wait on the in-flight one instead of starting it again (single-flight per realpath + package name + directory-search flag). New packages still aren't started after cancellation.
the kept results get dropped as soon as anything under node_modules changes, any file is created or deleted, a package.json changes, bucket build preferences or project-reference outputs change, and once a build completes, so memory goes back to what it was once the index is ready.
other edits are logged per snapshot generation. Each kept extraction remembers the generation that produced it and every file it read (entrypoints, alias-resolved files, project-reference sources), and it's only reused if none of those files, nor anything under the package dir, changed between the two generations. That also covers a cancelled build finishing a package after an edit already landed.
recharts src/cartesian/Line.tsx, 120 keystrokes at 150 ms, diagnostics + semantic tokens per key
main (7 runs)
this PR (3 runs)
process CPU over 18 s of typing
42.6–44.8 s
13.5–14.0 s (−68%), 13.7–13.8 s after review fixes
first completion after typing stops
217–257 ms (index never got built)
11–13 ms (index ready)
textDocument/diagnostic median
53–55 ms
47.5–50 ms
completion-heavy typing (completion + diag + semantic tokens per key), CPU
new TestCancelledBuildKeepsExtractedPackages: a build cancelled mid-extraction doesn't extract that package again on the next build (fails on main), and a node_modules change still re-extracts it.
new TestCancelledBuildTracksExtractionDependencies: a workspace package re-exporting ../shared gets re-extracted when shared changes after a cancelled build.
new TestPartialExtractionsInvalidation: deterministic interleavings (older build publishing after an edit, edits outside the package root, unrelated edits, sibling snapshots, replacement set after a prefs change).
completion lists are identical to main on the same session across 13 prefixes: 21,610 items, 6,335 of them auto-imports.
go test ./..., npx hereby lint and npx hereby check:format pass.
AI disclosure: I polished my solution, my breakdown of it and -most importantly- did most of the measurements with help from an AI agent. I've gone 2x through everything and I'll handle any follow-up myself ofc.
The auto-import warm-up is cancelled on every keystroke and used to drop
every package it had already extracted, so while typing it re-read and
re-parsed node_modules over and over without ever finishing. Started
extractions now run to completion and are shared with later builds until
node_modules, a kept package's directory or the build inputs change.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Active cache invalidation gaps can reuse stale exports after workspace edits, while cancelled warm-ups can repeatedly restart extraction because compatible cache replacements are not retained.
Addresses #65410 by retaining package extraction work across cancelled auto-import builds, reducing repeated parsing while typing in the language server.
Changes:
Shares completed and in-flight package extractions across registry clones.
Adds cache invalidation for file and build-input changes, plus cleanup after successful builds.
Adds regression tests for reuse after cancellation and invalidation after node_modules changes.
File
Description
tsc/internal/ls/autoimport/registry.go
Adds shared extraction caching, single-flight coordination, and invalidation.
tsc/internal/ls/autoimport/registry_test.go
Tests cancelled-build reuse and re-extraction after package changes.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Critical invalidation gaps can reuse stale auto-import exports after filesystem changes, while cancellation handling and repeated validation also risk obsolete work and excessive locking.
The reason will be displayed to describe this comment to others. Learn more.
good bot, yep. RegistryChange now has an InvalidateAll flag, Snapshot.Clone passes it through, and it drops the whole partial extraction cache. fwiw completed buckets have the same gap on main since the flag never reached the registry before, I kept that out of this PR to keep it scoped.
The reason will be displayed to describe this comment to others. Learn more.
nice catch clanker, that one's sneaky. When processFileChanges clears a bulk batch because nothing overlaps the snapshot cache, it now marks the summary, and the registry gets that as InvalidateAll, so the partial cache goes away. Simpler than matching every dependency against the batch, and bulk batches are rare enough that losing the reuse there kinda doesn't matter. Regression test: bulk node_modules changes discard extracted packages (1001 watch events, fails on the previous commit).
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Cache invalidation gaps can reuse stale auto-import exports after manifest changes or directory deletions, while unbounded change history and detached cancellation can cause memory growth and delay session shutdown.
Add concurrent single-flight and waiter-cancellation tests
tsc/internal/ls/autoimport/registry.go:475
The new single-flight path has no concurrent test; the current cancellation subtest cancels before calling extract, so it never exercises a blocked waiter. Add deterministic testing/synctest coverage that blocks the owner, starts another caller for the same key, and verifies both receive the same result from one extraction callback. Also cancel a caller while it is actually waiting and assert that it returns nil without starting another extraction.
On a case-insensitive filesystem, a watch event for packages/shared/Package.json refers to the same manifest the resolver reads as package.json, but this comparison does not invalidate it. If cached workspace package a re-exports ../shared, changing that manifest's types field can therefore reuse stale exports after cancellation: the manifest is neither a GetSourceFile dependency nor inside a's directory. Compare the canonical basename using the supplied case sensitivity.
Partial-cache change history grows without bound
tsc/internal/ls/autoimport/registry.go:577
Once a cancelled build leaves partial results for project B, typing in an already-prepared project A can skip warm-up but still append a change record on every snapshot. These records are never pruned, so memory grows with the editing session and returning to B makes each retained package scan the accumulated history under p.mu. Bound the history by rotating the partial cache after a limit, or prune it while preserving generations still reachable by snapshots and pending extractions.
Warm-up tasks can block session shutdown
tsc/internal/ls/autoimport/registry.go:1644
context.WithoutCancel also removes session-shutdown cancellation, while Session.Close() waits in backgroundQueue.Close() for the warm-up task. On large workspaces, shutdown can therefore wait for all already-started package extractions to finish. Detach from build cancellation while retaining a session-lifetime cancellation signal, and remove aborted pending entries rather than caching incomplete results.
Directory deletions can leave stale partial-cache exports
tsc/internal/project/snapshot.go:682
After a cancelled extraction of packages/a re-exporting ../shared/index, a single deletion event for packages/shared is discarded by expandAndFilterWatchEvents because its auto-import-only files are not in the snapshot cache. DroppedWatchChanges is only set for bulk events, so the retry can reuse exports from the deleted dependency. Preserve partial-cache invalidation before ordinary directory-event filtering, and add a directory-deletion variant of TestCancelledBuildTracksExtractionDependencies.
ok clanker, went through the 5 "previously missed" ones from the last review:
single-flight / waiter cancellation tests: added TestPartialExtractionsConcurrency on testing/synctest. The owner gets blocked, a second caller for the same key gets the same result from one extraction, and a caller cancelled while waiting returns nil without starting another one.
Package.json on case-insensitive fs: the basename now gets compared through the case sensitivity.
unbounded change history: capped at 256 records, after that the next clone with changes rotates the partial cache. Typing in a prepared project with a leftover partial cache can't grow it forever anymore.
shutdown blocked by WithoutCancel: the warm-up context now carries the session's background context as the extraction lifetime, so started extractions survive a keystroke cancel but stop on Session.Close. If an extraction gets aborted that way, its pending entry gets removed instead of caching a partial result, and any waiters retry or bail on their own ctx.
directory deletes dropped by expandAndFilterWatchEvents: dropping a delete there now sets DroppedWatchChanges too, same path as the bulk case. Test: deleted dependency directory in TestCancelledBuildTracksExtractionDependencies, fails without the fix.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Cache ownership and cancellation interactions require human assessment because they can retain obsolete snapshot state and allow duplicate package extraction across builds.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #65410
While typing, the auto-import warm-up gets cancelled on every keystroke and used to throw away every package it had already extracted, so it kept re-reading and re-parsing
node_modulesfrom scratch and never finished if you type faster than one full build. This keeps per-package extraction results on the registry, shared from one clone to the next, so a cancelled build leaves its finished packages for the next one:node_moduleschanges, any file is created or deleted, apackage.jsonchanges, bucket build preferences or project-reference outputs change, and once a build completes, so memory goes back to what it was once the index is ready.src/cartesian/Line.tsx, 120 keystrokes at 150 ms, diagnostics + semantic tokens per keymain(7 runs)textDocument/diagnosticmedianMeasured with this harness: https://gist.github.com/hood/506e87027c83bbec1b44c1f687273349
Tests:
TestCancelledBuildKeepsExtractedPackages: a build cancelled mid-extraction doesn't extract that package again on the next build (fails onmain), and anode_moduleschange still re-extracts it.TestCancelledBuildTracksExtractionDependencies: a workspace package re-exporting../sharedgets re-extracted whensharedchanges after a cancelled build.TestPartialExtractionsInvalidation: deterministic interleavings (older build publishing after an edit, edits outside the package root, unrelated edits, sibling snapshots, replacement set after a prefs change).mainon the same session across 13 prefixes: 21,610 items, 6,335 of them auto-imports.go test ./...,npx hereby lintandnpx hereby check:formatpass.AI disclosure: I polished my solution, my breakdown of it and -most importantly- did most of the measurements with help from an AI agent. I've gone 2x through everything and I'll handle any follow-up myself ofc.