Skip to content

Commit

Permalink
Enable per-file state caching in analyzer. The state can be shared ac…
Browse files Browse the repository at this point in the history
…ross analyzer actions and also across different analyzer instances.

Fixes dotnet#6324
  • Loading branch information
mavasani committed Feb 5, 2016
1 parent 7f527b5 commit c42a411
Show file tree
Hide file tree
Showing 18 changed files with 626 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@ public void TestReportingDiagnosticWithInvalidLocation()
var compilation = CreateCompilationWithMscorlib45(source1);
var anotherCompilation = CreateCompilationWithMscorlib45(source2);
var treeInAnotherCompilation = anotherCompilation.SyntaxTrees.Single();

string message = new ArgumentException(
string.Format(CodeAnalysisResources.InvalidDiagnosticLocationReported, AnalyzerWithInvalidDiagnosticLocation.Descriptor.Id, treeInAnotherCompilation.FilePath), "diagnostic").Message;

Expand Down Expand Up @@ -1446,7 +1446,7 @@ private static DiagnosticDescription[] GetExpectedGeneratedCodeAnalyzerDiagnosti

if (compilation.Options.GeneralDiagnosticOption == ReportDiagnostic.Error)
{
for(int i = 0; i < builder.Count; i++)
for (int i = 0; i < builder.Count; i++)
{
if (((string)builder[i].Code) != GeneratedCodeAnalyzer.Error.Id)
{
Expand Down Expand Up @@ -1497,7 +1497,7 @@ private static void AddExpectedDiagnostic(ArrayBuilder<DiagnosticDescription> bu
public void TestEnsureNoMergedNamespaceSymbolAnalyzer()
{
var source = @"namespace N1.N2 { }";

var metadataReference = CreateCompilationWithMscorlib(source).ToMetadataReference();
var compilation = CreateCompilationWithMscorlib(source, new[] { metadataReference });
compilation.VerifyDiagnostics();
Expand All @@ -1506,5 +1506,44 @@ public void TestEnsureNoMergedNamespaceSymbolAnalyzer()
var analyzers = new DiagnosticAnalyzer[] { new EnsureNoMergedNamespaceSymbolAnalyzer() };
compilation.VerifyAnalyzerDiagnostics(analyzers);
}

[Fact, WorkItem(6324, "https://github.com/dotnet/roslyn/issues/6324")]
public void TestSharedStateAnalyzer()
{
string source1 = @"
public partial class C { }
";
string source2 = @"
public partial class C2 { }
";
string source3 = @"
public partial class C33 { }
";
var tree1 = CSharpSyntaxTree.ParseText(source1, path: "Source1_File1.cs");
var tree2 = CSharpSyntaxTree.ParseText(source1, path: "Source1_File2.cs");
var tree3 = CSharpSyntaxTree.ParseText(source2, path: "Source2_File3.cs");
var tree4 = CSharpSyntaxTree.ParseText(source3, path: "Source3_File4.generated.cs");
var tree5 = CSharpSyntaxTree.ParseText(source3, path: "Source3_File5.designer.cs");

var compilation = CreateCompilationWithMscorlib45(new[] { tree1, tree2, tree3, tree4, tree5 });
compilation.VerifyDiagnostics();

var analyzers = new DiagnosticAnalyzer[] { new SharedStateAnalyzer() };
compilation.VerifyAnalyzerDiagnostics(analyzers, null, null, true,
Diagnostic("UserCodeDiagnostic").WithArguments("Source1_File1.cs").WithLocation(1, 1),
Diagnostic("UniqueTextFileDiagnostic").WithArguments("Source1_File1.cs").WithLocation(1, 1),
Diagnostic("GeneratedCodeDiagnostic", "C33").WithArguments("C33").WithLocation(2, 22),
Diagnostic("UserCodeDiagnostic", "C2").WithArguments("C2").WithLocation(2, 22),
Diagnostic("UserCodeDiagnostic", "C").WithArguments("C").WithLocation(2, 22),
Diagnostic("UserCodeDiagnostic").WithArguments("Source1_File2.cs").WithLocation(1, 1),
Diagnostic("UniqueTextFileDiagnostic").WithArguments("Source1_File2.cs").WithLocation(1, 1),
Diagnostic("UserCodeDiagnostic").WithArguments("Source2_File3.cs").WithLocation(1, 1),
Diagnostic("UniqueTextFileDiagnostic").WithArguments("Source2_File3.cs").WithLocation(1, 1),
Diagnostic("GeneratedCodeDiagnostic").WithArguments("Source3_File4.generated.cs").WithLocation(1, 1),
Diagnostic("UniqueTextFileDiagnostic").WithArguments("Source3_File4.generated.cs").WithLocation(1, 1),
Diagnostic("GeneratedCodeDiagnostic").WithArguments("Source3_File5.designer.cs").WithLocation(1, 1),
Diagnostic("UniqueTextFileDiagnostic").WithArguments("Source3_File5.designer.cs").WithLocation(1, 1),
Diagnostic("NumberOfUniqueTextFileDescriptor").WithArguments("3").WithLocation(1, 1));
}
}
}
}
7 changes: 7 additions & 0 deletions src/Compilers/Core/Portable/CodeAnalysis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,14 @@
<Compile Include="Compilation\OperationVisitor.cs" />
<Compile Include="Compilation\OperationWalker.cs" />
<Compile Include="Compilation\ScriptCompilationInfo.cs" />
<Compile Include="DiagnosticAnalyzer\SyntaxTreeValueProvider.cs" />
<Compile Include="DiagnosticAnalyzer\SourceTextValueProvider.cs" />
<Compile Include="DiagnosticAnalyzer\CompilationAnalysisValueProviderFactory.cs" />
<Compile Include="DiagnosticAnalyzer\CompilationAnalysisValueProvider.cs" />
<Compile Include="DiagnosticAnalyzer\AnalyzerDriver.GeneratedCodeUtilities.cs" />
<Compile Include="DiagnosticAnalyzer\AnalyzerDriver.CompilationData.cs" />
<Compile Include="DiagnosticAnalyzer\AnalysisContextInfo.cs" />
<Compile Include="DiagnosticAnalyzer\AnalysisValueProvider.cs" />
<Compile Include="DiagnosticAnalyzer\SuppressMessageInfo.cs" />
<Compile Include="Diagnostic\SuppressionInfo.cs" />
<Compile Include="InternalUtilities\SetWithInsertionOrder.cs" />
Expand All @@ -76,9 +81,11 @@
<Compile Include="RealParser.cs" />
<Compile Include="ReferenceManager\MergedAliases.cs" />
<Compile Include="StrongName\CryptoBlobParser.cs" />
<Compile Include="Syntax\SyntaxTreeComparer.cs" />
<Compile Include="Syntax\ICompilationUnitSyntax.cs" />
<Compile Include="Syntax\ISkippedTokensTriviaSyntax.cs" />
<Compile Include="Text\LargeTextWriter.cs" />
<Compile Include="Text\SourceTextComparer.cs" />
<Compile Include="Text\SourceTextWriter.cs" />
<Compile Include="Text\StringTextWriter.cs" />
<Compile Include="UnicodeCharacterUtilities.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.IO;
using System.Threading;
using Microsoft.CodeAnalysis.Text;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

namespace Microsoft.CodeAnalysis.Diagnostics
{
internal class AnalysisValueProvider<TKey, TValue>
where TKey : class
{
private readonly Func<TKey, TValue> _computeValue;

// This provider holds a weak reference to the key-value pairs, as AnalysisValueProvider might outlive individual compilations.
// CompilationAnalysisValueProvider, which wraps this provider and lives for the lifetime of specific compilation, holds a strong reference to the key-value pairs, providing an overall performance benefit.
private readonly ConditionalWeakTable<TKey, WrappedValue> _valueCache;
private readonly ConditionalWeakTable<TKey, WrappedValue>.CreateValueCallback _valueCacheCallback;

internal IEqualityComparer<TKey> KeyComparer { get; private set; }

public AnalysisValueProvider(Func<TKey, TValue> computeValue, IEqualityComparer<TKey> keyComparer)
{
_computeValue = computeValue;
KeyComparer = keyComparer ?? EqualityComparer<TKey>.Default;
_valueCache = new ConditionalWeakTable<TKey, WrappedValue>();
_valueCacheCallback = new ConditionalWeakTable<TKey, WrappedValue>.CreateValueCallback(ComputeValue);
}

private sealed class WrappedValue
{
public TValue Value { get; set; }
}

private WrappedValue ComputeValue(TKey key)
{
var value = _computeValue(key);
return new WrappedValue { Value = value };
}

internal bool TryGetValue(TKey key, out TValue value)
{
// Catch any exceptions from the computeValue callback, which calls into user code.
try
{
value = _valueCache.GetValue(key, _valueCacheCallback).Value;
return true;
}
catch (Exception)
{
value = default(TValue);
return false;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ internal class AnalyzerExecutor
private readonly Func<DiagnosticAnalyzer, bool> _shouldSkipAnalysisOnGeneratedCode;
private readonly Func<Diagnostic, DiagnosticAnalyzer, Compilation, CancellationToken, bool> _shouldSuppressGeneratedCodeDiagnostic;
private readonly ConcurrentDictionary<DiagnosticAnalyzer, TimeSpan> _analyzerExecutionTimeMapOpt;
private readonly CompilationAnalysisValueProviderFactory _compilationAnalysisValueProviderFactory;
private readonly CancellationToken _cancellationToken;

/// <summary>
Expand Down Expand Up @@ -163,6 +164,8 @@ private AnalyzerExecutor(
_addCategorizedLocalDiagnosticOpt = addCategorizedLocalDiagnosticOpt;
_addCategorizedNonLocalDiagnosticOpt = addCategorizedNonLocalDiagnosticOpt;
_cancellationToken = cancellationToken;

_compilationAnalysisValueProviderFactory = new CompilationAnalysisValueProviderFactory();
}

public AnalyzerExecutor WithCancellationToken(CancellationToken cancellationToken)
Expand Down Expand Up @@ -212,7 +215,8 @@ public void ExecuteCompilationStartActions(ImmutableArray<CompilationStartAnalyz
_cancellationToken.ThrowIfCancellationRequested();

ExecuteAndCatchIfThrows(startAction.Analyzer,
() => startAction.Action(new AnalyzerCompilationStartAnalysisContext(startAction.Analyzer, compilationScope, _compilation, _analyzerOptions, _cancellationToken)),
() => startAction.Action(new AnalyzerCompilationStartAnalysisContext(startAction.Analyzer, compilationScope,
_compilation, _analyzerOptions, _compilationAnalysisValueProviderFactory, _cancellationToken)),
new AnalysisContextInfo(_compilation));
}
}
Expand Down Expand Up @@ -263,7 +267,7 @@ private void ExecuteCompilationActionsCore(ImmutableArray<CompilationAnalyzerAct
ExecuteAndCatchIfThrows(endAction.Analyzer,
() => endAction.Action(new CompilationAnalysisContext(
_compilation, _analyzerOptions, addDiagnostic,
d => IsSupportedDiagnostic(endAction.Analyzer, d), _cancellationToken)),
d => IsSupportedDiagnostic(endAction.Analyzer, d), _compilationAnalysisValueProviderFactory, _cancellationToken)),
new AnalysisContextInfo(_compilation));

analyzerStateOpt?.ProcessedActions.Add(endAction);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;

namespace Microsoft.CodeAnalysis.Diagnostics
{
/// <summary>
/// Wrapper over the core <see cref="AnalysisValueProvider{TKey, TValue}"/> which holds a strong reference to key-value pairs for the lifetime of a compilation that this provider is associated with.
/// This ensures that values are never re-computed for equivalent keys while analyzing each compilation, improving overall analyzer performance.
/// </summary>
internal sealed class CompilationAnalysisValueProvider<TKey, TValue>
where TKey : class
{
private readonly AnalysisValueProvider<TKey, TValue> _analysisValueProvider;
private readonly Dictionary<TKey, TValue> _valueMap;

public CompilationAnalysisValueProvider(AnalysisValueProvider<TKey, TValue> analysisValueProvider)
{
_analysisValueProvider = analysisValueProvider;
_valueMap = new Dictionary<TKey, TValue>(analysisValueProvider.KeyComparer);
}

internal bool TryGetValue(TKey key, out TValue value)
{
// First try to get the cached value for this compilation.
lock (_valueMap)
{
if (_valueMap.TryGetValue(key, out value))
{
return true;
}
}

// Ask the core analysis value provider for the value.
if (!_analysisValueProvider.TryGetValue(key, out value))
{
value = default(TValue);
return false;
}

// Store the value for the lifetime of the compilation.
lock (_valueMap)
{
_valueMap[key] = value;
}

return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Threading;

namespace Microsoft.CodeAnalysis.Diagnostics
{
internal sealed class CompilationAnalysisValueProviderFactory
{
private Dictionary<object, object> _lazySharedStateProviderMap;

public CompilationAnalysisValueProvider<TKey, TValue> GetValueProvider<TKey, TValue>(AnalysisValueProvider<TKey, TValue> analysisSharedStateProvider)
where TKey : class
{
if (_lazySharedStateProviderMap == null)
{
Interlocked.CompareExchange(ref _lazySharedStateProviderMap, new Dictionary<object, object>(), null);
}

object value;
lock (_lazySharedStateProviderMap)
{
if (!_lazySharedStateProviderMap.TryGetValue(analysisSharedStateProvider, out value))
{
value = new CompilationAnalysisValueProvider<TKey, TValue>(analysisSharedStateProvider);
_lazySharedStateProviderMap[analysisSharedStateProvider] = value;
}
}

return value as CompilationAnalysisValueProvider<TKey, TValue>;
}
}
}
Loading

0 comments on commit c42a411

Please sign in to comment.