Skip to content

Commit 2798396

Browse files
Improve solution load performance (#11591)
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
2 parents 26f8f49 + 9951f45 commit 2798396

File tree

29 files changed

+532
-579
lines changed

29 files changed

+532
-579
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultLanguageServerFeatureOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,6 @@ internal class DefaultLanguageServerFeatureOptions : LanguageServerFeatureOption
4242
public override bool SupportsSoftSelectionInCompletion => true;
4343

4444
public override bool UseVsCodeCompletionTriggerCharacters => false;
45+
46+
public override bool DoNotInitializeMiscFilesProjectFromWorkspace => false;
4547
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,8 @@ public static void AddDocumentManagementServices(this IServiceCollection service
214214
services.AddSingleton<IRazorStartupService, OpenDocumentGenerator>();
215215
services.AddSingleton<IDocumentMappingService, LspDocumentMappingService>();
216216
services.AddSingleton<IEditMappingService, LspEditMappingService>();
217-
services.AddSingleton<RazorFileChangeDetectorManager>();
218-
services.AddSingleton<IOnInitialized>(sp => sp.GetRequiredService<RazorFileChangeDetectorManager>());
219-
220-
services.AddSingleton<IRazorFileChangeListener, RazorFileSynchronizer>();
221-
services.AddSingleton<IFileChangeDetector, RazorFileChangeDetector>();
217+
services.AddSingleton<WorkspaceRootPathWatcher>();
218+
services.AddSingleton<IOnInitialized>(sp => sp.GetRequiredService<WorkspaceRootPathWatcher>());
222219

223220
// Document processed listeners
224221
if (!featureOptions.SingleServerSupport)

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/ConfigurableLanguageServerFeatureOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal class ConfigurableLanguageServerFeatureOptions : LanguageServerFeatureO
2323
private readonly bool? _useRazorCohostServer;
2424
private readonly bool? _forceRuntimeCodeGeneration;
2525
private readonly bool? _useNewFormattingEngine;
26+
private readonly bool? _doNotInitializeMiscFilesProjectFromWorkspace;
2627

2728
public override bool SupportsFileManipulation => _supportsFileManipulation ?? _defaults.SupportsFileManipulation;
2829
public override string CSharpVirtualDocumentSuffix => _csharpVirtualDocumentSuffix ?? DefaultLanguageServerFeatureOptions.DefaultCSharpVirtualDocumentSuffix;
@@ -40,6 +41,12 @@ internal class ConfigurableLanguageServerFeatureOptions : LanguageServerFeatureO
4041
public override bool SupportsSoftSelectionInCompletion => false;
4142
public override bool UseVsCodeCompletionTriggerCharacters => true;
4243

44+
// Note: This option is defined in the negative because the default behavior should be to add documents to misc files project
45+
// when the language server is initialized. Adding the option at the command-line should disable that behavior.
46+
//
47+
// This is a temporary option and should be removed as part of https://github.com/dotnet/razor/issues/11594.
48+
public override bool DoNotInitializeMiscFilesProjectFromWorkspace => _doNotInitializeMiscFilesProjectFromWorkspace ?? _defaults.DoNotInitializeMiscFilesProjectFromWorkspace;
49+
4350
public ConfigurableLanguageServerFeatureOptions(string[] args)
4451
{
4552
for (var i = 0; i < args.Length; i++)
@@ -62,6 +69,7 @@ public ConfigurableLanguageServerFeatureOptions(string[] args)
6269
TryProcessBoolOption(nameof(UseRazorCohostServer), ref _useRazorCohostServer, option, args, i);
6370
TryProcessBoolOption(nameof(ForceRuntimeCodeGeneration), ref _forceRuntimeCodeGeneration, option, args, i);
6471
TryProcessBoolOption(nameof(UseNewFormattingEngine), ref _useNewFormattingEngine, option, args, i);
72+
TryProcessBoolOption(nameof(DoNotInitializeMiscFilesProjectFromWorkspace), ref _doNotInitializeMiscFilesProjectFromWorkspace, option, args, i);
6573
}
6674
}
6775

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IFileChangeDetector.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ILoggerExtensions.cs

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/IRazorFileChangeListener.cs

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IProjectSnapshotManagerExtensions.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics.CodeAnalysis;
66
using System.Linq;
77
using Microsoft.AspNetCore.Razor.PooledObjects;
8+
using Microsoft.AspNetCore.Razor.ProjectSystem;
89
using Microsoft.AspNetCore.Razor.Utilities;
910
using Microsoft.CodeAnalysis.Razor;
1011
using Microsoft.CodeAnalysis.Razor.Logging;
@@ -26,7 +27,7 @@ public static ImmutableArray<ProjectSnapshot> FindPotentialProjects(this Project
2627
foreach (var project in projectManager.GetProjects())
2728
{
2829
// Always exclude the miscellaneous project.
29-
if (project.FilePath == MiscFilesProject.FilePath)
30+
if (project.IsMiscellaneousProject())
3031
{
3132
continue;
3233
}
@@ -69,6 +70,21 @@ public static bool TryResolveAllProjects(
6970
return projects.Length > 0;
7071
}
7172

73+
public static bool TryFindContainingProject(this ProjectSnapshotManager projectManager, string documentFilePath, out ProjectKey projectKey)
74+
{
75+
foreach (var project in projectManager.GetProjects())
76+
{
77+
if (project.ContainsDocument(documentFilePath))
78+
{
79+
projectKey = project.Key;
80+
return true;
81+
}
82+
}
83+
84+
projectKey = default;
85+
return false;
86+
}
87+
7288
public static bool TryResolveDocumentInAnyProject(
7389
this ProjectSnapshotManager projectManager,
7490
string documentFilePath,

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IRazorProjectService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
1313

1414
internal interface IRazorProjectService
1515
{
16+
Task AddDocumentsToMiscProjectAsync(ImmutableArray<string> filePaths, CancellationToken cancellationToken);
1617
Task AddDocumentToMiscProjectAsync(string filePath, CancellationToken cancellationToken);
1718
Task OpenDocumentAsync(string filePath, SourceText sourceText, CancellationToken cancellationToken);
1819
Task UpdateDocumentAsync(string filePath, SourceText sourceText, CancellationToken cancellationToken);

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -139,41 +139,89 @@ await AddOrUpdateProjectCoreAsync(
139139
.ConfigureAwait(false);
140140
}
141141

142+
public async Task AddDocumentsToMiscProjectAsync(ImmutableArray<string> filePaths, CancellationToken cancellationToken)
143+
{
144+
await WaitForInitializationAsync().ConfigureAwait(false);
145+
146+
await _projectManager
147+
.UpdateAsync(
148+
(updater, cancellationToken) =>
149+
{
150+
var projects = _projectManager.GetProjects();
151+
152+
// For each file, check to see if it's already in a project.
153+
// If it is, we don't want to add it to the misc project.
154+
foreach (var filePath in filePaths)
155+
{
156+
var add = true;
157+
158+
foreach (var project in projects)
159+
{
160+
if (project.ContainsDocument(filePath))
161+
{
162+
// The file is already in a project, so we shouldn't add it to the misc project.
163+
add = false;
164+
break;
165+
}
166+
}
167+
168+
if (cancellationToken.IsCancellationRequested)
169+
{
170+
break;
171+
}
172+
173+
if (add)
174+
{
175+
AddDocumentToMiscProjectCore(updater, filePath);
176+
}
177+
}
178+
},
179+
state: cancellationToken,
180+
cancellationToken)
181+
.ConfigureAwait(false);
182+
}
183+
142184
public async Task AddDocumentToMiscProjectAsync(string filePath, CancellationToken cancellationToken)
143185
{
144186
await WaitForInitializationAsync().ConfigureAwait(false);
145187

146188
await _projectManager
147189
.UpdateAsync(
148-
updater: AddDocumentToMiscProjectCore,
149-
state: filePath,
190+
updater =>
191+
{
192+
if (!_projectManager.TryFindContainingProject(filePath, out _))
193+
{
194+
AddDocumentToMiscProjectCore(updater, filePath);
195+
}
196+
},
150197
cancellationToken)
151198
.ConfigureAwait(false);
152199
}
153200

154-
private void AddDocumentToMiscProjectCore(ProjectSnapshotManager.Updater updater, string filePath)
201+
private void AddDocumentToMiscProjectCore(ProjectSnapshotManager.Updater updater, string filePath, SourceText? sourceText = null)
155202
{
156-
var textDocumentPath = FilePathNormalizer.Normalize(filePath);
157-
_logger.LogDebug($"Asked to add {textDocumentPath} to the miscellaneous files project, because we don't have project info (yet?)");
203+
Debug.Assert(
204+
!_projectManager.TryFindContainingProject(filePath, out _),
205+
$"File already belongs to a project and can't be added to the misc files project");
158206

159-
if (_projectManager.TryResolveDocumentInAnyProject(textDocumentPath, _logger, out var document))
160-
{
161-
// Already in a known project, so we don't want it in the misc files project
162-
_logger.LogDebug($"File {textDocumentPath} is already in {document.Project.Key}, so we're not adding it to the miscellaneous files project");
163-
return;
164-
}
207+
_logger.LogInformation($"Adding document '{filePath}' to miscellaneous files project.");
165208

166209
var miscFilesProject = _projectManager.GetMiscellaneousProject();
210+
var textDocumentPath = FilePathNormalizer.Normalize(filePath);
167211

168212
// Representing all of our host documents with a re-normalized target path to workaround GetRelatedDocument limitations.
169213
var normalizedTargetFilePath = textDocumentPath.Replace('/', '\\').TrimStart('\\');
170214

171215
var hostDocument = new HostDocument(textDocumentPath, normalizedTargetFilePath);
172-
var textLoader = _remoteTextLoaderFactory.Create(textDocumentPath);
173216

174-
_logger.LogInformation($"Adding document '{textDocumentPath}' to project '{miscFilesProject.Key}'.");
175-
176-
updater.AddDocument(miscFilesProject.Key, hostDocument, textLoader);
217+
if (sourceText is not null)
218+
{
219+
updater.AddDocument(miscFilesProject.Key, hostDocument, sourceText);
220+
}
221+
else
222+
{
223+
updater.AddDocument(miscFilesProject.Key, hostDocument, _remoteTextLoaderFactory.Create(textDocumentPath));
224+
}
177225
}
178226

179227
public async Task OpenDocumentAsync(string filePath, SourceText sourceText, CancellationToken cancellationToken)
@@ -183,17 +231,15 @@ public async Task OpenDocumentAsync(string filePath, SourceText sourceText, Canc
183231
await _projectManager.UpdateAsync(
184232
updater =>
185233
{
186-
var textDocumentPath = FilePathNormalizer.Normalize(filePath);
187-
188234
// We are okay to use the non-project-key overload of TryResolveDocument here because we really are just checking if the document
189235
// has been added to _any_ project. AddDocument will take care of adding to all of the necessary ones, and then below we ensure
190236
// we process them all too
191-
if (!_projectManager.TryResolveDocumentInAnyProject(textDocumentPath, _logger, out var document))
237+
if (!_projectManager.TryFindContainingProject(filePath, out _))
192238
{
193239
// Document hasn't been added. This usually occurs when VSCode trumps all other initialization
194240
// processes and pre-initializes already open documents. We add this to the misc project, and
195241
// if/when we get project info from the client, it will be migrated to a real project.
196-
AddDocumentToMiscProjectCore(updater, filePath);
242+
AddDocumentToMiscProjectCore(updater, filePath, sourceText);
197243
}
198244

199245
ActOnDocumentInMultipleProjects(

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorFileChangeDetectorManager.cs

Lines changed: 0 additions & 66 deletions
This file was deleted.

0 commit comments

Comments
 (0)