Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance improvements #134

Merged
merged 2 commits into from Jul 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 33 additions & 2 deletions coverlet.sln
@@ -1,4 +1,5 @@
Microsoft Visual Studio Solution File, Format Version 12.00

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Expand All @@ -14,7 +15,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.core.tests", "test
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.console", "src\coverlet.console\coverlet.console.csproj", "{F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.tracker", "src\coverlet.tracker\coverlet.tracker.csproj", "{F4273009-536D-4999-A126-B0A2E3AA3E70}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.tracker", "src\coverlet.tracker\coverlet.tracker.csproj", "{F4273009-536D-4999-A126-B0A2E3AA3E70}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.testsubject", "test\coverlet.testsubject\coverlet.testsubject.csproj", "{AE117FAA-C21D-4F23-917E-0C8050614750}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.core.performancetest", "test\coverlet.core.performancetest\coverlet.core.performancetest.csproj", "{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -86,6 +91,30 @@ Global
{F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|x64.Build.0 = Release|Any CPU
{F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|x86.ActiveCfg = Release|Any CPU
{F4273009-536D-4999-A126-B0A2E3AA3E70}.Release|x86.Build.0 = Release|Any CPU
{AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|x64.ActiveCfg = Debug|Any CPU
{AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|x64.Build.0 = Debug|Any CPU
{AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|x86.ActiveCfg = Debug|Any CPU
{AE117FAA-C21D-4F23-917E-0C8050614750}.Debug|x86.Build.0 = Debug|Any CPU
{AE117FAA-C21D-4F23-917E-0C8050614750}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE117FAA-C21D-4F23-917E-0C8050614750}.Release|Any CPU.Build.0 = Release|Any CPU
{AE117FAA-C21D-4F23-917E-0C8050614750}.Release|x64.ActiveCfg = Release|Any CPU
{AE117FAA-C21D-4F23-917E-0C8050614750}.Release|x64.Build.0 = Release|Any CPU
{AE117FAA-C21D-4F23-917E-0C8050614750}.Release|x86.ActiveCfg = Release|Any CPU
{AE117FAA-C21D-4F23-917E-0C8050614750}.Release|x86.Build.0 = Release|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|x64.ActiveCfg = Debug|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|x64.Build.0 = Debug|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|x86.ActiveCfg = Debug|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Debug|x86.Build.0 = Debug|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|Any CPU.Build.0 = Release|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|x64.ActiveCfg = Release|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|x64.Build.0 = Release|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|x86.ActiveCfg = Release|Any CPU
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -96,6 +125,8 @@ Global
{E7637CC6-43F7-461A-A0BF-3C14562419BD} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
{F3DBE7C3-ABBB-4B8B-A6CB-A1D3D607163E} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD}
{F4273009-536D-4999-A126-B0A2E3AA3E70} = {E877EBA4-E78B-4F7D-A2D3-1E070FED04CD}
{AE117FAA-C21D-4F23-917E-0C8050614750} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
{C68CF6DE-F86C-4BCF-BAB9-7A60C320E1F9} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9CA57C02-97B0-4C38-A027-EA61E8741F10}
Expand Down
79 changes: 38 additions & 41 deletions src/coverlet.core/Coverage.cs
Expand Up @@ -55,10 +55,10 @@ public CoverageResult GetCoverageResult()
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))
{
Expand Down Expand Up @@ -91,7 +91,7 @@ public CoverageResult GetCoverageResult()
}

// Construct Branch Results
foreach (var branch in doc.Branches)
foreach (var branch in doc.Branches.Values)
{
if (documents.TryGetValue(doc.Path, out Classes classes))
{
Expand Down Expand Up @@ -147,55 +147,52 @@ private void CalculateCoverage()
{
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 != 4)
continue;
var info = row.Split(',');
// Ignore malformed lines
if (info.Length != 5)
continue;

bool isBranch = info[0] == "B";
bool isBranch = info[0] == "B";

var document = result.Documents.FirstOrDefault(d => d.Path == info[1]);
if (document == null)
continue;
if (!result.Documents.TryGetValue(info[1], out var document))
{
continue;
}

int start = int.Parse(info[2]);
int start = int.Parse(info[2]);
int hits = int.Parse(info[4]);

if (isBranch)
{
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
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++)
{
int end = int.Parse(info[3]);
for (int j = start; j <= end; j++)
{
var line = document.Lines.First(l => l.Number == j);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line was the core of the bottleneck (together with the similar line doing document.Branches.First above. Having the lines in a list means that it finding the correct line for an event will have to search through n/2 items of the n items in the list on average. The number of events in a test can be described as kn, where k is the number of times the average line is executed during the test.

Putting that together means that processing all events in the test will take (k/2) * n^2 operations. In complexity theory the factor is not interesting since when n gets big, the value of k is irrelevant, so the time complexity of this operation is expressed as O(n^2) in the big-O notation, i.e. quadratic time complexity.

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);
}
}
}
Expand Down
23 changes: 11 additions & 12 deletions src/coverlet.core/Instrumentation/Instrumenter.cs
Expand Up @@ -169,17 +169,16 @@ private void InstrumentIL(MethodDefinition method)

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}";
Expand All @@ -197,15 +196,15 @@ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor

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,
Expand Down
17 changes: 11 additions & 6 deletions src/coverlet.core/Instrumentation/InstrumenterResult.cs
Expand Up @@ -22,21 +22,26 @@ internal class Document
{
public Document()
{
Lines = new List<Line>();
Branches = new List<Branch>();
Lines = new Dictionary<int, Line>();
Branches = new Dictionary<(int Line, int Ordinal), Branch>();
}

public string Path;
public List<Line> Lines { get; private set; }
public List<Branch> Branches { get; private set; }

public Dictionary<int, Line> Lines { get; private set; }
public Dictionary<(int Line, int Ordinal), Branch> Branches { get; private set; }
}

internal class InstrumenterResult
{
public InstrumenterResult() => Documents = new List<Document>();
public InstrumenterResult()
{
Documents = new Dictionary<string, Document>();
}

public string Module;
public string HitsFilePath;
public string ModulePath;
public List<Document> Documents { get; private set; }
public Dictionary<string, Document> Documents { get; private set; }
}
}
5 changes: 5 additions & 0 deletions src/coverlet.msbuild.tasks/CoverageResultTask.cs
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
Expand Down Expand Up @@ -50,8 +51,12 @@ public override bool Execute()
try
{
Console.WriteLine("\nCalculating coverage result...");
var duration = new Stopwatch();
duration.Start();
var coverage = InstrumentationTask.Coverage;
var result = coverage.GetCoverageResult();
duration.Stop();
Console.WriteLine($"Results calculated in {duration.Elapsed.TotalSeconds} seconds");

var directory = Path.GetDirectoryName(_filename);
if (!Directory.Exists(directory))
Expand Down
55 changes: 23 additions & 32 deletions src/coverlet.tracker/CoverageTracker.cs
Expand Up @@ -2,70 +2,61 @@
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<string, List<string>> _markers;
private static Dictionary<string, int> _markerFileCount;
private static Dictionary<string, Dictionary<string, int>> _events;

[ExcludeFromCodeCoverage]
static CoverageTracker()
{
_markers = new Dictionary<string, List<string>>();
_markerFileCount = new Dictionary<string, int>();
_events = new Dictionary<string, Dictionary<string, int>>();
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<string>());
_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<string, int>();
_events.Add(file, fileEvents);
}

if (!fileEvents.TryGetValue(evt, out var count))
{
fileEvents.Add(evt, 1);
}
else if (count < int.MaxValue)
Copy link
Collaborator

@tonerdo tonerdo Jun 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if count is less than int.MaxValue? Shouldn't we try a long?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I grabbed this code (but I see I changed the condition slightly) from the former site of the hit counting:
https://github.com/tonerdo/coverlet/pull/134/files/1f4da61d7aaa444ff1857f65ddddaec6f01e1f95#diff-9eb1afa9bd8141e48d456c120df924caL189

I doubt that anyone who gets 2G hits on a line cares much if the counter stops then. :)

{
fileEvents[evt] = count + 1;
}
}
}

[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();
}
}
}
Expand Down
27 changes: 27 additions & 0 deletions test/coverlet.core.performancetest/PerformanceTest.cs
@@ -0,0 +1,27 @@
using coverlet.testsubject;
using Xunit;

namespace coverlet.core.performancetest
{
/// <summary>
/// Test the performance of coverlet by running a unit test that calls a reasonably big and complex test class.
/// Enable the test, compile, then run the test in the command line:
/// <code>
/// dotnet test -p:CollectCoverage=true -p:CoverletOutputFormat=opencover test/coverlet.core.performa ncetest/
/// </code>
/// </summary>
public class PerformanceTest
{
[Theory(Skip = "Only enabled when explicitly testing performance.")]
[InlineData(150)]
public void TestPerformance(int iterations)
{
var big = new BigClass();

for (var i = 0; i < iterations; i++)
{
big.Do(i);
}
}
}
}