perf(typescript-language): reuse one program per project instead of per file - #3257
Draft
JoshuaKGoldberg wants to merge 2 commits into
Draft
perf(typescript-language): reuse one program per project instead of per file#3257JoshuaKGoldberg wants to merge 2 commits into
JoshuaKGoldberg wants to merge 2 commits into
Conversation
…er file Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for flint-fyi ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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.
SASSAFRAS
PR Checklist
status: accepting prsOverview
createFileasked theProjectServicefor a project and a program once per file: anopenClientFile, agetDefaultProjectForFile, a forcedgetLanguageService(true).getProgram(), and a matchingcloseClientFileon dispose, 1024 times over for 1024 files that all belong to the sametsconfig.json.The file factory now remembers, per
tsconfig.json, the project that owns it. A file is served out of an already-open project — with noopenClientFileat all — when both hold:tsconfig.jsonabove the file is the one that project was created for, sotsserverwould resolve the file to that project anyway (findConfigPathwalks up from the file's directory, memoized per directory, the same rule asgetConfigFileNameForFile).Project#containsScriptInfo's test for the file belonging to the project.Anything else — a file under a different
tsconfig.json, a file notsconfig.jsoncovers (theallowDefaultProject/inferred-project path), a file the project's program doesn't have, or a path opened earlier whose text may since have changed — falls through to the original open-and-resolve path. The program is always re-fetched throughgetLanguageService(true), so a reused project still hands out a synchronized program.Symbol.disposecloses only the files it opened.For the 1024-file fixture this is 1
openClientFileand 1closeClientFileinstead of 1024 of each.What the profile actually says
Worth recording, since it differs from the issue's analysis. With
ts.serverinstrumented onmainat 1024 files × 1 rule:ProjectService#openClientFile: 1024 calls, 876 ms — of whichtryFindDefaultConfiguredProjectAndLoadAncestorsForOpenScriptInfois 669 ms,printProjects196 ms,cleanupProjectsAndScriptInfos79 ms — plus 129 ms ofcloseClientFile.Project#getLanguageService(true)andLanguageService#getProgram(): 1024 calls each, ~0 ms total.ConfiguredProject#updateGraph: 2 calls total, not 1024.So the per-file forced synchronization the issue targets was already free —
updateProjectIfDirtyandsynchronizeHostDataboth short-circuit on an unchanged project version. All of the waste was the per-file open/close bookkeeping around it, which is what this removes.That also answers the issue's open question about superlinearity. The marginal cost per added file at 1 rule climbs on
main(1.18 ms/file going 2 → 256, 1.29 ms/file going 256 → 1024) and falls on this branch (0.96 ms/file, then 0.81 ms/file). The growth was not repeated graph updates; it wasScriptInfobookkeeping,realpathcanonicalization, and the per-open project walks getting more expensive as more files were open.Overlap with #3256: ~196 ms of the ~420 ms saved here is
printProjectslogging insideopenClientFile/closeClientFile, which #3256 removes by another route. The two fixes overlap and are not additive.Measurements
Cold cache (
--cache-ignore --skip-formatting --skip-language-reports),hyperfine -r 8, back to back on the same machine:Warm cache, same runs:
Findings are byte-identical to
mainon the 2-file, 256-file, and 1024-file fixtures and on Flint's own source (28 reports, plus the same language reports in the same order).Not covered
tsconfig.json, so a project reached through project references or a solution-style config — wheretsserver's chosen config isn't the closest one — keeps opening every file. That direction fails safe: it only loses the speedup.jsconfig.jsonisn't part of the lookup, so ajsconfig.json-only project keeps the old per-file path.readdirandvisitDirectoryfor the config's file glob,readFileUtf8, parse), which is untouched here.