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

Some improvement for import completion #43548

Merged
merged 19 commits into from
Oct 28, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,20 @@ public class ExtensionMethodImportCompletionProviderTests : AbstractCSharpComple

private bool? ShowImportCompletionItemsOptionValue { get; set; } = true;

// -1 would disable timebox, whereas 0 means always timeout.
genlu marked this conversation as resolved.
Show resolved Hide resolved
private int TimeoutInMilliseconds { get; set; } = -1;

private bool IsExpandedCompletion { get; set; } = true;

private bool HideAdvancedMembers { get; set; } = false;
private bool HideAdvancedMembers { get; set; }

protected override OptionSet WithChangedOptions(OptionSet options)
{
return options
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, ShowImportCompletionItemsOptionValue)
.WithChangedOption(CompletionServiceOptions.IsExpandedCompletion, IsExpandedCompletion)
.WithChangedOption(CompletionOptions.HideAdvancedMembers, LanguageNames.CSharp, HideAdvancedMembers);
.WithChangedOption(CompletionOptions.HideAdvancedMembers, LanguageNames.CSharp, HideAdvancedMembers)
.WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForExtensionMethodImportCompletion, TimeoutInMilliseconds);
}

protected override TestComposition GetComposition()
Expand Down Expand Up @@ -1884,6 +1888,44 @@ await VerifyImportItemIsAbsentAsync(
}
}

[Fact, Trait(Traits.Feature, Traits.Features.Completion)]
public async Task TestTimeBox()
{
var file1 = @"
using System;
namespace Foo
{
public static class ExtensionClass
{
public static bool ExtentionMethod(this int x)
=> true;
}
}";
var file2 = @"
using System;
namespace Baz
{
public class Bat
{
public void M(int x)
{
x.$$
}
}
}";

IsExpandedCompletion = false;
TimeoutInMilliseconds = 0; //timeout immediately
var markup = GetMarkup(file2, file1, ReferenceType.None);

await VerifyImportItemIsAbsentAsync(
markup,
"ExtentionMethod",
inlineDescription: "Foo");
}

private Task VerifyImportItemExistsAsync(string markup, string expectedItem, int glyph, string inlineDescription, string displayTextSuffix = null, string expectedDescriptionOrNull = null)
=> VerifyItemExistsAsync(markup, expectedItem, displayTextSuffix: displayTextSuffix, glyph: glyph, inlineDescription: inlineDescription, expectedDescriptionOrNull: expectedDescriptionOrNull);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.PatternMatching;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
Expand Down Expand Up @@ -176,8 +174,23 @@ private FilteredCompletionModel UpdateCompletionList(

// Use a monotonically increasing integer to keep track the original alphabetical order of each item.
var currentIndex = 0;

var builder = ArrayBuilder<MatchResult>.GetInstance();
Copy link
Member

@sharwell sharwell Oct 1, 2020

Choose a reason for hiding this comment

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

💡 You can pass a length to GetInstance. That may or may not help here.

Copy link
Member Author

Choose a reason for hiding this comment

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

genlu marked this conversation as resolved.
Show resolved Hide resolved
if (KeepAllItemsInTheList(initialRoslynTriggerKind, filterText) && !needToFilterExpanded && !needToFilter)
{
// PERF: Conditionally set the capacity of the ArrayBuilder, i.e. if we think it's likely that the filtering result
// would be similar in size to the initial list, in which case, setting the capacity would avoid calls to Resize later.
// Otherwise, don't set capacity as an attempt to avoid allocating large array which might unnecessarily end up in LOH.
// The trade-off of not setting capacity is when we are wrong (large amount of items post-filtering), more allocation
// would occur because of Resize, but in reality we believe such cost is much lower than the benefit of not allocating
// large array upfront everytime we refresh completion list.
// We say it's likely that we end up with a list of similar size after filtering if:
// 1. We need to show items even if they don't match filter text
// 2. No filter is selected
builder.EnsureCapacity(data.InitialSortedList.Length);
}
Copy link
Member

Choose a reason for hiding this comment

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

was this a speculative perf change? or was this driven from some actual data? i have no problem with it either way, i'm just curious.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is addressing RPS regression caught in validation PR.

Before: +6.3mb LOH allocation
https://devdiv.visualstudio.com/DefaultCollection/DevDiv/_git/VS/pullrequest/278169

After: +610kb LOH allocation
https://devdiv.visualstudio.com/DefaultCollection/DevDiv/_git/VS/pullrequest/278694

Copy link
Member

Choose a reason for hiding this comment

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

@sharwell is your sdgmented array ready?


// Filter items based on the selected filters and matching.
foreach (var item in data.InitialSortedList)
{
cancellationToken.ThrowIfCancellationRequested();
Expand Down Expand Up @@ -570,6 +583,21 @@ private static RoslynCompletionItem GetOrAddRoslynCompletionItem(VSCompletionIte
return roslynItem;
}

// If the item didn't match the filter text, we still keep it in the list
// if one of two things is true:
// 1. The user has typed nothing or only typed a single character. In this case they might
// have just typed the character to get completion. Filtering out items
// here is not desirable.
//
// 2. They brought up completion with ctrl-j or through deletion. In these
// cases we just always keep all the items in the list.
private static bool KeepAllItemsInTheList(CompletionTriggerKind initialTriggerKind, string filterText)
{
return filterText.Length <= 1 ||
initialTriggerKind == CompletionTriggerKind.Invoke ||
initialTriggerKind == CompletionTriggerKind.Deletion;
Copy link
Member

Choose a reason for hiding this comment

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

i wish this was shared with some of our other logic elsewhere so that this would stay consistent with that if we ever change things.

Copy link
Member Author

Choose a reason for hiding this comment

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

You mean LSP?

}

private static bool TryCreateMatchResult(
CompletionHelper completionHelper,
VSCompletionItem item,
Expand Down Expand Up @@ -601,31 +629,25 @@ private static bool TryCreateMatchResult(
recentItems,
patternMatch);

// If the item didn't match the filter text, we still keep it in the list
// if one of two things is true:
//
// 1. The user has typed nothing or only typed a single character. In this case they might
// have just typed the character to get completion. Filtering out items
// here is not desirable.
//
// 2. They brought up completion with ctrl-j or through deletion. In these
// cases we just always keep all the items in the list.
if (matchedFilterText ||
initialTriggerKind == CompletionTriggerKind.Deletion ||
initialTriggerKind == CompletionTriggerKind.Invoke ||
filterText.Length <= 1)
if (matchedFilterText || KeepAllItemsInTheList(initialTriggerKind, filterText))
{
matchResult = new MatchResult(
roslynItem, item, matchedFilterText: matchedFilterText,
patternMatch: patternMatch, index: currentIndex++, GetHighlightedSpans());
patternMatch: patternMatch, index: currentIndex++, GetHighlightedSpans(completionHelper, item, filterText, highlightMatchingPortions, roslynItem, patternMatch));

return true;
}

matchResult = default;
return false;

ImmutableArray<Span> GetHighlightedSpans()
static ImmutableArray<Span> GetHighlightedSpans(
CompletionHelper completionHelper,
VSCompletionItem item,
string filterText,
bool highlightMatchingPortions,
RoslynCompletionItem roslynItem,
PatternMatch? patternMatch)
{
if (!highlightMatchingPortions)
{
Expand All @@ -647,7 +669,7 @@ ImmutableArray<Span> GetHighlightedSpans()
// Since VS item's display text is created as Prefix + DisplayText + Suffix,
// we can calculate the highlighted span by adding an offset that is the length of the Prefix.
return patternMatch.Value.MatchedSpans
.SelectAsArray(s => s.MoveTo(roslynItem.DisplayTextPrefix?.Length ?? 0).ToSpan());
.SelectAsArray(s_highlightSpanGetter, roslynItem);
Copy link
Member

Choose a reason for hiding this comment

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

alternative: just mark the lambda as static.

Copy link
Member Author

Choose a reason for hiding this comment

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

so the static lambda will be compiled into same thing?

Copy link
Member

Choose a reason for hiding this comment

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

yup.

}

// If there's no match for Roslyn item's filter text which is identical to its display text,
Expand All @@ -656,6 +678,10 @@ ImmutableArray<Span> GetHighlightedSpans()
}
}

// PERF: Create a singleton to avoid lambda allocation on hot path
private static readonly Func<TextSpan, RoslynCompletionItem, Span> s_highlightSpanGetter
= (span, item) => span.MoveTo(item.DisplayTextPrefix?.Length ?? 0).ToSpan();

private static bool MatchesFilterText(
RoslynCompletionItem item,
string filterText,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6090,6 +6090,7 @@ namespace NS2

Dim workspace = state.Workspace
workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options _
.WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForExtensionMethodImportCompletion, -1) _
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, False)))

' trigger completion with import completion disabled
Expand Down Expand Up @@ -6160,6 +6161,7 @@ namespace NS2

Dim workspace = state.Workspace
workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options _
.WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForExtensionMethodImportCompletion, -1) _
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, True)))

' trigger completion with import completion enabled
Expand Down Expand Up @@ -6203,6 +6205,7 @@ namespace NS1

Dim workspace = state.Workspace
workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options _
.WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForExtensionMethodImportCompletion, -1) _
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, True)))

' trigger completion with import completion enabled
Expand Down Expand Up @@ -6638,6 +6641,7 @@ namespace NS2

Dim workspace = state.Workspace
workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options _
.WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForExtensionMethodImportCompletion, -1) _
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, True)))

state.SendInvokeCompletionList()
Expand Down Expand Up @@ -6712,6 +6716,7 @@ namespace NS2

Dim workspace = state.Workspace
workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options _
.WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForExtensionMethodImportCompletion, -1) _
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, True)))

state.SendInvokeCompletionList()
Expand Down Expand Up @@ -6786,6 +6791,7 @@ namespace NS2

Dim workspace = state.Workspace
workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options _
.WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForExtensionMethodImportCompletion, -1) _
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, True)))

state.SendInvokeCompletionList()
Expand Down Expand Up @@ -6860,6 +6866,7 @@ namespace NS2

Dim workspace = state.Workspace
workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options _
.WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForExtensionMethodImportCompletion, -1) _
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, True)))

state.SendInvokeCompletionList()
Expand Down Expand Up @@ -6935,6 +6942,7 @@ namespace NS2

Dim workspace = state.Workspace
workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options _
.WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForExtensionMethodImportCompletion, -1) _
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, True)))

state.SendInvokeCompletionList()
Expand Down Expand Up @@ -6980,6 +6988,7 @@ namespace OtherNS

Dim workspace = state.Workspace
workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options _
.WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForExtensionMethodImportCompletion, -1) _
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, True)))

state.SendInvokeCompletionList()
Expand Down Expand Up @@ -7099,6 +7108,7 @@ namespace NS2
Await state.AssertNoCompletionSession()

workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options _
.WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForExtensionMethodImportCompletion, -1) _
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, True)))

state.SendTypeChars("mytask")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.Completion.Complet

Private Property IsExpandedCompletion As Boolean = True

Private Property ShowImportCompletionItemsOptionValue As Boolean = True

' -1 would disable timebox, whereas 0 means always timeout.
Private Property TimeoutInMilliseconds As Integer = -1

Private Property ShowImportCompletionItemsOptionValue As Boolean = True

Protected Overrides Function WithChangedOptions(options As OptionSet) As OptionSet
Return options _
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.VisualBasic, ShowImportCompletionItemsOptionValue).WithChangedOption(CompletionServiceOptions.IsExpandedCompletion, IsExpandedCompletion)
.WithChangedOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, LanguageNames.VisualBasic, ShowImportCompletionItemsOptionValue) _
.WithChangedOption(CompletionServiceOptions.IsExpandedCompletion, IsExpandedCompletion) _
.WithChangedOption(CompletionServiceOptions.TimeoutInMillisecondsForExtensionMethodImportCompletion, TimeoutInMilliseconds)
End Function

Protected Overrides Function GetComposition() As TestComposition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using Microsoft.CodeAnalysis.Options;

namespace Microsoft.CodeAnalysis.Completion
Expand All @@ -21,5 +19,12 @@ public static readonly Option2<bool> IsExpandedCompletion
/// </summary>
public static readonly Option2<bool> DisallowAddingImports
= new(nameof(CompletionServiceOptions), nameof(DisallowAddingImports), defaultValue: false);

/// <summary>
/// Timeout value used for time-boxing completion of unimported extension methods.
/// Value less than 0 means no timebox; value == 0 means immediate timeout (for testing purpose)
/// </summary>
public static readonly Option2<int> TimeoutInMillisecondsForExtensionMethodImportCompletion
= new(nameof(CompletionServiceOptions), nameof(TimeoutInMillisecondsForExtensionMethodImportCompletion), defaultValue: 500);
Copy link
Member

Choose a reason for hiding this comment

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

is there some other value we can read in for this? i.e. i feel like this should be driven somewhat by:

image

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea, we are actually discussing this right now. Will probably come up with something soon (not in this PR though)

Copy link
Member

Choose a reason for hiding this comment

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

ok thanks :)

Copy link
Member

Choose a reason for hiding this comment

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

I wonder what's the designed user scenario here?
After this change, if my computer is slow and it can't get the completion finished in 0.5s then does it mean I am unable to use this feature?

Copy link
Member Author

Choose a reason for hiding this comment

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

Right now, we unconditionally apply the timebox, so it means the if it takes more than 500ms, unimported extension methods wouldn't show by default even if you enabled the feature. But when that happens, you can still use expander button to make explicit request, which ignores the timebox.

}
}
Loading