Repository navigation
Description
Acknowledgement
- I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion.
Comment
Summary: after every edit, the language server type-checks the edited file twice. Diagnostics check it on the dedicated diagnostics checker. Semantic tokens then re-infer most of the same types on a fresh query checker, because reclassifyByType calls GetTypeAtLocation for every variable, property and parameter (ls/semantictokens.go:421). On a real component file that's ~40 ms of extra CPU per semantic tokens request. I'd like to know whether reusing the diagnostics checker, once it has finished with the file, fits the checker pool design.
Measurements (main @ f797316, Apple M1 Pro, recharts src/cartesian/Line.tsx: 1,093 lines in a 658-file program; 120 keystrokes, 3 runs)
| Request | median |
|---|---|
textDocument/diagnostic |
52.6–53.2 ms |
textDocument/semanticTokens/full on a cold query checker |
41.3–41.7 ms |
| the same request again on the now-warm query checker | ~2 ms |
About 28 ms of each semantic tokens request is first-touch type inference: destructured props, const initializers that are calls, large annotation types. It's all local to the file, so I couldn't find a per-token shortcut that keeps the classification identical.
Why reuse would usually apply: VS Code pulls diagnostics as soon as a document changes (diagnosticPullOptions.onChange: true in packages/vscode-typescript/src/client.ts). It requests semantic tokens at least 300 ms after a content change (REQUEST_MIN_DELAY = 300 in vscode's documentSemanticTokens.ts). So by the time semantic tokens arrive, the diagnostics checker has normally finished with the file.
What I tried (branch: hood/TypeScript perf/semantic-tokens, +116/−5)
The pool lends the diagnostics checker to a semantic tokens request only when all of these hold:
- the checker is idle (non-blocking
diagSem); - it has fully checked the file (type check and unused check, not cancelled);
- no diagnostics request for that file is still in flight.
The in-flight guard is needed because a diagnostics request uses the checker three times (semantic, suggestion and declaration diagnostics) and releases it in between. In every other case the request gets a query checker exactly as today, and it never waits. So queries never run on the diagnostics checker before or during a file's diagnostics, and the order in which the checker walks the file is unchanged. That ordering is what prevents bugs like #51661, where a query asked for a type before the normal walk and the result was an order-dependent false implicit-any in the editor.
recharts Line.tsx, 120 keystrokes, 3 runs |
main |
branch |
|---|---|---|
| Diagnostics, then semantic tokens: semantic tokens median | 41.3–41.7 ms | 2.88–2.91 ms |
| Same: diagnostics median | 52.6–53.2 ms | 52.4–52.9 ms |
| Same: process CPU | 14.5–14.7 s | 8.6 s (−41%) |
| Both requests sent at once: keystroke median / CPU | 53–55 ms / 14.7 s | 54 ms / 14.7 s (falls back, no change) |
Correctness so far:
- Semantic tokens and diagnostics for
Line.tsxand three other open files are byte-identical tomainacross 600 responses, both sequential and concurrent. That includes TS2883 errors that print types, unused-variable suggestions, and the transient errors you get mid-typing. hereby test,lintandcheck:formatpass.- The new pool test fails if the in-flight guard is removed.
The part I'm not sure about: after the file is checked, the classification queries still create a few types and symbols on the diagnostics checker (~6 types and 32 symbols per request here). Most likely these are throwaway re-checks of expressions. I saw no change in any diagnostics, but I can't prove that holds for every program. If keeping that checker completely free of query work is a hard requirement, this approach doesn't fit, and I'd be glad to hear whether another direction would be welcome.
I worked on this with help from an AI coding assistant (Claude Code). I've reviewed the change, and I'll handle any follow-up myself.
Activity
RyanCavanaugh commentedon Nov 9, 2026
This isn't safe in principle due to order sensitivity issues in the checker combined with cross-file operations.
A minimal example looks like this:
Testing this against the provided PR:
Once we get all the checker order sensitivities removed (this is a long work in progress) we can start doing much more aggressive re-use, but not until then.