Repository navigation
Description
Acknowledgement
- I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion.
Comment
Summary: while we type, the language server keeps rebuilding the auto-import index in the background and throwing the work away. Every keystroke cancels the warm-up and starts a new one from scratch, which re-reads and re-parses node_modules each time. By typing faster than one full build takes (>~400 ms on a reasonably sized app), it never finishes and just keeps burning a couple of cores.
Where (main @ 1623a19)
Session.warmAutoImportCache(project/session.go:2080) starts building the registry after every single-file snapshot change.DidChangeFilecancels it right away on the next keystroke (project/session.go:378).- Finished packages live in
extractionCache, which is local to one build (ls/autoimport/registry.go:921). Cancelling a build drops all of them, and a "big" package (e.g.lodash/d3/react types) can take longer than the gap between keystrokes on its own, so it never gets done.
It hits whenever we type without triggering completions: comments, strings, JSX text, clients that don't ask for completions on every keystroke, and the first seconds after opening a project. A completion request builds the index synchronously and stops the waste, but it can get cancelled the same way.
Measured (Apple M1 Pro, on recharts's src/cartesian/Line.tsx file: 1,093 lines in a 658-file repo; 120 keystrokes at 150 ms each, pulling diagnostics + semantic tokens after every keystroke)
main (7 runs) |
with the fix below (3 runs) | |
|---|---|---|
| process CPU over 18 s of typing | 42.6–44.8 s | 13.5–14.0 s (−68%) |
| 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 |
A CPU profile of a semantic-tokens-only session (on f7973164a, same code in these files) shows 10.3 s of ReadFile syscalls in a 6.9 s run, nearly all from autoImportBuilderFS re-reading package files 🫠.
Possible fix (already in my proposal branch: hood/TypeScript perf/autoimport-partial-extraction, one file + tests)
Keep per-package extraction results on the registry, shared from one clone to the next, so a cancelled build still leaves its finished packages for the next attempt:
- a package extraction that has already started finishes even if the build gets cancelled, concurrent builds wait on the in-flight extraction instead of starting it again, and new packages still aren't started after cancellation.
- the kept results are dropped as soon as any file under
node_modules, or under a kept package's own directory, changes (workspace packages), when the build preferences or project-reference outputs change, and once a build completes.
On the same session, the completion lists are identical to main for 13 different prefixes: 21,610 items, 6,335 of them auto-imports. Completion-heavy typing (completion + diagnostics + semantic tokens on every keystroke) stays basically the same at ~17.9 s CPU on both. Sanity checked with tests, and the full Go suite passes. Note that I also added new tests to cover reusing packages extracted before a cancellation and re-extracting after a node_modules change.
Happy to open a PR if this approach sounds right to you, or change it if you'd rather handle it differently, e.g. start the warm-up only after an idle pause.
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.
Activity