-
Notifications
You must be signed in to change notification settings - Fork 231
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
Cache CreateCfg #5403
Cache CreateCfg #5403
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/* | ||
* SonarAnalyzer for .NET | ||
* Copyright (C) 2015-2022 SonarSource SA | ||
* mailto: contact AT sonarsource DOT com | ||
* | ||
* This program is free software; you can redistribute it and/or | ||
* modify it under the terms of the GNU Lesser General Public | ||
* License as published by the Free Software Foundation; either | ||
* version 3 of the License, or (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
* Lesser General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Lesser General Public License | ||
* along with this program; if not, write to the Free Software Foundation, | ||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
*/ | ||
|
||
using System.Linq; | ||
using System.Runtime.CompilerServices; | ||
using Microsoft.CodeAnalysis; | ||
using SonarAnalyzer.Extensions; | ||
using StyleCop.Analyzers.Lightup; | ||
|
||
namespace SonarAnalyzer.CFG.Roslyn | ||
{ | ||
public abstract class ControlFlowGraphCacheBase | ||
{ | ||
private readonly ConditionalWeakTable<SyntaxNode, Wrapper> cache = new(); | ||
|
||
protected abstract bool HasNestedCfg(SyntaxNode node); | ||
protected abstract bool IsLocalFunction(SyntaxNode node); | ||
|
||
public ControlFlowGraph FindOrCreate(SyntaxNode declaration, SemanticModel model) | ||
{ | ||
var rootSyntax = model.GetOperation(declaration).RootOperation().Syntax; | ||
var wrapper = cache.GetValue(rootSyntax, x => new Wrapper(ControlFlowGraph.Create(x, model))); | ||
if (HasNestedCfg(declaration)) | ||
{ | ||
// We need to go up and track all possible enclosing lambdas, local functions and other FlowAnonymousFunctionOperations | ||
foreach (var node in declaration.AncestorsAndSelf().TakeWhile(x => x != rootSyntax).Reverse()) | ||
{ | ||
if (IsLocalFunction(node)) | ||
{ | ||
wrapper = new(wrapper.Cfg.GetLocalFunctionControlFlowGraph(node)); | ||
} | ||
else if (wrapper.FlowOperation(node) is { WrappedOperation: not null } flowOperation) | ||
{ | ||
wrapper = new(wrapper.Cfg.GetAnonymousFunctionControlFlowGraph(flowOperation)); | ||
} | ||
else if (node == declaration) | ||
{ | ||
return null; // Lambda syntax is not always recognized as a FlowOperation for invalid syntaxes | ||
} | ||
} | ||
} | ||
return wrapper.Cfg; | ||
} | ||
|
||
private sealed class Wrapper | ||
{ | ||
private IFlowAnonymousFunctionOperationWrapper[] flowOperations; | ||
|
||
public ControlFlowGraph Cfg { get; } | ||
|
||
public Wrapper(ControlFlowGraph cfg) => | ||
Cfg = cfg; | ||
|
||
public IFlowAnonymousFunctionOperationWrapper FlowOperation(SyntaxNode node) | ||
{ | ||
flowOperations ??= Cfg.FlowAnonymousFunctionOperations().ToArray(); // Avoid recomputing, it's expensive and called many times for a single CFG | ||
return flowOperations.SingleOrDefault(x => x.WrappedOperation.Syntax == node); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,8 +20,11 @@ | |
|
||
extern alias csharp; | ||
extern alias vbnet; | ||
|
||
using System; | ||
using System.Linq; | ||
using FluentAssertions; | ||
using FluentAssertions.Extensions; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.Operations; | ||
|
@@ -330,6 +333,68 @@ public void CreateCfg_InvalidSyntax_ReturnsCfg_CS(string code) | |
SyntaxNodeExtensionsCS.CreateCfg(lambda.Body, model).Should().NotBeNull(); | ||
} | ||
|
||
[TestMethod] | ||
public void CreateCfg_Performance_UsesCache_CS() | ||
{ | ||
const string code = @" | ||
using System; | ||
public class Sample | ||
{ | ||
public void Main(string noiseToHaveMoreOperations) | ||
{ | ||
noiseToHaveMoreOperations.ToString(); | ||
noiseToHaveMoreOperations.ToString(); | ||
noiseToHaveMoreOperations.ToString(); | ||
void LocalFunction() | ||
{ | ||
noiseToHaveMoreOperations.ToString(); | ||
noiseToHaveMoreOperations.ToString(); | ||
noiseToHaveMoreOperations.ToString(); | ||
Action a = () => 42.ToString(); | ||
} | ||
} | ||
}"; | ||
var (tree, model) = TestHelper.CompileCS(code); | ||
var lambda = tree.GetRoot().DescendantNodes().OfType<CS.ParenthesizedLambdaExpressionSyntax>().Single(); | ||
Action a = () => | ||
{ | ||
for (var i = 0; i < 10000; i++) | ||
{ | ||
SyntaxNodeExtensionsCS.CreateCfg(lambda, model); | ||
} | ||
}; | ||
a.ExecutionTime().Should().BeLessThan(1.Seconds()); // Takes roughly 0.2 sec on CI | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you tried to run without the cache? I'm curious about the improvement. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Roughly 5x slower on my machine without cache on this very small method. The bigger the method is, the worse it gets. I used the full user repro project build to see how it works. It went down from 8 minutes to 20 sec with the cache. |
||
} | ||
|
||
[TestMethod] | ||
public void CreateCfg_Performance_UsesCache_VB() | ||
{ | ||
const string code = @" | ||
Public Class Sample | ||
Public Sub Main(NoiseToHaveMoreOperations As String) | ||
NoiseToHaveMoreOperations.ToString() | ||
NoiseToHaveMoreOperations.ToString() | ||
NoiseToHaveMoreOperations.ToString() | ||
Dim Outer As Action = Sub() | ||
NoiseToHaveMoreOperations.ToString() | ||
NoiseToHaveMoreOperations.ToString() | ||
NoiseToHaveMoreOperations.ToString() | ||
Dim Inner As Action = Sub() NoiseToHaveMoreOperations.ToString() | ||
End Sub | ||
End Sub | ||
End Class"; | ||
var (tree, model) = TestHelper.CompileVB(code); | ||
var lambda = tree.GetRoot().DescendantNodes().OfType<VB.SingleLineLambdaExpressionSyntax>().Single(); | ||
Action a = () => | ||
{ | ||
for (var i = 0; i < 10000; i++) | ||
{ | ||
SyntaxNodeExtensionsCS.CreateCfg(lambda, model); | ||
} | ||
}; | ||
a.ExecutionTime().Should().BeLessThan(1.Seconds()); // Takes roughly 0.4 sec on CI | ||
} | ||
|
||
private static SyntaxToken GetFirstTokenOfKind(SyntaxTree syntaxTree, SyntaxKind kind) => | ||
syntaxTree.GetRoot().DescendantTokens().First(token => token.IsKind(kind)); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand how this is meant to work. If we take the example from
CreateCfg_Performance_UsesCache_CS
and we request the CFG for the lambda, we will receive the CFG of the root operation this is the method declaration. Is this right?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly. Roslyn doesn't let us directly create CFG for Lambda/Local function. We need to create a CFG from the root (method). And find our desired CFG in the CFG tree.
That's also the reason for the high cache hit rate.