From 1f4da61d7aaa444ff1857f65ddddaec6f01e1f95 Mon Sep 17 00:00:00 2001 From: Peter Liljenberg Date: Tue, 26 Jun 2018 15:25:59 +0200 Subject: [PATCH] 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. --- src/coverlet.core/Coverage.cs | 81 +++++++++---------- .../Instrumentation/Instrumenter.cs | 23 +++--- .../Instrumentation/InstrumenterResult.cs | 17 ++-- src/coverlet.tracker/CoverageTracker.cs | 55 ++++++------- .../Instrumentation/InstrumenterTests.cs | 4 +- 5 files changed, 86 insertions(+), 94 deletions(-) diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index f41755f..420575d 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -55,10 +55,10 @@ namespace Coverlet.Core foreach (var result in _results) { Documents documents = new Documents(); - foreach (var doc in result.Documents) + foreach (var doc in result.Documents.Values) { // Construct Line Results - foreach (var line in doc.Lines) + foreach (var line in doc.Lines.Values) { if (documents.TryGetValue(doc.Path, out Classes classes)) { @@ -91,7 +91,7 @@ namespace Coverlet.Core } // Construct Branch Results - foreach (var branch in doc.Branches) + foreach (var branch in doc.Branches.Values) { if (documents.TryGetValue(doc.Path, out Classes classes)) { @@ -147,55 +147,52 @@ namespace Coverlet.Core { foreach (var result in _results) { - var i = 0; - while (true) + if (!File.Exists(result.HitsFilePath)) { - var file = $"{result.HitsFilePath}_compressed_{i}"; - if(!File.Exists(file)) break; - - using (var fs = new FileStream(file, FileMode.Open)) - using (var gz = new GZipStream(fs, CompressionMode.Decompress)) - using (var sr = new StreamReader(gz)) + // File not instrumented, or nothing in it called. Warn about this? + continue; + } + + using (var fs = new FileStream(result.HitsFilePath, FileMode.Open)) + using (var sr = new StreamReader(fs)) + { + string row; + while ((row = sr.ReadLine()) != null) { - string row; - while ((row = sr.ReadLine()) != null) + var info = row.Split(','); + // Ignore malformed lines + if (info.Length != 5) + continue; + + bool isBranch = info[0] == "B"; + + if (!result.Documents.TryGetValue(info[1], out var document)) { - var info = row.Split(','); - // Ignore malformed lines - if (info.Length != 4) - continue; + continue; + } - bool isBranch = info[0] == "B"; + int start = int.Parse(info[2]); + int hits = int.Parse(info[4]); - var document = result.Documents.FirstOrDefault(d => d.Path == info[1]); - if (document == null) - continue; - - int start = int.Parse(info[2]); - - if (isBranch) + if (isBranch) + { + int ordinal = int.Parse(info[3]); + var branch = document.Branches[(start, ordinal)]; + branch.Hits = hits; + } + else + { + int end = int.Parse(info[3]); + for (int j = start; j <= end; j++) { - uint ordinal = uint.Parse(info[3]); - var branch = document.Branches.First(b => b.Number == start && b.Ordinal == ordinal); - if (branch.Hits != int.MaxValue) - branch.Hits += branch.Hits + 1; - } - else - { - int end = int.Parse(info[3]); - for (int j = start; j <= end; j++) - { - var line = document.Lines.First(l => l.Number == j); - if (line.Hits != int.MaxValue) - line.Hits = line.Hits + 1; - } + var line = document.Lines[j]; + line.Hits = hits; } } } - - InstrumentationHelper.DeleteHitsFile(file); - i++; } + + InstrumentationHelper.DeleteHitsFile(result.HitsFilePath); } } } diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index e1f833d..3b63e76 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -169,17 +169,16 @@ namespace Coverlet.Core.Instrumentation private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, SequencePoint sequencePoint) { - var document = _result.Documents.FirstOrDefault(d => d.Path == sequencePoint.Document.Url); - if (document == null) - { + if (!_result.Documents.TryGetValue(sequencePoint.Document.Url, out var document)) + { document = new Document { Path = sequencePoint.Document.Url }; - _result.Documents.Add(document); + _result.Documents.Add(document.Path, document); } for (int i = sequencePoint.StartLine; i <= sequencePoint.EndLine; i++) { - if (!document.Lines.Exists(l => l.Number == i)) - document.Lines.Add(new Line { Number = i, Class = method.DeclaringType.FullName, Method = method.FullName }); + if (!document.Lines.ContainsKey(i)) + document.Lines.Add(i, new Line { Number = i, Class = method.DeclaringType.FullName, Method = method.FullName }); } string marker = $"L,{document.Path},{sequencePoint.StartLine},{sequencePoint.EndLine}"; @@ -197,15 +196,15 @@ namespace Coverlet.Core.Instrumentation private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, BranchPoint branchPoint) { - var document = _result.Documents.FirstOrDefault(d => d.Path == branchPoint.Document); - if (document == null) - { + if (!_result.Documents.TryGetValue(branchPoint.Document, out var document)) + { document = new Document { Path = branchPoint.Document }; - _result.Documents.Add(document); + _result.Documents.Add(document.Path, document); } - if (!document.Branches.Exists(l => l.Number == branchPoint.StartLine && l.Ordinal == branchPoint.Ordinal)) - document.Branches.Add( + var key = (branchPoint.StartLine, (int)branchPoint.Ordinal); + if (!document.Branches.ContainsKey(key)) + document.Branches.Add(key, new Branch { Number = branchPoint.StartLine, diff --git a/src/coverlet.core/Instrumentation/InstrumenterResult.cs b/src/coverlet.core/Instrumentation/InstrumenterResult.cs index a2b92cd..615e2e3 100644 --- a/src/coverlet.core/Instrumentation/InstrumenterResult.cs +++ b/src/coverlet.core/Instrumentation/InstrumenterResult.cs @@ -22,21 +22,26 @@ namespace Coverlet.Core.Instrumentation { public Document() { - Lines = new List(); - Branches = new List(); + Lines = new Dictionary(); + Branches = new Dictionary<(int Line, int Ordinal), Branch>(); } public string Path; - public List Lines { get; private set; } - public List Branches { get; private set; } + + public Dictionary Lines { get; private set; } + public Dictionary<(int Line, int Ordinal), Branch> Branches { get; private set; } } internal class InstrumenterResult { - public InstrumenterResult() => Documents = new List(); + public InstrumenterResult() + { + Documents = new Dictionary(); + } + public string Module; public string HitsFilePath; public string ModulePath; - public List Documents { get; private set; } + public Dictionary Documents { get; private set; } } } \ No newline at end of file diff --git a/src/coverlet.tracker/CoverageTracker.cs b/src/coverlet.tracker/CoverageTracker.cs index 50f7f11..ce59608 100644 --- a/src/coverlet.tracker/CoverageTracker.cs +++ b/src/coverlet.tracker/CoverageTracker.cs @@ -2,47 +2,39 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.IO.Compression; - -using Coverlet.Tracker.Extensions; namespace Coverlet.Tracker { public static class CoverageTracker { - private static Dictionary> _markers; - private static Dictionary _markerFileCount; + private static Dictionary> _events; [ExcludeFromCodeCoverage] static CoverageTracker() { - _markers = new Dictionary>(); - _markerFileCount = new Dictionary(); + _events = new Dictionary>(); AppDomain.CurrentDomain.ProcessExit += new EventHandler(CurrentDomain_ProcessExit); AppDomain.CurrentDomain.DomainUnload += new EventHandler(CurrentDomain_ProcessExit); } [ExcludeFromCodeCoverage] - public static void MarkExecuted(string path, string marker) + public static void MarkExecuted(string file, string evt) { - lock (_markers) + lock (_events) { - _markers.TryAdd(path, new List()); - _markers[path].Add(marker); - _markerFileCount.TryAdd(path, 0); - if (_markers[path].Count >= 100000) + if (!_events.TryGetValue(file, out var fileEvents)) { - using (var fs = new FileStream($"{path}_compressed_{_markerFileCount[path]}", FileMode.OpenOrCreate)) - using (var gz = new GZipStream(fs, CompressionMode.Compress)) - using (var sw = new StreamWriter(gz)) - { - foreach (var line in _markers[path]) - { - sw.WriteLine(line); - } - } - _markers[path].Clear(); - _markerFileCount[path] = _markerFileCount[path] + 1; + fileEvents = new Dictionary(); + _events.Add(file, fileEvents); + } + + if (!fileEvents.TryGetValue(evt, out var count)) + { + fileEvents.Add(evt, 1); + } + else if (count < int.MaxValue) + { + fileEvents[evt] = count + 1; } } } @@ -50,22 +42,21 @@ namespace Coverlet.Tracker [ExcludeFromCodeCoverage] public static void CurrentDomain_ProcessExit(object sender, EventArgs e) { - lock (_markers) + lock (_events) { - foreach (var kvp in _markers) + foreach (var files in _events) { - using (var fs = new FileStream($"{kvp.Key}_compressed_{_markerFileCount[kvp.Key]}", FileMode.OpenOrCreate)) - using (var gz = new GZipStream(fs, CompressionMode.Compress)) - using (var sw = new StreamWriter(gz)) + using (var fs = new FileStream(files.Key, FileMode.Create)) + using (var sw = new StreamWriter(fs)) { - foreach (var line in kvp.Value) + foreach (var evt in files.Value) { - sw.WriteLine(line); + sw.WriteLine($"{evt.Key},{evt.Value}"); } } } - _markers.Clear(); + _events.Clear(); } } } diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs index 8f3c11b..196d0e3 100644 --- a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs +++ b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs @@ -30,10 +30,10 @@ namespace Coverlet.Core.Instrumentation.Tests var instrumenterTest = CreateInstrumentor(); var result = instrumenterTest.Instrumenter.Instrument(); - var doc = result.Documents.FirstOrDefault(d => Path.GetFileName(d.Path) == "Samples.cs"); + var doc = result.Documents.Values.FirstOrDefault(d => Path.GetFileName(d.Path) == "Samples.cs"); Assert.NotNull(doc); - var found = doc.Lines.Any(l => l.Class == excludedType.FullName); + var found = doc.Lines.Values.Any(l => l.Class == excludedType.FullName); Assert.False(found, "Class decorated with with exclude attribute should be excluded"); instrumenterTest.Directory.Delete(true);