Skip to content

Commit ae6d690

Browse files
Add code for analzying copilot changes. (#78353)
2 parents f1ada62 + e0a7ed0 commit ae6d690

File tree

16 files changed

+579
-113
lines changed

16 files changed

+579
-113
lines changed

src/EditorFeatures/Core.Wpf/Copilot/CopilotWpfTextCreationListener.cs

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
1515
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
1616
using Microsoft.CodeAnalysis.Host.Mef;
17+
using Microsoft.CodeAnalysis.Options;
1718
using Microsoft.CodeAnalysis.Shared.TestHooks;
1819
using Microsoft.CodeAnalysis.Text;
1920
using Microsoft.CodeAnalysis.Threading;
@@ -29,27 +30,31 @@ namespace Microsoft.CodeAnalysis.Copilot;
2930
[TextViewRole(PredefinedTextViewRoles.Document)]
3031
internal sealed class CopilotWpfTextViewCreationListener : IWpfTextViewCreationListener
3132
{
33+
private readonly IGlobalOptionService _globalOptions;
3234
private readonly IThreadingContext _threadingContext;
3335
private readonly Lazy<SuggestionServiceBase> _suggestionServiceBase;
3436
private readonly IAsynchronousOperationListener _listener;
3537

36-
private readonly AsyncBatchingWorkQueue<SuggestionAcceptedEventArgs> _workQueue;
38+
private readonly AsyncBatchingWorkQueue<(bool accepted, ProposalBase proposal)> _completionWorkQueue;
3739

3840
private int _started;
3941

4042
[ImportingConstructor]
4143
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
4244
public CopilotWpfTextViewCreationListener(
45+
IGlobalOptionService globalOptions,
4346
IThreadingContext threadingContext,
4447
Lazy<SuggestionServiceBase> suggestionServiceBase,
4548
IAsynchronousOperationListenerProvider listenerProvider)
4649
{
50+
_globalOptions = globalOptions;
4751
_threadingContext = threadingContext;
4852
_suggestionServiceBase = suggestionServiceBase;
4953
_listener = listenerProvider.GetListener(FeatureAttribute.CopilotChangeAnalysis);
50-
_workQueue = new AsyncBatchingWorkQueue<SuggestionAcceptedEventArgs>(
54+
55+
_completionWorkQueue = new AsyncBatchingWorkQueue<(bool accepted, ProposalBase proposal)>(
5156
DelayTimeSpan.Idle,
52-
ProcessEventsAsync,
57+
ProcessCompletionEventsAsync,
5358
_listener,
5459
_threadingContext.DisposalToken);
5560
}
@@ -64,30 +69,41 @@ public void TextViewCreated(IWpfTextView textView)
6469
Task.Run(() =>
6570
{
6671
var suggestionService = _suggestionServiceBase.Value;
67-
suggestionService.SuggestionAccepted += OnSuggestionAccepted;
72+
suggestionService.SuggestionAccepted += OnCompletionSuggestionAccepted;
73+
suggestionService.SuggestionDismissed += OnCompletionSuggestionDismissed;
6874
}).CompletesAsyncOperation(token);
6975
}
7076
}
7177

72-
private void OnSuggestionAccepted(object sender, SuggestionAcceptedEventArgs e)
78+
private void OnCompletionSuggestionAccepted(object sender, SuggestionAcceptedEventArgs e)
79+
=> OnCompletionSuggestionEvent(accepted: true, e.FinalProposal);
80+
81+
private void OnCompletionSuggestionDismissed(object sender, SuggestionDismissedEventArgs e)
82+
=> OnCompletionSuggestionEvent(accepted: false, e.FinalProposal);
83+
84+
private void OnCompletionSuggestionEvent(bool accepted, ProposalBase? proposal)
7385
{
74-
if (e.FinalProposal.Edits.Count == 0)
86+
if (proposal is not { Edits.Count: > 0 })
7587
return;
7688

77-
_workQueue.AddWork(e);
89+
_completionWorkQueue.AddWork((accepted, proposal));
7890
}
7991

80-
private async ValueTask ProcessEventsAsync(
81-
ImmutableSegmentedList<SuggestionAcceptedEventArgs> list, CancellationToken cancellationToken)
92+
private async ValueTask ProcessCompletionEventsAsync(
93+
ImmutableSegmentedList<(bool accepted, ProposalBase proposal)> list, CancellationToken cancellationToken)
8294
{
83-
foreach (var eventArgs in list)
84-
await ProcessEventAsync(eventArgs, cancellationToken).ConfigureAwait(false);
95+
// Ignore if analyzing changes is disabled for this user.
96+
if (!_globalOptions.GetOption(CopilotOptions.AnalyzeCopilotChanges))
97+
return;
98+
99+
foreach (var (accepted, proposal) in list)
100+
await ProcessCompletionEventAsync(accepted, proposal, cancellationToken).ConfigureAwait(false);
85101
}
86102

87-
private static async ValueTask ProcessEventAsync(
88-
SuggestionAcceptedEventArgs eventArgs, CancellationToken cancellationToken)
103+
private static async ValueTask ProcessCompletionEventAsync(
104+
bool accepted, ProposalBase proposal, CancellationToken cancellationToken)
89105
{
90-
var proposal = eventArgs.FinalProposal;
106+
const string featureId = "Completion";
91107
var proposalId = proposal.ProposalId;
92108

93109
foreach (var editGroup in proposal.Edits.GroupBy(e => e.Span.Snapshot))
@@ -100,13 +116,22 @@ private static async ValueTask ProcessEventAsync(
100116
if (document is null)
101117
continue;
102118

119+
// Currently we do not support analyzing languges other than C# and VB. This is because we only want to do
120+
// this analsis in our OOP process to avoid perf impact on the VS process. And we don't have OOP for other
121+
// languages yet.
122+
if (!document.SupportsSemanticModel)
123+
continue;
124+
103125
var normalizedEdits = Normalize(editGroup);
104126
if (normalizedEdits.IsDefaultOrEmpty)
105127
continue;
106128

107129
var changeAnalysisService = document.Project.Solution.Services.GetRequiredService<ICopilotChangeAnalysisService>();
108-
await changeAnalysisService.AnalyzeChangeAsync(
109-
document, normalizedEdits, proposalId, cancellationToken).ConfigureAwait(false);
130+
var analysisResult = await changeAnalysisService.AnalyzeChangeAsync(
131+
document, normalizedEdits, cancellationToken).ConfigureAwait(false);
132+
133+
CopilotChangeAnalysisUtilities.LogCopilotChangeAnalysis(
134+
featureId, accepted, proposalId, analysisResult, cancellationToken).Dispose();
110135
}
111136
}
112137

src/EditorFeatures/TestUtilities/Diagnostics/DiagnosticTaggerWrapper.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using System.Threading.Tasks;
1010
using Microsoft.CodeAnalysis.Diagnostics;
1111
using Microsoft.CodeAnalysis.Editor.InlineDiagnostics;
12-
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
1312
using Microsoft.CodeAnalysis.Shared.TestHooks;
1413
using Microsoft.CodeAnalysis.SolutionCrawler;
1514
using Microsoft.CodeAnalysis.Test.Utilities;
@@ -23,7 +22,6 @@ internal sealed class DiagnosticTaggerWrapper<TProvider, TTag>
2322
where TTag : ITag
2423
{
2524
private readonly EditorTestWorkspace _workspace;
26-
private readonly IThreadingContext _threadingContext;
2725
private readonly IAsynchronousOperationListenerProvider _listenerProvider;
2826

2927
private AbstractDiagnosticsTaggerProvider<TTag>? _taggerProvider;
@@ -33,7 +31,6 @@ public DiagnosticTaggerWrapper(
3331
IReadOnlyDictionary<string, ImmutableArray<DiagnosticAnalyzer>>? analyzerMap = null,
3432
bool createTaggerProvider = true)
3533
{
36-
_threadingContext = workspace.GetService<IThreadingContext>();
3734
_listenerProvider = workspace.GetService<IAsynchronousOperationListenerProvider>();
3835

3936
var analyzerReference = new TestAnalyzerReferenceByLanguage(analyzerMap ?? DiagnosticExtensions.GetCompilerDiagnosticAnalyzersMap());

src/Features/Core/Portable/Copilot/CopilotChangeAnalysis.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,17 @@ namespace Microsoft.CodeAnalysis.Copilot;
1515
[DataContract]
1616
internal readonly record struct CopilotChangeAnalysis(
1717
[property: DataMember(Order = 0)] bool Succeeded,
18-
[property: DataMember(Order = 1)] TimeSpan TotalAnalysisTime,
19-
[property: DataMember(Order = 2)] TimeSpan TotalDiagnosticComputationTime,
20-
[property: DataMember(Order = 3)] ImmutableArray<CopilotDiagnosticAnalysis> DiagnosticAnalyses,
21-
[property: DataMember(Order = 4)] CopilotCodeFixAnalysis CodeFixAnalysis);
18+
[property: DataMember(Order = 1)] int OldDocumentLength,
19+
[property: DataMember(Order = 2)] int NewDocumentLength,
20+
[property: DataMember(Order = 3)] int TextChangeDelta,
21+
[property: DataMember(Order = 4)] int ProjectDocumentCount,
22+
[property: DataMember(Order = 5)] int ProjectSourceGeneratedDocumentCount,
23+
[property: DataMember(Order = 6)] int ProjectConeCount,
24+
[property: DataMember(Order = 7)] TimeSpan TotalAnalysisTime,
25+
[property: DataMember(Order = 8)] TimeSpan ForkingTime,
26+
[property: DataMember(Order = 9)] TimeSpan TotalDiagnosticComputationTime,
27+
[property: DataMember(Order = 10)] ImmutableArray<CopilotDiagnosticAnalysis> DiagnosticAnalyses,
28+
[property: DataMember(Order = 11)] CopilotCodeFixAnalysis CodeFixAnalysis);
2229

2330
/// <param name="Kind">What diagnostic kind this is analysis data for.</param>
2431
/// <param name="ComputationTime">How long it took to produce the diagnostics for this diagnostic kind.</param>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.ComponentModel;
8+
using System.Linq;
9+
using System.Threading;
10+
using Microsoft.CodeAnalysis.Internal.Log;
11+
12+
namespace Microsoft.CodeAnalysis.Copilot;
13+
14+
internal static class CopilotChangeAnalysisUtilities
15+
{
16+
public static IDisposable LogCopilotChangeAnalysis(
17+
string featureId, bool accepted, string proposalId, CopilotChangeAnalysis analysisResult, CancellationToken cancellationToken)
18+
{
19+
return Logger.LogBlock(FunctionId.Copilot_AnalyzeChange, KeyValueLogMessage.Create(static (d, args) =>
20+
{
21+
var (featureId, accepted, proposalId, analysisResult) = args;
22+
d["Accepted"] = accepted;
23+
d["FeatureId"] = featureId;
24+
d["ProposalId"] = proposalId;
25+
26+
d["Succeeded"] = analysisResult.Succeeded;
27+
28+
d["OldDocumentLength"] = analysisResult.OldDocumentLength;
29+
d["NewDocumentLength"] = analysisResult.NewDocumentLength;
30+
d["TextChangeDelta"] = analysisResult.TextChangeDelta;
31+
32+
d["ProjectDocumentCount"] = analysisResult.ProjectDocumentCount;
33+
d["ProjectSourceGeneratedDocumentCount"] = analysisResult.ProjectSourceGeneratedDocumentCount;
34+
d["ProjectConeCount"] = analysisResult.ProjectConeCount;
35+
36+
foreach (var diagnosticAnalysis in analysisResult.DiagnosticAnalyses)
37+
{
38+
var keyPrefix = $"DiagnosticAnalysis_{diagnosticAnalysis.Kind}";
39+
40+
d[$"{keyPrefix}_ComputationTime"] = Stringify(diagnosticAnalysis.ComputationTime);
41+
d[$"{keyPrefix}_IdToCount"] = StringifyDictionary(diagnosticAnalysis.IdToCount);
42+
d[$"{keyPrefix}_CategoryToCount"] = StringifyDictionary(diagnosticAnalysis.CategoryToCount);
43+
d[$"{keyPrefix}_SeverityToCount"] = StringifyDictionary(diagnosticAnalysis.SeverityToCount);
44+
}
45+
46+
d["CodeFixAnalysis_TotalComputationTime"] = Stringify(analysisResult.CodeFixAnalysis.TotalComputationTime);
47+
d["CodeFixAnalysis_TotalApplicationTime"] = Stringify(analysisResult.CodeFixAnalysis.TotalApplicationTime);
48+
d["CodeFixAnalysis_DiagnosticIdToCount"] = StringifyDictionary(analysisResult.CodeFixAnalysis.DiagnosticIdToCount);
49+
d["CodeFixAnalysis_DiagnosticIdToApplicationTime"] = StringifyDictionary(analysisResult.CodeFixAnalysis.DiagnosticIdToApplicationTime);
50+
d["CodeFixAnalysis_DiagnosticIdToProviderName"] = StringifyDictionary(analysisResult.CodeFixAnalysis.DiagnosticIdToProviderName);
51+
d["CodeFixAnalysis_ProviderNameToApplicationTime"] = StringifyDictionary(analysisResult.CodeFixAnalysis.ProviderNameToApplicationTime);
52+
}, args: (featureId, accepted, proposalId, analysisResult)),
53+
cancellationToken);
54+
}
55+
56+
private static string StringifyDictionary<TKey, TValue>(Dictionary<TKey, TValue> dictionary) where TKey : notnull where TValue : notnull
57+
=> string.Join(",", dictionary.Select(kvp => FormattableString.Invariant($"{kvp.Key}_{Stringify(kvp.Value)}")).OrderBy(v => v));
58+
59+
private static string Stringify<TValue>(TValue value) where TValue : notnull
60+
{
61+
if (value is IEnumerable<string> strings)
62+
return string.Join(":", strings.OrderBy(v => v));
63+
64+
if (value is TimeSpan timeSpan)
65+
return timeSpan.TotalMilliseconds.ToString("G17");
66+
67+
return value.ToString() ?? "";
68+
}
69+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis.Options;
6+
7+
namespace Microsoft.CodeAnalysis.Copilot;
8+
9+
internal static class CopilotOptions
10+
{
11+
public static Option2<bool> AnalyzeCopilotChanges { get; } = new Option2<bool>("dotnet_analyze_copilot_changes", defaultValue: true);
12+
}

0 commit comments

Comments
 (0)