Skip to content

Async lightbulb performance improvement for compiler analyzer execution#56844

Merged
mavasani merged 13 commits intodotnet:mainfrom
mavasani:AsyncLightbulbPerf
Oct 19, 2021
Merged

Async lightbulb performance improvement for compiler analyzer execution#56844
mavasani merged 13 commits intodotnet:mainfrom
mavasani:AsyncLightbulbPerf

Conversation

@mavasani
Copy link
Copy Markdown
Contributor

Addresses the first bullet point in #56843.

Prior to this change, compiler diagnostic analyzer could only be run from the IDE for entire document span. This is primarily due to lack of an analyzer API that allows the analyzer to register a span-based semantic diagnostic callback. Though we can consider designing and adding such an analyzer API, it would be purely an IDE-only analyzer action, as analyzers are never executed for a span in batch compilation mode - this would make it difficult to justify adding such an API.

With this PR, we add a workaround in the analyzer driver to allow executing compiler analyzer's semantic model action scoped to a filter span. This should speed up executing the compiler analyzer for lightbulb scenarios in the IDE, which are always scoped to current line span.

Verified that both the compiler and IDE tests added with this PR fail prior to the product changes.

NOTE: This change would not give us any perceivable improvement in non-async lightbulb scenario, as we are extremely likely to have at least one other analyzer that needs to run on the entire document. The performance improvement should be perceivable in async lightbulb scenario as the high priority code fixes bucket (add usings and merge conflict resolution) only run the compiler analyzer.

Prior to this change, compiler diagnostic analyzer could only be run from the IDE for entire document span. This is primarily due to lack of an analyzer API that allows the analyzer to register a span-based semantic diagnostic callback. Though we can consider designing and adding such an analyzer API, it would be purely an IDE-only analyzer action, as analyzers are never executed for a span in batch compilation mode - this would make it difficult to justify adding such an API.

With this PR, we add a workaround in the analyzer driver to allow executing compiler analyzer's semantic model action scoped to a filter span. This should speed up executing the compiler analyzer for lightbulb scenarios in the IDE, which are always scoped to current line span.

Verified that both the compiler and IDE tests added with this PR fail prior to the product changes.

**NOTE:** This change would not give us any perceivable improvement in non-async lightbulb scenario, as we are extremely likely to have at least one other analyzer that needs to run on the entire document. The performance improvement should be perceivable in async lightbulb scenario as the high priority code fixes bucket (add usings and merge conflict resolution) only run the compiler analyzer.
@mavasani mavasani added this to the 17.0 milestone Sep 29, 2021
@mavasani mavasani requested review from a team as code owners September 29, 2021 11:50
@mavasani mavasani changed the title Addresses the first bullet point in #56843. Async lightbulb performance improvement for compiler analyzer execution Sep 29, 2021
Comment on lines +41 to +42
var declDiagnostics = context.SemanticModel.GetDeclarationDiagnostics(context.FilterSpan, context.CancellationToken);
var bodyDiagnostics = context.SemanticModel.GetMethodBodyDiagnostics(context.FilterSpan, context.CancellationToken);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is where we get the core performance boost.

Comment thread src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticAnalysisContext.cs Outdated
Comment thread src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs
Comment thread src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.cs
Comment thread src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationUnitCompletedEvent.cs Outdated
Comment thread src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzers.cs Outdated
Dim diagnostic = Assert.Single(diagnostics)
Assert.Equal("CS0219", diagnostic.Id)
End Using
End Function
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

similar point about testing different spans.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@CyrusNajmabadi Adding the IDE test for testing different spans failed as the IDE seems to be querying the compiler diagnostics for entire containing member instead of just the line span:

async Task<TextSpan?> GetAdjustedSpanForCompilerAnalyzerAsync()
{
// This method is to workaround a bug (https://github.com/dotnet/roslyn/issues/1557)
// once that bug is fixed, we should be able to use given span as it is.
Debug.Assert(isCompilerAnalyzer);
if (!span.HasValue)
{
return null;
}
var service = document.GetRequiredLanguageService<ISyntaxFactsService>();
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var startNode = service.GetContainingMemberDeclaration(root, span.Value.Start);
var endNode = service.GetContainingMemberDeclaration(root, span.Value.End);
if (startNode == endNode)
{
// use full member span
if (service.IsMethodLevelMember(startNode))
{
return startNode.FullSpan;
}
// use span as it is
return span;
}
var startSpan = service.IsMethodLevelMember(startNode) ? startNode.FullSpan : span.Value;
var endSpan = service.IsMethodLevelMember(endNode) ? endNode.FullSpan : span.Value;
return TextSpan.FromBounds(Math.Min(startSpan.Start, endSpan.Start), Math.Max(startSpan.End, endSpan.End));
}

This adjusted span workaround seems to be added for #1557, which is pretty old and has no failing tests or scenarios. I am going to just remove that adjusted span computation code and address any fallout subsequently based on failing scenarios.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Works for me.

Copy link
Copy Markdown
Contributor Author

@mavasani mavasani Sep 30, 2021

Choose a reason for hiding this comment

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

So attempting to remove GetAdjustedSpanForCompilerAnalyzerAsync workaround leads to few integration test failures. So the claim in #1557 is true - invoking SemanticModel.Get*Diagnostics(lineSpan) with just the current line span instead of entire method body span indeed does not return all the diagnostics for the current line. I have reverted the commit that removed the workaround and instead adjusted the above test to also account for this fact.

@333fred @jcouv - #1557 does represent a potential perf improvement case for light bulb - IDE is currently being forced to always fetch compiler semantic diagnostics for entire method body span instead of line span due to this bug. If compiler team were to fix this, IDE can remove this workaround and see a perf improvement by requesting the diagnostics just for the current line span.

Copy link
Copy Markdown
Contributor

@CyrusNajmabadi CyrusNajmabadi left a comment

Choose a reason for hiding this comment

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

Overall looks good. I don't know the compiler side well enough to be confident there though.

@mavasani
Copy link
Copy Markdown
Contributor Author

@dotnet/roslyn-compiler for reviews

Comment thread src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs Outdated
Comment thread src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisResultBuilder.cs Outdated
@333fred
Copy link
Copy Markdown
Member

333fred commented Oct 13, 2021

Done review pass (commit 11).

Copy link
Copy Markdown
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

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

LGTM (commit 12)

@mavasani
Copy link
Copy Markdown
Contributor Author

@dotnet/roslyn-compiler for an additional review.

}

return builder.ToImmutable();
if (needsSpanBasedCompilationUnitCompletedEvent)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

needsSpanBasedCompilationUnitCompletedEvent

Do we only reach here if compilation has completed for the compilation unit? In short, are we sure we're not generating a CompilationUnitCompletedEvent before compilation has completed?

Copy link
Copy Markdown
Contributor Author

@mavasani mavasani Oct 19, 2021

Choose a reason for hiding this comment

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

We will only reach here if compilation has not completed for the entire compilation unit. The whole idea here is to generate a synthesized event for the compilation unit that can trigger semantic model actions with partial tree span before the real compilation unit completed event is generated. We can probably use a different type to represent such synthesized events instead of CompilationUnitCompletedEvent, but then we would have to duplicate the code in the analyzer driver that invokes semantic model actions for compilation unit completed event and this new event type.

@mavasani mavasani enabled auto-merge October 19, 2021 16:04
@mavasani mavasani merged commit 403ec52 into dotnet:main Oct 19, 2021
@ghost ghost modified the milestones: 17.0, Next Oct 19, 2021
@mavasani mavasani deleted the AsyncLightbulbPerf branch October 20, 2021 02:40
@RikkiGibson RikkiGibson modified the milestones: Next, 17.1.P1 Oct 25, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants