From 92b26a4c375cb2b6348a505a654569a3f46d1650 Mon Sep 17 00:00:00 2001 From: Sam Harwell <sam@tunnelvisionlabs.com> Date: Tue, 15 Jan 2019 14:47:52 -0600 Subject: [PATCH 1/2] Remove reliance on s_isTracking to detect recursion Fixes dotnet/coreclr#21994 --- .../ModuleTrackerTemplate.cs | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/coverlet.template/ModuleTrackerTemplate.cs b/src/coverlet.template/ModuleTrackerTemplate.cs index 52cb332f8..ab9d0c1d1 100644 --- a/src/coverlet.template/ModuleTrackerTemplate.cs +++ b/src/coverlet.template/ModuleTrackerTemplate.cs @@ -20,16 +20,10 @@ public static class ModuleTrackerTemplate public static string HitsFilePath; public static int[] HitsArray; - // Special case when instrumenting CoreLib, the static below prevents infinite loop in CoreLib - // while allowing the tracker template to call any of its types and functions. - private static bool s_isTracking; - static ModuleTrackerTemplate() { - s_isTracking = true; AppDomain.CurrentDomain.ProcessExit += new EventHandler(UnloadModule); AppDomain.CurrentDomain.DomainUnload += new EventHandler(UnloadModule); - s_isTracking = false; // At the end of the instrumentation of a module, the instrumenter needs to add code here // to initialize the static fields according to the values derived from the instrumentation of @@ -38,7 +32,9 @@ static ModuleTrackerTemplate() public static void RecordHit(int hitLocationIndex) { - if (s_isTracking) + // Make sure to avoid recording if this is a call to RecordHit within the AppDomain setup code in an + // instrumented build of System.Private.CoreLib. + if (HitsArray is null) return; Interlocked.Increment(ref HitsArray[hitLocationIndex]); @@ -46,7 +42,8 @@ public static void RecordHit(int hitLocationIndex) public static void UnloadModule(object sender, EventArgs e) { - s_isTracking = true; + // Claim the current hits array and reset it to prevent double-counting scenarios. + var hitsArray = Interlocked.Exchange(ref HitsArray, new int[HitsArray.Length]); // The same module can be unloaded multiple times in the same process via different app domains. // Use a global mutex to ensure no concurrent access. @@ -61,8 +58,8 @@ public static void UnloadModule(object sender, EventArgs e) using (var fs = new FileStream(HitsFilePath, FileMode.CreateNew)) using (var bw = new BinaryWriter(fs)) { - bw.Write(HitsArray.Length); - foreach (int hitCount in HitsArray) + bw.Write(hitsArray.Length); + foreach (int hitCount in hitsArray) { bw.Write(hitCount); } @@ -82,25 +79,21 @@ public static void UnloadModule(object sender, EventArgs e) using (var bw = new BinaryWriter(fs)) { int hitsLength = br.ReadInt32(); - if (hitsLength != HitsArray.Length) + if (hitsLength != hitsArray.Length) { throw new InvalidOperationException( - $"{HitsFilePath} has {hitsLength} entries but on memory {nameof(HitsArray)} has {HitsArray.Length}"); + $"{HitsFilePath} has {hitsLength} entries but on memory {nameof(HitsArray)} has {hitsArray.Length}"); } for (int i = 0; i < hitsLength; ++i) { int oldHitCount = br.ReadInt32(); bw.Seek(-sizeof(int), SeekOrigin.Current); - bw.Write(HitsArray[i] + oldHitCount); + bw.Write(hitsArray[i] + oldHitCount); } } } - // Prevent any double counting scenario, i.e.: UnloadModule called twice (not sure if this can happen in practice ...) - // Only an issue if DomainUnload and ProcessExit can both happens, perhaps can be removed... - Array.Clear(HitsArray, 0, HitsArray.Length); - // On purpose this is not under a try-finally: it is better to have an exception if there was any error writing the hits file // this case is relevant when instrumenting corelib since multiple processes can be running against the same instrumented dll. mutex.ReleaseMutex(); From 96b1594905369a2e6a755ac5ef12e165dd6ad4ec Mon Sep 17 00:00:00 2001 From: Sam Harwell <sam@tunnelvisionlabs.com> Date: Tue, 15 Jan 2019 16:11:27 -0600 Subject: [PATCH 2/2] Remove the need to register events when instrumenting System.Private.CoreLib --- .../Instrumentation/Instrumenter.cs | 52 ++++++++++++++++--- .../ModuleTrackerTemplate.cs | 12 +++-- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index 3fc14c059..7422d34d8 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -29,6 +29,7 @@ internal class Instrumenter private FieldDefinition _customTrackerHitsFilePath; private ILProcessor _customTrackerClassConstructorIl; private TypeDefinition _customTrackerTypeDef; + private MethodReference _customTrackerRegisterUnloadEventsMethod; private MethodReference _customTrackerRecordHitMethod; public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles, string[] excludedAttributes) @@ -76,6 +77,7 @@ private void InstrumentModule() using (var module = ModuleDefinition.ReadModule(stream, parameters)) { + var containsAppContext = module.GetType(nameof(System), nameof(AppContext)) != null; var types = module.GetTypes(); AddCustomModuleTrackerToModule(module); @@ -95,13 +97,49 @@ private void InstrumentModule() } // Fixup the custom tracker class constructor, according to all instrumented types + if (_customTrackerRegisterUnloadEventsMethod == null) + { + _customTrackerRegisterUnloadEventsMethod = new MethodReference( + nameof(ModuleTrackerTemplate.RegisterUnloadEvents), module.TypeSystem.Void, _customTrackerTypeDef); + } + Instruction lastInstr = _customTrackerClassConstructorIl.Body.Instructions.Last(); + + if (!containsAppContext) + { + // For "normal" cases, where the instrumented assembly is not the core library, we add a call to + // RegisterUnloadEvents to the static constructor of the generated custom tracker. Due to static + // initialization constraints, the core library is handled separately below. + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Call, _customTrackerRegisterUnloadEventsMethod)); + } + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldc_I4, _result.HitCandidates.Count)); _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Newarr, module.TypeSystem.Int32)); _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsArray)); _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath)); _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsFilePath)); + if (containsAppContext) + { + // Handle the core library by instrumenting System.AppContext.OnProcessExit to directly call + // the UnloadModule method of the custom tracker type. This avoids loops between the static + // initialization of the custom tracker and the static initialization of the hosting AppDomain + // (which for the core library case will be instrumented code). + var eventArgsType = new TypeReference(nameof(System), nameof(EventArgs), module, module.TypeSystem.CoreLibrary); + var customTrackerUnloadModule = new MethodReference(nameof(ModuleTrackerTemplate.UnloadModule), module.TypeSystem.Void, _customTrackerTypeDef); + customTrackerUnloadModule.Parameters.Add(new ParameterDefinition(module.TypeSystem.Object)); + customTrackerUnloadModule.Parameters.Add(new ParameterDefinition(eventArgsType)); + + var appContextType = new TypeReference(nameof(System), nameof(AppContext), module, module.TypeSystem.CoreLibrary); + var onProcessExitMethod = new MethodReference("OnProcessExit", module.TypeSystem.Void, appContextType).Resolve(); + var onProcessExitIl = onProcessExitMethod.Body.GetILProcessor(); + + lastInstr = onProcessExitIl.Body.Instructions.Last(); + onProcessExitIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldnull)); + onProcessExitIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldnull)); + onProcessExitIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Call, customTrackerUnloadModule)); + } + module.Write(stream); } } @@ -135,12 +173,9 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module) { MethodDefinition methodOnCustomType = new MethodDefinition(methodDef.Name, methodDef.Attributes, methodDef.ReturnType); - if (methodDef.Name == "RecordHit") + foreach (var parameter in methodDef.Parameters) { - foreach (var parameter in methodDef.Parameters) - { - methodOnCustomType.Parameters.Add(new ParameterDefinition(module.ImportReference(parameter.ParameterType))); - } + methodOnCustomType.Parameters.Add(new ParameterDefinition(module.ImportReference(parameter.ParameterType))); } foreach (var variable in methodDef.Body.Variables) @@ -166,8 +201,11 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module) else { // Move to the custom type - instr.Operand = new MethodReference( - methodReference.Name, methodReference.ReturnType, _customTrackerTypeDef); + var updatedMethodReference = new MethodReference(methodReference.Name, methodReference.ReturnType, _customTrackerTypeDef); + foreach (var parameter in methodReference.Parameters) + updatedMethodReference.Parameters.Add(new ParameterDefinition(parameter.Name, parameter.Attributes, module.ImportReference(parameter.ParameterType))); + + instr.Operand = updatedMethodReference; } } else if (instr.Operand is FieldReference fieldReference) diff --git a/src/coverlet.template/ModuleTrackerTemplate.cs b/src/coverlet.template/ModuleTrackerTemplate.cs index ab9d0c1d1..eb460ffc9 100644 --- a/src/coverlet.template/ModuleTrackerTemplate.cs +++ b/src/coverlet.template/ModuleTrackerTemplate.cs @@ -22,14 +22,20 @@ public static class ModuleTrackerTemplate static ModuleTrackerTemplate() { - AppDomain.CurrentDomain.ProcessExit += new EventHandler(UnloadModule); - AppDomain.CurrentDomain.DomainUnload += new EventHandler(UnloadModule); - // At the end of the instrumentation of a module, the instrumenter needs to add code here // to initialize the static fields according to the values derived from the instrumentation of // the module. } + // A call to this method will be injected in the static constructor above for most cases. However, if the + // current assembly is System.Private.CoreLib (or more specifically, defines System.AppDomain), a call directly + // to UnloadModule will be injected in System.AppContext.OnProcessExit. + public static void RegisterUnloadEvents() + { + AppDomain.CurrentDomain.ProcessExit += new EventHandler(UnloadModule); + AppDomain.CurrentDomain.DomainUnload += new EventHandler(UnloadModule); + } + public static void RecordHit(int hitLocationIndex) { // Make sure to avoid recording if this is a call to RecordHit within the AppDomain setup code in an