From e5e277550ed6507d163a320ece800ebdc51a99f5 Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Wed, 28 Aug 2024 10:35:10 +0100 Subject: [PATCH] Performance improvements to CoberturaParser - 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. --- .../Parser/CoberturaParser.cs | 90 +++++++++---------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/src/ReportGenerator.Core/Parser/CoberturaParser.cs b/src/ReportGenerator.Core/Parser/CoberturaParser.cs index a31ab06c..14cc965c 100644 --- a/src/ReportGenerator.Core/Parser/CoberturaParser.cs +++ b/src/ReportGenerator.Core/Parser/CoberturaParser.cs @@ -68,19 +68,13 @@ public ParserResult Parse(XContainer report) var assemblies = new List(); - 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()); @@ -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(); @@ -115,14 +109,16 @@ public ParserResult Parse(XContainer report) /// The modules. /// Name of the assembly. /// The . - private Assembly ProcessAssembly(XElement[] modules, string assemblyName) + private Assembly ProcessAssembly(IEnumerable 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() @@ -132,7 +128,7 @@ 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; } @@ -140,21 +136,29 @@ private Assembly ProcessAssembly(XElement[] modules, string assemblyName) /// /// Processes the given class. /// - /// The modules. + /// All class elements /// The assembly. /// Name of the class. /// Diesplay name of the class. - 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(); @@ -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); @@ -180,26 +187,14 @@ private void ProcessClass(XElement[] modules, Assembly assembly, string classNam /// /// Processes the file. /// - /// The modules. + /// The class elements for the file. /// The class. /// Name of the class. /// The file path. /// The . - 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(); @@ -207,8 +202,12 @@ private CodeFile ProcessFile(XElement[] modules, Class @class, string className, .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)) @@ -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);