Skip to content

Commit 0211c26

Browse files
Cleanup the code we have to run copilot code in parallel (#78324)
2 parents 6911efb + a978dea commit 0211c26

File tree

4 files changed

+53
-88
lines changed

4 files changed

+53
-88
lines changed

src/Features/ExternalAccess/Copilot/Internal/Completion/CSharpContextProviderService.cs

Lines changed: 23 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -6,80 +6,34 @@
66
using System.Collections.Generic;
77
using System.Collections.Immutable;
88
using System.Composition;
9-
using System.Linq;
10-
using System.Runtime.CompilerServices;
119
using System.Threading;
12-
using System.Threading.Tasks;
13-
using Microsoft.CodeAnalysis;
14-
using Microsoft.CodeAnalysis.ErrorReporting;
1510
using Microsoft.CodeAnalysis.ExternalAccess.Copilot.Completion;
1611
using Microsoft.CodeAnalysis.Host.Mef;
17-
using Microsoft.VisualStudio.Threading;
12+
using Microsoft.CodeAnalysis.Shared.Utilities;
1813

1914
namespace Microsoft.CodeAnalysis.ExternalAccess.Copilot.Internal.Completion;
2015

21-
[Shared]
22-
[Export(typeof(ICSharpCopilotContextProviderService))]
23-
internal sealed class CSharpContextProviderService : ICSharpCopilotContextProviderService
16+
[Export(typeof(ICSharpCopilotContextProviderService)), Shared]
17+
[method: ImportingConstructor]
18+
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
19+
internal sealed class CSharpContextProviderService([ImportMany] IEnumerable<IContextProvider> providers)
20+
: ICSharpCopilotContextProviderService
2421
{
25-
// Exposed for testing
26-
public ImmutableArray<IContextProvider> Providers { get; }
27-
28-
[ImportingConstructor]
29-
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
30-
public CSharpContextProviderService([ImportMany] IEnumerable<IContextProvider> providers)
31-
{
32-
Providers = providers.ToImmutableArray();
33-
}
34-
35-
public async IAsyncEnumerable<IContextItem> GetContextItemsAsync(Document document, int position, IReadOnlyDictionary<string, object> activeExperiments, [EnumeratorCancellation] CancellationToken cancellationToken)
36-
{
37-
var queue = new AsyncQueue<IContextItem>();
38-
var tasks = this.Providers.Select(provider => Task.Run(async () =>
39-
{
40-
try
41-
{
42-
await provider.ProvideContextItemsAsync(document, position, activeExperiments, ProvideItemsAsync, cancellationToken).ConfigureAwait(false);
43-
}
44-
catch (Exception exception) when (FatalError.ReportAndCatchUnlessCanceled(exception, ErrorSeverity.General))
45-
{
46-
}
47-
},
48-
cancellationToken));
49-
50-
// Let all providers run in parallel in the background, so we can steam results as they come in.
51-
// Complete the queue when all providers are done.
52-
_ = Task.WhenAll(tasks)
53-
.ContinueWith((_, __) => queue.Complete(),
54-
null,
55-
cancellationToken,
56-
TaskContinuationOptions.ExecuteSynchronously,
57-
TaskScheduler.Default);
58-
59-
while (true)
60-
{
61-
IContextItem item;
62-
try
63-
{
64-
item = await queue.DequeueAsync(cancellationToken).ConfigureAwait(false);
65-
}
66-
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
67-
{
68-
// Dequeue is cancelled because the queue is empty and completed, we can break out of the loop.
69-
break;
70-
}
71-
72-
yield return item;
73-
}
74-
75-
ValueTask ProvideItemsAsync(ImmutableArray<IContextItem> items, CancellationToken cancellationToken)
76-
{
77-
foreach (var item in items)
78-
{
79-
queue.Enqueue(item);
80-
}
81-
82-
return default;
83-
}
84-
}
22+
private readonly ImmutableArray<IContextProvider> _providers = [.. providers];
23+
24+
public IAsyncEnumerable<IContextItem> GetContextItemsAsync(Document document, int position, IReadOnlyDictionary<string, object> activeExperiments, CancellationToken cancellationToken)
25+
=> ProducerConsumer<IContextItem>.RunParallelStreamAsync(
26+
_providers,
27+
static async (provider, callback, args, cancellationToken) =>
28+
await provider.ProvideContextItemsAsync(
29+
args.document, args.position, args.activeExperiments,
30+
(items, cancellationToken) =>
31+
{
32+
foreach (var item in items)
33+
callback(item);
34+
35+
return default;
36+
}, cancellationToken).ConfigureAwait(false),
37+
args: (document, position, activeExperiments),
38+
cancellationToken);
8539
}

src/Features/ExternalAccess/Copilot/Microsoft.CodeAnalysis.ExternalAccess.Copilot.csproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@
3030
<ProjectReference Include="..\..\Core\Portable\Microsoft.CodeAnalysis.Features.csproj" />
3131
</ItemGroup>
3232

33-
<ItemGroup>
34-
<PackageReference Include="Microsoft.VisualStudio.Threading" />
35-
</ItemGroup>
36-
3733
<ItemGroup>
3834
<PublicAPI Include="PublicAPI.Shipped.txt" />
3935
<PublicAPI Include="PublicAPI.Unshipped.txt" />

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/CopilotCompletion/CopilotCompletionResolveContextHandler.cs

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,29 @@
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-
using System.Collections.Immutable;
65
using System.Composition;
76
using Microsoft.CodeAnalysis.ExternalAccess.Copilot.Completion;
7+
using Microsoft.CodeAnalysis.PooledObjects;
88
using Microsoft.CodeAnalysis.Text;
99
using Roslyn.LanguageServer.Protocol;
1010

1111
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Copilot;
1212

13-
[Shared]
1413
[Method(MethodName)]
15-
[ExportCSharpVisualBasicStatelessLspService(typeof(CopilotCompletionResolveContextHandler), WellKnownLspServerKinds.Any)]
16-
internal sealed class CopilotCompletionResolveContextHandler : ILspServiceDocumentRequestHandler<ContextResolveParam, IContextItem[]>
14+
[ExportCSharpVisualBasicStatelessLspService(typeof(CopilotCompletionResolveContextHandler), WellKnownLspServerKinds.Any), Shared]
15+
[method: ImportingConstructor]
16+
[method: Obsolete("This exported object must be obtained through the MEF export provider.", error: true)]
17+
internal sealed class CopilotCompletionResolveContextHandler(ICSharpCopilotContextProviderService contextProviderService)
18+
: ILspServiceDocumentRequestHandler<ContextResolveParam, IContextItem[]>
1719
{
1820
// "@2" prefix to differentiate it from the implementation previously located in devkit extension.
1921
private const string MethodName = "roslyn/resolveContext@2";
2022

21-
[ImportingConstructor]
22-
[Obsolete("This exported object must be obtained through the MEF export provider.", error: true)]
23-
public CopilotCompletionResolveContextHandler(ICSharpCopilotContextProviderService contextProviderService)
24-
{
25-
ContextProviderService = contextProviderService;
26-
}
27-
2823
public bool MutatesSolutionState => false;
2924

3025
public bool RequiresLSPSolution => true;
3126

32-
public ICSharpCopilotContextProviderService ContextProviderService { get; }
27+
public ICSharpCopilotContextProviderService ContextProviderService { get; } = contextProviderService;
3328

3429
public TextDocumentIdentifier GetTextDocumentIdentifier(ContextResolveParam request)
3530
=> request.DocumentContext.TextDocument;
@@ -41,13 +36,11 @@ public async Task<IContextItem[]> HandleRequestAsync(ContextResolveParam param,
4136

4237
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
4338
var position = text.Lines.GetPosition(linePosition);
44-
var builder = ImmutableArray.CreateBuilder<IContextItem>();
4539
var activeExperiments = param.GetUnpackedActiveExperiments();
4640

41+
using var _ = ArrayBuilder<IContextItem>.GetInstance(out var builder);
4742
await foreach (var item in ContextProviderService.GetContextItemsAsync(document, position, activeExperiments, cancellationToken).ConfigureAwait(false))
48-
{
4943
builder.Add(item);
50-
}
5144

5245
return builder.ToArray();
5346
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,28 @@ public static async IAsyncEnumerable<TItem> RunAsync<TArgs>(
281281
yield return item;
282282
}
283283

284+
/// <summary>
285+
/// Equivalent to <see cref="RunParallelAsync{TSource, TArgs}(IEnumerable{TSource}, Func{TSource, Action{TItem}, TArgs, CancellationToken, Task}, TArgs, CancellationToken)"/>,
286+
/// but returns value as an <see cref="IAsyncEnumerable{TItem}"/>. Versus an <see cref="ImmutableArray{TItem}"/>.
287+
/// This is useful for cases where the caller wants to stream over the results as they are produced, rather than
288+
/// waiting on the full set to be produced before processing them.
289+
/// </summary>
290+
public static IAsyncEnumerable<TItem> RunParallelStreamAsync<TSource, TArgs>(
291+
IEnumerable<TSource> source,
292+
Func<TSource, Action<TItem>, TArgs, CancellationToken, Task> produceItems,
293+
TArgs args,
294+
CancellationToken cancellationToken)
295+
{
296+
return RunAsync(
297+
static (callback, args, cancellationToken) =>
298+
RoslynParallel.ForEachAsync(
299+
args.source, cancellationToken,
300+
async (source, cancellationToken) => await args.produceItems(
301+
source, callback, args.args, cancellationToken).ConfigureAwait(false)),
302+
args: (source, produceItems, args),
303+
cancellationToken);
304+
}
305+
284306
/// <summary>
285307
/// Helper utility for the pattern of a pair of a production routine and consumption routine using a channel to
286308
/// coordinate data transfer. The provided <paramref name="options"/> are used to create a <see

0 commit comments

Comments
 (0)