Skip to content
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

Parallelise Report Generator #696

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Palmmedia.ReportGenerator.Core.Parser.Analysis;
Expand All @@ -18,7 +19,7 @@ public class HtmlBlueRedReportBuilder : HtmlReportBuilderBase
/// <summary>
/// Dictionary containing the filenames of the class reports by class.
/// </summary>
private readonly IDictionary<string, string> fileNameByClass = new Dictionary<string, string>();
private readonly ConcurrentDictionary<string, string> fileNameByClass = new ConcurrentDictionary<string, string>();

/// <summary>
/// Initializes a new instance of the <see cref="HtmlBlueRedReportBuilder" /> class.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Palmmedia.ReportGenerator.Core.Parser.Analysis;
Expand Down Expand Up @@ -33,7 +34,7 @@ public override void CreateClassReport(Class @class, IEnumerable<FileAnalysis> f
/// <param name="summaryResult">The summary result.</param>
public override void CreateSummaryReport(SummaryResult summaryResult)
{
using (var renderer = new HtmlRenderer(new Dictionary<string, string>(), true, HtmlMode.InlineCssAndJavaScript, new string[] { "custom_adaptive.css", "custom_bluered.css" }, "custom.css"))
using (var renderer = new HtmlRenderer(new ConcurrentDictionary<string, string>(), true, HtmlMode.InlineCssAndJavaScript, new string[] { "custom_adaptive.css", "custom_bluered.css" }, "custom.css"))
{
this.CreateSummaryReport(renderer, summaryResult);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Palmmedia.ReportGenerator.Core.Parser.Analysis;
Expand All @@ -18,7 +19,7 @@ public class HtmlDarkReportBuilder : HtmlReportBuilderBase
/// <summary>
/// Dictionary containing the filenames of the class reports by class.
/// </summary>
private readonly IDictionary<string, string> fileNameByClass = new Dictionary<string, string>();
private readonly ConcurrentDictionary<string, string> fileNameByClass = new ConcurrentDictionary<string, string>();

/// <summary>
/// Initializes a new instance of the <see cref="HtmlDarkReportBuilder" /> class.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Palmmedia.ReportGenerator.Core.Parser.Analysis;
Expand All @@ -13,7 +14,7 @@ public class HtmlInlineAzurePipelinesDarkReportBuilder : HtmlReportBuilderBase
/// <summary>
/// Dictionary containing the filenames of the class reports by class.
/// </summary>
private readonly IDictionary<string, string> fileNameByClass = new Dictionary<string, string>();
private readonly ConcurrentDictionary<string, string> fileNameByClass = new ConcurrentDictionary<string, string>();

/// <summary>
/// Gets the report type.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Palmmedia.ReportGenerator.Core.Parser.Analysis;
Expand All @@ -13,7 +14,7 @@ public class HtmlInlineAzurePipelinesLightReportBuilder : HtmlReportBuilderBase
/// <summary>
/// Dictionary containing the filenames of the class reports by class.
/// </summary>
private readonly IDictionary<string, string> fileNameByClass = new Dictionary<string, string>();
private readonly ConcurrentDictionary<string, string> fileNameByClass = new ConcurrentDictionary<string, string>();

/// <summary>
/// Gets the report type.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Palmmedia.ReportGenerator.Core.Parser.Analysis;
Expand All @@ -13,7 +14,7 @@ public class HtmlInlineAzurePipelinesReportBuilder : HtmlReportBuilderBase
/// <summary>
/// Dictionary containing the filenames of the class reports by class.
/// </summary>
private readonly IDictionary<string, string> fileNameByClass = new Dictionary<string, string>();
private readonly ConcurrentDictionary<string, string> fileNameByClass = new ConcurrentDictionary<string, string>();

/// <summary>
/// Gets the report type.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Palmmedia.ReportGenerator.Core.Parser.Analysis;
Expand All @@ -13,7 +14,7 @@ public class HtmlInlineCssAndJavaScriptReportBuilder : HtmlReportBuilderBase
/// <summary>
/// Dictionary containing the filenames of the class reports by class.
/// </summary>
private readonly IDictionary<string, string> fileNameByClass = new Dictionary<string, string>();
private readonly ConcurrentDictionary<string, string> fileNameByClass = new ConcurrentDictionary<string, string>();

/// <summary>
/// Gets the report type.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Palmmedia.ReportGenerator.Core.Parser.Analysis;
Expand All @@ -18,7 +19,7 @@ public class HtmlLightReportBuilder : HtmlReportBuilderBase
/// <summary>
/// Dictionary containing the filenames of the class reports by class.
/// </summary>
private readonly IDictionary<string, string> fileNameByClass = new Dictionary<string, string>();
private readonly ConcurrentDictionary<string, string> fileNameByClass = new ConcurrentDictionary<string, string>();

/// <summary>
/// Initializes a new instance of the <see cref="HtmlLightReportBuilder" /> class.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Palmmedia.ReportGenerator.Core.Parser.Analysis;
Expand All @@ -18,7 +19,7 @@ public class HtmlReportBuilder : HtmlReportBuilderBase
/// <summary>
/// Dictionary containing the filenames of the class reports by class.
/// </summary>
private readonly IDictionary<string, string> fileNameByClass = new Dictionary<string, string>();
private readonly ConcurrentDictionary<string, string> fileNameByClass = new ConcurrentDictionary<string, string>();

/// <summary>
/// Initializes a new instance of the <see cref="HtmlReportBuilder" /> class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Palmmedia.ReportGenerator.Core.Reporting.Builders
/// <summary>
/// Implementation of <see cref="IReportBuilder"/> that uses <see cref="IHtmlRenderer"/> to create reports.
/// </summary>
public abstract class HtmlReportBuilderBase : IReportBuilder
public abstract class HtmlReportBuilderBase : IParallelisableReportBuilder
{
/// <summary>
/// The Logger.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Palmmedia.ReportGenerator.Core.Parser.Analysis;
Expand Down Expand Up @@ -33,7 +34,7 @@ public override void CreateClassReport(Class @class, IEnumerable<FileAnalysis> f
/// <param name="summaryResult">The summary result.</param>
public override void CreateSummaryReport(SummaryResult summaryResult)
{
using (var renderer = new HtmlRenderer(new Dictionary<string, string>(), true, HtmlMode.InlineCssAndJavaScript))
using (var renderer = new HtmlRenderer(new ConcurrentDictionary<string, string>(), true, HtmlMode.InlineCssAndJavaScript))
{
this.CreateSummaryReport(renderer, summaryResult);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
Expand Down Expand Up @@ -58,7 +59,7 @@ internal class HtmlRenderer : IHtmlRenderer, IDisposable
/// <summary>
/// Dictionary containing the filenames of the class reports by class.
/// </summary>
private readonly IDictionary<string, string> fileNameByClass;
private readonly ConcurrentDictionary<string, string> fileNameByClass;

/// <summary>
/// Indicates that only a summary report is created (no class reports).
Expand Down Expand Up @@ -109,7 +110,7 @@ internal class HtmlRenderer : IHtmlRenderer, IDisposable
/// <param name="cssFileResource">Optional CSS file resource.</param>
/// <param name="additionalCssFileResource">Optional additional CSS file resource.</param>
internal HtmlRenderer(
IDictionary<string, string> fileNameByClass,
ConcurrentDictionary<string, string> fileNameByClass,
bool onlySummary,
HtmlMode htmlMode,
string cssFileResource = "custom.css",
Expand All @@ -132,7 +133,7 @@ internal HtmlRenderer(
/// <param name="additionalCssFileResources">Optional additional CSS file resources.</param>
/// <param name="cssFileResource">Optional CSS file resource.</param>
internal HtmlRenderer(
IDictionary<string, string> fileNameByClass,
ConcurrentDictionary<string, string> fileNameByClass,
bool onlySummary,
HtmlMode htmlMode,
string[] additionalCssFileResources,
Expand Down Expand Up @@ -1557,7 +1558,7 @@ private string GetClassReportFilename(Assembly assembly, string className)
while (this.fileNameByClass.Values.Any(v => v.Equals(fileName, StringComparison.OrdinalIgnoreCase)));
}

this.fileNameByClass.Add(key, fileName);
this.fileNameByClass[key] = fileName;
}

return fileName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Palmmedia.ReportGenerator.Core.Reporting
{
/// <summary>
/// Interface indicating that an <see cref="IReportBuilder"/> can build multiple reports concurrently.
/// </summary>
public interface IParallelisableReportBuilder : IReportBuilder { }
}
134 changes: 96 additions & 38 deletions src/ReportGenerator.Core/Reporting/ReportGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Palmmedia.ReportGenerator.Core.Common;
using Palmmedia.ReportGenerator.Core.Logging;
Expand All @@ -16,6 +18,9 @@ namespace Palmmedia.ReportGenerator.Core.Reporting
/// </summary>
internal class ReportGenerator
{
// TODO: Make this configurable
private const int MaxConcurrency = 8;

/// <summary>
/// The Logger.
/// </summary>
Expand All @@ -34,7 +39,7 @@ internal class ReportGenerator
/// <summary>
/// The renderers.
/// </summary>
private readonly IEnumerable<IReportBuilder> renderers;
private readonly List<IReportBuilder> renderers;

/// <summary>
/// Initializes a new instance of the <see cref="ReportGenerator" /> class.
Expand All @@ -46,7 +51,7 @@ internal ReportGenerator(IFileReader fileReader, ParserResult parserResult, IEnu
{
this.fileReader = fileReader ?? throw new ArgumentNullException(nameof(fileReader));
this.parserResult = parserResult ?? throw new ArgumentNullException(nameof(parserResult));
this.renderers = renderers ?? throw new ArgumentNullException(nameof(renderers));
this.renderers = renderers?.ToList() ?? throw new ArgumentNullException(nameof(renderers));
}

/// <summary>
Expand All @@ -58,54 +63,73 @@ internal ReportGenerator(IFileReader fileReader, ParserResult parserResult, IEnu
/// <param name="tag">The custom tag (e.g. build number).</param>
internal void CreateReport(bool addHistoricCoverage, List<HistoricCoverage> overallHistoricCoverages, DateTime executionTime, string tag)
{
// TODO: Can we change overallHistoricCoverages to a ConcurrentBag to avoid this?
object overallHistoricCoveragesLock = new object();

// TODO: This probably belongs in the ReportGenerator Console and/or Global Tool
// AND can probably be a bit smarter.
//
// If there aren't enough threads in the thread pool, the Parallelism here can deadlock until the pool grows large enough
// With all avaialble threads being used in class analysis, but with ConcurrentReportBuilder threads having
// to wait for the thread pool to grow. By default, .Net core adds 2 threads every 0.5 secs
// App will become responsive within a few seconds, but given that report generator can complete in tens of seconds,
// these stalls become significant
ThreadPool.SetMinThreads(200, 200);

var allClasses = this.parserResult.Assemblies.SelectMany(a => a.Classes);
var classAnalysis = Partitioner.Create(allClasses, EnumerablePartitionerOptions.NoBuffering)
.AsParallel()
.AsOrdered()
.WithDegreeOfParallelism(MaxConcurrency)
.Select(AnalyseClass)
.AsSequential();

int numberOfClasses = this.parserResult.Assemblies.SafeSum(a => a.Classes.Count());

Logger.DebugFormat(Resources.AnalyzingClasses, numberOfClasses);

int counter = 0;
var concurrentRenderers = this.renderers.OfType<IParallelisableReportBuilder>().ToList();
var sequentialRenderers = this.renderers.Except(concurrentRenderers).ToList();

foreach (var assembly in this.parserResult.Assemblies)
var concurrentRenderQueue = new BlockingCollection<(IReportBuilder renderer, Class @class, List<FileAnalysis> analysis)>(MaxConcurrency);
Task concurrentRendererTask = Task.CompletedTask;

if (concurrentRenderers.Any())
{
foreach (var @class in assembly.Classes)
concurrentRendererTask = Task.Factory.StartNew(() =>
{
counter++;

Logger.DebugFormat(
Resources.CreatingReport,
counter,
numberOfClasses,
@class.Assembly.ShortName,
@class.Name);

var fileAnalyses = @class.Files.Select(f => f.AnalyzeFile(this.fileReader)).ToArray();

if (addHistoricCoverage)
{
var historicCoverage = new HistoricCoverage(@class, executionTime, tag);
@class.AddHistoricCoverage(historicCoverage);
overallHistoricCoverages.Add(historicCoverage);
}
Partitioner.Create(concurrentRenderQueue.GetConsumingEnumerable(), EnumerablePartitionerOptions.NoBuffering)
.AsParallel()
.WithDegreeOfParallelism(MaxConcurrency)
.ForAll(x => RenderReport(x.renderer, x.@class, x.analysis));
});
}

Parallel.ForEach(
this.renderers,
renderer =>
{
try
{
renderer.CreateClassReport(@class, fileAnalyses);
}
catch (Exception ex)
{
Logger.ErrorFormat(
Resources.ErrorDuringRenderingClassReport,
@class.Name,
renderer.ReportType,
ex.GetExceptionMessageForDisplay());
}
});
foreach (var (@class, analysis) in classAnalysis)
{
counter++;
Logger.DebugFormat(
Resources.CreatingReport,
counter,
numberOfClasses,
@class.Assembly.ShortName,
@class.Name);

foreach (var renderer in concurrentRenderers)
{
concurrentRenderQueue.Add((renderer, @class, analysis));
}

sequentialRenderers
.AsParallel()
.WithMergeOptions(ParallelMergeOptions.NotBuffered)
.ForAll(x => RenderReport(x, @class, analysis));
}

concurrentRenderQueue.CompleteAdding();
concurrentRendererTask.Wait();

Logger.Debug(Resources.CreatingSummary);
SummaryResult summaryResult = new SummaryResult(this.parserResult);

Expand All @@ -128,6 +152,40 @@ internal void CreateReport(bool addHistoricCoverage, List<HistoricCoverage> over
}
}
}

(Class @class, List<FileAnalysis> analysis) AnalyseClass(Class @class)
{
var fileAnalyses = @class.Files.Select(f => f.AnalyzeFile(this.fileReader)).ToList();

if (addHistoricCoverage)
{
var historicCoverage = new HistoricCoverage(@class, executionTime, tag);
@class.AddHistoricCoverage(historicCoverage);

lock (overallHistoricCoveragesLock)
{
overallHistoricCoverages.Add(historicCoverage);
}
}

return (@class, fileAnalyses);
}
}

private static void RenderReport(IReportBuilder renderer, Class @class, List<FileAnalysis> analysis)
{
try
{
renderer.CreateClassReport(@class, analysis);
}
catch (Exception ex)
{
Logger.ErrorFormat(
Resources.ErrorDuringRenderingClassReport,
@class.Name,
renderer.ReportType,
ex.GetExceptionMessageForDisplay());
}
}
}
}
Loading