Skip to content
Merged
Show file tree
Hide file tree
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 @@ -46,11 +46,13 @@ internal readonly record struct CopilotDiagnosticAnalysis(
/// <param name="DiagnosticIdToApplicationTime">Mapping from diagnostic id to the total time taken to fix diagnostics with that id.</param>
/// <param name="DiagnosticIdToProviderName">Mapping from diagnostic id to the name of the provider that provided the fix.</param>
/// <param name="ProviderNameToApplicationTime">Mapping from provider name to the total time taken to fix diagnostics with that provider.</param>
/// <param name="ProviderNameToHasConflict">Mapping from provider name to whether or not that provider conflicted with another provider in producing a fix.</param>
[DataContract]
internal readonly record struct CopilotCodeFixAnalysis(
[property: DataMember(Order = 0)] TimeSpan TotalComputationTime,
[property: DataMember(Order = 1)] TimeSpan TotalApplicationTime,
[property: DataMember(Order = 2)] Dictionary<string, int> DiagnosticIdToCount,
[property: DataMember(Order = 3)] Dictionary<string, TimeSpan> DiagnosticIdToApplicationTime,
[property: DataMember(Order = 4)] Dictionary<string, HashSet<string>> DiagnosticIdToProviderName,
[property: DataMember(Order = 5)] Dictionary<string, TimeSpan> ProviderNameToApplicationTime);
[property: DataMember(Order = 5)] Dictionary<string, TimeSpan> ProviderNameToApplicationTime,
[property: DataMember(Order = 6)] Dictionary<string, bool> ProviderNameToHasConflict);
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public static IDisposable LogCopilotChangeAnalysis(
d["CodeFixAnalysis_DiagnosticIdToApplicationTime"] = StringifyDictionary(analysisResult.CodeFixAnalysis.DiagnosticIdToApplicationTime);
d["CodeFixAnalysis_DiagnosticIdToProviderName"] = StringifyDictionary(analysisResult.CodeFixAnalysis.DiagnosticIdToProviderName);
d["CodeFixAnalysis_ProviderNameToApplicationTime"] = StringifyDictionary(analysisResult.CodeFixAnalysis.ProviderNameToApplicationTime);
d["CodeFixAnalysis_ProviderNameToHasConflict"] = StringifyDictionary(analysisResult.CodeFixAnalysis.ProviderNameToHasConflict);
}, args: (featureId, accepted, proposalId, analysisResult)),
cancellationToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared;
Expand Down Expand Up @@ -255,13 +254,14 @@ private async Task<CopilotCodeFixAnalysis> ComputeCodeFixAnalysisAsync(
var diagnosticIdToApplicationTime = new Dictionary<string, TimeSpan>();
var diagnosticIdToProviderName = new Dictionary<string, HashSet<string>>();
var providerNameToApplicationTime = new Dictionary<string, TimeSpan>();
var providerNameToHasConflict = new Dictionary<string, bool>();

var totalApplicationTimeStopWatch = SharedStopwatch.StartNew();
await ProducerConsumer<(CodeFixCollection collection, TimeSpan elapsedTime)>.RunParallelAsync(
codeFixCollections,
produceItems: static async (codeFixCollection, callback, args, cancellationToken) =>
{
var (@this, solution, _, _, _, _) = args;
var (@this, solution, _, _, _, _, _) = args;
var firstAction = GetFirstAction(codeFixCollection.Fixes[0]);

var applicationTimeStopWatch = SharedStopwatch.StartNew();
Expand All @@ -270,19 +270,48 @@ private async Task<CopilotCodeFixAnalysis> ComputeCodeFixAnalysisAsync(
},
consumeItems: static async (values, args, cancellationToken) =>
{
var (@this, solution, diagnosticIdToCount, diagnosticIdToApplicationTime, diagnosticIdToProviderName, providerNameToApplicationTime) = args;
var (@this, solution, diagnosticIdToCount, diagnosticIdToApplicationTime, diagnosticIdToProviderName, providerNameToApplicationTime, providerNameToHasConflict) = args;

// Track which text span each code fix says it will be fixing. We can use this to efficiently determine
// which codefixes 'conflict' with some other codefix (in that that multiple features think they can fix
// the same span of code). We would need some mechanism to determine which we would prefer to take in
// order to have a good experience in such a case.
var intervalTree = new SimpleMutableIntervalTree<CodeFixCollection, CodeFixCollectionIntervalIntrospector>(new CodeFixCollectionIntervalIntrospector());

await foreach (var (codeFixCollection, applicationTime) in values)
{
var diagnosticId = codeFixCollection.FirstDiagnostic.Id;
var providerName = codeFixCollection.Provider.GetType().FullName![RoslynPrefix.Length..];
var providerName = GetProviderName(codeFixCollection);

IncrementCount(diagnosticIdToCount, diagnosticId);
IncrementElapsedTime(diagnosticIdToApplicationTime, diagnosticId, applicationTime);
diagnosticIdToProviderName.MultiAdd(diagnosticId, providerName);
IncrementElapsedTime(providerNameToApplicationTime, providerName, applicationTime);

intervalTree.AddIntervalInPlace(codeFixCollection);
}

// Now go over the fixed spans and see which intersect with other spans
using var intersectingCollections = TemporaryArray<CodeFixCollection>.Empty;
foreach (var codeFixCollection in intervalTree)
{
intersectingCollections.Clear();
intervalTree.FillWithIntervalsThatIntersectWith(
codeFixCollection.TextSpan.Start,
codeFixCollection.TextSpan.Length,
ref intersectingCollections.AsRef());

var providerName = GetProviderName(codeFixCollection);

// >= 2 because we want to see how many total fixers fix a particular span, and we only care if
// we're seeing multiple.
var newHasConflictValue = intersectingCollections.Count >= 2;
var storedHasConflictValue = providerNameToHasConflict.TryGetValue(providerName, out var currentHasConflictValue) && currentHasConflictValue;

providerNameToHasConflict[providerName] = storedHasConflictValue || newHasConflictValue;
}
},
args: (@this: this, newDocument.Project.Solution, diagnosticIdToCount, diagnosticIdToApplicationTime, diagnosticIdToProviderName, providerNameToApplicationTime),
args: (@this: this, newDocument.Project.Solution, diagnosticIdToCount, diagnosticIdToApplicationTime, diagnosticIdToProviderName, providerNameToApplicationTime, providerNameToHasConflict),
cancellationToken).ConfigureAwait(false);
var totalApplicationTime = totalApplicationTimeStopWatch.Elapsed;

Expand All @@ -292,16 +321,15 @@ private async Task<CopilotCodeFixAnalysis> ComputeCodeFixAnalysisAsync(
diagnosticIdToCount,
diagnosticIdToApplicationTime,
diagnosticIdToProviderName,
providerNameToApplicationTime);
providerNameToApplicationTime,
providerNameToHasConflict);

Task<ImmutableArray<CodeFixCollection>> ComputeCodeFixCollectionsAsync()
{
return ProducerConsumer<CodeFixCollection>.RunParallelAsync(
newSpans,
static async (span, callback, args, cancellationToken) =>
{
var intervalTree = new TextSpanMutableIntervalTree();

var (@this, newDocument) = args;
await foreach (var codeFixCollection in @this._codeFixService.StreamFixesAsync(
newDocument, span, cancellationToken).ConfigureAwait(false))
Expand All @@ -316,17 +344,21 @@ static async (span, callback, args, cancellationToken) =>
IsVisibleDiagnostic(codeFix.PrimaryDiagnostic.IsSuppressed, codeFix.PrimaryDiagnostic.Severity) &&
(codeFixCollection.Provider.GetType().Namespace ?? "").StartsWith(RoslynPrefix))
{
// The first for a particular span is the one we would apply. Ignore others that fix the same span.
if (intervalTree.HasIntervalThatOverlapsWith(codeFixCollection.TextSpan))
continue;

intervalTree.AddIntervalInPlace(codeFixCollection.TextSpan);
callback(codeFixCollection);
}
}
},
args: (@this: this, newDocument),
cancellationToken);
}

static string GetProviderName(CodeFixCollection codeFixCollection)
=> codeFixCollection.Provider.GetType().FullName![RoslynPrefix.Length..];
}

private readonly struct CodeFixCollectionIntervalIntrospector : IIntervalIntrospector<CodeFixCollection>
{
public TextSpan GetSpan(CodeFixCollection value)
=> value.TextSpan;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ namespace Microsoft.CodeAnalysis.Shared.Collections;
internal interface IIntervalTreeWitness<T, TIntervalTree, TNode>
where TIntervalTree : IIntervalTree<T>
{
public bool TryGetRoot(TIntervalTree tree, [NotNullWhen(true)] out TNode? root);
public bool TryGetLeftNode(TIntervalTree tree, TNode node, [NotNullWhen(true)] out TNode? leftNode);
public bool TryGetRightNode(TIntervalTree tree, TNode node, [NotNullWhen(true)] out TNode? rightNode);
bool TryGetRoot(TIntervalTree tree, [NotNullWhen(true)] out TNode? root);
bool TryGetLeftNode(TIntervalTree tree, TNode node, [NotNullWhen(true)] out TNode? leftNode);
bool TryGetRightNode(TIntervalTree tree, TNode node, [NotNullWhen(true)] out TNode? rightNode);

public T GetValue(TIntervalTree tree, TNode node);
public TNode GetMaxEndNode(TIntervalTree tree, TNode node);
T GetValue(TIntervalTree tree, TNode node);
TNode GetMaxEndNode(TIntervalTree tree, TNode node);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@ internal class SimpleMutableIntervalTree<T, TIntrospector> : MutableIntervalTree
{
private readonly TIntrospector _introspector;

public SimpleMutableIntervalTree(in TIntrospector introspector, IEnumerable<T>? values)
public SimpleMutableIntervalTree(in TIntrospector introspector, IEnumerable<T>? values = null)
{
_introspector = introspector;

if (values != null)
{
foreach (var value in values)
{
root = Insert(root, new Node(value), introspector);
}
}
}

Expand Down
Loading