Skip to content
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

Improve solution load performance #11591

Merged
merged 11 commits into from
Mar 6, 2025

Conversation

DustinCampbell
Copy link
Member

@DustinCampbell DustinCampbell commented Mar 5, 2025

Razor's language server does a lot of extra work to initialize that is really no longer necessary, at least in Visual Studio. This pull request is an attempt to remove a bunch of that extra work.

  • In Visual Studio, no longer initialize the misc files project with _all- of the *.razor and *.cshtml files in the workspace root path. With the IRazorProjectInfoDriver in place, I believe this to be unnecessary work. Note that I've left this behavior for VS Code for now, but we might remove it later.
  • If we do initialize the misc files project with all Razor files, add a method to IRazorProjectServer to add all of the documents to the ProjectSnapshotManager at once. Adding them one at a time results in a ton of extra asynchrony that slows solution load down as each document add hops around the thread pool.
  • When IRazorProjectService.OpenDocumentAsync(...) is called, it first attempts to add the open document to the misc files project. This is because we might learn of an open document before the project system is done loading. However, it first adds the document using a TextLoader that loads the file off disk. Then, it updates the document with the SourceText that was passed to OpenDocumentAsync. However, this causes the file to be loaded so that its contents can be compared with the new SourceText. To avoid this additional I/O, OpenDocumentAsync will now add the document to the misc files project with the SourceText rather than a TextLoader.
  • Don't create DocumentSnapshot instances unnecessarily when checking to see if a project contains a document file path.

In addition, I've refactored all of the IFileChangeDetector and IRazorFileChangeListener abstractions into the singleton, WorkspaceRootPathWatcher. There was only a single implementation of each, so they really don't have a lot of value. I've similar unified a bunch of relevant tests.

CI Build: https://dev.azure.com/dnceng/internal/_build/results?buildId=2656343&view=results
VS Test Insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/616458

The IFileChangeDetector and IRazorFileChangeListener abstractions are no longer needed. This change removes them and merges the functionality into a single WorkspaceRootPathWatcher (replacing FileChangeDetectorManager).
When the WorkspaceRootPathWatcher starts it (currently) adds all *.razor and *.cshtml files under the root path to the RazorProjectService. It does this one file path at a time, which is pretty inefficient in large solutions, requiring lots of thread hopping for updates to the ProjectSnapshotManager. This change adds a bulk method that can be called to add an array of file paths in one update.
When RazorProjectEngine.OpenDocumentAsync is called, if the document needs to be added to the misc files project, it is added with a text loader that loads from disk. However, OpenDocumentAsync already has a SourceText in hand.
RazorProjectEngine calls into the TryResolveDocumentInAnyProject extension method, but that unnecessarily realizes a DocumentSnapshot. This change introduces TryFindContainingProject, which returns the first project that contains a document file path, rather than the DocumentSnapshot itself.
The language server always starts up by loading the misc files project with every *.razor or *.cshtml file in the workspace root path. This *might* be helpful for VS Code (though I'm unsure about that), but in VS, it's clearly unnecessary overhead. The RazorProjectService will regularly receive new project information from the IRazorProjectInfoDriver, so this is really unnecessary overhead.
@DustinCampbell DustinCampbell requested a review from a team as a code owner March 5, 2025 00:55
Copy link
Member

@davidwengier davidwengier left a comment

Choose a reason for hiding this comment

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

I'd be fine with the removal of the feature flag, and just never adding every file to the project manager, but I understand if you prefer to be cautious.

I think the string check is wrong though.

@davidwengier
Copy link
Member

To expand on my above comment a bit: in VS Code we sent textDocument/open for every file, regardless of whether it's open, so things would end up in misc files anyway I think.

@ryzngard
Copy link
Contributor

ryzngard commented Mar 5, 2025

Note that I've left this behavior for VS Code for now, but we might remove it later.

I think we absolutely should do this. From my understanding have them in the misc project provides some functionality but not anything needed if it's not open (I'm still reading the PR). Definitely can be a separate PR but this would also help alleviate some of the delay in startup that @dibarbet has been looking at for vs code.

@DustinCampbell
Copy link
Member Author

Note that I've left this behavior for VS Code for now, but we might remove it later.

I think we absolutely should do this. From my understanding have them in the misc project provides some functionality but not anything needed if it's not open (I'm still reading the PR). Definitely can be a separate PR but this would also help alleviate some of the delay in startup that @dibarbet has been looking at for vs code.

I'd be delighted for somebody else to set the flag to test this out in VS Code. 😄

To expand on my above comment a bit: in VS Code we sent textDocument/open for every file, regardless of whether it's open, so things would end up in misc files anyway I think.

Why would we load every file from disk to send a textDocument/open for each? If I'm understanding the impact correctly, that seems to me like a clear performance problem.

Copy link
Contributor

@ryzngard ryzngard left a comment

Choose a reason for hiding this comment

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

Love it. Been on my backlog for a bit and glad you got to it first :)

@ryzngard
Copy link
Contributor

ryzngard commented Mar 5, 2025

Why would we load every file from disk to send a textDocument/open for each? If I'm understanding the impact correctly, that seems to me like a clear performance problem.

This is no longer true as of dotnet/vscode-csharp#7826

@davidwengier
Copy link
Member

If I'm understanding the impact correctly, that seems to me like a clear performance problem.

Needs must. It was the only way to get rzls to start generating code, to send to Roslyn, so that references to Razor components were valid.

I guess I'm forgetting how we ended up getting that to happen. @ryzngard can you remind me? Is it just a side-effect of the named pipe, and our normal background document generator kicks in?

@ryzngard
Copy link
Contributor

ryzngard commented Mar 5, 2025

Is it just a side-effect of the named pipe, and our normal background document generator kicks in?

If we're doing it at all it would be because of the background document generator. Let's get an issue filed on VS Code to make sure we include this fix there. I don't think we do the add anymore... if anything it would go through the dynamic file updates. That would also make this change safe on VS Code but it's worth testing

This fixes a bug introduced in a prior commit to properly verify file extensions when a file is renamed. To do this, I've added a couple of polyfill-style methods to a new PathUtilities class located in Microsoft.AspNetCore.Utilities.Shared. These are the same as Path.GetExtension and Path.HasExtension but they provide span-based overloads for non-.NET Core scenarios.
The logic was doing the exact opposite of what the comments stated. We should *not* add a file path to the misc files project if it is already contained in a project. Instead, files were added if they were included in a project.
@DustinCampbell DustinCampbell requested a review from a team as a code owner March 5, 2025 18:23
Currently, we're only disabling the behavior that fills the misc files project at language server initialization in VS-only. This commit makes the option more convenient to configure from the command-line to enable easier testing in VS Code. dotnet#11594 tracks that testing work and removing the behavior (and this option) completely.
@DustinCampbell DustinCampbell merged commit 2798396 into dotnet:main Mar 6, 2025
17 checks passed
@DustinCampbell DustinCampbell deleted the solution-load-perf branch March 6, 2025 21:44
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Mar 6, 2025
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.

4 participants