Skip to content

Commit 9b001ad

Browse files
committed
Merge branch 'main-master' into memory-mapped-hit-counts
# Conflicts: # src/coverlet.core/Instrumentation/Instrumenter.cs # src/coverlet.template/ModuleTrackerTemplate.cs
2 parents 138c8b0 + 63dff09 commit 9b001ad

File tree

3 files changed

+75
-49
lines changed

3 files changed

+75
-49
lines changed

README.md

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ There is a simple performance test for the hit counting instrumentation in the t
440440
441441
The duration of the test can be tweaked by changing the number of iterations in the `[InlineData]` in the `PerformanceTest` class.
442442
443-
For more realistic testing it is recommended to try out any changes to the hit counting code paths on large, realistic projects. If you don't have any handy https://github.com/dotnet/corefx is an excellent candidate. [This page](https://github.com/dotnet/corefx/blob/master/Documentation/building/code-coverage.md) describes how to run code coverage tests for both the full solution and for individual projects with coverlet from nuget. Suitable projects (listed in order of escalating test durations):
443+
For more realistic testing it is recommended to try out any changes to the hit counting code paths on large, realistic projects. If you don't have any handy https://github.com/dotnet/corefx is an excellent candidate. [This page](https://github.com/dotnet/corefx/blob/master/Documentation/building/code-coverage.md) describes how to run code coverage tests for both the full solution and for individual projects with coverlet from nuget. Suitable projects (listed in order of escalating test durations):
444444
445445
* System.Collections.Concurrent.Tests
446446
* System.Collections.Tests
@@ -451,22 +451,10 @@ For more realistic testing it is recommended to try out any changes to the hit c
451451
Change to the directory of the library and run the msbuild code coverage command:
452452
453453
dotnet msbuild /t:BuildAndTest /p:Coverage=true
454-
455-
Look for a line like this:
456-
457-
----- start 18:13:36,59 =============== To repro directly: =====================================================
458-
459-
It is followed by a `pushd` command into the artefact directory, and a command to run coverlet. Run these to get to see the coverlet output and especially the test duration. E.g.:
460-
461-
C:\...\corefx\artifacts\tools\coverlet "System.Collections.Concurrent.Tests.dll" --target ...
462-
454+
463455
To run with a development version of coverlet call `dotnet run` instead of the installed coverlet version, e.g.:
464456
465-
dotnet run -p C:\...\coverlet\src\coverlet.console\coverlet.console.csproj -c Release -- "System.Collections.Concurrent.Tests.dll" --target ...
466-
467-
468-
469-
457+
dotnet msbuild /t:BuildAndTest /p:Coverage=true /p:CoverageExecutablePath="dotnet run -p C:\...\coverlet\src\coverlet.console\coverlet.console.csproj"
470458
471459
## Code of Conduct
472460

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ internal class Instrumenter
2929
private FieldDefinition _customTrackerHitsMemoryMapName;
3030
private ILProcessor _customTrackerClassConstructorIl;
3131
private TypeDefinition _customTrackerTypeDef;
32+
private MethodReference _customTrackerRegisterUnloadEventsMethod;
3233
private MethodReference _customTrackerRecordHitMethod;
3334

3435
public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles, string[] excludedAttributes)
@@ -77,6 +78,7 @@ private void InstrumentModule()
7778

7879
using (var module = ModuleDefinition.ReadModule(stream, parameters))
7980
{
81+
var containsAppContext = module.GetType(nameof(System), nameof(AppContext)) != null;
8082
var types = module.GetTypes();
8183
AddCustomModuleTrackerToModule(module);
8284

@@ -96,15 +98,50 @@ private void InstrumentModule()
9698
}
9799

98100
// Fixup the custom tracker class constructor, according to all instrumented types
99-
Instruction firstInstr = _customTrackerClassConstructorIl.Body.Instructions.First();
100-
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Nop));
101-
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath));
102-
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsFilePath));
103-
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Ldc_I4, _result.HitCandidates.Count));
104-
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Newarr, module.TypeSystem.Int32));
105-
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsArray));
106-
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsResultGuid));
107-
_customTrackerClassConstructorIl.InsertBefore(firstInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsMemoryMapName));
101+
if (_customTrackerRegisterUnloadEventsMethod == null)
102+
{
103+
_customTrackerRegisterUnloadEventsMethod = new MethodReference(
104+
nameof(ModuleTrackerTemplate.RegisterUnloadEvents), module.TypeSystem.Void, _customTrackerTypeDef);
105+
}
106+
107+
Instruction lastInstr = _customTrackerClassConstructorIl.Body.Instructions.Last();
108+
109+
if (!containsAppContext)
110+
{
111+
// For "normal" cases, where the instrumented assembly is not the core library, we add a call to
112+
// RegisterUnloadEvents to the static constructor of the generated custom tracker. Due to static
113+
// initialization constraints, the core library is handled separately below.
114+
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Call, _customTrackerRegisterUnloadEventsMethod));
115+
}
116+
117+
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldc_I4, _result.HitCandidates.Count));
118+
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Newarr, module.TypeSystem.Int32));
119+
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsArray));
120+
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath));
121+
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsFilePath));
122+
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsResultGuid));
123+
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsMemoryMapName));
124+
125+
if (containsAppContext)
126+
{
127+
// Handle the core library by instrumenting System.AppContext.OnProcessExit to directly call
128+
// the UnloadModule method of the custom tracker type. This avoids loops between the static
129+
// initialization of the custom tracker and the static initialization of the hosting AppDomain
130+
// (which for the core library case will be instrumented code).
131+
var eventArgsType = new TypeReference(nameof(System), nameof(EventArgs), module, module.TypeSystem.CoreLibrary);
132+
var customTrackerUnloadModule = new MethodReference(nameof(ModuleTrackerTemplate.UnloadModule), module.TypeSystem.Void, _customTrackerTypeDef);
133+
customTrackerUnloadModule.Parameters.Add(new ParameterDefinition(module.TypeSystem.Object));
134+
customTrackerUnloadModule.Parameters.Add(new ParameterDefinition(eventArgsType));
135+
136+
var appContextType = new TypeReference(nameof(System), nameof(AppContext), module, module.TypeSystem.CoreLibrary);
137+
var onProcessExitMethod = new MethodReference("OnProcessExit", module.TypeSystem.Void, appContextType).Resolve();
138+
var onProcessExitIl = onProcessExitMethod.Body.GetILProcessor();
139+
140+
lastInstr = onProcessExitIl.Body.Instructions.Last();
141+
onProcessExitIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldnull));
142+
onProcessExitIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldnull));
143+
onProcessExitIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Call, customTrackerUnloadModule));
144+
}
108145

109146
module.Write(stream);
110147
}
@@ -141,12 +178,9 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module)
141178
{
142179
MethodDefinition methodOnCustomType = new MethodDefinition(methodDef.Name, methodDef.Attributes, methodDef.ReturnType);
143180

144-
if (methodDef.Name == nameof(ModuleTrackerTemplate.RecordHit))
181+
foreach (var parameter in methodDef.Parameters)
145182
{
146-
foreach (var parameter in methodDef.Parameters)
147-
{
148-
methodOnCustomType.Parameters.Add(new ParameterDefinition(module.ImportReference(parameter.ParameterType)));
149-
}
183+
methodOnCustomType.Parameters.Add(new ParameterDefinition(module.ImportReference(parameter.ParameterType)));
150184
}
151185

152186
foreach (var variable in methodDef.Body.Variables)
@@ -172,8 +206,11 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module)
172206
else
173207
{
174208
// Move to the custom type
175-
instr.Operand = new MethodReference(
176-
methodReference.Name, methodReference.ReturnType, _customTrackerTypeDef);
209+
var updatedMethodReference = new MethodReference(methodReference.Name, methodReference.ReturnType, _customTrackerTypeDef);
210+
foreach (var parameter in methodReference.Parameters)
211+
updatedMethodReference.Parameters.Add(new ParameterDefinition(parameter.Name, parameter.Attributes, module.ImportReference(parameter.ParameterType)));
212+
213+
instr.Operand = updatedMethodReference;
177214
}
178215
}
179216
else if (instr.Operand is FieldReference fieldReference)
@@ -204,6 +241,7 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module)
204241
module.Types.Add(_customTrackerTypeDef);
205242
}
206243

244+
Debug.Assert(_customTrackerHitsArray != null);
207245
Debug.Assert(_customTrackerClassConstructorIl != null);
208246
}
209247

src/coverlet.template/ModuleTrackerTemplate.cs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,33 +26,36 @@ public static class ModuleTrackerTemplate
2626
public static string HitsMemoryMapName;
2727
public static int[] HitsArray;
2828

29-
// Special case when instrumenting CoreLib, the static below prevents infinite loop in CoreLib
30-
// while allowing the tracker template to call any of its types and functions.
31-
private static bool s_isTracking;
32-
3329
static ModuleTrackerTemplate()
3430
{
35-
s_isTracking = true;
36-
AppDomain.CurrentDomain.ProcessExit += new EventHandler(UnloadModule);
37-
AppDomain.CurrentDomain.DomainUnload += new EventHandler(UnloadModule);
38-
s_isTracking = false;
39-
4031
// At the end of the instrumentation of a module, the instrumenter needs to add code here
4132
// to initialize the static fields according to the values derived from the instrumentation of
4233
// the module.
4334
}
4435

36+
// A call to this method will be injected in the static constructor above for most cases. However, if the
37+
// current assembly is System.Private.CoreLib (or more specifically, defines System.AppDomain), a call directly
38+
// to UnloadModule will be injected in System.AppContext.OnProcessExit.
39+
public static void RegisterUnloadEvents()
40+
{
41+
AppDomain.CurrentDomain.ProcessExit += new EventHandler(UnloadModule);
42+
AppDomain.CurrentDomain.DomainUnload += new EventHandler(UnloadModule);
43+
}
44+
4545
public static void RecordHit(int hitLocationIndex)
4646
{
47-
if (s_isTracking)
47+
// Make sure to avoid recording if this is a call to RecordHit within the AppDomain setup code in an
48+
// instrumented build of System.Private.CoreLib.
49+
if (HitsArray is null)
4850
return;
4951

5052
Interlocked.Increment(ref HitsArray[hitLocationIndex]);
5153
}
5254

5355
public static void UnloadModule(object sender, EventArgs e)
5456
{
55-
s_isTracking = true;
57+
// Claim the current hits array and reset it to prevent double-counting scenarios.
58+
var hitsArray = Interlocked.Exchange(ref HitsArray, new int[HitsArray.Length]);
5659

5760
// The same module can be unloaded multiple times in the same process via different app domains.
5861
// Use a global mutex to ensure no concurrent access.
@@ -91,10 +94,12 @@ public static void UnloadModule(object sender, EventArgs e)
9194
// the shared data.
9295
Interlocked.Increment(ref *(intPointer + HitsResultUnloadStarted));
9396

94-
for (var i = 0; i < HitsArray.Length; i++)
97+
for (var i = 0; i < hitsArray.Length; i++)
9598
{
96-
var count = HitsArray[i];
99+
var count = hitsArray[i];
97100

101+
// By only modifying the memory map pages where there have been hits
102+
// unnecessary allocation of all-zero pages is avoided.
98103
if (count > 0)
99104
{
100105
var hitLocationArrayOffset = intPointer + i + HitsResultHeaderSize;
@@ -117,15 +122,10 @@ public static void UnloadModule(object sender, EventArgs e)
117122
}
118123
finally
119124
{
120-
// Avoid double counting if unloaded multiple times (if that can happen?)
121-
HitsArray = new int[HitsArray.Length];
122-
123125
mutex.ReleaseMutex();
124126
memoryMap?.Dispose();
125127
}
126128
}
127-
128-
s_isTracking = false;
129129
}
130130
}
131131
}

0 commit comments

Comments
 (0)