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

Switch to a producer/consumer queue to search for add-using results. #73320

Merged
merged 5 commits into from
May 3, 2024
Merged
Changes from all 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 @@ -20,6 +20,7 @@
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.SymbolSearch;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
Expand Down Expand Up @@ -215,28 +216,30 @@ private static async Task FindResultsInUnreferencedProjectSourceSymbolsAsync(

var viableUnreferencedProjects = GetViableUnreferencedProjects(project);

// Search all unreferenced projects in parallel.
using var _ = ArrayBuilder<Task>.GetInstance(out var findTasks);

// Create another cancellation token so we can both search all projects in parallel,
// but also stop any searches once we get enough results.
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

foreach (var unreferencedProject in viableUnreferencedProjects)
{
if (!unreferencedProject.SupportsCompilation)
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 line moved into the functoin that returns the viableUnreferencedProjects

continue;

// Search in this unreferenced project. But don't search in any of its'
// direct references. i.e. we don't want to search in its metadata references
// or in the projects it references itself. We'll be searching those entities
// individually.
findTasks.Add(ProcessReferencesAsync(
allSymbolReferences, maxResults, linkedTokenSource,
finder.FindInSourceSymbolsInProjectAsync(projectToAssembly, unreferencedProject, exact, linkedTokenSource.Token)));
}

await Task.WhenAll(findTasks).ConfigureAwait(false);
// Defer to the ProducerConsumer. We're search the unreferenced projects in parallel. As we get results, we'll
// add them to the 'allSymbolReferences' queue. If we get enough results, we'll cancel all the other work.
await ProducerConsumer<ImmutableArray<SymbolReference>>.RunAsync(
ProducerConsumerOptions.SingleReaderOptions,
produceItems: static (onItemsFound, args) => RoslynParallel.ForEachAsync(
args.viableUnreferencedProjects,
args.linkedTokenSource.Token,
async (project, cancellationToken) =>
{
// Search in this unreferenced project. But don't search in any of its' direct references. i.e. we
// don't want to search in its metadata references or in the projects it references itself. We'll be
// searching those entities individually.
var references = await args.finder.FindInSourceSymbolsInProjectAsync(
args.projectToAssembly, project, args.exact, cancellationToken).ConfigureAwait(false);
onItemsFound(references);
}),
consumeItems: static (symbolReferencesEnumerable, args) =>
ProcessReferencesAsync(args.allSymbolReferences, args.maxResults, symbolReferencesEnumerable, args.linkedTokenSource),
args: (projectToAssembly, allSymbolReferences, maxResults, finder, exact, viableUnreferencedProjects, linkedTokenSource),
linkedTokenSource.Token).ConfigureAwait(false);
}

private async Task FindResultsInUnreferencedMetadataSymbolsAsync(
Expand All @@ -246,7 +249,7 @@ private async Task FindResultsInUnreferencedMetadataSymbolsAsync(
{
// Only do this if none of the project searches produced any results. We may have a
// lot of metadata to search through, and it would be good to avoid that if we can.
if (allSymbolReferences.Count > 0)
if (!allSymbolReferences.IsEmpty)
return;

// Keep track of the references we've seen (so that we don't process them multiple times
Expand All @@ -257,29 +260,36 @@ private async Task FindResultsInUnreferencedMetadataSymbolsAsync(

var newReferences = GetUnreferencedMetadataReferences(project, seenReferences);

// Search all metadata references in parallel.
using var _ = ArrayBuilder<Task>.GetInstance(out var findTasks);

// Create another cancellation token so we can both search all projects in parallel,
// but also stop any searches once we get enough results.
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

foreach (var (referenceProject, reference) in newReferences)
{
var compilation = referenceToCompilation.GetOrAdd(
reference, r => CreateCompilation(project, r));

// Ignore netmodules. First, they're incredibly esoteric and barely used.
// Second, the SymbolFinder API doesn't even support searching them.
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
{
findTasks.Add(ProcessReferencesAsync(
allSymbolReferences, maxResults, linkedTokenSource,
finder.FindInMetadataSymbolsAsync(assembly, referenceProject, reference, exact, linkedTokenSource.Token)));
}
}

await Task.WhenAll(findTasks).ConfigureAwait(false);
// Defer to the ProducerConsumer. We're search the metadata references in parallel. As we get results, we'll
// add them to the 'allSymbolReferences' queue. If we get enough results, we'll cancel all the other work.
await ProducerConsumer<ImmutableArray<SymbolReference>>.RunAsync(
ProducerConsumerOptions.SingleReaderOptions,
produceItems: static (onItemsFound, args) => RoslynParallel.ForEachAsync(
args.newReferences,
args.linkedTokenSource.Token,
async (tuple, cancellationToken) =>
{
var (referenceProject, reference) = tuple;
var compilation = args.referenceToCompilation.GetOrAdd(
reference, r => CreateCompilation(args.project, r));

// Ignore netmodules. First, they're incredibly esoteric and barely used.
// Second, the SymbolFinder API doesn't even support searching them.
if (compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol assembly)
return;

var references = await args.finder.FindInMetadataSymbolsAsync(
assembly, referenceProject, reference, args.exact, args.linkedTokenSource.Token).ConfigureAwait(false);
onItemsFound(references);
}),
consumeItems: static (symbolReferencesEnumerable, args) =>
ProcessReferencesAsync(args.allSymbolReferences, args.maxResults, symbolReferencesEnumerable, args.linkedTokenSource),
args: (referenceToCompilation, project, allSymbolReferences, maxResults, finder, exact, newReferences, linkedTokenSource),
linkedTokenSource.Token).ConfigureAwait(false);
}

/// <summary>
Expand All @@ -290,7 +300,7 @@ private async Task FindResultsInUnreferencedMetadataSymbolsAsync(
private static ImmutableArray<(Project, PortableExecutableReference)> GetUnreferencedMetadataReferences(
Project project, HashSet<PortableExecutableReference> seenReferences)
{
var result = ArrayBuilder<(Project, PortableExecutableReference)>.GetInstance();
using var _ = ArrayBuilder<(Project, PortableExecutableReference)>.GetInstance(out var result);

var solution = project.Solution;
foreach (var p in solution.Projects)
Expand All @@ -311,27 +321,31 @@ private async Task FindResultsInUnreferencedMetadataSymbolsAsync(
}
}

return result.ToImmutableAndFree();
return result.ToImmutableAndClear();
}

private static async Task ProcessReferencesAsync(
ConcurrentQueue<Reference> allSymbolReferences,
int maxResults,
CancellationTokenSource linkedTokenSource,
Task<ImmutableArray<SymbolReference>> task)
IAsyncEnumerable<ImmutableArray<SymbolReference>> reader,
CancellationTokenSource linkedTokenSource)
{
AddRange(allSymbolReferences, await task.ConfigureAwait(false));

// If we've gone over the max amount of items we're looking for, attempt to cancel all existing work that is
// still searching.
if (allSymbolReferences.Count >= maxResults)
await foreach (var symbolReferences in reader)
{
try
{
linkedTokenSource.Cancel();
}
catch (ObjectDisposedException)
linkedTokenSource.Token.ThrowIfCancellationRequested();
AddRange(allSymbolReferences, symbolReferences);

// If we've gone over the max amount of items we're looking for, attempt to cancel all existing work that is
// still searching.
if (allSymbolReferences.Count >= maxResults)
{
try
{
linkedTokenSource.Cancel();
}
catch (ObjectDisposedException)
{
}
}
}
}
Expand Down Expand Up @@ -419,7 +433,7 @@ int IEqualityComparer<PortableExecutableReference>.GetHashCode(PortableExecutabl
private static HashSet<Project> GetViableUnreferencedProjects(Project project)
{
var solution = project.Solution;
var viableProjects = new HashSet<Project>(solution.Projects);
var viableProjects = new HashSet<Project>(solution.Projects.Where(p => p.SupportsCompilation));

// Clearly we can't reference ourselves.
viableProjects.Remove(project);
Expand Down
Loading