Skip to content

Commit 443b51a

Browse files
Fix a couple of Razor project issues (#11645)
I was debugging through `ProjectSnapshotManager` and noticed a couple of issues: 1. Documents won't be cleared from `ProjectSnapshotManager`'s open documents on solution close. 2. The algorithm to request tag helpers from OOP has some undesirable behavior. Essentially, any tag helpers that it had to fetch from OOP using checksums will be added to the end of the result. That can result in cases when the `ProjectWorkspaceState` is updated for a project with exactly the same tag helpers in a different order. This results in `SequenceEquals(...)` returning false when comparing old and new `ProjectWorkspaceState` and all of the documents being invalidated. CI Build: https://dev.azure.com/dnceng/internal/_build/results?buildId=2668868&view=results Test Insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/621272
2 parents dff00b2 + 142be8c commit 443b51a

File tree

3 files changed

+96
-39
lines changed

3 files changed

+96
-39
lines changed

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs

+8-27
Original file line numberDiff line numberDiff line change
@@ -301,30 +301,22 @@ private void RemoveDocument(ProjectKey projectKey, string documentFilePath)
301301

302302
private void OpenDocument(ProjectKey projectKey, string documentFilePath, SourceText text)
303303
{
304-
if (TryUpdateProject(
305-
projectKey,
306-
transformer: state => state.WithDocumentText(documentFilePath, text),
307-
onAfterUpdate: () => _openDocumentSet.Add(documentFilePath),
308-
out var oldProject,
309-
out var newProject,
310-
out var isSolutionClosing))
304+
using (_readerWriterLock.DisposableWrite())
311305
{
312-
NotifyListeners(ProjectChangeEventArgs.DocumentChanged(oldProject, newProject, documentFilePath, isSolutionClosing));
306+
_openDocumentSet.Add(documentFilePath);
313307
}
308+
309+
UpdateDocumentText(projectKey, documentFilePath, text);
314310
}
315311

316312
private void CloseDocument(ProjectKey projectKey, string documentFilePath, TextLoader textLoader)
317313
{
318-
if (TryUpdateProject(
319-
projectKey,
320-
transformer: state => state.WithDocumentText(documentFilePath, textLoader),
321-
onAfterUpdate: () => _openDocumentSet.Remove(documentFilePath),
322-
out var oldProject,
323-
out var newProject,
324-
out var isSolutionClosing))
314+
using (_readerWriterLock.DisposableWrite())
325315
{
326-
NotifyListeners(ProjectChangeEventArgs.DocumentChanged(oldProject, newProject, documentFilePath, isSolutionClosing));
316+
_openDocumentSet.Remove(documentFilePath);
327317
}
318+
319+
UpdateDocumentText(projectKey, documentFilePath, textLoader);
328320
}
329321

330322
private void UpdateDocumentText(ProjectKey projectKey, string documentFilePath, SourceText text)
@@ -413,15 +405,6 @@ private bool TryUpdateProject(
413405
[NotNullWhen(true)] out ProjectSnapshot? oldProject,
414406
[NotNullWhen(true)] out ProjectSnapshot? newProject,
415407
out bool isSolutionClosing)
416-
=> TryUpdateProject(projectKey, transformer, onAfterUpdate: null, out oldProject, out newProject, out isSolutionClosing);
417-
418-
private bool TryUpdateProject(
419-
ProjectKey projectKey,
420-
Func<ProjectState, ProjectState> transformer,
421-
Action? onAfterUpdate,
422-
[NotNullWhen(true)] out ProjectSnapshot? oldProject,
423-
[NotNullWhen(true)] out ProjectSnapshot? newProject,
424-
out bool isSolutionClosing)
425408
{
426409
if (_initialized)
427410
{
@@ -459,8 +442,6 @@ private bool TryUpdateProject(
459442
var newEntry = new Entry(newState);
460443
_projectMap[projectKey] = newEntry;
461444

462-
onAfterUpdate?.Invoke();
463-
464445
oldProject = oldEntry.GetSnapshot();
465446
newProject = newEntry.GetSnapshot();
466447

src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Discovery/OutOfProcTagHelperResolver.cs

+60-11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
using System;
55
using System.Collections.Immutable;
66
using System.ComponentModel.Composition;
7+
using System.Diagnostics;
8+
using System.Linq;
9+
using System.Runtime.InteropServices;
710
using System.Threading;
811
using System.Threading.Tasks;
912
using Microsoft.AspNetCore.Razor.Language;
@@ -87,39 +90,52 @@ protected virtual async ValueTask<ImmutableArray<TagHelperDescriptor>> ResolveTa
8790

8891
if (deltaResult is null)
8992
{
93+
// For some reason, TryInvokeAsync can return null if it is cancelled while fetching the client.
9094
return default;
9195
}
9296

9397
// Apply the delta we received to any cached checksums for the current project.
9498
var checksums = ProduceChecksumsFromDelta(project.Id, lastResultId, deltaResult);
9599

96-
using var tagHelpers = new PooledArrayBuilder<TagHelperDescriptor>(capacity: checksums.Length);
97-
using var checksumsToFetch = new PooledArrayBuilder<Checksum>(capacity: checksums.Length);
100+
// Create an array to hold the result. We'll wrap it in an ImmutableArray at the end.
101+
var result = new TagHelperDescriptor[checksums.Length];
98102

99-
foreach (var checksum in checksums)
103+
// We need to keep track of which checksums we still need to fetch tag helpers for from OOP.
104+
// In addition, we'll track the indices in tagHelpers that we'll need to replace with those we
105+
// fetch to ensure that the results stay in the same order.
106+
using var checksumsToFetchBuilder = new PooledArrayBuilder<Checksum>(capacity: checksums.Length);
107+
using var checksumIndicesBuilder = new PooledArrayBuilder<int>(capacity: checksums.Length);
108+
109+
for (var i = 0; i < checksums.Length; i++)
100110
{
111+
var checksum = checksums[i];
112+
101113
// See if we have a cached version of this tag helper. If not, we'll need to fetch it from OOP.
102114
if (TagHelperCache.Default.TryGet(checksum, out var tagHelper))
103115
{
104-
tagHelpers.Add(tagHelper);
116+
result[i] = tagHelper;
105117
}
106118
else
107119
{
108-
checksumsToFetch.Add(checksum);
120+
checksumsToFetchBuilder.Add(checksum);
121+
checksumIndicesBuilder.Add(i);
109122
}
110123
}
111124

112-
if (checksumsToFetch.Count > 0)
125+
if (checksumsToFetchBuilder.Count > 0)
113126
{
127+
var checksumsToFetch = checksumsToFetchBuilder.DrainToImmutable();
128+
114129
// There are checksums that we don't have cached tag helpers for, so we need to fetch them from OOP.
115130
var fetchResult = await _remoteServiceInvoker.TryInvokeAsync<IRemoteTagHelperProviderService, FetchTagHelpersResult>(
116131
project.Solution,
117132
(service, solutionInfo, innerCancellationToken) =>
118-
service.FetchTagHelpersAsync(solutionInfo, projectHandle, checksumsToFetch.DrainToImmutable(), innerCancellationToken),
133+
service.FetchTagHelpersAsync(solutionInfo, projectHandle, checksumsToFetch, innerCancellationToken),
119134
cancellationToken);
120135

121136
if (fetchResult is null)
122137
{
138+
// For some reason, TryInvokeAsync can return null if it is cancelled while fetching the client.
123139
return default;
124140
}
125141

@@ -131,16 +147,49 @@ protected virtual async ValueTask<ImmutableArray<TagHelperDescriptor>> ResolveTa
131147
throw new InvalidOperationException("Tag helpers could not be fetched from the Roslyn OOP.");
132148
}
133149

150+
Debug.Assert(
151+
checksumsToFetch.Length == fetchedTagHelpers.Length,
152+
$"{nameof(FetchTagHelpersResult)} should return the same number of tag helpers as checksums requested.");
153+
154+
Debug.Assert(
155+
checksumsToFetch.SequenceEqual(fetchedTagHelpers.Select(static t => t.Checksum)),
156+
$"{nameof(FetchTagHelpersResult)} should return tag helpers that match the checksums requested.");
157+
134158
// Be sure to add the tag helpers we just fetched to the cache.
135159
var cache = TagHelperCache.Default;
136-
foreach (var tagHelper in fetchedTagHelpers)
160+
161+
for (var i = 0; i < fetchedTagHelpers.Length; i++)
162+
{
163+
var index = checksumIndicesBuilder[i];
164+
Debug.Assert(result[index] is null);
165+
166+
var fetchedTagHelper = fetchedTagHelpers[i];
167+
result[index] = fetchedTagHelper;
168+
cache.TryAdd(fetchedTagHelper.Checksum, fetchedTagHelper);
169+
}
170+
171+
if (checksumsToFetch.Length != fetchedTagHelpers.Length)
137172
{
138-
tagHelpers.Add(tagHelper);
139-
cache.TryAdd(tagHelper.Checksum, tagHelper);
173+
_logger.LogWarning($"Expected to receive {checksumsToFetch.Length} tag helpers from Roslyn OOP, " +
174+
$"but received {fetchedTagHelpers.Length} instead. Returning a partial set of tag helpers.");
175+
176+
// We didn't receive all the tag helpers we requested. This is bad. However, instead of failing,
177+
// we'll just return the tag helpers we were able to retrieve.
178+
using var resultBuilder = new PooledArrayBuilder<TagHelperDescriptor>(capacity: result.Length);
179+
180+
foreach (var tagHelper in result)
181+
{
182+
if (tagHelper is not null)
183+
{
184+
resultBuilder.Add(tagHelper);
185+
}
186+
}
187+
188+
return resultBuilder.DrainToImmutable();
140189
}
141190
}
142191

143-
return tagHelpers.DrainToImmutable();
192+
return ImmutableCollectionsMarshal.AsImmutableArray(result);
144193
}
145194

146195
// Protected virtual for testing

src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/ProjectSnapshotManagerTest.cs

+28-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public ProjectSnapshotManagerTest(ITestOutputHelper testOutput)
6363
}
6464

6565
[UIFact]
66-
public async Task Initialize_DoneInCorrectOrderBasedOnInitializePriorityPriority()
66+
public async Task Initialize_DoneInCorrectOrderBasedOnInitializePriority()
6767
{
6868
// Arrange
6969
var initializedOrder = new List<string>();
@@ -892,4 +892,31 @@ await _projectManager.UpdateAsync(updater =>
892892

893893
textLoader.Verify(d => d.LoadTextAndVersionAsync(It.IsAny<LoadTextOptions>(), It.IsAny<CancellationToken>()), Times.Never());
894894
}
895+
896+
[Fact]
897+
public async Task SolutionClosing_RemovesProjectAndClosesDocument()
898+
{
899+
// Arrange
900+
901+
// Add project and open document.
902+
await _projectManager.UpdateAsync(updater =>
903+
{
904+
updater.AddProject(s_hostProject);
905+
updater.AddDocument(s_hostProject.Key, s_documents[0], EmptyTextLoader.Instance);
906+
updater.OpenDocument(s_hostProject.Key, s_documents[0].FilePath, _sourceText);
907+
});
908+
909+
// Act
910+
await _projectManager.UpdateAsync(updater =>
911+
{
912+
updater.SolutionClosed();
913+
updater.RemoveProject(s_hostProject.Key);
914+
updater.CloseDocument(s_hostProject.Key, s_documents[0].FilePath, EmptyTextLoader.Instance);
915+
});
916+
917+
// Assert
918+
Assert.False(_projectManager.ContainsDocument(s_hostProject.Key, s_documents[0].FilePath));
919+
Assert.False(_projectManager.ContainsProject(s_hostProject.Key));
920+
Assert.False(_projectManager.IsDocumentOpen(s_documents[0].FilePath));
921+
}
895922
}

0 commit comments

Comments
 (0)