Skip to content

Significant performance improvement#668

Open
gelverpl wants to merge 7 commits intoAcumatica:devfrom
gelverpl:playground
Open

Significant performance improvement#668
gelverpl wants to merge 7 commits intoAcumatica:devfrom
gelverpl:playground

Conversation

@gelverpl
Copy link
Copy Markdown

@gelverpl gelverpl commented May 6, 2026

Proposed changes

  1. NestedInvocationWalker now caches retrieved ISymbol instances using a new SymbolInfoCache.
  2. Fixed a serious issue that was causing a large amount of redundant work: multiple call graph traversals, which in turn caused an excessive number of semantic queries to resolve ISymbol.

Motivation

While profiling the analyzer, I noticed extremely high memory usage, and running solution analysis from the CLI took more than 40 minutes.
dotTrace pointed to Acuminator.Utilities.Roslyn.NestedInvocationWalker.GetSymbol(ExpressionSyntax) as a primary hotspot:
Screenshot 2026-05-06 201213
Screenshot 2026-05-06 201339

Memory profiling confirmed the same method:
Screenshot 2026-05-06 204141

Caching

As a load-reducing measure, I implemented caching. This significantly reduced both execution time and memory consumption.

Yes, caching is often a last-resort approach and ideally shouldn't be here. However, when implemented carefully it can also provide valuable diagnostic data that helps guide further optimization work.

Cache statistics

SymbolInfoCache can optionally collect statistics. Enable it by defining the SYMBOL_INFO_CACHE_STATISTICS constant, then inspect Acuminator.Utilities.Roslyn.SymbolInfoCache.SymbolInfoCacheStatisticsContext.GetStatistics under the debugger.

I intentionally did not add any logging of it because the statistics can become very large. It's much more practical to analyze it interactively (e.g., via LINQ in a debug session).

While digging into the stats, I noticed something weird: the same expression was cached under two different key types: InvocationExpressionSyntax and MemberAccessExpressionSyntax. Debugging led to VisitMemberAccessExpression.

foo.Bar() is both MemberAccessExpressionSyntax and InvocationExpressionSyntax

By filtering out redundant invocation processing inside MemberAccessExpressionSyntax, I added an additional fix. This change, similarly to caching, delivered a substantial improvement in both time and memory.

Benchmarks

I measured the impact of each fix independently, as well as both together.

Total runtime (wall-clock):
Screenshot 2026-05-06 211607

  • Speedup ranges from 1.30x to 7.43x (about 4x on average)

Memory allocation / GC pressure:
Screenshot 2026-05-06 211648

  • Allocations reduced by 1.59x to 8.78x (about 5x on average)
  • Significantly less GC overhead due to drastically fewer allocations

With both fixes applied, traces no longer show GetSymbol as a top hotspot; instead, the cache path (GetOrCreate) appears, and overall memory usage is dramatically improved.
Screenshot 2026-05-06 203630
Screenshot 2026-05-06 204713

Conclusion

Even though caching contributes less after the second fix, I'd still like to keep it (at least for upcoming optimization work) as a way to collect detailed statistics. The goal is to reach a hit rate that is 0 or as close to 0 as possible (i.e., eliminate the underlying redundant symbol queries altogether).

I plan to continue working on this area and believe we can achieve even more significant improvements.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR targets analyzer performance hotspots in NestedInvocationWalker by reducing redundant Roslyn semantic symbol queries and avoiding duplicate traversal work, with an additional (unrelated) addition of Rider/JetBrains project configuration files.

Changes:

  • Introduced SymbolInfoCache to cache SemanticModel.GetSymbolInfo(...) results per ExpressionSyntax.
  • Updated NestedInvocationWalker.GetSymbol<T> to use the cache and reduced redundant processing in VisitMemberAccessExpression for invocation callees.
  • Added Rider/JetBrains .idea configuration files under src/.idea/.idea.Acuminator/.idea/.

Reviewed changes

Copilot reviewed 2 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs New symbol-info cache (and optional statistics collection) used to reduce repeated semantic queries.
src/Acuminator/Acuminator.Utilities/Roslyn/NestedInvocationWalker.cs Uses the cache in GetSymbol<T> and avoids redundant member-access processing for foo.Bar() patterns.
src/.idea/.idea.Acuminator/.idea/vcs.xml Adds Rider VCS mapping configuration.
src/.idea/.idea.Acuminator/.idea/indexLayout.xml Adds Rider index layout configuration.
src/.idea/.idea.Acuminator/.idea/.name / .gitignore Adds Rider project name and IDE-local ignore rules.
Files not reviewed (4)
  • src/.idea/.idea.Acuminator/.idea/.gitignore: Language not supported
  • src/.idea/.idea.Acuminator/.idea/.name: Language not supported
  • src/.idea/.idea.Acuminator/.idea/indexLayout.xml: Language not supported
  • src/.idea/.idea.Acuminator/.idea/vcs.xml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/.idea/.idea.Acuminator/.idea/vcs.xml
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
@SENya1990 SENya1990 self-requested a review May 6, 2026 20:10
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/NestedInvocationWalker.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
/// </summary>
public sealed class SymbolInfoCache
{
private readonly Dictionary<ExpressionSyntax, SymbolInfo> _map = new();
Copy link
Copy Markdown
Collaborator

@SENya1990 SENya1990 May 6, 2026

Choose a reason for hiding this comment

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

@gelverpl we should cache ISymbol?, there is no need to keep intermediate symbol info DTO.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I find this type more convenient for analysis.

Won't be fixed.

var semanticModel = GetSemanticModel(node.SyntaxTree);

if (semanticModel != null)
SymbolInfo? cached = _cache.GetOrCreate(node, () =>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here should be the retrieval of the symbol from cache, not SymbolInfo.
As I wrote in https://github.com/Acumatica/Acuminator/pull/668/changes#r3197690911, cache should keep ISymbol?.

Also, since the symbol is not always cached, IMHO it's better to call it just symbol

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I find this type more convenient for analysis.

Won't be fixed.

Copy link
Copy Markdown
Collaborator

@SENya1990 SENya1990 left a comment

Choose a reason for hiding this comment

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

Overall, the PR content is good, there is no need to significantly change anything.
But I have some remarks that should be processed:

  • about the type of cached data,
  • some minor remarks about the concurrent code
  • remarks about the code style.

Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Comment thread src/Acuminator/Acuminator.Utilities/Roslyn/SymbolInfoCache.cs Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants