diff --git a/src/EditorFeatures/Test2/FindReferences/FindReferencesTests.vb b/src/EditorFeatures/Test2/FindReferences/FindReferencesTests.vb index 2f1e8761c5f16..a844959deb7a0 100644 --- a/src/EditorFeatures/Test2/FindReferences/FindReferencesTests.vb +++ b/src/EditorFeatures/Test2/FindReferences/FindReferencesTests.vb @@ -51,10 +51,11 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.FindReferences Await TestStreamingFeature(element, searchSingleFileOnly, uiVisibleOnly, host) End Function - Private Shared Async Function TestStreamingFeature(element As XElement, - searchSingleFileOnly As Boolean, - uiVisibleOnly As Boolean, - host As TestHost) As Task + Private Shared Async Function TestStreamingFeature( + element As XElement, + searchSingleFileOnly As Boolean, + uiVisibleOnly As Boolean, + host As TestHost) As Task ' We don't support testing features that only expect partial results. If searchSingleFileOnly OrElse uiVisibleOnly Then Return @@ -254,7 +255,19 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.FindReferences Optional uiVisibleOnly As Boolean = False, Optional options As FindReferencesSearchOptions = Nothing) As Task + Await TestAPI(definition, host, explicit:=False, searchSingleFileOnly, uiVisibleOnly, options) + Await TestAPI(definition, host, explicit:=True, searchSingleFileOnly, uiVisibleOnly, options) + End Function + + Private Async Function TestAPI( + definition As XElement, + host As TestHost, + explicit As Boolean, + searchSingleFileOnly As Boolean, + uiVisibleOnly As Boolean, + options As FindReferencesSearchOptions) As Task options = If(options, FindReferencesSearchOptions.Default) + options = options.With(explicit:=explicit) Using workspace = TestWorkspace.Create(definition, composition:=s_composition.WithTestHostParts(host)) workspace.SetTestLogger(AddressOf _outputHelper.WriteLine) diff --git a/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementExplicitlyCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementExplicitlyCodeRefactoringProvider.cs index b61bbfe4973a0..92aa021771479 100644 --- a/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementExplicitlyCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ImplementInterface/CSharpImplementExplicitlyCodeRefactoringProvider.cs @@ -53,7 +53,7 @@ protected override async Task UpdateReferencesAsync( // // This can save a lot of extra time spent finding callers, especially for methods with // high fan-out (like IDisposable.Dispose()). - var findRefsOptions = FindReferencesSearchOptions.Default.WithCascade(false); + var findRefsOptions = FindReferencesSearchOptions.Default.With(cascade: false); var references = await SymbolFinder.FindReferencesAsync( implMember, solution, findRefsOptions, cancellationToken).ConfigureAwait(false); diff --git a/src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs b/src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs index f88628805530b..f25391ebe9b17 100644 --- a/src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs +++ b/src/Features/Core/Portable/CodeLens/CodeLensReferencesService.cs @@ -26,6 +26,12 @@ internal sealed class CodeLensReferencesService : ICodeLensReferencesService typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, memberOptions: SymbolDisplayMemberOptions.IncludeContainingType); + // Set ourselves as an implicit invocation of FindReferences. This will cause the finding operation to operate + // in serial, not parallel. We're running ephemerally in the BG and do not want to saturate the system with + // work that then slows the user down. + private static readonly FindReferencesSearchOptions s_nonParallelSearch = + FindReferencesSearchOptions.Default.With(@explicit: false); + private static async Task FindAsync(Solution solution, DocumentId documentId, SyntaxNode syntaxNode, Func> onResults, Func> onCapped, int searchCap, CancellationToken cancellationToken) where T : struct @@ -49,8 +55,8 @@ internal sealed class CodeLensReferencesService : ICodeLensReferencesService using var progress = new CodeLensFindReferencesProgress(symbol, syntaxNode, searchCap, cancellationToken); try { - await SymbolFinder.FindReferencesAsync(symbol, solution, progress, null, - progress.CancellationToken).ConfigureAwait(false); + await SymbolFinder.FindReferencesAsync( + symbol, solution, progress, documents: null, s_nonParallelSearch, progress.CancellationToken).ConfigureAwait(false); return await onResults(progress).ConfigureAwait(false); } diff --git a/src/Features/Core/Portable/InlineTemporary/AbstractInlineTemporaryCodeRefactoringProvider.cs b/src/Features/Core/Portable/InlineTemporary/AbstractInlineTemporaryCodeRefactoringProvider.cs index 251a6111c6ee7..acff36fb962da 100644 --- a/src/Features/Core/Portable/InlineTemporary/AbstractInlineTemporaryCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/InlineTemporary/AbstractInlineTemporaryCodeRefactoringProvider.cs @@ -30,7 +30,7 @@ protected static async Task> GetReferenceLocat // Do not cascade when finding references to this local. Cascading can cause us to find linked // references as well which can throw things off for us. For inline variable, we only care about the // direct real references in this project context. - var options = FindReferencesSearchOptions.Default.WithCascade(cascade: false); + var options = FindReferencesSearchOptions.Default.With(cascade: false); var findReferencesResult = await SymbolFinder.FindReferencesAsync( local, document.Project.Solution, options, cancellationToken).ConfigureAwait(false); diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.cs index 5ac5b885504da..d914bc68fd48a 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.cs @@ -29,6 +29,13 @@ internal partial class FindReferencesSearchEngine private readonly CancellationToken _cancellationToken; private readonly FindReferencesSearchOptions _options; + /// + /// Scheduler to run our tasks on. If we're in mode, we'll + /// run all our tasks concurrently. Otherwise, we will run them serially using + /// + private readonly TaskScheduler _scheduler; + private static readonly TaskScheduler s_exclusiveScheduler = new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler; + public FindReferencesSearchEngine( Solution solution, IImmutableSet? documents, @@ -45,6 +52,12 @@ public FindReferencesSearchEngine( _options = options; _progressTracker = progress.ProgressTracker; + + // If we're an explicit invocation, just defer to the threadpool to execute all our work in parallel to get + // things done as quickly as possible. If we're running implicitly, then use a + // ConcurrentExclusiveSchedulerPair's exclusive scheduler as that's the most built-in way in the TPL to get + // will run things serially. + _scheduler = _options.Explicit ? TaskScheduler.Default : s_exclusiveScheduler; } public async Task FindReferencesAsync(ISymbol symbol) @@ -87,7 +100,7 @@ private async Task ProcessAsync(ProjectToDocumentMap projectToDocumentMap) using var _ = ArrayBuilder.GetInstance(out var tasks); foreach (var (project, documentMap) in projectToDocumentMap) - tasks.Add(Task.Run(() => ProcessProjectAsync(project, documentMap), _cancellationToken)); + tasks.Add(Task.Factory.StartNew(() => ProcessProjectAsync(project, documentMap), _cancellationToken, TaskCreationOptions.None, _scheduler).Unwrap()); await Task.WhenAll(tasks).ConfigureAwait(false); } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_MapCreation.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_MapCreation.cs index a59c78e7eb398..81c18d64475d3 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_MapCreation.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_MapCreation.cs @@ -34,8 +34,8 @@ private async Task CreateProjectToDocumentMapAsync(Project { foreach (var (symbol, finder) in projectQueue) { - tasks.Add(Task.Run(() => - DetermineDocumentsToSearchAsync(project, symbol, finder), _cancellationToken)); + tasks.Add(Task.Factory.StartNew(() => + DetermineDocumentsToSearchAsync(project, symbol, finder), _cancellationToken, TaskCreationOptions.None, _scheduler).Unwrap()); } } @@ -139,7 +139,7 @@ private async Task DetermineAllSymbolsCoreAsync( using var _ = ArrayBuilder.GetInstance(out var finderTasks); foreach (var f in _finders) { - finderTasks.Add(Task.Run(async () => + finderTasks.Add(Task.Factory.StartNew(async () => { using var _ = ArrayBuilder.GetInstance(out var symbolTasks); @@ -159,7 +159,7 @@ private async Task DetermineAllSymbolsCoreAsync( _cancellationToken.ThrowIfCancellationRequested(); await Task.WhenAll(symbolTasks).ConfigureAwait(false); - }, _cancellationToken)); + }, _cancellationToken, TaskCreationOptions.None, _scheduler).Unwrap()); } await Task.WhenAll(finderTasks).ConfigureAwait(false); @@ -177,7 +177,8 @@ private void AddSymbolTasks( { Contract.ThrowIfNull(child); _cancellationToken.ThrowIfCancellationRequested(); - symbolTasks.Add(Task.Run(() => DetermineAllSymbolsCoreAsync(child, result), _cancellationToken)); + symbolTasks.Add(Task.Factory.StartNew( + () => DetermineAllSymbolsCoreAsync(child, result), _cancellationToken, TaskCreationOptions.None, _scheduler).Unwrap()); } } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_ProjectProcessing.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_ProjectProcessing.cs index aa7c49aae6f30..e35744ee0a02d 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_ProjectProcessing.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_ProjectProcessing.cs @@ -30,7 +30,7 @@ private async Task ProcessProjectAsync( foreach (var (document, documentQueue) in documentMap) { if (document.Project == project) - documentTasks.Add(Task.Run(() => ProcessDocumentQueueAsync(document, documentQueue), _cancellationToken)); + documentTasks.Add(Task.Factory.StartNew(() => ProcessDocumentQueueAsync(document, documentQueue), _cancellationToken, TaskCreationOptions.None, _scheduler).Unwrap()); } await Task.WhenAll(documentTasks).ConfigureAwait(false); diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchOptions.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchOptions.cs index eccb2f2421ad5..0581091aa9f4d 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchOptions.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchOptions.cs @@ -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 System.Runtime.Serialization; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -15,7 +13,8 @@ internal sealed class FindReferencesSearchOptions public static readonly FindReferencesSearchOptions Default = new( associatePropertyReferencesWithSpecificAccessor: false, - cascade: true); + cascade: true, + @explicit: true); /// /// When searching for property, associate specific references we find to the relevant @@ -39,19 +38,45 @@ internal sealed class FindReferencesSearchOptions [DataMember(Order = 1)] public bool Cascade { get; } + /// + /// Whether or not this find ref operation was explicitly invoked or not. If explicit invoked, the find + /// references operation may use more resources to get the results faster. + /// + /// + /// Features that run automatically should consider setting this to to avoid + /// unnecessarily impacting the user while they are doing other work. + /// + [DataMember(Order = 2)] + public bool Explicit { get; } + public FindReferencesSearchOptions( bool associatePropertyReferencesWithSpecificAccessor, - bool cascade) + bool cascade, + bool @explicit) { AssociatePropertyReferencesWithSpecificAccessor = associatePropertyReferencesWithSpecificAccessor; Cascade = cascade; + Explicit = @explicit; } - public FindReferencesSearchOptions WithAssociatePropertyReferencesWithSpecificAccessor(bool associatePropertyReferencesWithSpecificAccessor) - => new(associatePropertyReferencesWithSpecificAccessor, Cascade); + public FindReferencesSearchOptions With( + Optional associatePropertyReferencesWithSpecificAccessor = default, + Optional cascade = default, + Optional @explicit = default) + { + var newAssociatePropertyReferencesWithSpecificAccessor = associatePropertyReferencesWithSpecificAccessor.HasValue ? associatePropertyReferencesWithSpecificAccessor.Value : AssociatePropertyReferencesWithSpecificAccessor; + var newCascade = cascade.HasValue ? cascade.Value : Cascade; + var newExplicit = @explicit.HasValue ? @explicit.Value : Explicit; + + if (newAssociatePropertyReferencesWithSpecificAccessor == AssociatePropertyReferencesWithSpecificAccessor && + newCascade == Cascade && + newExplicit == Explicit) + { + return this; + } - public FindReferencesSearchOptions WithCascade(bool cascade) - => new(AssociatePropertyReferencesWithSpecificAccessor, cascade); + return new FindReferencesSearchOptions(newAssociatePropertyReferencesWithSpecificAccessor, newCascade, newExplicit); + } /// /// For IDE features, if the user starts searching on an accessor, then we want to give @@ -59,6 +84,6 @@ public FindReferencesSearchOptions WithCascade(bool cascade) /// then associate everything with the property. /// public static FindReferencesSearchOptions GetFeatureOptionsForStartingSymbol(ISymbol symbol) - => Default.WithAssociatePropertyReferencesWithSpecificAccessor(symbol.IsPropertyAccessor()); + => Default.With(associatePropertyReferencesWithSpecificAccessor: symbol.IsPropertyAccessor()); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertyAccessorSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertyAccessorSymbolReferenceFinder.cs index 2831f558a81a0..a88717d9f8c3e 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertyAccessorSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertyAccessorSymbolReferenceFinder.cs @@ -56,7 +56,7 @@ protected override async Task> DetermineDocumentsToSear // defer to the Property finder to find these docs and combine them with the result. var propertyDocuments = await ReferenceFinders.Property.DetermineDocumentsToSearchAsync( property, project, documents, - options.WithAssociatePropertyReferencesWithSpecificAccessor(false), + options.With(associatePropertyReferencesWithSpecificAccessor: false), cancellationToken).ConfigureAwait(false); result = result.AddRange(propertyDocuments); @@ -77,7 +77,7 @@ protected override async ValueTask> FindReference { var propertyReferences = await ReferenceFinders.Property.FindReferencesInDocumentAsync( property, document, semanticModel, - options.WithAssociatePropertyReferencesWithSpecificAccessor(false), + options.With(associatePropertyReferencesWithSpecificAccessor: false), cancellationToken).ConfigureAwait(false); var syntaxFacts = document.GetRequiredLanguageService(); diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_FindReferences_Legacy.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_FindReferences_Legacy.cs index 8389c3d45c788..72dc8dc1151da 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_FindReferences_Legacy.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_FindReferences_Legacy.cs @@ -82,7 +82,7 @@ public static async Task> FindReferencesAsync( FindReferencesSearchOptions.Default, cancellationToken).ConfigureAwait(false); } - private static async Task> FindReferencesAsync( + internal static async Task> FindReferencesAsync( ISymbol symbol, Solution solution, IFindReferencesProgress progress,