Skip to content

Commit d222e4e

Browse files
OutOfProcTagHelperResolver: Don't reorder tag helpers after fetching
Retrieving TagHelperDescriptors from OOP is a two step process. 1. Call into OOP to get the checksum delta for tag helpers that are different from the last request. 2. If there are any checksums that aren't cached on the client-side, call into OOP to fetch them. Unfortunately, the OutOfProcTagHelperResolver would always add any fetched tag helpers to the end of the array, effectively reordering them. Once ProjectState is updated with a ProjectWorkspaceState created with this set of tag helpers, they wouldn't equal the same tag helpers assigned by subsequent calls to OOP because SequenceEquals(...) wouldn't return true.
1 parent 599d97c commit d222e4e

File tree

1 file changed

+57
-11
lines changed

1 file changed

+57
-11
lines changed

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

Lines changed: 57 additions & 11 deletions
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,46 @@ 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+
// We didn't receive all the tag helpers we requested. This is bad. However, instead of failing,
174+
// we'll just return the tag helpers we were able to retrieve.
175+
using var resultBuilder = new PooledArrayBuilder<TagHelperDescriptor>(capacity: result.Length);
176+
177+
foreach (var tagHelper in result)
178+
{
179+
if (tagHelper is not null)
180+
{
181+
resultBuilder.Add(tagHelper);
182+
}
183+
}
184+
185+
return resultBuilder.DrainToImmutable();
140186
}
141187
}
142188

143-
return tagHelpers.DrainToImmutable();
189+
return ImmutableCollectionsMarshal.AsImmutableArray(result);
144190
}
145191

146192
// Protected virtual for testing

0 commit comments

Comments
 (0)