Skip to content

Introduce fast path for program updates in language service #879

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 20, 2025

Conversation

ahejlsberg
Copy link
Member

This PR introduces a fast path for program updates in the language service. When only a single file has been modified and none of the changes impact the program's structure, we create a new program by just cloning the existing program and replacing the single file. This dramatically improves responsiveness in the typical path of typing in a file.

@ahejlsberg ahejlsberg requested review from Copilot, andrewbranch, DanielRosenwasser and jakebailey and removed request for Copilot May 18, 2025 22:28
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds a fast-path for program updates by tracking when exactly one file is modified and reusing the existing program to improve responsiveness.

  • Track a single dirty file (dirtyFilePath) and version in Project
  • Conditionally choose between a full rebuild or a clone-and-replace fast path in updateProgram
  • Add NewProgramWithChangedFile (and helpers) to perform the fast-path clone in the compiler

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
internal/project/project.go Introduce dirtyFilePath, update locking in markFileAsDirty/markAsDirty, and add fast-path branch in updateProgram
internal/compiler/program.go Refactor checker-pool init, add NewProgramWithChangedFile, initCheckerPool, and comparison helpers for file replacement
Comments suppressed due to low confidence (1)

internal/project/project.go:471

  • Add unit tests covering the fast path in updateProgram, verifying that NewProgramWithChangedFile is invoked when only one file is dirty and a full rebuild is skipped.
if p.program == nil || p.dirtyFilePath == "" {

defer p.dirtyStateMu.Unlock()
if !p.dirty {
p.dirty = true
p.dirtyFilePath = path
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may need to be a Set or something; file watching can cause this to be overwritten with subsequent updates, or even open files on branch switches or similar.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but the minute anything else changes, we clear out p.dirtyFilePath and updateGraph will then completely re-create the program.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see what you mean in updateProgram; you're right that it's safe. I guess we don't need to be super optimal (though I think it might just be a couple lines to keep track of multiple dirty files).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this temporary? because we have this https://github.com/microsoft/TypeScript/blob/main/src/compiler/program.ts#L2458 in strada thats much more resilient to multiple file changes (can happen as part of open and close when navigating source code)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But as a trade-off, typing performance in Strada is extremely poor compared to what it could be. We definitely need new optimizations for the 99 percentile case of typing in a single file. Program construction shouldn’t have to loop over every source file to determine what’s changed when the project infrastructure already knows what changed.

@andrewbranch andrewbranch requested a review from sheetalkamat May 19, 2025 14:37
if p.program == nil || p.dirtyFilePath == "" {
rootFileNames := p.GetRootFileNames()
compilerOptions := p.compilerOptions
p.program = compiler.NewProgram(compiler.ProgramOptions{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we somehow propagate out whether the old program was cloned or not, we can use that to skip reevaluating the file watchers at the end of updateGraph too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now in my latest commit.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
@ahejlsberg
Copy link
Member Author

Just as an FYI, with this PR, diagnostics requests in checker.ts (a 50K line file in the old TS code base) take about 60ms following a change in the file. In that time we do full parsing, binding, and checking of the file. With that level of performance it is clear that we no longer need the incremental parse logic we had in the old compiler.

@ahejlsberg ahejlsberg added this pull request to the merge queue May 20, 2025
Merged via the queue into main with commit a775ef4 May 20, 2025
23 checks passed
}

func (p *Project) markAsDirty() {
p.dirtyStateMu.Lock()
defer p.dirtyStateMu.Unlock()
if !p.dirty {
p.dirty = true
p.dirtyFilePath = ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to reset p.dirtyFilePath unconditionally here. Suppose we call p.markFileAsDirty() and it sets p.dirtyFilePath to some path, and then right after we call p.markAsDirty(): we won't reset p.dirtyFilePath because p.dirty is already true.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

@jakebailey jakebailey deleted the program-update-fast-path branch June 2, 2025 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants