Skip to content

Commit e5e2775

Browse files
committed
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.
1 parent 4444f14 commit e5e2775

File tree

1 file changed

+42
-48
lines changed

1 file changed

+42
-48
lines changed

src/ReportGenerator.Core/Parser/CoberturaParser.cs

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,13 @@ public ParserResult Parse(XContainer report)
6868

6969
var assemblies = new List<Assembly>();
7070

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

74-
var assemblyNames = modules
75-
.Select(m => m.Attribute("name").Value)
76-
.Distinct()
77-
.Where(a => this.AssemblyFilter.IsElementIncludedInReport(a))
78-
.OrderBy(a => a)
79-
.ToArray();
80-
81-
foreach (var assemblyName in assemblyNames)
75+
foreach (var elements in assemblyElementGrouping)
8276
{
83-
assemblies.Add(this.ProcessAssembly(modules, assemblyName));
77+
assemblies.Add(this.ProcessAssembly(elements, elements.Key));
8478
}
8579

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

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

122-
var classNames = modules
123-
.Where(m => m.Attribute("name").Value.Equals(assemblyName))
116+
var classes = modules
124117
.Elements("classes")
125118
.Elements("class")
119+
.ToArray();
120+
121+
var classNames = classes
126122
.Select(c => ClassNameParser.ParseClassName(c.Attribute("name").Value, this.RawMode))
127123
.Where(c => c.Include)
128124
.Distinct()
@@ -132,29 +128,37 @@ private Assembly ProcessAssembly(XElement[] modules, string assemblyName)
132128

133129
var assembly = new Assembly(assemblyName);
134130

135-
Parallel.ForEach(classNames, c => this.ProcessClass(modules, assembly, c.Name, c.DisplayName));
131+
Parallel.ForEach(classNames, c => this.ProcessClass(classes, assembly, c.Name, c.DisplayName));
136132

137133
return assembly;
138134
}
139135

140136
/// <summary>
141137
/// Processes the given class.
142138
/// </summary>
143-
/// <param name="modules">The modules.</param>
139+
/// <param name="allClasses">All class elements</param>
144140
/// <param name="assembly">The assembly.</param>
145141
/// <param name="className">Name of the class.</param>
146142
/// <param name="classDisplayName">Diesplay name of the class.</param>
147-
private void ProcessClass(XElement[] modules, Assembly assembly, string className, string classDisplayName)
143+
private void ProcessClass(XElement[] allClasses, Assembly assembly, string className, string classDisplayName)
148144
{
149-
var files = modules
150-
.Where(m => m.Attribute("name").Value.Equals(assembly.Name))
151-
.Elements("classes")
152-
.Elements("class")
153-
.Where(c => c.Attribute("name").Value.Equals(className)
145+
bool FilterClass(XElement element)
146+
{
147+
var name = element.Attribute("name").Value;
148+
149+
return name.Equals(className)
154150
|| (!this.RawMode
155-
&& (c.Attribute("name").Value.StartsWith(className + "$", StringComparison.Ordinal)
156-
|| c.Attribute("name").Value.StartsWith(className + "/", StringComparison.Ordinal)
157-
|| c.Attribute("name").Value.StartsWith(className + ".", StringComparison.Ordinal))))
151+
&& name.StartsWith(className, StringComparison.Ordinal)
152+
&& (name[className.Length] == '$'
153+
|| name[className.Length] == '/'
154+
|| name[className.Length] == '.'));
155+
}
156+
157+
var classes = allClasses
158+
.Where(FilterClass)
159+
.ToArray();
160+
161+
var files = classes
158162
.Select(c => c.Attribute("filename").Value)
159163
.Distinct()
160164
.ToArray();
@@ -170,7 +174,10 @@ private void ProcessClass(XElement[] modules, Assembly assembly, string classNam
170174

171175
foreach (var file in filteredFiles)
172176
{
173-
@class.AddFile(this.ProcessFile(modules, @class, className, file));
177+
var fileClasses = classes
178+
.Where(c => c.Attribute("filename").Value.Equals(file))
179+
.ToArray();
180+
@class.AddFile(this.ProcessFile(fileClasses, @class, className, file));
174181
}
175182

176183
assembly.AddClass(@class);
@@ -180,35 +187,27 @@ private void ProcessClass(XElement[] modules, Assembly assembly, string classNam
180187
/// <summary>
181188
/// Processes the file.
182189
/// </summary>
183-
/// <param name="modules">The modules.</param>
190+
/// <param name="classElements">The class elements for the file.</param>
184191
/// <param name="class">The class.</param>
185192
/// <param name="className">Name of the class.</param>
186193
/// <param name="filePath">The file path.</param>
187194
/// <returns>The <see cref="CodeFile"/>.</returns>
188-
private CodeFile ProcessFile(XElement[] modules, Class @class, string className, string filePath)
195+
private CodeFile ProcessFile(XElement[] classElements, Class @class, string className, string filePath)
189196
{
190-
var classes = modules
191-
.Where(m => m.Attribute("name").Value.Equals(@class.Assembly.Name))
192-
.Elements("classes")
193-
.Elements("class")
194-
.Where(c => c.Attribute("name").Value.Equals(className)
195-
|| (!this.RawMode
196-
&& (c.Attribute("name").Value.StartsWith(className + "$", StringComparison.Ordinal)
197-
|| c.Attribute("name").Value.StartsWith(className + "/", StringComparison.Ordinal)
198-
|| c.Attribute("name").Value.StartsWith(className + ".", StringComparison.Ordinal))))
199-
.Where(c => c.Attribute("filename").Value.Equals(filePath))
200-
.ToArray();
201-
202-
var lines = classes.Elements("lines")
197+
var lines = classElements.Elements("lines")
203198
.Elements("line")
204199
.ToArray();
205200

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

210-
var additionalLinesInMethodElement = classes.Elements("methods")
205+
var methodsOfFile = classElements
206+
.Elements("methods")
211207
.Elements("method")
208+
.ToArray();
209+
210+
var additionalLinesInMethodElement = methodsOfFile
212211
.Elements("lines")
213212
.Elements("line")
214213
.Where(l => !lineNumbers.Contains(l.Attribute("number").Value))
@@ -254,11 +253,6 @@ private CodeFile ProcessFile(XElement[] modules, Class @class, string className,
254253
}
255254
}
256255

257-
var methodsOfFile = classes
258-
.Elements("methods")
259-
.Elements("method")
260-
.ToArray();
261-
262256
var codeFile = new CodeFile(filePath, coverage, lineVisitStatus, branches);
263257

264258
SetMethodMetrics(codeFile, methodsOfFile);

0 commit comments

Comments
 (0)