Use file-backed mmaps on Linux

This commit is contained in:
Peter Liljenberg
2018-12-28 18:16:07 +01:00
parent 4bc572c557
commit 2fb0d73e93
8 changed files with 122 additions and 46 deletions
+19 -2
View File
@@ -89,8 +89,22 @@ namespace Coverlet.Core
foreach (var result in _results)
{
var count = result.HitCandidates.Count + HitsResultHeaderSize;
_resultMemoryMaps.Add(result.HitsResultGuid, MemoryMappedFile.CreateNew(result.HitsResultGuid, count * sizeof(int)));
var size = (result.HitCandidates.Count + HitsResultHeaderSize) * sizeof(int);
MemoryMappedFile mmap;
try
{
// Try using a named memory map not backed by a file (currently only supported on Windows)
mmap = MemoryMappedFile.CreateNew(result.HitsResultGuid, size);
}
catch (PlatformNotSupportedException)
{
// Fall back on a file-backed memory map
mmap = MemoryMappedFile.CreateFromFile(result.HitsFilePath, FileMode.CreateNew, null, size);
}
_resultMemoryMaps.Add(result.HitsResultGuid, mmap);
}
}
@@ -270,6 +284,9 @@ namespace Coverlet.Core
document.Value.Branches.Remove(branchToRemove.Key);
}
}
// There's only a hits file on Linux, but if the file doesn't exist this is just a no-op
InstrumentationHelper.DeleteHitsFile(result.HitsFilePath);
}
}
@@ -104,6 +104,13 @@ namespace Coverlet.Core.Helpers
}, retryStrategy, 10);
}
public static void DeleteHitsFile(string path)
{
// Retry hitting the hits file - retry up to 10 times, since the file could be locked
// See: https://github.com/tonerdo/coverlet/issues/25
var retryStrategy = CreateRetryStrategy();
RetryHelper.Retry(() => File.Delete(path), retryStrategy, 10);
}
public static bool IsValidFilterExpression(string filter)
{
@@ -24,6 +24,7 @@ namespace Coverlet.Core.Instrumentation
private readonly string[] _excludedFiles;
private readonly string[] _excludedAttributes;
private InstrumenterResult _result;
private FieldDefinition _customTrackerHitsFilePath;
private FieldDefinition _customTrackerHitsArraySize;
private FieldDefinition _customTrackerHitsMemoryMapName;
private ILProcessor _customTrackerClassConstructorIl;
@@ -44,9 +45,15 @@ namespace Coverlet.Core.Instrumentation
public InstrumenterResult Instrument()
{
string hitsFilePath = Path.Combine(
Path.GetTempPath(),
Path.GetFileNameWithoutExtension(_module) + "_" + _identifier
);
_result = new InstrumenterResult
{
Module = Path.GetFileNameWithoutExtension(_module),
HitsFilePath = hitsFilePath,
HitsResultGuid = Guid.NewGuid().ToString(),
ModulePath = _module
};
@@ -91,6 +98,8 @@ namespace Coverlet.Core.Instrumentation
// Fixup the custom tracker class constructor, according to all instrumented types
Instruction firstInstr = _customTrackerClassConstructorIl.Body.Instructions.First();
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Nop));
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath));
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsFilePath));
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Ldc_I4, _result.HitCandidates.Count));
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsArraySize));
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsResultGuid));
@@ -119,7 +128,9 @@ namespace Coverlet.Core.Instrumentation
_customTrackerTypeDef.Fields.Add(fieldClone);
if (fieldClone.Name == nameof(ModuleTrackerTemplate.HitsMemoryMapName))
if (fieldClone.Name == nameof(ModuleTrackerTemplate.HitsFilePath))
_customTrackerHitsFilePath = fieldClone;
else if (fieldClone.Name == nameof(ModuleTrackerTemplate.HitsMemoryMapName))
_customTrackerHitsMemoryMapName = fieldClone;
else if (fieldClone.Name == nameof(ModuleTrackerTemplate.HitsArraySize))
_customTrackerHitsArraySize = fieldClone;
@@ -41,6 +41,7 @@ namespace Coverlet.Core.Instrumentation
}
public string Module;
public string HitsFilePath;
public string HitsResultGuid;
public string ModulePath;
public string SourceLink;
@@ -18,6 +18,7 @@ namespace Coverlet.Core.Instrumentation
[ExcludeFromCodeCoverage]
public static class ModuleTrackerTemplate
{
public static string HitsFilePath;
public static string HitsMemoryMapName;
public static int HitsArraySize;
@@ -88,54 +89,62 @@ namespace Coverlet.Core.Instrumentation
if (!createdNew)
mutex.WaitOne();
MemoryMappedFile memoryMap = null;
try
{
// Tally hit counts from all threads in memory mapped area
using (var memoryMap = MemoryMappedFile.OpenExisting(HitsMemoryMapName))
try
{
var accessor = memoryMap.CreateViewAccessor();
using (var buffer = accessor.SafeMemoryMappedViewHandle)
memoryMap = MemoryMappedFile.OpenExisting(HitsMemoryMapName);
}
catch (PlatformNotSupportedException)
{
memoryMap = MemoryMappedFile.CreateFromFile(HitsFilePath, FileMode.Open, null, (HitsArraySize + Coverage.HitsResultHeaderSize) * sizeof(int));
}
// Tally hit counts from all threads in memory mapped area
var accessor = memoryMap.CreateViewAccessor();
using (var buffer = accessor.SafeMemoryMappedViewHandle)
{
unsafe
{
unsafe
byte* pointer = null;
buffer.AcquirePointer(ref pointer);
try
{
byte* pointer = null;
buffer.AcquirePointer(ref pointer);
try
var intPointer = (int*) pointer;
// Signal back to coverage analysis that we've started transferring hit counts.
// Use interlocked here to ensure a memory barrier before the Coverage class reads
// the shared data.
Interlocked.Increment(ref *(intPointer + Coverage.HitsResultUnloadStarted));
for (var i = 0; i < HitsArraySize; i++)
{
var intPointer = (int*) pointer;
var count = 0;
// Signal back to coverage analysis that we've started transferring hit counts.
// Use interlocked here to ensure a memory barrier before the Coverage class reads
// the shared data.
Interlocked.Increment(ref *(intPointer + Coverage.HitsResultUnloadStarted));
for (var i = 0; i < HitsArraySize; i++)
foreach (var threadHits in threads)
{
var count = 0;
foreach (var threadHits in threads)
{
count += threadHits[i];
}
if (count > 0)
{
// There's a header of one int before the hit counts
var hitLocationArrayOffset = intPointer + i + Coverage.HitsResultHeaderSize;
// No need to use Interlocked here since the mutex ensures only one thread updates
// the shared memory map.
*hitLocationArrayOffset += count;
}
count += threadHits[i];
}
// Signal back to coverage analysis that all hit counts were successfully tallied.
Interlocked.Increment(ref *(intPointer + Coverage.HitsResultUnloadFinished));
}
finally
{
buffer.ReleasePointer();
if (count > 0)
{
// There's a header of one int before the hit counts
var hitLocationArrayOffset = intPointer + i + Coverage.HitsResultHeaderSize;
// No need to use Interlocked here since the mutex ensures only one thread updates
// the shared memory map.
*hitLocationArrayOffset += count;
}
}
// Signal back to coverage analysis that all hit counts were successfully tallied.
Interlocked.Increment(ref *(intPointer + Coverage.HitsResultUnloadFinished));
}
finally
{
buffer.ReleasePointer();
}
}
}
@@ -143,6 +152,7 @@ namespace Coverlet.Core.Instrumentation
finally
{
mutex.ReleaseMutex();
memoryMap?.Dispose();
}
}
+4 -1
View File
@@ -34,7 +34,10 @@ namespace Coverlet.Core.Tests
coverage.PrepareModules();
// The module hit tracker must signal to Coverage that it has done its job, so call it manually
ModuleTrackerTemplate.HitsMemoryMapName = coverage.Results.Single().HitsResultGuid;
var instrumenterResult = coverage.Results.Single();
ModuleTrackerTemplate.HitsArraySize = instrumenterResult.HitCandidates.Count;
ModuleTrackerTemplate.HitsFilePath = instrumenterResult.HitsFilePath;
ModuleTrackerTemplate.HitsMemoryMapName = instrumenterResult.HitsResultGuid;
ModuleTrackerTemplate.UnloadModule(null, null);
var result = coverage.GetCoverageResult();
@@ -53,6 +53,15 @@ namespace Coverlet.Core.Helpers.Tests
Assert.False(InstrumentationHelper.IsValidFilterExpression(null));
}
[Fact]
public void TestDeleteHitsFile()
{
var tempFile = Path.GetTempFileName();
Assert.True(File.Exists(tempFile));
InstrumentationHelper.DeleteHitsFile(tempFile);
Assert.False(File.Exists(tempFile));
}
public static IEnumerable<object[]> GetExcludedFilesReturnsEmptyArgs =>
new[]
@@ -1,4 +1,5 @@
using Coverlet.Core.Instrumentation;
using Coverlet.Core.Helpers;
using System;
using System.Collections.Generic;
using System.IO;
@@ -17,7 +18,6 @@ namespace Coverlet.Core.Tests.Instrumentation
public ModuleTrackerTemplateTestsFixture()
{
_semaphore.Wait();
ModuleTrackerTemplate.HitsMemoryMapName = Guid.NewGuid().ToString();
}
public void Dispose()
@@ -33,18 +33,36 @@ namespace Coverlet.Core.Tests.Instrumentation
// Prevent parallel execution of these tests
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);
private MemoryMappedFile _mmap;
private readonly MemoryMappedFile _mmap;
public ModuleTrackerTemplateTests()
{
_semaphore.Wait();
_mmap = MemoryMappedFile.CreateNew(ModuleTrackerTemplate.HitsMemoryMapName, 100 * sizeof(int));
ModuleTrackerTemplate.HitsArraySize = 4;
ModuleTrackerTemplate.HitsMemoryMapName = Guid.NewGuid().ToString();
ModuleTrackerTemplate.HitsFilePath = Path.Combine(Path.GetTempPath(), $"coverlet.test_{ModuleTrackerTemplate.HitsMemoryMapName}");
var size = (ModuleTrackerTemplate.HitsArraySize + Coverage.HitsResultHeaderSize) * sizeof(int);
try
{
_mmap = MemoryMappedFile.CreateNew(ModuleTrackerTemplate.HitsMemoryMapName, size);
}
catch (PlatformNotSupportedException)
{
_mmap = MemoryMappedFile.CreateFromFile(ModuleTrackerTemplate.HitsFilePath, FileMode.CreateNew, null, size);
}
}
public void Dispose()
{
_mmap.Dispose();
var hitsFilePath = ModuleTrackerTemplate.HitsFilePath;
_semaphore.Release();
_mmap.Dispose();
InstrumentationHelper.DeleteHitsFile(hitsFilePath);
}
[Fact]
@@ -125,7 +143,7 @@ namespace Coverlet.Core.Tests.Instrumentation
// then dropped by UnloadModule the hit counting must be done
// in a new thread for each test
ModuleTrackerTemplate.HitsArraySize = hitCounts.Length;
Assert.Equal(ModuleTrackerTemplate.HitsArraySize, hitCounts.Length);
var thread = new Thread(() =>
{