Skip to content

Commit ad71cae

Browse files
Simplify channel-processing code in SemanticSearch. (#78060)
2 parents 1bc6acb + 98be4d4 commit ad71cae

File tree

2 files changed

+46
-71
lines changed

2 files changed

+46
-71
lines changed

src/Features/Core/Portable/SemanticSearch/Tools/ReferencingSyntaxFinder.cs

Lines changed: 23 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,17 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
#if NET6_0_OR_GREATER
5+
#if NET
66

77
using System;
88
using System.Collections.Generic;
99
using System.Collections.Immutable;
1010
using System.Threading;
11-
using System.Threading.Channels;
1211
using System.Threading.Tasks;
1312
using Microsoft.CodeAnalysis.FindSymbols;
14-
using Microsoft.CodeAnalysis.FindUsages;
13+
using Microsoft.CodeAnalysis.PooledObjects;
1514
using Microsoft.CodeAnalysis.Shared.Extensions;
1615
using Microsoft.CodeAnalysis.Shared.Utilities;
17-
using Microsoft.CodeAnalysis.Threading;
1816

1917
namespace Microsoft.CodeAnalysis.SemanticSearch;
2018

@@ -34,85 +32,39 @@ public IEnumerable<SyntaxNode> Find(ISymbol symbol)
3432

3533
public async IAsyncEnumerable<SyntaxNode> FindAsync(ISymbol symbol)
3634
{
37-
var channel = Channel.CreateUnbounded<ReferenceLocation>(new()
35+
using var _ = PooledHashSet<SyntaxNode>.GetInstance(out var cachedRoots);
36+
37+
// Kick off the SymbolFinder.FindReferencesAsync call on the provided symbol/solution. As it finds
38+
// ReferenceLocations, it will push those into the 'callback' delegate passed into it. ProducerConsumer will
39+
// then convert this to a simple IAsyncEnumerable<ReferenceLocation> that we can iterate over, converting those
40+
// locations to SyntaxNodes in the corresponding C# or VB document.
41+
await foreach (var item in ProducerConsumer<ReferenceLocation>.RunAsync(
42+
FindReferencesAsync, args: (solution, symbol), cancellationToken))
3843
{
39-
SingleReader = true,
40-
SingleWriter = false,
41-
});
42-
43-
using var _ = cancellationToken.Register(
44-
static (obj, cancellationToken) => ((Channel<SourceReferenceItem>)obj!).Writer.TryComplete(new OperationCanceledException(cancellationToken)),
45-
state: channel);
46-
47-
var progress = new Progress(channel);
48-
49-
var writeTask = ProduceItemsAndWriteToChannelAsync();
50-
51-
await foreach (var reference in channel.Reader.ReadAllAsync(cancellationToken))
52-
{
53-
// TODO: consider grouping by document to avoid repeated syntax root lookup
54-
55-
var root = await reference.Document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
44+
var root = await item.Document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
5645
if (root == null)
57-
{
5846
continue;
59-
}
60-
61-
yield return root.FindNode(reference.Location.SourceSpan, findInTrivia: true, getInnermostNodeForTie: true);
62-
}
63-
64-
await writeTask.ConfigureAwait(false);
65-
66-
async Task ProduceItemsAndWriteToChannelAsync()
67-
{
68-
await Task.Yield().ConfigureAwait(false);
6947

70-
Exception? exception = null;
71-
try
72-
{
73-
//await service.FindReferencesAsync(context, document, location.SourceSpan.Start, classificationOptions, cancellationToken).ConfigureAwait(false);
74-
await SymbolFinder.FindReferencesAsync(
75-
symbol,
76-
solution,
77-
progress,
78-
documents: null,
79-
s_options,
80-
cancellationToken).ConfigureAwait(false);
81-
}
82-
catch (Exception ex) when ((exception = ex) == null)
83-
{
84-
throw ExceptionUtilities.Unreachable();
85-
}
86-
finally
87-
{
88-
// No matter what path we take (exceptional or non-exceptional), always complete the channel so the
89-
// writing task knows it's done.
90-
channel.Writer.TryComplete(exception);
91-
}
48+
// Hold onto the root so that if we find more references in the same document, we don't have to reparse it.
49+
cachedRoots.Add(root);
50+
yield return item.Location.FindNode(findInsideTrivia: true, getInnermostNodeForTie: true, cancellationToken);
9251
}
9352
}
9453

95-
private sealed class Progress(Channel<ReferenceLocation> channel) : IStreamingFindReferencesProgress
96-
{
97-
public ValueTask OnStartedAsync(CancellationToken cancellationToken)
98-
=> ValueTask.CompletedTask;
99-
100-
public ValueTask OnCompletedAsync(CancellationToken cancellationToken)
101-
=> ValueTask.CompletedTask;
54+
private static Task FindReferencesAsync(Action<ReferenceLocation> callback, (Solution solution, ISymbol symbol) args, CancellationToken cancellationToken)
55+
=> SymbolFinder.FindReferencesAsync(
56+
args.symbol, args.solution, new Progress(callback), documents: null, s_options, cancellationToken);
10257

103-
public ValueTask OnDefinitionFoundAsync(SymbolGroup group, CancellationToken cancellationToken)
104-
=> ValueTask.CompletedTask;
58+
private sealed class Progress(Action<ReferenceLocation> callback) : IStreamingFindReferencesProgress
59+
{
60+
public ValueTask OnStartedAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
61+
public ValueTask OnCompletedAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
62+
public ValueTask OnDefinitionFoundAsync(SymbolGroup group, CancellationToken cancellationToken) => ValueTask.CompletedTask;
10563

10664
public ValueTask OnReferencesFoundAsync(ImmutableArray<(SymbolGroup group, ISymbol symbol, ReferenceLocation location)> references, CancellationToken cancellationToken)
10765
{
10866
foreach (var (_, _, location) in references)
109-
{
110-
// It's ok to use TryWrite here. TryWrite always succeeds unless the channel is completed. And the
111-
// channel is only ever completed by us (after produceItems completes or throws an exception) or if the
112-
// cancellationToken is triggered above in RunAsync. In that latter case, it's ok for writing to the
113-
// channel to do nothing as we no longer need to write out those assets to the pipe.
114-
_ = channel.Writer.TryWrite(location);
115-
}
67+
callback(location);
11668

11769
return ValueTask.CompletedTask;
11870
}

src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ProducerConsumer.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.Collections.Generic;
77
using System.Collections.Immutable;
8+
using System.Runtime.CompilerServices;
89
using System.Threading;
910
using System.Threading.Channels;
1011
using System.Threading.Tasks;
@@ -258,6 +259,28 @@ await args.produceItems(source, callback, args.args, cancellationToken).Configur
258259
cancellationToken);
259260
}
260261

262+
/// <summary>
263+
/// Version of <see cref="RunChannelAsync"/> when caller the prefers to just push all the results into a channel
264+
/// that it receives in the return value to process asynchronously.
265+
/// </summary>
266+
public static async IAsyncEnumerable<TItem> RunAsync<TArgs>(
267+
Func<Action<TItem>, TArgs, CancellationToken, Task> produceItems,
268+
TArgs args,
269+
[EnumeratorCancellation] CancellationToken cancellationToken)
270+
{
271+
var channelReader = await RunChannelAsync(
272+
// We're the only reader (in the foreach loop below). So we can use the single reader options.
273+
ProducerConsumerOptions.SingleReaderOptions,
274+
produceItems,
275+
// Trivially grab the reader and return it. We don't need to do any processing of the values, as the
276+
// callers just wants them as is.
277+
consumeItems: static (reader, _, _) => Task.FromResult(reader),
278+
args, cancellationToken).ConfigureAwait(false);
279+
280+
await foreach (var item in channelReader.ReadAllAsync(cancellationToken))
281+
yield return item;
282+
}
283+
261284
/// <summary>
262285
/// Helper utility for the pattern of a pair of a production routine and consumption routine using a channel to
263286
/// coordinate data transfer. The provided <paramref name="options"/> are used to create a <see

0 commit comments

Comments
 (0)