You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
For 1024 files that is 1024 openClientFile calls, 1024 matching closeClientFile calls from the Symbol.dispose, and 1024 sync-then-getProgram cycles — even though in a single-tsconfig project every one of those files resolves to the same project and the same program. The true passed to getLanguageService forces a project synchronization, and we ask for it immediately after having dirtied the project by opening a new file.
We already compute the information needed to avoid this: orderTypeScriptFilePaths groups files by owning tsconfig.json and orders them so same-project files are adjacent, specifically to reduce project thrash. The next step is to hoist the project/program lookup to the project group rather than repeat it per file, and to open the group's files before asking for a program instead of interleaving the two.
In the 1024-file × 272-rule profile the ProjectService layer accounts for roughly 290 ms of 2959 ms: @typescript-eslint/project-service 51 ms, ProjectService bookkeeping inside typescript.js (project lookup and ScriptInfo management) ~120 ms, and realpath 120 ms from ScriptInfo path canonicalization across the pnpm symlink farm.
There is weak evidence the per-file cost grows with file count. Measuring at one rule, so program construction dominates and rule traversal does not:
files
wall time
marginal cost per added file
2
843 ms
—
256
1.030 s
0.74 ms
1024
1.929 s
1.17 ms
That 1.6× worsening is what per-file graph updates would look like, but I have not proven the mechanism and three points across 1024 files is not enough range to be confident. Enabling the 8192-file case in testCases.ts and counting updateGraph calls would settle whether this is genuinely superlinear or just cache effects.
Additional Info
This is deliberately not a proposal to stop using ProjectService. We considered the alternatives and none are worth switching to:
ts.createProgram is cheaper for one-shot runs, and is what typescript-eslint used for years — but it means owning tsconfig discovery and file-to-project mapping ourselves, which is exactly the problem they moved to ProjectService to escape, and exactly what 🚀 Feature: Config file projects support #64 needs.
createIncrementalProgram/.tsbuildinfo is largely redundant with our own cache, which skips work at a coarser granularity.
createWatchProgram suits --watch and wastes setup on one-shot runs.
Rolling our own LanguageServiceHost is interesting mainly because we already depend on Volar for .vue/.astro/.svelte and could collapse the two code paths in language.ts — but that is an architecture argument, not a performance one.
Related: #3251 is the discarded-log-message waste in the same service, which is independent of this. Worth re-profiling after #3249 lands, since removing ~540 ms of redundant traversal will change what dominates here.
Feature Request Checklist
Overview
createFileasks theProjectServicefor a project and a program once per file:For 1024 files that is 1024
openClientFilecalls, 1024 matchingcloseClientFilecalls from theSymbol.dispose, and 1024 sync-then-getProgramcycles — even though in a single-tsconfig project every one of those files resolves to the same project and the same program. Thetruepassed togetLanguageServiceforces a project synchronization, and we ask for it immediately after having dirtied the project by opening a new file.We already compute the information needed to avoid this:
orderTypeScriptFilePathsgroups files by owningtsconfig.jsonand orders them so same-project files are adjacent, specifically to reduce project thrash. The next step is to hoist the project/program lookup to the project group rather than repeat it per file, and to open the group's files before asking for a program instead of interleaving the two.In the 1024-file × 272-rule profile the
ProjectServicelayer accounts for roughly 290 ms of 2959 ms:@typescript-eslint/project-service51 ms, ProjectService bookkeeping insidetypescript.js(project lookup andScriptInfomanagement) ~120 ms, andrealpath120 ms fromScriptInfopath canonicalization across the pnpm symlink farm.There is weak evidence the per-file cost grows with file count. Measuring at one rule, so program construction dominates and rule traversal does not:
That 1.6× worsening is what per-file graph updates would look like, but I have not proven the mechanism and three points across 1024 files is not enough range to be confident. Enabling the 8192-file case in
testCases.tsand countingupdateGraphcalls would settle whether this is genuinely superlinear or just cache effects.Additional Info
This is deliberately not a proposal to stop using
ProjectService. We considered the alternatives and none are worth switching to:ts.createProgramis cheaper for one-shot runs, and is what typescript-eslint used for years — but it means owning tsconfig discovery and file-to-project mapping ourselves, which is exactly the problem they moved toProjectServiceto escape, and exactly what 🚀 Feature: Config file projects support #64 needs.createIncrementalProgram/.tsbuildinfois largely redundant with our own cache, which skips work at a coarser granularity.createWatchProgramsuits--watchand wastes setup on one-shot runs.LanguageServiceHostis interesting mainly because we already depend on Volar for.vue/.astro/.svelteand could collapse the two code paths inlanguage.ts— but that is an architecture argument, not a performance one.Related: #3251 is the discarded-log-message waste in the same service, which is independent of this. Worth re-profiling after #3249 lands, since removing ~540 ms of redundant traversal will change what dominates here.
❤️🔥