Skip to content

⚡️ Performance: Hoist the TypeScript program per project instead of per file #3252

Description

@JoshuaKGoldberg

Feature Request Checklist

  • I am using the latest published version of all Flint packages.
  • I have searched for related issues and found none that matched my issue.

Overview

createFile asks the ProjectService for a project and a program once per file:

service.openClientFile(data.filePathAbsolute);
const scriptInfo = service.getScriptInfo(data.filePathAbsolute);
const defaultProject = service.getDefaultProjectForFile(scriptInfo.fileName, true);
const program = defaultProject.getLanguageService(true).getProgram();

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.

❤️‍🔥

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceImproving the runtime efficiency, memory, and raw speed of Flint itself.status: in discussionNot yet ready for implementation or a pull requesttype: featureNew enhancement or request 🚀

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions