From 5d8c55ec9104594c7c73bc58dbc157d3652353bf Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Dec 2023 12:51:51 +0100 Subject: [PATCH 1/9] Enable Concurrent execution --- .../SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs index 4bd0ba429fa..8f028c07883 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs @@ -35,7 +35,6 @@ public abstract class UtilityAnalyzerBase : SonarDiagnosticAnalyzer { protected static readonly ISet FileExtensionWhitelist = new HashSet { ".cs", ".csx", ".vb" }; private readonly DiagnosticDescriptor rule; - protected override bool EnableConcurrentExecution => false; public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(rule); From 61bfb19b5762e9b977425d4a2dfda430b4fd4bd9 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Dec 2023 12:54:06 +0100 Subject: [PATCH 2/9] Use BlockingCollection --- .../Rules/Utilities/UtilityAnalyzerBase.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs index 8f028c07883..5ac5555a340 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs @@ -97,27 +97,33 @@ protected sealed override void Initialize(SonarAnalysisContext context) => { return; } - var treeMessages = new ConcurrentStack(); + var treeMessages = new BlockingCollection(); + var consumerTask = Task.Factory.StartNew(() => + { + Directory.CreateDirectory(parameters.OutPath); + using var stream = File.Create(Path.Combine(parameters.OutPath, FileName)); + while (treeMessages.TryTake(out var message)) + { + message.WriteDelimitedTo(stream); + } + }, TaskCreationOptions.LongRunning); startContext.RegisterSemanticModelAction(modelContext => { if (ShouldGenerateMetrics(parameters, modelContext)) { var message = CreateMessage(parameters, modelContext.Tree, modelContext.SemanticModel); - treeMessages.Push(message); + treeMessages.Add(message); } }); startContext.RegisterCompilationEndAction(endContext => { - var allMessages = CreateAnalysisMessages(endContext) - .Concat(treeMessages) - .WhereNotNull() - .ToArray(); - Directory.CreateDirectory(parameters.OutPath); - using var stream = File.Create(Path.Combine(parameters.OutPath, FileName)); - foreach (var message in allMessages) + var analysisMessages = CreateAnalysisMessages(endContext); + foreach (var message in analysisMessages) { - message.WriteDelimitedTo(stream); + treeMessages.Add(message); } + treeMessages.CompleteAdding(); + consumerTask.Wait(); }); }); From 95054cb205e7fc033fa5b45f3bc3bd998fe51b88 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Dec 2023 12:57:15 +0100 Subject: [PATCH 3/9] Dispose collection after use --- .../SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs index 5ac5555a340..5a548591440 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs @@ -124,6 +124,7 @@ protected sealed override void Initialize(SonarAnalysisContext context) => } treeMessages.CompleteAdding(); consumerTask.Wait(); + treeMessages.Dispose(); }); }); From d5edb2155d376d6d991554eb6a160fd211134a9e Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Dec 2023 13:00:21 +0100 Subject: [PATCH 4/9] Use GetConsumingEnumerable --- .../SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs index 5a548591440..9db46e08eab 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs @@ -102,7 +102,7 @@ protected sealed override void Initialize(SonarAnalysisContext context) => { Directory.CreateDirectory(parameters.OutPath); using var stream = File.Create(Path.Combine(parameters.OutPath, FileName)); - while (treeMessages.TryTake(out var message)) + foreach (var message in treeMessages.GetConsumingEnumerable()) { message.WriteDelimitedTo(stream); } From 68961aae0c571f4473a45f7a442dfc585dc5f706 Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Dec 2023 13:13:22 +0100 Subject: [PATCH 5/9] Add cancelation support --- .../Rules/Utilities/UtilityAnalyzerBase.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs index 9db46e08eab..d9603b277fe 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs @@ -92,24 +92,25 @@ protected UtilityAnalyzerBase(string diagnosticId, string title) : base(diagnost protected sealed override void Initialize(SonarAnalysisContext context) => context.RegisterCompilationStartAction(startContext => { - var parameters = ReadParameters(startContext); + var parameters = base.ReadParameters(startContext); if (!parameters.IsAnalyzerEnabled) { return; } var treeMessages = new BlockingCollection(); + var cancel = startContext.Cancel; var consumerTask = Task.Factory.StartNew(() => { Directory.CreateDirectory(parameters.OutPath); using var stream = File.Create(Path.Combine(parameters.OutPath, FileName)); - foreach (var message in treeMessages.GetConsumingEnumerable()) + foreach (var message in treeMessages.GetConsumingEnumerable(cancel)) { message.WriteDelimitedTo(stream); } - }, TaskCreationOptions.LongRunning); + }, cancel, TaskCreationOptions.LongRunning, TaskScheduler.Default); startContext.RegisterSemanticModelAction(modelContext => { - if (ShouldGenerateMetrics(parameters, modelContext)) + if (ShouldGenerateMetrics(parameters, modelContext) && !cancel.IsCancellationRequested) { var message = CreateMessage(parameters, modelContext.Tree, modelContext.SemanticModel); treeMessages.Add(message); From 2a8327e874f57241ea86f6458e2f88abfa4874ee Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Dec 2023 13:21:22 +0100 Subject: [PATCH 6/9] Filter null messages --- .../SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs index d9603b277fe..9123abbc39b 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs @@ -103,7 +103,7 @@ protected sealed override void Initialize(SonarAnalysisContext context) => { Directory.CreateDirectory(parameters.OutPath); using var stream = File.Create(Path.Combine(parameters.OutPath, FileName)); - foreach (var message in treeMessages.GetConsumingEnumerable(cancel)) + foreach (var message in treeMessages.GetConsumingEnumerable(cancel).WhereNotNull()) { message.WriteDelimitedTo(stream); } From 0c94a2c6fcf6d754f516c90bdd030650cef4d20d Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Dec 2023 13:22:18 +0100 Subject: [PATCH 7/9] Remove "base" --- .../SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs index 9123abbc39b..bc1000ef60a 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs @@ -92,7 +92,7 @@ protected UtilityAnalyzerBase(string diagnosticId, string title) : base(diagnost protected sealed override void Initialize(SonarAnalysisContext context) => context.RegisterCompilationStartAction(startContext => { - var parameters = base.ReadParameters(startContext); + var parameters = ReadParameters(startContext); if (!parameters.IsAnalyzerEnabled) { return; From 44df974ff1e1d8aa3d67d44489230c8a761bf70f Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Dec 2023 14:11:10 +0100 Subject: [PATCH 8/9] Add comment and dispose on failure --- .../Rules/Utilities/UtilityAnalyzerBase.cs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs index bc1000ef60a..6aa11212cf1 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs @@ -97,12 +97,19 @@ protected sealed override void Initialize(SonarAnalysisContext context) => { return; } - var treeMessages = new BlockingCollection(); var cancel = startContext.Cancel; + var outPath = parameters.OutPath; + var treeMessages = new BlockingCollection(); var consumerTask = Task.Factory.StartNew(() => { - Directory.CreateDirectory(parameters.OutPath); - using var stream = File.Create(Path.Combine(parameters.OutPath, FileName)); + // Consume all messages as they arrive during the compilation and write them to disk. + // The Task starts on CompilationStart and in CompilationEnd we block until it is finished via CompleteAdding(). + // Note: CompilationEndAction is not guaranteed to be called for each CompilationStart. + // Therefore it is important to properly handle cancelation here. + // LongRunning: We probably run on a dedicated thread outside of the thread pool + // If any of the IO operations throw, CompilationEnd takes care of the clean up. + Directory.CreateDirectory(outPath); + using var stream = File.Create(Path.Combine(outPath, FileName)); foreach (var message in treeMessages.GetConsumingEnumerable(cancel).WhereNotNull()) { message.WriteDelimitedTo(stream); @@ -124,8 +131,14 @@ protected sealed override void Initialize(SonarAnalysisContext context) => treeMessages.Add(message); } treeMessages.CompleteAdding(); - consumerTask.Wait(); - treeMessages.Dispose(); + try + { + consumerTask.Wait(cancel); // Throws, if the task failed. + } + finally + { + treeMessages.Dispose(); + } }); }); From 576b11f86b198b91224bfed716bdb69166133cfd Mon Sep 17 00:00:00 2001 From: Martin Strecker Date: Mon, 11 Dec 2023 14:15:27 +0100 Subject: [PATCH 9/9] More comments. --- .../SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs index 6aa11212cf1..867f4b6965f 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Utilities/UtilityAnalyzerBase.cs @@ -133,7 +133,7 @@ protected sealed override void Initialize(SonarAnalysisContext context) => treeMessages.CompleteAdding(); try { - consumerTask.Wait(cancel); // Throws, if the task failed. + consumerTask.Wait(cancel); // Wait until all messages are written to disk. Throws, if the task failed. } finally {