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

Improve performance of FAR #73523

Merged
merged 5 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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 @@ -236,7 +236,7 @@ private static async Task<ImmutableArray<ReferencedSymbol>> FindChangeSignatureR
var engine = new FindReferencesSearchEngine(
solution,
documents: null,
ReferenceFinders.DefaultReferenceFinders.Add(DelegateInvokeMethodReferenceFinder.DelegateInvokeMethod),
[.. ReferenceFinders.DefaultReferenceFinders, DelegateInvokeMethodReferenceFinder.Instance],
streamingProgress,
FindReferencesSearchOptions.Default);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ namespace Microsoft.CodeAnalysis.ChangeSignature;
/// <remarks>
/// TODO: Rewrite this to track backward through references instead of binding everything
/// </remarks>
internal class DelegateInvokeMethodReferenceFinder : AbstractReferenceFinder<IMethodSymbol>
internal sealed class DelegateInvokeMethodReferenceFinder : AbstractReferenceFinder<IMethodSymbol>
{
public static readonly IReferenceFinder DelegateInvokeMethod = new DelegateInvokeMethodReferenceFinder();
public static readonly DelegateInvokeMethodReferenceFinder Instance = new();

private DelegateInvokeMethodReferenceFinder()
{
}

protected override bool CanFind(IMethodSymbol symbol)
=> symbol.MethodKind == MethodKind.DelegateInvoke;
Expand Down Expand Up @@ -76,7 +80,7 @@ protected override Task DetermineDocumentsToSearchAsync<TData>(
return Task.CompletedTask;
}

protected override async ValueTask FindReferencesInDocumentAsync<TData>(
protected override void FindReferencesInDocument<TData>(
IMethodSymbol methodSymbol,
FindReferencesDocumentState state,
Action<FinderLocation, TData> processResult,
Expand Down Expand Up @@ -105,8 +109,7 @@ protected override async ValueTask FindReferencesInDocumentAsync<TData>(
var convertedType = (ISymbol?)state.SemanticModel.GetTypeInfo(node, cancellationToken).ConvertedType;
if (convertedType != null)
{
convertedType = await SymbolFinder.FindSourceDefinitionAsync(convertedType, state.Solution, cancellationToken).ConfigureAwait(false)
?? convertedType;
convertedType = SymbolFinder.FindSourceDefinition(convertedType, state.Solution, cancellationToken) ?? convertedType;
}

if (convertedType == methodSymbol.ContainingType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,14 @@

using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.FindUsages;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
Expand Down Expand Up @@ -241,10 +243,34 @@ public static SerializableDefinitionItem Dehydrate(int id, DefinitionItem item)
}
}

[DataContract]
internal readonly struct SerializableClassifiedSpansAndHighlightSpan(
SerializableClassifiedSpans classifiedSpans, TextSpan highlightSpan)
{
private static readonly ObjectPool<SegmentedList<ClassifiedSpan>> s_listPool = new(() => new());

[DataMember(Order = 0)]
public readonly SerializableClassifiedSpans ClassifiedSpans = classifiedSpans;

[DataMember(Order = 1)]
public readonly TextSpan HighlightSpan = highlightSpan;

public static SerializableClassifiedSpansAndHighlightSpan Dehydrate(ClassifiedSpansAndHighlightSpan classifiedSpansAndHighlightSpan)
=> new(SerializableClassifiedSpans.Dehydrate(classifiedSpansAndHighlightSpan.ClassifiedSpans), classifiedSpansAndHighlightSpan.HighlightSpan);

public ClassifiedSpansAndHighlightSpan Rehydrate()
{
using var pooledObject = s_listPool.GetPooledObject();
this.ClassifiedSpans.Rehydrate(pooledObject.Object);
return new ClassifiedSpansAndHighlightSpan(pooledObject.Object.ToImmutableArray(), this.HighlightSpan);
}
}

[DataContract]
internal readonly struct SerializableSourceReferenceItem(
int definitionId,
SerializableDocumentSpan sourceSpan,
SerializableClassifiedSpansAndHighlightSpan classifiedSpans,
SymbolUsageInfo symbolUsageInfo,
ImmutableDictionary<string, string> additionalProperties)
{
Expand All @@ -255,22 +281,26 @@ internal readonly struct SerializableSourceReferenceItem(
public readonly SerializableDocumentSpan SourceSpan = sourceSpan;

[DataMember(Order = 2)]
public readonly SymbolUsageInfo SymbolUsageInfo = symbolUsageInfo;
public readonly SerializableClassifiedSpansAndHighlightSpan ClassifiedSpans = classifiedSpans;

[DataMember(Order = 3)]
public readonly SymbolUsageInfo SymbolUsageInfo = symbolUsageInfo;

[DataMember(Order = 4)]
public readonly ImmutableDictionary<string, string> AdditionalProperties = additionalProperties;

public static SerializableSourceReferenceItem Dehydrate(int definitionId, SourceReferenceItem item)
=> new(definitionId,
SerializableDocumentSpan.Dehydrate(item.SourceSpan),
// We're always have classified spans for C#/VB, which are the only languages used in OOP find-references.
SerializableClassifiedSpansAndHighlightSpan.Dehydrate(item.ClassifiedSpans!.Value),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the computation always happens here:

var options = await optionsProvider.GetOptionsAsync(document.Project.Services, cancellationToken).ConfigureAwait(false);
var documentSpan = new DocumentSpan(document, sourceSpan);
var classifiedSpans = await ClassifiedSpansAndHighlightSpanFactory.ClassifyAsync(
documentSpan, classifiedSpans: null, options, cancellationToken).ConfigureAwait(false);
return new SourceReferenceItem(
definitionItem, documentSpan, classifiedSpans, referenceLocation.SymbolUsageInfo, referenceLocation.AdditionalProperties);

we just were dropping the value on the floor here.

item.SymbolUsageInfo,
item.AdditionalProperties);

public async Task<SourceReferenceItem> RehydrateAsync(Solution solution, DefinitionItem definition, CancellationToken cancellationToken)
=> new(definition,
await SourceSpan.RehydrateAsync(solution, cancellationToken).ConfigureAwait(false),
// Todo: consider serializing this over.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. this was a good thing to consider :D

classifiedSpans: null,
this.ClassifiedSpans.Rehydrate(),
SymbolUsageInfo,
AdditionalProperties.ToImmutableDictionary(t => t.Key, t => t.Value));
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Simplification;
Expand Down Expand Up @@ -66,7 +67,7 @@ protected override async Task<IEnumerable<CodeActionOperation>> ComputeOperation
// we already have our destination type, but we need to find the document it is in
// When it is an existing type, "FileName" points to a full path rather than just the name
// There should be no two docs that have the same file path
var destinationDocId = _document.Project.Solution.GetDocumentIdsWithFilePath(moveOptions.FileName).Single();
var destinationDocId = sourceDoc.Project.Solution.GetDocumentIdsWithFilePath(moveOptions.FileName).Single();
var fixedSolution = await RefactorAndMoveAsync(
moveOptions.SelectedMembers,
memberNodes,
Expand All @@ -77,7 +78,7 @@ protected override async Task<IEnumerable<CodeActionOperation>> ComputeOperation
sourceDoc.Id,
destinationDocId,
cancellationToken).ConfigureAwait(false);
return new CodeActionOperation[] { new ApplyChangesOperation(fixedSolution) };
return [new ApplyChangesOperation(fixedSolution)];
}

// otherwise, we need to create a destination ourselves
Expand All @@ -100,10 +101,10 @@ protected override async Task<IEnumerable<CodeActionOperation>> ComputeOperation
sourceDoc.Project.Solution,
moveOptions.NamespaceDisplay,
moveOptions.FileName,
_document.Project.Id,
_document.Folders,
sourceDoc.Project.Id,
sourceDoc.Folders,
newType,
_document,
sourceDoc,
_fallbackOptions,
cancellationToken).ConfigureAwait(false);

Expand All @@ -113,7 +114,7 @@ protected override async Task<IEnumerable<CodeActionOperation>> ComputeOperation
newType = (INamedTypeSymbol)destSemanticModel.GetRequiredDeclaredSymbol(destRoot.GetAnnotatedNodes(annotation).Single(), cancellationToken);

// refactor references across the entire solution
var memberReferenceLocations = await FindMemberReferencesAsync(moveOptions.SelectedMembers, newDoc.Project.Solution, cancellationToken).ConfigureAwait(false);
var memberReferenceLocations = await FindMemberReferencesAsync(newDoc.Project.Solution, newDoc.Project.Id, moveOptions.SelectedMembers, cancellationToken).ConfigureAwait(false);
var projectToLocations = memberReferenceLocations.ToLookup(loc => loc.location.Document.Project.Id);
var solutionWithFixedReferences = await RefactorReferencesAsync(projectToLocations, newDoc.Project.Solution, newType, typeArgIndices, cancellationToken).ConfigureAwait(false);

Expand All @@ -130,7 +131,7 @@ protected override async Task<IEnumerable<CodeActionOperation>> ComputeOperation
var pullMembersUpOptions = PullMembersUpOptionsBuilder.BuildPullMembersUpOptions(newType, members);
var movedSolution = await MembersPuller.PullMembersUpAsync(sourceDoc, pullMembersUpOptions, _fallbackOptions, cancellationToken).ConfigureAwait(false);

return new CodeActionOperation[] { new ApplyChangesOperation(movedSolution) };
return [new ApplyChangesOperation(movedSolution)];
}

/// <summary>
Expand Down Expand Up @@ -174,7 +175,8 @@ private async Task<Solution> RefactorAndMoveAsync(
oldSolution = newTypeDoc.WithSyntaxRoot(newTypeRoot).Project.Solution;

// refactor references across the entire solution
var memberReferenceLocations = await FindMemberReferencesAsync(selectedMembers, oldSolution, cancellationToken).ConfigureAwait(false);
var memberReferenceLocations = await FindMemberReferencesAsync(
oldSolution, sourceDocId.ProjectId, selectedMembers, cancellationToken).ConfigureAwait(false);
var projectToLocations = memberReferenceLocations.ToLookup(loc => loc.location.Document.Project.Id);
var solutionWithFixedReferences = await RefactorReferencesAsync(projectToLocations, oldSolution, newType, typeArgIndices, cancellationToken).ConfigureAwait(false);

Expand Down Expand Up @@ -326,11 +328,27 @@ private static async Task<SyntaxNode> FixReferencesSingleDocumentAsync(
}

private static async Task<ImmutableArray<(ReferenceLocation location, bool isExtension)>> FindMemberReferencesAsync(
ImmutableArray<ISymbol> members,
Solution solution,
ProjectId projectId,
ImmutableArray<ISymbol> members,
CancellationToken cancellationToken)
{
var tasks = members.Select(symbol => SymbolFinder.FindReferencesAsync(symbol, solution, cancellationToken));
var project = solution.GetRequiredProject(projectId);
var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);

using var _ = ArrayBuilder<Task<IEnumerable<ReferencedSymbol>>>.GetInstance(out var tasks);
foreach (var member in members)
{
tasks.Add(Task.Run(async () =>
{
var symbolKey = member.GetSymbolKey(cancellationToken);
var resolvedMember = symbolKey.Resolve(compilation, ignoreAssemblyKey: false, cancellationToken).GetAnySymbol();
return resolvedMember is null
? []
: await SymbolFinder.FindReferencesAsync(resolvedMember, solution, cancellationToken).ConfigureAwait(false);
}));
}

var symbolRefs = await Task.WhenAll(tasks).ConfigureAwait(false);
return symbolRefs
.Flatten()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,14 @@ static async Task<FindReferenceCache> ComputeCacheAsync(Document document, Cance
// Find-Refs is not impacted by nullable types at all. So get a nullable-disabled semantic model to avoid
// unnecessary costs while binding.
var model = await document.GetRequiredNullableDisabledSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var nullableEnableSemanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

// It's very costly to walk an entire tree. So if the tree is simple and doesn't contain
// any unicode escapes in it, then we do simple string matching to find the tokens.
var index = await SyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false);

return new(document, text, model, root, index);
return new(document, text, model, nullableEnableSemanticModel, root, index);
}
}

Expand All @@ -54,18 +55,27 @@ static async Task<FindReferenceCache> ComputeCacheAsync(Document document, Cance
public readonly ISyntaxFactsService SyntaxFacts;
public readonly SyntaxTreeIndex SyntaxTreeIndex;

/// <summary>
/// Not used by FAR directly. But we compute and cache this while processing a document so that if we call any
/// other services that use this semantic model, that they don't end up recreating it.
/// </summary>
#pragma warning disable IDE0052 // Remove unread private members
private readonly SemanticModel _nullableEnabledSemanticModel;
#pragma warning restore IDE0052 // Remove unread private members
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 is part of the change. ideally nothing will be using nullable-enabled-semantic-model (And i want to stamp out anything that is using it). but if they do, this at least means we cache the results while processing the doc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

next week i'll be setting BPs in the normal 'get semantic model' codepath so that i can update any final services still using it during FAR to stop.


private readonly ConcurrentDictionary<SyntaxNode, SymbolInfo> _symbolInfoCache = [];
private readonly ConcurrentDictionary<string, ImmutableArray<SyntaxToken>> _identifierCache;

private ImmutableHashSet<string>? _aliasNameSet;
private ImmutableArray<SyntaxToken> _constructorInitializerCache;

private FindReferenceCache(
Document document, SourceText text, SemanticModel semanticModel, SyntaxNode root, SyntaxTreeIndex syntaxTreeIndex)
Document document, SourceText text, SemanticModel semanticModel, SemanticModel nullableEnabledSemanticModel, SyntaxNode root, SyntaxTreeIndex syntaxTreeIndex)
{
Document = document;
Text = text;
SemanticModel = semanticModel;
_nullableEnabledSemanticModel = nullableEnabledSemanticModel;
Root = root;
SyntaxTreeIndex = syntaxTreeIndex;
SyntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,19 +290,20 @@ private async ValueTask ProcessDocumentAsync(
await RoslynParallel.ForEachAsync(
symbols,
GetParallelOptions(cancellationToken),
async (symbol, cancellationToken) =>
(symbol, cancellationToken) =>
{
// symbolToGlobalAliases is safe to read in parallel. It is created fully before this point and is no
// longer mutated.
var state = new FindReferencesDocumentState(
cache, TryGet(symbolToGlobalAliases, symbol));

await ProcessDocumentAsync(symbol, state, onReferenceFound).ConfigureAwait(false);
ProcessDocument(symbol, state, onReferenceFound);
return ValueTaskFactory.CompletedTask;
}).ConfigureAwait(false);

return;

async Task ProcessDocumentAsync(
void ProcessDocument(
ISymbol symbol, FindReferencesDocumentState state, Action<Reference> onReferenceFound)
{
cancellationToken.ThrowIfCancellationRequested();
Expand All @@ -318,12 +319,12 @@ async Task ProcessDocumentAsync(
// and only do interesting work on the single relevant one.
foreach (var finder in _finders)
{
await finder.FindReferencesInDocumentAsync(
finder.FindReferencesInDocument(
symbol, state,
static (loc, tuple) => tuple.onReferenceFound((tuple.group, tuple.symbol, loc.Location)),
(group, symbol, onReferenceFound),
_options,
cancellationToken).ConfigureAwait(false);
cancellationToken);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ async ValueTask DirectSymbolSearchAsync(ISymbol symbol, FindReferencesDocumentSt
using var _ = ArrayBuilder<FinderLocation>.GetInstance(out var referencesForFinder);
foreach (var finder in _finders)
{
await finder.FindReferencesInDocumentAsync(
symbol, state, StandardCallbacks<FinderLocation>.AddToArrayBuilder, referencesForFinder, _options, cancellationToken).ConfigureAwait(false);
finder.FindReferencesInDocument(
symbol, state, StandardCallbacks<FinderLocation>.AddToArrayBuilder, referencesForFinder, _options, cancellationToken);
}

if (referencesForFinder.Count > 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ protected sealed override Task DetermineDocumentsToSearchAsync<TData>(
return Task.CompletedTask;
}

protected sealed override ValueTask FindReferencesInDocumentAsync<TData>(
protected sealed override void FindReferencesInDocument<TData>(
TSymbol symbol,
FindReferencesDocumentState state,
Action<FinderLocation, TData> processResult,
Expand All @@ -67,8 +67,6 @@ protected sealed override ValueTask FindReferencesInDocumentAsync<TData>(
var tokens = FindMatchingIdentifierTokens(state, symbol.Name, cancellationToken);
FindReferencesInTokens(symbol, state, tokens, processResult, processResultData, cancellationToken);
}

return ValueTaskFactory.CompletedTask;
}

private static ISymbol? GetContainer(ISymbol symbol)
Expand Down
Loading
Loading