Skip to content

Commit 1f4da61

Browse files
committed
Reduce coverage analysis run time to almost nothing.
This is done by aggregating hits in the CoverateTracker directly, rather than just recording events in files, and by avoiding the former quadratic complexity when translating these event files into hits on lines and branches.
1 parent b3c080c commit 1f4da61

File tree

5 files changed

+85
-93
lines changed

5 files changed

+85
-93
lines changed

src/coverlet.core/Coverage.cs

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ public CoverageResult GetCoverageResult()
5555
foreach (var result in _results)
5656
{
5757
Documents documents = new Documents();
58-
foreach (var doc in result.Documents)
58+
foreach (var doc in result.Documents.Values)
5959
{
6060
// Construct Line Results
61-
foreach (var line in doc.Lines)
61+
foreach (var line in doc.Lines.Values)
6262
{
6363
if (documents.TryGetValue(doc.Path, out Classes classes))
6464
{
@@ -91,7 +91,7 @@ public CoverageResult GetCoverageResult()
9191
}
9292

9393
// Construct Branch Results
94-
foreach (var branch in doc.Branches)
94+
foreach (var branch in doc.Branches.Values)
9595
{
9696
if (documents.TryGetValue(doc.Path, out Classes classes))
9797
{
@@ -147,55 +147,52 @@ private void CalculateCoverage()
147147
{
148148
foreach (var result in _results)
149149
{
150-
var i = 0;
151-
while (true)
150+
if (!File.Exists(result.HitsFilePath))
152151
{
153-
var file = $"{result.HitsFilePath}_compressed_{i}";
154-
if(!File.Exists(file)) break;
155-
156-
using (var fs = new FileStream(file, FileMode.Open))
157-
using (var gz = new GZipStream(fs, CompressionMode.Decompress))
158-
using (var sr = new StreamReader(gz))
152+
// File not instrumented, or nothing in it called. Warn about this?
153+
continue;
154+
}
155+
156+
using (var fs = new FileStream(result.HitsFilePath, FileMode.Open))
157+
using (var sr = new StreamReader(fs))
158+
{
159+
string row;
160+
while ((row = sr.ReadLine()) != null)
159161
{
160-
string row;
161-
while ((row = sr.ReadLine()) != null)
162-
{
163-
var info = row.Split(',');
164-
// Ignore malformed lines
165-
if (info.Length != 4)
166-
continue;
162+
var info = row.Split(',');
163+
// Ignore malformed lines
164+
if (info.Length != 5)
165+
continue;
167166

168-
bool isBranch = info[0] == "B";
167+
bool isBranch = info[0] == "B";
169168

170-
var document = result.Documents.FirstOrDefault(d => d.Path == info[1]);
171-
if (document == null)
172-
continue;
169+
if (!result.Documents.TryGetValue(info[1], out var document))
170+
{
171+
continue;
172+
}
173173

174-
int start = int.Parse(info[2]);
174+
int start = int.Parse(info[2]);
175+
int hits = int.Parse(info[4]);
175176

176-
if (isBranch)
177-
{
178-
uint ordinal = uint.Parse(info[3]);
179-
var branch = document.Branches.First(b => b.Number == start && b.Ordinal == ordinal);
180-
if (branch.Hits != int.MaxValue)
181-
branch.Hits += branch.Hits + 1;
182-
}
183-
else
177+
if (isBranch)
178+
{
179+
int ordinal = int.Parse(info[3]);
180+
var branch = document.Branches[(start, ordinal)];
181+
branch.Hits = hits;
182+
}
183+
else
184+
{
185+
int end = int.Parse(info[3]);
186+
for (int j = start; j <= end; j++)
184187
{
185-
int end = int.Parse(info[3]);
186-
for (int j = start; j <= end; j++)
187-
{
188-
var line = document.Lines.First(l => l.Number == j);
189-
if (line.Hits != int.MaxValue)
190-
line.Hits = line.Hits + 1;
191-
}
188+
var line = document.Lines[j];
189+
line.Hits = hits;
192190
}
193191
}
194192
}
195-
196-
InstrumentationHelper.DeleteHitsFile(file);
197-
i++;
198193
}
194+
195+
InstrumentationHelper.DeleteHitsFile(result.HitsFilePath);
199196
}
200197
}
201198
}

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -169,17 +169,16 @@ private void InstrumentIL(MethodDefinition method)
169169

170170
private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, SequencePoint sequencePoint)
171171
{
172-
var document = _result.Documents.FirstOrDefault(d => d.Path == sequencePoint.Document.Url);
173-
if (document == null)
174-
{
172+
if (!_result.Documents.TryGetValue(sequencePoint.Document.Url, out var document))
173+
{
175174
document = new Document { Path = sequencePoint.Document.Url };
176-
_result.Documents.Add(document);
175+
_result.Documents.Add(document.Path, document);
177176
}
178177

179178
for (int i = sequencePoint.StartLine; i <= sequencePoint.EndLine; i++)
180179
{
181-
if (!document.Lines.Exists(l => l.Number == i))
182-
document.Lines.Add(new Line { Number = i, Class = method.DeclaringType.FullName, Method = method.FullName });
180+
if (!document.Lines.ContainsKey(i))
181+
document.Lines.Add(i, new Line { Number = i, Class = method.DeclaringType.FullName, Method = method.FullName });
183182
}
184183

185184
string marker = $"L,{document.Path},{sequencePoint.StartLine},{sequencePoint.EndLine}";
@@ -197,15 +196,15 @@ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor
197196

198197
private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, BranchPoint branchPoint)
199198
{
200-
var document = _result.Documents.FirstOrDefault(d => d.Path == branchPoint.Document);
201-
if (document == null)
202-
{
199+
if (!_result.Documents.TryGetValue(branchPoint.Document, out var document))
200+
{
203201
document = new Document { Path = branchPoint.Document };
204-
_result.Documents.Add(document);
202+
_result.Documents.Add(document.Path, document);
205203
}
206204

207-
if (!document.Branches.Exists(l => l.Number == branchPoint.StartLine && l.Ordinal == branchPoint.Ordinal))
208-
document.Branches.Add(
205+
var key = (branchPoint.StartLine, (int)branchPoint.Ordinal);
206+
if (!document.Branches.ContainsKey(key))
207+
document.Branches.Add(key,
209208
new Branch
210209
{
211210
Number = branchPoint.StartLine,

src/coverlet.core/Instrumentation/InstrumenterResult.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,26 @@ internal class Document
2222
{
2323
public Document()
2424
{
25-
Lines = new List<Line>();
26-
Branches = new List<Branch>();
25+
Lines = new Dictionary<int, Line>();
26+
Branches = new Dictionary<(int Line, int Ordinal), Branch>();
2727
}
2828

2929
public string Path;
30-
public List<Line> Lines { get; private set; }
31-
public List<Branch> Branches { get; private set; }
30+
31+
public Dictionary<int, Line> Lines { get; private set; }
32+
public Dictionary<(int Line, int Ordinal), Branch> Branches { get; private set; }
3233
}
3334

3435
internal class InstrumenterResult
3536
{
36-
public InstrumenterResult() => Documents = new List<Document>();
37+
public InstrumenterResult()
38+
{
39+
Documents = new Dictionary<string, Document>();
40+
}
41+
3742
public string Module;
3843
public string HitsFilePath;
3944
public string ModulePath;
40-
public List<Document> Documents { get; private set; }
45+
public Dictionary<string, Document> Documents { get; private set; }
4146
}
4247
}

src/coverlet.tracker/CoverageTracker.cs

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,70 +2,61 @@
22
using System.Collections.Generic;
33
using System.Diagnostics.CodeAnalysis;
44
using System.IO;
5-
using System.IO.Compression;
6-
7-
using Coverlet.Tracker.Extensions;
85

96
namespace Coverlet.Tracker
107
{
118
public static class CoverageTracker
129
{
13-
private static Dictionary<string, List<string>> _markers;
14-
private static Dictionary<string, int> _markerFileCount;
10+
private static Dictionary<string, Dictionary<string, int>> _events;
1511

1612
[ExcludeFromCodeCoverage]
1713
static CoverageTracker()
1814
{
19-
_markers = new Dictionary<string, List<string>>();
20-
_markerFileCount = new Dictionary<string, int>();
15+
_events = new Dictionary<string, Dictionary<string, int>>();
2116
AppDomain.CurrentDomain.ProcessExit += new EventHandler(CurrentDomain_ProcessExit);
2217
AppDomain.CurrentDomain.DomainUnload += new EventHandler(CurrentDomain_ProcessExit);
2318
}
2419

2520
[ExcludeFromCodeCoverage]
26-
public static void MarkExecuted(string path, string marker)
21+
public static void MarkExecuted(string file, string evt)
2722
{
28-
lock (_markers)
23+
lock (_events)
2924
{
30-
_markers.TryAdd(path, new List<string>());
31-
_markers[path].Add(marker);
32-
_markerFileCount.TryAdd(path, 0);
33-
if (_markers[path].Count >= 100000)
25+
if (!_events.TryGetValue(file, out var fileEvents))
3426
{
35-
using (var fs = new FileStream($"{path}_compressed_{_markerFileCount[path]}", FileMode.OpenOrCreate))
36-
using (var gz = new GZipStream(fs, CompressionMode.Compress))
37-
using (var sw = new StreamWriter(gz))
38-
{
39-
foreach (var line in _markers[path])
40-
{
41-
sw.WriteLine(line);
42-
}
43-
}
44-
_markers[path].Clear();
45-
_markerFileCount[path] = _markerFileCount[path] + 1;
27+
fileEvents = new Dictionary<string, int>();
28+
_events.Add(file, fileEvents);
29+
}
30+
31+
if (!fileEvents.TryGetValue(evt, out var count))
32+
{
33+
fileEvents.Add(evt, 1);
34+
}
35+
else if (count < int.MaxValue)
36+
{
37+
fileEvents[evt] = count + 1;
4638
}
4739
}
4840
}
4941

5042
[ExcludeFromCodeCoverage]
5143
public static void CurrentDomain_ProcessExit(object sender, EventArgs e)
5244
{
53-
lock (_markers)
45+
lock (_events)
5446
{
55-
foreach (var kvp in _markers)
47+
foreach (var files in _events)
5648
{
57-
using (var fs = new FileStream($"{kvp.Key}_compressed_{_markerFileCount[kvp.Key]}", FileMode.OpenOrCreate))
58-
using (var gz = new GZipStream(fs, CompressionMode.Compress))
59-
using (var sw = new StreamWriter(gz))
49+
using (var fs = new FileStream(files.Key, FileMode.Create))
50+
using (var sw = new StreamWriter(fs))
6051
{
61-
foreach (var line in kvp.Value)
52+
foreach (var evt in files.Value)
6253
{
63-
sw.WriteLine(line);
54+
sw.WriteLine($"{evt.Key},{evt.Value}");
6455
}
6556
}
6657
}
6758

68-
_markers.Clear();
59+
_events.Clear();
6960
}
7061
}
7162
}

test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ public void TestInstrument_ClassesWithExcludeAttributeAreExcluded(Type excludedT
3030
var instrumenterTest = CreateInstrumentor();
3131
var result = instrumenterTest.Instrumenter.Instrument();
3232

33-
var doc = result.Documents.FirstOrDefault(d => Path.GetFileName(d.Path) == "Samples.cs");
33+
var doc = result.Documents.Values.FirstOrDefault(d => Path.GetFileName(d.Path) == "Samples.cs");
3434
Assert.NotNull(doc);
3535

36-
var found = doc.Lines.Any(l => l.Class == excludedType.FullName);
36+
var found = doc.Lines.Values.Any(l => l.Class == excludedType.FullName);
3737
Assert.False(found, "Class decorated with with exclude attribute should be excluded");
3838

3939
instrumenterTest.Directory.Delete(true);

0 commit comments

Comments
 (0)