Repository navigation
LSP perf optimizations - #64877
Draft
Andrew Branch (andrewbranch) wants to merge 12 commits into
Draft
LSP perf optimizations#64877Andrew Branch (andrewbranch) wants to merge 12 commits into
Andrew Branch (andrewbranch) wants to merge 12 commits into
Conversation
dirty.SyncMap.Load allocates an entry wrapper on every hit in the base map. snapshotFSBuilder reads only need the value, so add LoadClean, which returns base values without allocating when the key is not dirty. Perf (vscode, unpinned; indicative): EditImports -4.7% allocs; CreateDeleteFile -4.6% allocs, -1.9% B/op. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afcfca01-a049-4749-ad08-1c00c586f6a1
parseTask is allocated for every file reference edge, but most edges point at files that are already loaded. Moving load results into a side record and removing two unused fields shrinks parseTask from 368B to 184B. Perf (vscode, unpinned; indicative): EditImports and CreateDeleteFile -7.4% B/op, +0.3% allocs; time within noise. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afcfca01-a049-4749-ad08-1c00c586f6a1
Member
Author
|
TypeScript Bot (@typescript-bot) perf test this |
Contributor
Contributor
|
Andrew Branch (@andrewbranch) Here they are:
tscComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
lspComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
startupComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
Developer Information: |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Andrew Branch (andrewbranch)
force-pushed
the
lsp-perf
branch
from
October 19, 2026 20:56
d248609 to
8384f49
Compare
Config file-name expansion read the include tree with sequential ReadDir calls. Up to GOMAXPROCS readers now read the same directories first (honoring excludes and depth), then the unchanged ordered walk runs over the prefetched entries, so results and ordering are identical. The caller is the first reader and more workers start only while queued directories outnumber idle ones, so leaf scans start no goroutines. Directories under overlapping base paths are read once. Symlinked directories are left to the ordered walk, which owns realpath deduplication; each directory spelling is read at most once by the prefetch. A host panic stops the workers and is re-raised on the caller. With --singleThreaded, config expansion reads directories serially. Perf (vscode@9c4bcec0bb91, machine under load, interleaved A/B): CreateDeleteFile 374 -> 218 ms, TsconfigChange 354 -> 178 ms (p=0.002, n=6, wide variance); +1% B/op, +0.5% allocs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afcfca01-a049-4749-ad08-1c00c586f6a1
…files Also return host directory entries directly from overlayFS when no overlay directory affects them. Perf (vscode, unpinned; indicative): program-rebuild benchmarks -2-3% allocs, -2% B/op. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afcfca01-a049-4749-ad08-1c00c586f6a1
ReuseProgram clones the package.json info cache on every edit; copying a sync.Map entry-by-entry allocated per entry. Reads now hit an immutable base map without locking, new entries go to a sync.Map overlay (first writer wins), and Clone flattens both into a new base. Perf (vscode, unpinned; indicative): WatchChange -90% allocs; EditImports and EditOpenFile time not regressed (an RWMutex variant regressed EditImports +38% from read-lock contention). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afcfca01-a049-4749-ad08-1c00c586f6a1
Store ModuleID's path/specifier in a single string (only one is ever set), narrow ExportSyntax and ScriptElementKind to uint8, and reorder small fields to avoid padding. Perf (vscode, unpinned; indicative): AutoImportInitial retained memory -15.7% (19.8 -> 16.7 MB); time and allocs unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afcfca01-a049-4749-ad08-1c00c586f6a1
Perf (vscode, unpinned; indicative): TsconfigChange and CreateDeleteFile -2.1% B/op, -1.8% allocs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afcfca01-a049-4749-ad08-1c00c586f6a1
Perf (vscode, unpinned; indicative): TsconfigChange -12.8% B/op, -13.2% allocs; InitialLoad -5% allocs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afcfca01-a049-4749-ad08-1c00c586f6a1
…tring Perf (vscode, unpinned; indicative): TsconfigChange -12.1% B/op, -12.9% allocs; InitialLoad -4.8% allocs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afcfca01-a049-4749-ad08-1c00c586f6a1
…tes) Perf (vscode, unpinned; indicative): TsconfigChange -3.8% B/op; allocs unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afcfca01-a049-4749-ad08-1c00c586f6a1
… 80B/1) Perf (vscode, unpinned; indicative): TsconfigChange -4.8% allocs, -1% B/op; InitialLoad retained memory -2 MB. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afcfca01-a049-4749-ad08-1c00c586f6a1
Perf (vscode, unpinned; indicative): TsconfigChange -5.6% allocs; InitialLoad -2% allocs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: afcfca01-a049-4749-ad08-1c00c586f6a1
Andrew Branch (andrewbranch)
force-pushed
the
lsp-perf
branch
from
October 19, 2026 21:09
8384f49 to
06d2d96
Compare
Member
Author
|
TypeScript Bot (@typescript-bot) perf test |
Contributor
Contributor
|
Andrew Branch (@andrewbranch) Here they are:
tscComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
lspComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
startupComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
Developer Information: |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.

I let Copilot spin for a while on LSP performance, profiling the vscode repo. Several of these changes affect batch compilation too.
Baseline (vscode)
Heap of initial load: ~95% of retained memory is ASTs/symbols in the parse cache (core compiler).
Project-package retained structures are small; the project package's leverage is mostly transient
allocation and per-change latency.
Cumulative result (merge-base 3789ddb vs HEAD, vscode, interleaved runs, n=6)
* EditImports: 165 → 97 ms in a direct A/B of the final fix; ~ in the noisy cumulative run.
Timings were taken with load average 10–20 from other processes; B/op and allocs/op are exact.
Experiments
1.
dirty.SyncMap.LoadCleanfast path in snapshotFSBuilder — COMMITTED (0a35842; benchmarks in dd354e654af)dirty.SyncMap.Loadallocates aSyncMapEntrywrapper on every hit in the immutable base map.snapshotFSBuilder's
GetFileByPath/FileExists/GetAccessibleEntriesonly need read access, soadded
LoadCleanreturning the base value without allocation when the key is not dirty.2. parseTask hot/cold split (compiler/filesparser.go) — COMMITTED (cea07f4)
Program rebuild profile showed
parseTaskallocations prominent. Removed two dead fields and movedload-result fields to a lazily allocated
*parseTaskResult(only tasks that actually load needthem; failed-lookup/duplicate tasks don't).
parseTask368 B → 184 B.3. Concurrent directory prefetch in vfsmatch — COMMITTED (39911ff)
CPU profile of CreateDeleteFile/TsconfigChange: ~80% of
updateProgramCPU isReloadFileNamesOfParsedCommandLine→vfsmatch.matchFileNames→ sequentialos.ReadDirover the whole include tree (snapshot cachedvfs is fresh per snapshot, so it hits disk every time).
Added a prefetch phase that reads the same directories concurrently, then the original
serial walk runs unchanged using the prefetched entries (falling back to the host for anything
missing). Results/ordering/symlink-cycle semantics are unchanged.
tscCLI config parsing.4. sourceFS seen-file tracking + overlayFS no-clone — COMMITTED (039e5dd)
sourceFS.FileExistscomputed a PathKey for tracking and again for the lookup, and stored intoseenFilesunconditionally. Compute the key once,LoadbeforeStore; overlayFS stopped cloningits overlay map on read paths.
5. packagejson.InfoCache cheap Clone — COMMITTED (14e9882; RWMutex variant squashed away)
ReuseProgram(every edit / watch change) clones the package.json info cache; cloning async.Mapentry-by-entry allocated per entry (~6.6k allocs per WatchChange).But EditImports regressed +38% time (119 → 165 ms): RWMutex reader-count contention during
parallel module resolution. Found only via a cumulative merge-base A/B + bisect — lesson: every
concurrency-touching change needs a rebuild-heavy timing check.
basemap read lock-free +collections.SyncMapoverlay for new entries;Clone=maps.Clone(base)+ overlay merge. Keeps clone wins; EditImports 165 → 97 ms(faster than the original 119 ms), −2% allocs.
-raceclean.6. autoimport
Export248 → 192 B — COMMITTED (b975755)Registry retained memory is dominated by
Export(~52k exports on vscode).ModuleIDstores onestring + kind instead of two strings;
ExportSyntax,lsutil.ScriptElementKindnarrowed to uint8;fields reordered to remove padding.
7. tsoptions extension-priority sibling keys — COMMITTED (c09c9b0)
getFileNamesFromConfigSpecsbuiltChangeExtension+ canonical key for each higher/lower-prioritysibling of every wildcard file and appended a flattened group slice per file (~57% of file-name
expansion allocs). Now derives sibling keys from one canonical key (
stemKey + ext, falling back tothe old path for unknown extensions / dynamic names), iterating groups in place.
Verified with a temporary 20k-case randomized differential test against the old implementation.
8. Module resolution: resolve candidate once — COMMITTED (de88374)
resolutionCandidateFromEncodedPathsresolved the same relative specifier twice (ResolveFileandResolveDirectory) whenever the file and directory inputs were identical (always, outside dynamicfiles); both produce identical strings. Resolve once.
9. tspath leading
.//../fast path — COMMITTED (9496c1e)ResolveFile/ResolveDirectoryfor./x,../../xconcatenated then normalized (2 allocs). Nowwalks the directory up for leading dot segments and concatenates once; falls back for any other dot
segments or ascending past root. 200k-case randomized differential test (disk, drive, URL, UNC,
dynamic roots) against the old path.
10. parseTask references resolution PackageId — COMMITTED (02e334f)
parseTask/resolvedRefcarried a 64 B inlinePackageId(empty for nearly all files). Now apointer into the already-heap-allocated resolution (nil when empty). parseTask 184 → 136 B
(192 → 144 size class), no new allocations.
11. Co-allocated referenced-file include reasons — COMMITTED (570e356)
Each import allocated
FileIncludeReason(56 B) +referencedFileData(32 B) separately; both areretained by the program.
fileIncludeKind→ uint8 (48 B) and one constructor co-allocates both(80 B, 1 alloc). Net −13 lines.
12.
parallelWorkGroup.Queuewrapper closure — COMMITTED (d248609)Queuewrappedfninfunc(){ fn() }beforewg.Go. Passfndirectly.