Skip to content

Commit 8b31f92

Browse files
committed
Filter invocations syntactically before binding them
Twelve of the sixteen analyzers register on SyntaxKind.InvocationExpression and call GetSymbolInfo() first, so each binds every call site in the compilation before discovering that the method is not one of the few the rule matches. On one 170k-line project that was 30.2s of 105.3s of analyzer time; on a 30k-line project of mostly generated EF Core migrations, 32.5s of 36.3s -- 89% of analyzer time, and two thirds of the whole compile. The invoked method's simple name is available syntactically and rejects nearly every call site, so check it first. InvokedSimpleName(), MethodNames() and CouldInvokeAnyOf() in CodeAnalysisExtensions do that; the name set is built once per compilation from the reference symbols each analyzer already resolves, so it tracks those automatically. All twelve get the pre-filter, and their reference symbol lookups move out of the per-node action. The lookups that cost the most: - AK1006 can skip registration entirely when Akka.Persistence is absent, and its Persist.AddRange(PersistAsync) allocated an ImmutableArray per call site. - AK2007's GetAllAggregateMethods() built a ten-element List and copied it into an ImmutableArray per call site. - AK1007 rebuilt two arrays per call site; AK2001, AK2003, AK2004 and AK2005 each called GetTypeByMetadataName per call site. AK1002 also moves its `Parent is not AwaitExpressionSyntax` test first: pure syntax, and it discards almost everything. AK1004 was the worst offender overall -- before binding the invocation it resolved the enclosing class symbol, for every call site in the compilation. Measured with csc run from a captured response file, comparing only the analyzer assembly (process CPU, min of 4 runs): Akka self-time whole compile 170k-line project 30.2s -> 3.7s 125.4s -> 103.0s (99.8s without it) 30k-line project 32.5s -> 1.5s 34.5s -> 11.6s (11.0s without it) That is 88% and 97% of what removing the assembly entirely would save. Those runs predate converting AK1003, AK1004, AK1007, AK2000, AK2001 and AK2007, and the residual 3.7s and 1.5s is largely what those six were still costing, so the table understates this branch. The four analyzers not registered on InvocationExpression -- AK1000, AK1005, AK2002 and AK2006 -- are untouched. No behavioural change intended: the pre-filter only skips nodes the subsequent symbol comparison would have rejected. Akka.Analyzers.Tests passes 329/331 with 2 pre-existing skips.
1 parent 3eb2a62 commit 8b31f92

13 files changed

Lines changed: 212 additions & 51 deletions

src/Akka.Analyzers/AK1000/MustNotAwaitGracefulStopInsideReceiveAsyncAnalyzer.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,25 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
2323
Guard.AssertIsNotNull(context);
2424
Guard.AssertIsNotNull(akkaContext);
2525

26+
// Per-compilation state, hoisted out of the per-node action.
27+
var gracefulStopMethods = akkaContext.AkkaCore.Actor.GracefulStopSupportSupport.GracefulStop;
28+
var gracefulStopNames = gracefulStopMethods.MethodNames();
29+
if (gracefulStopNames.IsEmpty)
30+
return;
31+
2632
context.RegisterSyntaxNodeAction(ctx =>
2733
{
2834
var invocationExpr = (InvocationExpressionSyntax)ctx.Node;
35+
36+
// Check 1: GracefulStop() must not be awaited. Done first -- pure syntax, and it
37+
// discards nearly every invocation.
38+
if (invocationExpr.Parent is not AwaitExpressionSyntax awaitExpression)
39+
return;
40+
41+
// Reject by name before binding.
42+
if (!invocationExpr.CouldInvokeAnyOf(gracefulStopNames))
43+
return;
44+
2945
var semanticModel = ctx.SemanticModel;
3046
var akkaCore = akkaContext.AkkaCore;
3147

@@ -36,12 +52,7 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
3652
methodSymbol = methodSymbol.ReducedFrom ?? methodSymbol;
3753

3854
// Method must be one of the GracefulStop() extension methods
39-
var refSymbols = akkaCore.Actor.GracefulStopSupportSupport.GracefulStop;
40-
if(!refSymbols.Any(s => ReferenceEquals(methodSymbol, s)))
41-
return;
42-
43-
// Check 1: GracefulStop() should not be awaited
44-
if (invocationExpr.Parent is not AwaitExpressionSyntax awaitExpression)
55+
if(!gracefulStopMethods.Any(s => ReferenceEquals(methodSymbol, s)))
4556
return;
4657

4758
// Check 2: Ensure called within ReceiveAsync<T> or ReceiveAnyAsync lambda expression

src/Akka.Analyzers/AK1000/MustNotUseIWithTimersInPreRestartAnalyzer.cs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,30 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
2121
Guard.AssertIsNotNull(context);
2222
Guard.AssertIsNotNull(akkaContext);
2323

24+
// Hoisted out of the per-node action, which rebuilt both arrays per call site.
25+
var iWithTimers = akkaContext.AkkaCore.Actor.ITimerScheduler;
26+
var timerMethods = iWithTimers.StartPeriodicTimer.AddRange(iWithTimers.StartSingleTimer);
27+
var timerMethodNames = timerMethods.MethodNames();
28+
if (timerMethodNames.IsEmpty)
29+
return;
30+
var actorBase = akkaContext.AkkaCore.Actor.ActorBase;
31+
var preRestartMethods = new[] { actorBase.PreRestart!, actorBase.AroundPreRestart! }.ToImmutableArray();
32+
2433
context.RegisterSyntaxNodeAction(ctx =>
2534
{
2635
var invocationExpr = (InvocationExpressionSyntax)ctx.Node;
36+
37+
// Reject by name before binding.
38+
if (!invocationExpr.CouldInvokeAnyOf(timerMethodNames))
39+
return;
40+
2741
var semanticModel = ctx.SemanticModel;
2842

2943
if (semanticModel.GetSymbolInfo(invocationExpr).Symbol is not IMethodSymbol methodInvocationSymbol)
3044
return;
3145

3246
// Invocation expression must be either `ITimerScheduler.StartPeriodicTimer()` or `ITimerScheduler.StartSingleTimer()`
33-
var iWithTimers = akkaContext.AkkaCore.Actor.ITimerScheduler;
34-
var refMethods = iWithTimers.StartPeriodicTimer.AddRange(iWithTimers.StartSingleTimer);
35-
if (!methodInvocationSymbol.MatchesAny(refMethods))
47+
if (!methodInvocationSymbol.MatchesAny(timerMethods))
3648
return;
3749

3850
// Grab the enclosing method declaration
@@ -45,9 +57,7 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
4557
return;
4658

4759
// Method declaration must be `ActorBase.PreRestart()` or `ActorBase.AroundPreRestart()`
48-
var actorBase = akkaContext.AkkaCore.Actor.ActorBase;
49-
refMethods = new[] { actorBase.PreRestart!, actorBase.AroundPreRestart! }.ToImmutableArray();
50-
if (!methodDeclarationSymbol.OverridesAny(refMethods))
60+
if (!methodDeclarationSymbol.OverridesAny(preRestartMethods))
5161
return;
5262

5363
var diagnostic = Diagnostic.Create(

src/Akka.Analyzers/AK1000/ShouldNotCallPersistInsideLoopAnalyzer.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,34 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
2020
Guard.AssertIsNotNull(context);
2121
Guard.AssertIsNotNull(akkaContext);
2222

23+
// Nothing to match without Akka.Persistence, and returning before registering spares such
24+
// solutions a per-node callback for every invocation in the compilation.
25+
if (!akkaContext.HasAkkaPersistenceInstalled)
26+
return;
27+
28+
// Hoisted out of the per-node action, where the AddRange allocated a fresh ImmutableArray
29+
// per call site.
30+
var eventsourcedContext = akkaContext.AkkaPersistence.Eventsourced;
31+
var refMethods = eventsourcedContext.Persist.AddRange(eventsourcedContext.PersistAsync);
32+
var refMethodNames = refMethods.MethodNames();
33+
if (refMethodNames.IsEmpty)
34+
return;
35+
2336
context.RegisterSyntaxNodeAction(ctx =>
2437
{
25-
// No need to check if Akka.Persistence is not installed
26-
if (!akkaContext.HasAkkaPersistenceInstalled)
27-
return;
28-
2938
var invocationExpression = (InvocationExpressionSyntax)ctx.Node;
39+
40+
// Reject by name before binding.
41+
if (!invocationExpression.CouldInvokeAnyOf(refMethodNames))
42+
return;
43+
3044
var semanticModel = ctx.SemanticModel;
3145

3246
// Get the member symbol from the invocation expression
3347
if(semanticModel.GetSymbolInfo(invocationExpression.Expression).Symbol is not IMethodSymbol methodInvocationSymbol)
3448
return;
35-
36-
var persistenceContext = akkaContext.AkkaPersistence;
37-
49+
3850
// Check if the method name is `Persist` or `PersistAsync`
39-
var eventsourcedContext = persistenceContext.Eventsourced;
40-
var refMethods = eventsourcedContext.Persist.AddRange(eventsourcedContext.PersistAsync);
4151
if (!methodInvocationSymbol.MatchesAny(refMethods))
4252
return;
4353

src/Akka.Analyzers/AK1000/ShouldNotUseReceiveAsyncWithoutAsyncLambdaAnalyzer.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,20 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
2121
Guard.AssertIsNotNull(context);
2222
Guard.AssertIsNotNull(akkaContext);
2323

24+
// Per-compilation state, hoisted out of the per-node action.
25+
var receiveActor = akkaContext.AkkaCore.Actor.ReceiveActor;
26+
var receiveAsyncNames = receiveActor.ReceiveAsync.AddRange(receiveActor.ReceiveAnyAsync).MethodNames();
27+
if (receiveAsyncNames.IsEmpty)
28+
return;
29+
2430
context.RegisterSyntaxNodeAction(ctx =>
2531
{
2632
var invocationExpr = (InvocationExpressionSyntax)ctx.Node;
33+
34+
// Reject by name before binding.
35+
if (!invocationExpr.CouldInvokeAnyOf(receiveAsyncNames))
36+
return;
37+
2738
var semanticModel = ctx.SemanticModel;
2839

2940
// check that the invocation is a valid ReceiveAsync or ReceiveAnyAsync method

src/Akka.Analyzers/AK1000/ShouldNotUseSystemToCreateChildActorsAnalyzer.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,26 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
2222
Guard.AssertIsNotNull(context);
2323
Guard.AssertIsNotNull(akkaContext);
2424

25+
// Both methods this rule matches are named ActorOf.
26+
var actorOfNames = new[]
27+
{
28+
akkaContext.AkkaCore.Actor.ActorSystem.ActorOf,
29+
akkaContext.AkkaCore.Actor.ActorRefFactoryExtensions.ActorOf
30+
}
31+
.Where(m => m is not null)
32+
.Select(m => m!.Name)
33+
.ToImmutableHashSet();
34+
if (actorOfNames.IsEmpty)
35+
return;
36+
2537
context.RegisterSyntaxNodeAction(ctx =>
2638
{
2739
var invocationExpression = (InvocationExpressionSyntax)ctx.Node;
40+
41+
// Reject by name before binding.
42+
if (!invocationExpression.CouldInvokeAnyOf(actorOfNames))
43+
return;
44+
2845
var semanticModel = ctx.SemanticModel;
2946

3047
// Get the member symbol from the invocation expression

src/Akka.Analyzers/AK1000/ShouldUseIWithTimersInsteadOfScheduleTellAnalyzer.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,22 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
2121
Guard.AssertIsNotNull(context);
2222
Guard.AssertIsNotNull(akkaContext);
2323

24+
// Per-compilation state, hoisted out of the per-node action. Rejecting by name matters most
25+
// here: this rule resolved the enclosing class symbol as well as the invocation, per call site.
26+
var scheduleTellOnce = akkaContext.AkkaCore.Actor.ITellScheduler.ScheduleTellOnce;
27+
var scheduleTellRepeatedly = akkaContext.AkkaCore.Actor.ITellScheduler.ScheduleTellRepeatedly;
28+
var scheduleTellNames = scheduleTellOnce.AddRange(scheduleTellRepeatedly).MethodNames();
29+
if (scheduleTellNames.IsEmpty)
30+
return;
31+
2432
context.RegisterSyntaxNodeAction(ctx =>
2533
{
2634
var invocationExpr = (InvocationExpressionSyntax)ctx.Node;
35+
36+
// Reject by name before binding.
37+
if (!invocationExpr.CouldInvokeAnyOf(scheduleTellNames))
38+
return;
39+
2740
var semanticModel = ctx.SemanticModel;
2841

2942
var classDeclaration = invocationExpr.FirstAncestorOrSelf<ClassDeclarationSyntax>();
@@ -47,16 +60,14 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
4760
// Check if the method name is `ScheduleTellOnce` or `ScheduleTellRepeatedly`
4861
ArgumentSyntax? receiver = null;
4962
ArgumentSyntax? sender = null;
50-
var refSymbols = akkaContext.AkkaCore.Actor.ITellScheduler.ScheduleTellOnce;
51-
if (refSymbols.Any(s => ReferenceEquals(methodSymbol, s)))
63+
if (scheduleTellOnce.Any(s => ReferenceEquals(methodSymbol, s)))
5264
{
5365
receiver = invocationExpr.ArgumentList.Arguments[1];
5466
sender = invocationExpr.ArgumentList.Arguments[3];
5567
}
5668
else
5769
{
58-
refSymbols = akkaContext.AkkaCore.Actor.ITellScheduler.ScheduleTellRepeatedly;
59-
if (refSymbols.Any(s => ReferenceEquals(methodSymbol, s)))
70+
if (scheduleTellRepeatedly.Any(s => ReferenceEquals(methodSymbol, s)))
6071
{
6172
receiver = invocationExpr.ArgumentList.Arguments[2];
6273
sender = invocationExpr.ArgumentList.Arguments[4];

src/Akka.Analyzers/AK2000/MustNotUseAutomaticallyHandledMessagesInsideMessageExtractorAnalyzer.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,24 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
3636
AnalyzeMethodDeclaration(ctx, akkaContext);
3737
}, SyntaxKind.MethodDeclaration);
3838

39+
// Per-compilation state; without the type there is nothing to match at all.
40+
var hashCodeMessageExtractorSymbol =
41+
context.Compilation.GetTypeByMetadataName("Akka.Cluster.Sharding.HashCodeMessageExtractor");
42+
if (hashCodeMessageExtractorSymbol == null)
43+
return; // couldn't find the type
44+
3945
context.RegisterSyntaxNodeAction(ctx =>
4046
{
4147
var invocationExpr = (InvocationExpressionSyntax)ctx.Node;
48+
49+
// The only match is HashCodeMessageExtractor.Create; reject by name before binding.
50+
if (invocationExpr.InvokedSimpleName() != "Create")
51+
return;
52+
4253
var semanticModel = ctx.SemanticModel;
4354
if (semanticModel.GetSymbolInfo(invocationExpr).Symbol is not IMethodSymbol methodSymbol)
4455
return; // couldn't find the symbol, bail out quickly
4556

46-
var hashCodeMessageExtractorSymbol =
47-
context.Compilation.GetTypeByMetadataName("Akka.Cluster.Sharding.HashCodeMessageExtractor");
48-
if (hashCodeMessageExtractorSymbol == null)
49-
return; // couldn't find the type
50-
5157
if (SymbolEqualityComparer.Default.Equals(methodSymbol.ContainingType, hashCodeMessageExtractorSymbol) &&
5258
methodSymbol is { IsStatic: true, Name: "Create" })
5359
{

src/Akka.Analyzers/AK2000/MustNotUseTimeSpanZeroWithAskAnalyzer.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
2525
{
2626
var invocationExpr = (InvocationExpressionSyntax)ctx.Node;
2727

28+
// The symbol check below only matches Ask; reject by name before binding.
29+
if (invocationExpr.InvokedSimpleName() != "Ask")
30+
return;
31+
2832
if (ctx.SemanticModel.GetSymbolInfo(invocationExpr).Symbol is IMethodSymbol { Name: "Ask" } methodSymbol &&
2933
methodSymbol.Parameters.Any(p => p.Type.ToString() == "System.TimeSpan?"))
3034
foreach (var argument in invocationExpr.ArgumentList.Arguments)

src/Akka.Analyzers/AK2000/MustNotUseVoidAsyncDelegateInDslActorReceiveAnalyzer.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,30 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
2121
Guard.AssertIsNotNull(context);
2222
Guard.AssertIsNotNull(akkaContext);
2323

24+
// Per-compilation state, hoisted out of the per-node action.
25+
var targetMethods = akkaContext.AkkaCore.Actor.Dsl.IActorDsl.Receive;
26+
var targetMethodNames = targetMethods.MethodNames();
27+
if (targetMethodNames.IsEmpty)
28+
return;
29+
var actionSymbol = context.Compilation.GetTypeByMetadataName("System.Action`2");
30+
2431
context.RegisterSyntaxNodeAction(ctx =>
2532
{
2633
var inv = (InvocationExpressionSyntax)ctx.Node;
34+
35+
// Reject by name before binding.
36+
if (!inv.CouldInvokeAnyOf(targetMethodNames))
37+
return;
38+
2739
if(ctx.SemanticModel.GetSymbolInfo(inv.Expression).Symbol is not IMethodSymbol method)
2840
return;
2941

3042
if (method.IsGenericMethod)
3143
method = method.OriginalDefinition;
32-
33-
var compilation = context.Compilation;
34-
3544

36-
if (!akkaContext.AkkaCore.Actor.Dsl.IActorDsl.Receive.Any(m => ReferenceEquals(method, m)))
45+
if (!targetMethods.Any(m => ReferenceEquals(method, m)))
3746
return;
3847

39-
var actionSymbol = compilation.GetTypeByMetadataName("System.Action`2");
40-
4148
var index = 0;
4249
foreach (var p in method.Parameters)
4350
{

src/Akka.Analyzers/AK2000/MustNotUseVoidAsyncDelegateInReceiveActorReceiveAnalyzer.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,30 @@ public override void AnalyzeCompilation(CompilationStartAnalysisContext context,
2121
Guard.AssertIsNotNull(context);
2222
Guard.AssertIsNotNull(akkaContext);
2323

24+
// Per-compilation state, hoisted out of the per-node action.
25+
var targetMethods = akkaContext.AkkaCore.Actor.ReceiveActor.Receive;
26+
var targetMethodNames = targetMethods.MethodNames();
27+
if (targetMethodNames.IsEmpty)
28+
return;
29+
var actionSymbol = context.Compilation.GetTypeByMetadataName("System.Action`1");
30+
2431
context.RegisterSyntaxNodeAction(ctx =>
2532
{
2633
var inv = (InvocationExpressionSyntax)ctx.Node;
34+
35+
// Reject by name before binding.
36+
if (!inv.CouldInvokeAnyOf(targetMethodNames))
37+
return;
38+
2739
if(ctx.SemanticModel.GetSymbolInfo(inv.Expression).Symbol is not IMethodSymbol method)
2840
return;
2941

3042
if (method.IsGenericMethod)
3143
method = method.OriginalDefinition;
3244

33-
var compilation = context.Compilation;
34-
if (!akkaContext.AkkaCore.Actor.ReceiveActor.Receive.Any(m => SymbolEqualityComparer.Default.Equals(method, m)))
45+
if (!targetMethods.Any(m => SymbolEqualityComparer.Default.Equals(method, m)))
3546
return;
36-
37-
var actionSymbol = compilation.GetTypeByMetadataName("System.Action`1");
38-
47+
3948
var index = 0;
4049
foreach (var p in method.Parameters)
4150
{

0 commit comments

Comments
 (0)