-
Notifications
You must be signed in to change notification settings - Fork 13
Significant performance improvement #668
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
base: dev
Are you sure you want to change the base?
Changes from all commits
ef253ce
4ad366f
5dfc45c
8a73725
85f2a69
51048e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,6 +53,8 @@ public abstract class NestedInvocationWalker : CSharpSyntaxWalker | |
|
|
||
| private readonly ISet<(SyntaxNode, DiagnosticDescriptor)> _reportedDiagnostics = new HashSet<(SyntaxNode, DiagnosticDescriptor)>(); | ||
|
|
||
| private readonly SymbolInfoCache _cache; | ||
|
|
||
| /// <summary> | ||
| /// Cancellation token | ||
| /// </summary> | ||
|
|
@@ -87,6 +89,8 @@ protected NestedInvocationWalker(PXContext pxContext, CancellationToken cancella | |
|
|
||
| //Use lazy to avoid calling virtual methods inside the constructor | ||
| _typesToBypass = new Lazy<HashSet<INamedTypeSymbol>>(valueFactory: GetTypesToBypass, isThreadSafe: false); | ||
|
|
||
| _cache = new SymbolInfoCache(GetType()); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -119,20 +123,22 @@ protected virtual HashSet<INamedTypeSymbol> GetTypesToBypass() => | |
| protected virtual T? GetSymbol<T>(ExpressionSyntax node) | ||
| where T : class, ISymbol | ||
| { | ||
| var semanticModel = GetSemanticModel(node.SyntaxTree); | ||
|
|
||
| if (semanticModel != null) | ||
| SymbolInfo? cached = _cache.GetOrCreate(node, () => | ||
|
Collaborator
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. Here should be the retrieval of the symbol from cache, not Also, since the symbol is not always cached, IMHO it's better to call it just |
||
| { | ||
| var symbolInfo = semanticModel.GetSymbolInfo(node, CancellationToken); | ||
| SemanticModel? semanticModel = GetSemanticModel(node.SyntaxTree); | ||
| return semanticModel?.GetSymbolInfo(node, CancellationToken); | ||
| }); | ||
|
|
||
| if (symbolInfo.Symbol is T symbol) | ||
| if (cached is not null) | ||
| { | ||
| if (cached.Value.Symbol is T symbol) | ||
| { | ||
| return symbol; | ||
| } | ||
|
|
||
| if (!symbolInfo.CandidateSymbols.IsEmpty) | ||
| if (!cached.Value.CandidateSymbols.IsEmpty) | ||
| { | ||
| return symbolInfo.CandidateSymbols.OfType<T>().FirstOrDefault(); | ||
| return cached.Value.CandidateSymbols.OfType<T>().FirstOrDefault(); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -204,8 +210,16 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node) | |
|
|
||
| public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node) | ||
| { | ||
| VisitPropertyOrIndexerAccessExpression(node); | ||
| base.VisitMemberAccessExpression(node); | ||
| if (node.Parent is InvocationExpressionSyntax invocation && invocation.Expression == node) | ||
| { | ||
| // we already visit this node by VisitInvocationExpression, so we just skip it here | ||
| base.VisitMemberAccessExpression(node); | ||
| } | ||
| else | ||
| { | ||
| VisitPropertyOrIndexerAccessExpression(node); | ||
| base.VisitMemberAccessExpression(node); | ||
| } | ||
| } | ||
|
|
||
| public override void VisitElementAccessExpression(ElementAccessExpressionSyntax node) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Threading; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
|
||
| namespace Acuminator.Utilities.Roslyn; | ||
|
|
||
| /// <summary> | ||
| /// Caches Roslyn symbol lookup results for expression syntax nodes visited by a syntax walker. | ||
|
Collaborator
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. IMHO, this can be already a bit generalized to be lookup for syntax nodes, not just expression syntax nodes. There are many other types of nodes that can be passed to the semantic model:
Author
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. The only user of this cache is Won't be fixed. |
||
| /// </summary> | ||
| public sealed class SymbolInfoCache | ||
| { | ||
| private readonly Dictionary<ExpressionSyntax, SymbolInfo> _map = new(); | ||
|
Collaborator
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. @gelverpl we should cache |
||
| #if SYMBOL_INFO_CACHE_STATISTICS | ||
|
Collaborator
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. @gelverpl we spoke today about using some kind of feature flags for performance profiling features. Did you decide not to use it in the end?
Author
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. I was more talking about enabling/disabling certain optimizations to make the measurement process more convenient. As for these statistics, I think the only person who will need them is me. So I don't see any need for anything more sophisticated here. |
||
| private readonly SymbolInfoCacheStatistics _statistics; | ||
| #endif | ||
|
|
||
| public SymbolInfoCache(Type owner) | ||
| { | ||
| #if SYMBOL_INFO_CACHE_STATISTICS | ||
| _statistics = SymbolInfoCacheStatisticsContext.Register(owner); | ||
| #endif | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the cached symbol information for the specified expression, or creates and stores it using the provided factory. | ||
| /// </summary> | ||
| public SymbolInfo? GetOrCreate(ExpressionSyntax key, Func<SymbolInfo?> factory) | ||
| { | ||
| if (_map.TryGetValue(key, out SymbolInfo cached)) | ||
| { | ||
| #if SYMBOL_INFO_CACHE_STATISTICS | ||
|
Collaborator
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. All counting methods can be annotated with the It won't compile without the pragma directive as is though. So, to make it compile and get rid of to many pragmas, maybe you can always keep the field with statistics object and for regular builds initialize it to null or an empty static object. |
||
| _statistics.Hit(); | ||
| #endif | ||
| return cached; | ||
| } | ||
|
|
||
| #if SYMBOL_INFO_CACHE_STATISTICS | ||
| _statistics.Miss(); | ||
| #endif | ||
|
|
||
| SymbolInfo? potentialValue = factory(); | ||
| if (potentialValue is not null) | ||
|
Collaborator
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. Why not caching null results? It's not like the semantic model will return a different result with another call. |
||
| { | ||
| _map[key] = potentialValue.Value; | ||
| #if SYMBOL_INFO_CACHE_STATISTICS | ||
| _statistics.Set(key.GetType()); | ||
| #endif | ||
| return potentialValue; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| #if SYMBOL_INFO_CACHE_STATISTICS | ||
| public sealed class SymbolInfoCacheStatistics | ||
|
Collaborator
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. Should be moved to a separate file. |
||
| { | ||
| private readonly Dictionary<Type, long> _byTypeCounter = new(); | ||
|
gelverpl marked this conversation as resolved.
|
||
| private readonly Type _owner; | ||
| private readonly int _cacheId; | ||
|
|
||
| private long _hitsTotal; | ||
| private long _missesTotal; | ||
| private long _setsTotal; | ||
|
|
||
| public SymbolInfoCacheStatistics(int cacheId, Type owner) | ||
| { | ||
| _owner = owner; | ||
| _cacheId = cacheId; | ||
| } | ||
|
|
||
| public void Hit() => _hitsTotal++; | ||
|
Collaborator
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. All counting methods should be annotated with the |
||
|
|
||
| public void Miss() => _missesTotal++; | ||
|
|
||
| public void Set(Type type) | ||
|
gelverpl marked this conversation as resolved.
|
||
| { | ||
| if (!_byTypeCounter.ContainsKey(type)) | ||
| { | ||
| _byTypeCounter[type] = 1; | ||
| } | ||
| else | ||
| { | ||
| _byTypeCounter[type]++; | ||
| } | ||
| _setsTotal++; | ||
| } | ||
|
Comment on lines
+73
to
+88
Collaborator
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. This shouldn't be a problem for now since each instance of symbol cache and related statistic is owned by only one dedicated nested invocation walker which is executed only by one thread. Later, when this cache will be expanded to be shared with all analyzers, this indeed will be a problem.
Author
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. This cache is strictly reserved for its intended code and must not be accessed or reused elsewhere. When and if we decide to implement some kind of shared cache, it will be a completely new and unrelated implementation. Won't be fixed. |
||
|
|
||
| public StatisticsSnapshot GetSnapshot() | ||
| { | ||
| var lookupsTotal = _hitsTotal + _missesTotal; | ||
| return new StatisticsSnapshot( | ||
| _cacheId, | ||
| _owner, | ||
| _hitsTotal, | ||
| _missesTotal, | ||
| _setsTotal, | ||
| _byTypeCounter, | ||
| CalcRatio(_hitsTotal, lookupsTotal), | ||
| CalcRatio(_missesTotal, lookupsTotal) | ||
| ); | ||
|
Comment on lines
+90
to
+102
Collaborator
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. @gelverpl this is a good point. It's not really a snapshot if you are passing the original reference which will be mutated after the creation of the snapshot.
Author
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. Actually, this is not. All of the above is irrelevant, as no one collects (and should never do) these statistics except the person who requests these statistics in debug mode. Won't be fixed.
Collaborator
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.
@gelverpl you can't guarantee that or predict who will work on this code in two-three years.
Again:
|
||
| } | ||
|
|
||
| private static double CalcRatio(long value, long total) => total == 0 ? 0 : (double)value / total; | ||
| } | ||
|
|
||
| public static class SymbolInfoCacheStatisticsContext | ||
|
Collaborator
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. @gelverpl please use 1 type per file convention (except maybe 1 line records types) and move this to a separate file. |
||
| { | ||
| private static readonly object Gate = new(); | ||
|
Collaborator
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. For private fields the current convention is to use underscore-prefixed camel case. Also it's not very important, but the established naming convention for these objects is to call them |
||
| private static readonly List<SymbolInfoCacheStatistics> Statistics = new(); | ||
| private static int _nextCacheId; | ||
|
|
||
| internal static SymbolInfoCacheStatistics Register(Type owner) | ||
|
Collaborator
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. @gelverpl there can be multiple statistics registered for the same owner (walker). Is it okay to not aggregate them into one statistic? |
||
| { | ||
| var cacheId = Interlocked.Increment(ref _nextCacheId); | ||
| var statistics = new SymbolInfoCacheStatistics(cacheId, owner); | ||
|
|
||
| lock (Gate) | ||
| { | ||
| Statistics.Add(statistics); | ||
| } | ||
|
|
||
| return statistics; | ||
| } | ||
|
|
||
|
SENya1990 marked this conversation as resolved.
|
||
| public static StatisticsSnapshot[] GetStatistics() | ||
| { | ||
| lock (Gate) | ||
|
Collaborator
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. This is not a big improvement, since |
||
| { | ||
| var result = new StatisticsSnapshot[Statistics.Count]; | ||
| for (var i = 0; i < Statistics.Count; i++) | ||
| { | ||
| result[i] = Statistics[i].GetSnapshot(); | ||
|
Collaborator
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. Since it's a static context, it can be shared by multiple analyzers running on different threads. Which means that while the call to For full snapshots you would need to use sync the updates of the counters with the retrieval of statistics... which will make things more difficult for us and introduce undesirable threads synchronization. I think it's OK to leave it as is since we are not micro benchmarking and looking for milliseconds. But this should be a conscious decision, so I think a comment about this decision should be added here describing it. |
||
| } | ||
|
|
||
| return result; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public record StatisticsSnapshot(int CacheId, Type Owner, long HitsTotal, long MissesTotal, long SetsTotal, Dictionary<Type, long> ByTypeCounter, double HitRatio, double MissRatio); | ||
|
Collaborator
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. Imho, this DTO can also be in a separate file, but I won't mind if it will be placed in a file with the And it also should be |
||
| #endif | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.