Skip to content

Commit

Permalink
Performance improvements to CoberturaParser
Browse files Browse the repository at this point in the history
- Try and re-use already filtered XElements in `CoberturaParser` rather than re-filtering the xml document all over the place.
- Optimise some `string.Concat` in `ProcessClass` method which were showing up in hot paths in profiling

In local testing, this reduces parsing time from roughly 11.5secs to 9 seconds, as well as reducing memory allocations from  17GB to 2.2GB.
  • Loading branch information
afscrome committed Aug 28, 2024
1 parent 4444f14 commit e5e2775
Showing 1 changed file with 42 additions and 48 deletions.
90 changes: 42 additions & 48 deletions src/ReportGenerator.Core/Parser/CoberturaParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,13 @@ public ParserResult Parse(XContainer report)

var assemblies = new List<Assembly>();

var modules = report.Descendants("package")
.ToArray();
var assemblyElementGrouping = report.Descendants("package")
.GroupBy(m => m.Attribute("name").Value)
.Where(a => this.AssemblyFilter.IsElementIncludedInReport(a.Key));

var assemblyNames = modules
.Select(m => m.Attribute("name").Value)
.Distinct()
.Where(a => this.AssemblyFilter.IsElementIncludedInReport(a))
.OrderBy(a => a)
.ToArray();

foreach (var assemblyName in assemblyNames)
foreach (var elements in assemblyElementGrouping)
{
assemblies.Add(this.ProcessAssembly(modules, assemblyName));
assemblies.Add(this.ProcessAssembly(elements, elements.Key));
}

var result = new ParserResult(assemblies.OrderBy(a => a.Name).ToList(), true, this.ToString());
Expand All @@ -92,7 +86,7 @@ public ParserResult Parse(XContainer report)

try
{
if (report.Element("sources").Parent.Attribute("timestamp") != null)
if (report.Element("sources")?.Parent.Attribute("timestamp") != null)
{
DateTime timeStamp = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
timeStamp = timeStamp.AddSeconds(double.Parse(report.Element("sources").Parent.Attribute("timestamp").Value)).ToLocalTime();
Expand All @@ -115,14 +109,16 @@ public ParserResult Parse(XContainer report)
/// <param name="modules">The modules.</param>
/// <param name="assemblyName">Name of the assembly.</param>
/// <returns>The <see cref="Assembly"/>.</returns>
private Assembly ProcessAssembly(XElement[] modules, string assemblyName)
private Assembly ProcessAssembly(IEnumerable<XElement> modules, string assemblyName)
{
Logger.DebugFormat(Resources.CurrentAssembly, assemblyName);

var classNames = modules
.Where(m => m.Attribute("name").Value.Equals(assemblyName))
var classes = modules
.Elements("classes")
.Elements("class")
.ToArray();

var classNames = classes
.Select(c => ClassNameParser.ParseClassName(c.Attribute("name").Value, this.RawMode))
.Where(c => c.Include)
.Distinct()
Expand All @@ -132,29 +128,37 @@ private Assembly ProcessAssembly(XElement[] modules, string assemblyName)

var assembly = new Assembly(assemblyName);

Parallel.ForEach(classNames, c => this.ProcessClass(modules, assembly, c.Name, c.DisplayName));
Parallel.ForEach(classNames, c => this.ProcessClass(classes, assembly, c.Name, c.DisplayName));

return assembly;
}

/// <summary>
/// Processes the given class.
/// </summary>
/// <param name="modules">The modules.</param>
/// <param name="allClasses">All class elements</param>
/// <param name="assembly">The assembly.</param>
/// <param name="className">Name of the class.</param>
/// <param name="classDisplayName">Diesplay name of the class.</param>
private void ProcessClass(XElement[] modules, Assembly assembly, string className, string classDisplayName)
private void ProcessClass(XElement[] allClasses, Assembly assembly, string className, string classDisplayName)
{
var files = modules
.Where(m => m.Attribute("name").Value.Equals(assembly.Name))
.Elements("classes")
.Elements("class")
.Where(c => c.Attribute("name").Value.Equals(className)
bool FilterClass(XElement element)
{
var name = element.Attribute("name").Value;

return name.Equals(className)
|| (!this.RawMode
&& (c.Attribute("name").Value.StartsWith(className + "$", StringComparison.Ordinal)
|| c.Attribute("name").Value.StartsWith(className + "/", StringComparison.Ordinal)
|| c.Attribute("name").Value.StartsWith(className + ".", StringComparison.Ordinal))))
&& name.StartsWith(className, StringComparison.Ordinal)
&& (name[className.Length] == '$'
|| name[className.Length] == '/'
|| name[className.Length] == '.'));
}

var classes = allClasses
.Where(FilterClass)
.ToArray();

var files = classes
.Select(c => c.Attribute("filename").Value)
.Distinct()
.ToArray();
Expand All @@ -170,7 +174,10 @@ private void ProcessClass(XElement[] modules, Assembly assembly, string classNam

foreach (var file in filteredFiles)
{
@class.AddFile(this.ProcessFile(modules, @class, className, file));
var fileClasses = classes
.Where(c => c.Attribute("filename").Value.Equals(file))
.ToArray();
@class.AddFile(this.ProcessFile(fileClasses, @class, className, file));
}

assembly.AddClass(@class);
Expand All @@ -180,35 +187,27 @@ private void ProcessClass(XElement[] modules, Assembly assembly, string classNam
/// <summary>
/// Processes the file.
/// </summary>
/// <param name="modules">The modules.</param>
/// <param name="classElements">The class elements for the file.</param>
/// <param name="class">The class.</param>
/// <param name="className">Name of the class.</param>
/// <param name="filePath">The file path.</param>
/// <returns>The <see cref="CodeFile"/>.</returns>
private CodeFile ProcessFile(XElement[] modules, Class @class, string className, string filePath)
private CodeFile ProcessFile(XElement[] classElements, Class @class, string className, string filePath)
{
var classes = modules
.Where(m => m.Attribute("name").Value.Equals(@class.Assembly.Name))
.Elements("classes")
.Elements("class")
.Where(c => c.Attribute("name").Value.Equals(className)
|| (!this.RawMode
&& (c.Attribute("name").Value.StartsWith(className + "$", StringComparison.Ordinal)
|| c.Attribute("name").Value.StartsWith(className + "/", StringComparison.Ordinal)
|| c.Attribute("name").Value.StartsWith(className + ".", StringComparison.Ordinal))))
.Where(c => c.Attribute("filename").Value.Equals(filePath))
.ToArray();

var lines = classes.Elements("lines")
var lines = classElements.Elements("lines")
.Elements("line")
.ToArray();

var lineNumbers = lines
.Select(l => l.Attribute("number").Value)
.ToHashSet();

var additionalLinesInMethodElement = classes.Elements("methods")
var methodsOfFile = classElements
.Elements("methods")
.Elements("method")
.ToArray();

var additionalLinesInMethodElement = methodsOfFile
.Elements("lines")
.Elements("line")
.Where(l => !lineNumbers.Contains(l.Attribute("number").Value))
Expand Down Expand Up @@ -254,11 +253,6 @@ private CodeFile ProcessFile(XElement[] modules, Class @class, string className,
}
}

var methodsOfFile = classes
.Elements("methods")
.Elements("method")
.ToArray();

var codeFile = new CodeFile(filePath, coverage, lineVisitStatus, branches);

SetMethodMetrics(codeFile, methodsOfFile);
Expand Down

0 comments on commit e5e2775

Please sign in to comment.