Skip to content

Auto-import warm-up restarts from scratch on every keystroke while typing #65410

Open

Description

@hood

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.
  • DidChangeFile cancels 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.

Metadata

Metadata

Labels

Needs InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions