-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Compute PatternMatching result in parallel when refreshing completion list #67249
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -3,6 +3,7 @@ | |||||
// See the LICENSE file in the project root for more information. | ||||||
|
||||||
using System; | ||||||
using System.Collections.Concurrent; | ||||||
using System.Collections.Generic; | ||||||
using System.Collections.Immutable; | ||||||
using System.Diagnostics; | ||||||
|
@@ -50,6 +51,9 @@ private sealed class CompletionListUpdater | |||||
private readonly bool _highlightMatchingPortions; | ||||||
private readonly bool _showCompletionItemFilters; | ||||||
|
||||||
// Used for building MatchResult list in parallel | ||||||
private readonly object _gate = new(); | ||||||
|
||||||
private readonly Action<IReadOnlyList<MatchResult>, string, IList<MatchResult>> _filterMethod; | ||||||
|
||||||
private bool ShouldSelectSuggestionItemWhenNoItemMatchesFilterText | ||||||
|
@@ -128,20 +132,24 @@ public CompletionListUpdater( | |||||
// since the completion list could be long with import completion enabled. | ||||||
var itemsToBeIncluded = s_listOfMatchResultPool.Allocate(); | ||||||
var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); | ||||||
var threadLocalPatternMatchHelper = new ThreadLocal<PatternMatchHelper>(() => new PatternMatchHelper(_filterText), trackAllValues: true); | ||||||
|
||||||
try | ||||||
{ | ||||||
// Determine the list of items to be included in the completion list. | ||||||
// This is computed based on the filter text as well as the current | ||||||
// selection of filters and expander. | ||||||
AddCompletionItems(itemsToBeIncluded, cancellationToken); | ||||||
AddCompletionItems(itemsToBeIncluded, threadLocalPatternMatchHelper, cancellationToken); | ||||||
|
||||||
// Decide if we want to dismiss an empty completion list based on CompletionRules and filter usage. | ||||||
if (itemsToBeIncluded.Count == 0) | ||||||
return HandleAllItemsFilteredOut(); | ||||||
|
||||||
// Sort items based on pattern matching result | ||||||
itemsToBeIncluded.Sort(MatchResult.SortingComparer); | ||||||
|
||||||
var highlightAndFilterTask = Task.Run( | ||||||
() => GetHighlightedListAndUpdatedFilters(session, itemsToBeIncluded, cancellationTokenSource.Token), | ||||||
() => GetHighlightedListAndUpdatedFilters(session, itemsToBeIncluded, threadLocalPatternMatchHelper, cancellationTokenSource.Token), | ||||||
cancellationTokenSource.Token); | ||||||
|
||||||
// Decide the item to be selected for this completion session. | ||||||
|
@@ -178,12 +186,18 @@ public CompletionListUpdater( | |||||
// Don't call ClearAndFree, which resets the capacity to a default value. | ||||||
itemsToBeIncluded.Clear(); | ||||||
s_listOfMatchResultPool.Free(itemsToBeIncluded); | ||||||
|
||||||
// Dispose PatternMatchers | ||||||
foreach (var helper in threadLocalPatternMatchHelper.Values) | ||||||
helper.Dispose(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah. interesting. you use it for disposal. interesting! |
||||||
|
||||||
threadLocalPatternMatchHelper.Dispose(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you just place the threadLocalPatternMatchHelper in a using block instead? |
||||||
} | ||||||
|
||||||
(CompletionList<CompletionItemWithHighlight>, ImmutableArray<CompletionFilterWithState>) GetHighlightedListAndUpdatedFilters( | ||||||
IAsyncCompletionSession session, IReadOnlyList<MatchResult> itemsToBeIncluded, CancellationToken cancellationToken) | ||||||
IAsyncCompletionSession session, IReadOnlyList<MatchResult> itemsToBeIncluded, ThreadLocal<PatternMatchHelper> patternMatcherHelper, CancellationToken cancellationToken) | ||||||
{ | ||||||
var highLightedList = GetHighlightedList(session, itemsToBeIncluded, cancellationToken); | ||||||
var highLightedList = GetHighlightedList(session, patternMatcherHelper.Value!, itemsToBeIncluded, cancellationToken); | ||||||
var updatedFilters = GetUpdatedFilters(itemsToBeIncluded, cancellationToken); | ||||||
return (highLightedList, updatedFilters); | ||||||
} | ||||||
|
@@ -228,7 +242,7 @@ static bool IsAfterDot(ITextSnapshot snapshot, ITrackingSpan applicableToSpan) | |||||
} | ||||||
} | ||||||
|
||||||
private void AddCompletionItems(List<MatchResult> list, CancellationToken cancellationToken) | ||||||
private void AddCompletionItems(List<MatchResult> list, ThreadLocal<PatternMatchHelper> threadLocalPatternMatchHelper, CancellationToken cancellationToken) | ||||||
{ | ||||||
// Convert initial and update trigger reasons to corresponding Roslyn type so | ||||||
// we can interact with Roslyn's completion system | ||||||
|
@@ -238,33 +252,36 @@ private void AddCompletionItems(List<MatchResult> list, CancellationToken cancel | |||||
// FilterStateHelper is used to decide whether a given item should be included in the list based on the state of filter/expander buttons. | ||||||
var filterHelper = new FilterStateHelper(_snapshotData.SelectedFilters); | ||||||
|
||||||
using var _1 = PooledHashSet<string>.GetInstance(out var includedPreferredItems); | ||||||
using var _2 = ArrayBuilder<MatchResult>.GetInstance(out var includedDefaults); | ||||||
var unmatchedDefaults = _snapshotData.Defaults; | ||||||
var includedPreferredItems = new ConcurrentSet<string>(); | ||||||
var includedDefaults = new ConcurrentDictionary<string, MatchResult>(); | ||||||
|
||||||
Enumerable.Range(0, _snapshotData.InitialSortedItemList.Count) | ||||||
.AsParallel() | ||||||
.WithCancellation(cancellationToken) | ||||||
.ForAll(CreateMatchResultAndProcessMatchingDefaults); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. curious why .ForAll, instead of .Where. Then we don't need a lock, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, you are right. I saw in my local measurement ForAll has small (but largely negligible) edge over Where, and since ForAll uses no buffer, I assume there would be less allocation (unconfrimed) That's why I picked ForAll. |
||||||
|
||||||
// Filter items based on the selected filters and matching. | ||||||
var totalCount = _snapshotData.InitialSortedItemList.Count; | ||||||
for (var currentIndex = 0; currentIndex < totalCount; currentIndex++) | ||||||
PromoteDefaultItemsToPreferredState(); | ||||||
|
||||||
void CreateMatchResultAndProcessMatchingDefaults(int index) | ||||||
{ | ||||||
cancellationToken.ThrowIfCancellationRequested(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i would not mind in a followup pr if we basically tried to be much clearer about setting up parallel state, and what that state mutated. e.g. by extracting a helper function and passing in things like the collections it is expected to mutate. it's pretty hard to reason about things here given how complex this is and how much state is implicitly captured. |
||||||
var item = _snapshotData.InitialSortedItemList[currentIndex]; | ||||||
var item = _snapshotData.InitialSortedItemList[index]; | ||||||
|
||||||
// All items passed in should contain a CompletionItemData object in the property bag, | ||||||
// which is guaranteed in `ItemManager.SortCompletionListAsync`. | ||||||
if (!CompletionItemData.TryGetData(item, out var itemData)) | ||||||
throw ExceptionUtilities.Unreachable(); | ||||||
|
||||||
if (filterHelper.ShouldBeFilteredOut(item)) | ||||||
continue; | ||||||
return; | ||||||
|
||||||
// currentIndex is used to track the index of the VS CompletionItem in the initial sorted list to maintain a map from Roslyn item to VS item. | ||||||
// It's also used to sort the items by pattern matching results while preserving the original alphabetical order for items with | ||||||
// same pattern match score since `List<T>.Sort` isn't stable. | ||||||
if (CompletionHelper.TryCreateMatchResult(_completionHelper, itemData.RoslynItem, _filterText, | ||||||
roslynInitialTriggerKind, roslynFilterReason, _recentItemsManager.GetRecentItemIndex(itemData.RoslynItem), _highlightMatchingPortions, currentIndex, | ||||||
out var matchResult)) | ||||||
if (threadLocalPatternMatchHelper.Value!.TryCreateMatchResult(itemData.RoslynItem, roslynInitialTriggerKind, roslynFilterReason, | ||||||
_recentItemsManager.GetRecentItemIndex(itemData.RoslynItem), _highlightMatchingPortions, index, out var matchResult)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
{ | ||||||
list.Add(matchResult); | ||||||
lock (_gate) | ||||||
list.Add(matchResult); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't love that inside the parallel we have to lock just to append to a list. would plinq's Where/Select methods (https://learn.microsoft.com/en-us/dotnet/api/system.linq.parallelenumerable.where?view=net-7.0) method work better? then you can just enumerate over those results that succeeded. |
||||||
|
||||||
if (!_snapshotData.Defaults.IsEmpty) | ||||||
{ | ||||||
|
@@ -277,26 +294,21 @@ private void AddCompletionItems(List<MatchResult> list, CancellationToken cancel | |||||
} | ||||||
else | ||||||
{ | ||||||
var defaultIndex = unmatchedDefaults.IndexOf(matchResult.CompletionItem.FilterText); | ||||||
if (defaultIndex >= 0) | ||||||
if (_snapshotData.Defaults.IndexOf(matchResult.CompletionItem.FilterText) >= 0) | ||||||
{ | ||||||
unmatchedDefaults = unmatchedDefaults.RemoveAt(defaultIndex); | ||||||
includedDefaults.Add(matchResult); | ||||||
includedDefaults.TryAdd(matchResult.CompletionItem.FilterText, matchResult); | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
PromoteDefaultItemsToPreferredState(); | ||||||
list.Sort(MatchResult.SortingComparer); | ||||||
|
||||||
// Go through items matched with defaults. If it doesn't have | ||||||
// a corresponding preferred items, we will add one that mimic | ||||||
// the "starred" item from Pythia. | ||||||
void PromoteDefaultItemsToPreferredState() | ||||||
{ | ||||||
foreach (var includedDefault in includedDefaults) | ||||||
foreach (var includedDefault in includedDefaults.Values) | ||||||
{ | ||||||
var completionItem = includedDefault.CompletionItem; | ||||||
|
||||||
|
@@ -568,23 +580,21 @@ static int GetPriority(RoslynCompletionItem item) | |||||
|
||||||
private CompletionList<CompletionItemWithHighlight> GetHighlightedList( | ||||||
IAsyncCompletionSession session, | ||||||
PatternMatchHelper patternMatchers, | ||||||
IReadOnlyList<MatchResult> matchResults, | ||||||
CancellationToken cancellationToken) | ||||||
{ | ||||||
return session.CreateCompletionList(matchResults.Select(matchResult => | ||||||
{ | ||||||
var vsItem = GetCorrespondingVsCompletionItem(matchResult, cancellationToken); | ||||||
var highlightedSpans = _highlightMatchingPortions | ||||||
? GetHighlightedSpans(matchResult, _completionHelper, _filterText) | ||||||
? GetHighlightedSpans(matchResult, patternMatchers) | ||||||
: ImmutableArray<Span>.Empty; | ||||||
|
||||||
return new CompletionItemWithHighlight(vsItem, highlightedSpans); | ||||||
})); | ||||||
|
||||||
static ImmutableArray<Span> GetHighlightedSpans( | ||||||
MatchResult matchResult, | ||||||
CompletionHelper completionHelper, | ||||||
string filterText) | ||||||
static ImmutableArray<Span> GetHighlightedSpans(MatchResult matchResult, PatternMatchHelper patternMatchers) | ||||||
{ | ||||||
if (matchResult.CompletionItem.HasDifferentFilterText || matchResult.CompletionItem.HasAdditionalFilterTexts) | ||||||
{ | ||||||
|
@@ -593,8 +603,8 @@ static ImmutableArray<Span> GetHighlightedSpans( | |||||
// However, if the Roslyn item's FilterText is different from its DisplayText, we need to do the match against the | ||||||
// display text of the VS item directly to get the highlighted spans. This is done in a best effort fashion and there | ||||||
// is no guarantee a proper match would be found for highlighting. | ||||||
return completionHelper.GetHighlightedSpans( | ||||||
matchResult.CompletionItem, filterText, CultureInfo.CurrentCulture).SelectAsArray(s => s.ToSpan()); | ||||||
return patternMatchers.GetHighlightedSpans(matchResult.CompletionItem.GetEntireDisplayText(), CultureInfo.CurrentCulture) | ||||||
.SelectAsArray(s => s.ToSpan()); | ||||||
} | ||||||
|
||||||
var patternMatch = matchResult.PatternMatch; | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,34 +56,39 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense | |
Dim culture = New CultureInfo("tr-TR", useUserOverride:=False) | ||
|
||
Dim workspace = New TestWorkspace | ||
Dim helper = New CompletionHelper(isCaseSensitive:=False) | ||
Dim helper = New PatternMatchHelper(pattern) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. using |
||
|
||
For Each wordMarkup In wordsToMatch | ||
Dim word As String = Nothing | ||
Dim wordMatchSpan As TextSpan = Nothing | ||
MarkupTestFile.GetSpan(wordMarkup, word, wordMatchSpan) | ||
|
||
Dim item = CompletionItem.Create(word) | ||
Assert.True(helper.MatchesPattern(item, pattern, culture), $"Expected item {word} does not match {pattern}") | ||
Assert.True(helper.MatchesPattern(item, culture), $"Expected item {word} does not match {pattern}") | ||
|
||
Dim highlightedSpans = helper.GetHighlightedSpans(item, pattern, culture) | ||
Dim highlightedSpans = helper.GetHighlightedSpans(item.GetEntireDisplayText(), culture) | ||
Assert.NotEmpty(highlightedSpans) | ||
Assert.Equal(1, highlightedSpans.Length) | ||
Assert.Equal(wordMatchSpan, highlightedSpans(0)) | ||
Next | ||
|
||
helper.Dispose() | ||
End Sub | ||
|
||
Private Shared Sub TestNotMatches(pattern As String, wordsToNotMatch() As String) | ||
Dim culture = New CultureInfo("tr-TR", useUserOverride:=False) | ||
Dim workspace = New TestWorkspace | ||
Dim helper = New CompletionHelper(isCaseSensitive:=True) | ||
Dim helper = New PatternMatchHelper(pattern) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. using? |
||
|
||
For Each word In wordsToNotMatch | ||
Dim item = CompletionItem.Create(word) | ||
Assert.False(helper.MatchesPattern(item, pattern, culture), $"Unexpected item {word} matches {pattern}") | ||
Assert.False(helper.MatchesPattern(item, culture), $"Unexpected item {word} matches {pattern}") | ||
|
||
Dim highlightedSpans = helper.GetHighlightedSpans(item, pattern, culture) | ||
Dim highlightedSpans = helper.GetHighlightedSpans(item.GetEntireDisplayText(), culture) | ||
Assert.Empty(highlightedSpans) | ||
Next | ||
|
||
helper.Dispose() | ||
End Sub | ||
End Class | ||
End Namespace |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what does 'trackAllValues' do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looking at the docs, unclear why we're setting that to true. can you doc in the code why?
also, doc in the code why we're using a ThreadLocal here (specifically that PatternMatchHelper is not threadsafe, so we want a dedicated one per thread that is active doing the filtering.