Skip to content

Commit c7bbbbd

Browse files
committed
[CI Visibility] Bound global coverage native memory
1 parent bd03e78 commit c7bbbbd

34 files changed

Lines changed: 2134 additions & 371 deletions

tracer/src/Datadog.Trace.Coverage.collector/AssemblyProcessor.cs

Lines changed: 112 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,24 @@ public unsafe void Process()
249249
var reportTypeGenericInstance = new GenericInstanceType(coverageReporterTypeReference);
250250
reportTypeGenericInstance.GenericArguments.Add(moduleCoverageMetadataImplTypeDef);
251251

252-
var reportGetCountersMethod = new MethodReference("GetFileCounter", new PointerType(module.TypeSystem.Void), reportTypeGenericInstance)
252+
var coverageProbeTypeDefinition = datadogTracerAssembly.MainModule.GetType(typeof(CoverageProbe).FullName);
253+
var coverageProbeTypeReference = module.ImportReference(coverageProbeTypeDefinition);
254+
var reportAcquireCountersMethod = new MethodReference("AcquireFileCounter", coverageProbeTypeReference, reportTypeGenericInstance)
253255
{
254256
HasThis = false,
255257
Parameters =
256258
{
257259
new ParameterDefinition(module.TypeSystem.Int32) { Name = "fileIndex" }
258260
}
259261
};
262+
var probePointerGetter = new MethodReference("get_Pointer", new PointerType(module.TypeSystem.Void), coverageProbeTypeReference)
263+
{
264+
HasThis = true
265+
};
266+
var probeDisposeMethod = new MethodReference(nameof(IDisposable.Dispose), module.TypeSystem.Void, coverageProbeTypeReference)
267+
{
268+
HasThis = true
269+
};
260270

261271
// GenericInstanceMethod? arrayEmptyOfIntMethodReference = null;
262272
for (var typeIndex = 0; typeIndex < moduleTypes.Count; typeIndex++)
@@ -432,6 +442,15 @@ public unsafe void Process()
432442

433443
var methodBody = moduleTypeMethod.Body;
434444
var instructions = methodBody.Instructions;
445+
if (instructions.Any(static instruction => instruction.OpCode == OpCodes.Tail))
446+
{
447+
// The invocation probe is released from an outer fault handler and a shared return
448+
// epilogue. Wrapping a tail transfer in that protected region would invalidate its
449+
// stack-constant semantics, so leave these uncommon methods untouched.
450+
_logger.Debug($"\t\t[NO] {moduleTypeMethod.FullName}, contains a tail call.");
451+
continue;
452+
}
453+
435454
var instructionsOriginalLength = instructions.Count;
436455
if (instructions.Capacity < instructionsOriginalLength * 2)
437456
{
@@ -455,22 +474,23 @@ public unsafe void Process()
455474
}
456475
}
457476

458-
VariableDefinition? countersVariable = null;
459-
if (instructionsWithValidSequencePoints.Count > 1 || instructions[0] != instructionsWithValidSequencePoints[0].Instruction)
477+
// The probe local owns the native buffer for the complete invocation. The generated
478+
// outer fault handler and shared return epilogue release it on every exit path.
479+
var probeVariable = new VariableDefinition(coverageProbeTypeReference);
480+
VariableDefinition countersVariable;
481+
if (_coverageMode == CoverageMode.LineExecution)
460482
{
461-
// Step 3 - Modify local var to add the Coverage counters instance.
462-
if (_coverageMode == CoverageMode.LineExecution)
463-
{
464-
countersVariable = new VariableDefinition(new PointerType(module.TypeSystem.Byte));
465-
}
466-
else
467-
{
468-
countersVariable = new VariableDefinition(new PointerType(module.TypeSystem.Int32));
469-
}
470-
471-
methodBody.Variables.Add(countersVariable);
483+
countersVariable = new VariableDefinition(new PointerType(module.TypeSystem.Byte));
484+
}
485+
else
486+
{
487+
countersVariable = new VariableDefinition(new PointerType(module.TypeSystem.Int32));
472488
}
473489

490+
methodBody.Variables.Add(probeVariable);
491+
methodBody.Variables.Add(countersVariable);
492+
methodBody.InitLocals = true;
493+
474494
// Step 4 - Insert the counter retriever
475495
FileMetadata fileMetadata;
476496
if (!fileDictionaryIndex.TryGetValue(filePath, out fileMetadata))
@@ -479,12 +499,13 @@ public unsafe void Process()
479499
fileDictionaryIndex[filePath] = fileMetadata;
480500
}
481501

502+
var probeProtectedStart = Instruction.Create(OpCodes.Ldloca, probeVariable);
482503
instructions.Insert(0, Instruction.Create(OpCodes.Ldc_I4, fileMetadata.Index));
483-
instructions.Insert(1, Instruction.Create(OpCodes.Call, reportGetCountersMethod));
484-
if (countersVariable is not null)
485-
{
486-
instructions.Insert(2, Instruction.Create(OpCodes.Stloc, countersVariable));
487-
}
504+
instructions.Insert(1, Instruction.Create(OpCodes.Call, reportAcquireCountersMethod));
505+
instructions.Insert(2, Instruction.Create(OpCodes.Stloc, probeVariable));
506+
instructions.Insert(3, probeProtectedStart);
507+
instructions.Insert(4, Instruction.Create(OpCodes.Call, probePointerGetter));
508+
instructions.Insert(5, Instruction.Create(OpCodes.Stloc, countersVariable));
488509

489510
// Step 5 - Insert line reporter
490511
for (var i = 0; i < instructionsWithValidSequencePoints.Count; i++)
@@ -635,6 +656,7 @@ public unsafe void Process()
635656
instructions.Insert(++optIdx, currentInstructionClone);
636657
}
637658

659+
AddProbeCleanup(methodBody, probeVariable, probeProtectedStart, probeDisposeMethod);
638660
isDirty = true;
639661
}
640662
}
@@ -955,6 +977,77 @@ private static void WriteInt32LittleEndian(byte[] buffer, int offset, int value)
955977
buffer[offset + 3] = (byte)(value >> 24);
956978
}
957979

980+
private static void AddProbeCleanup(
981+
Mono.Cecil.Cil.MethodBody methodBody,
982+
VariableDefinition probeVariable,
983+
Instruction protectedStart,
984+
MethodReference probeDisposeMethod)
985+
{
986+
var instructions = methodBody.Instructions;
987+
var returnInstructions = instructions.Where(static instruction => instruction.OpCode == OpCodes.Ret).ToArray();
988+
VariableDefinition? returnVariable = null;
989+
if (returnInstructions.Length > 0 && methodBody.Method.ReturnType.MetadataType != MetadataType.Void)
990+
{
991+
returnVariable = new VariableDefinition(methodBody.Method.ReturnType);
992+
methodBody.Variables.Add(returnVariable);
993+
}
994+
995+
var faultStart = Instruction.Create(OpCodes.Ldloca, probeVariable);
996+
var faultDispose = Instruction.Create(OpCodes.Call, probeDisposeMethod);
997+
var faultEnd = Instruction.Create(OpCodes.Endfinally);
998+
var epilogueStart = returnInstructions.Length == 0 ? null : Instruction.Create(OpCodes.Ldloca, probeVariable);
999+
1000+
foreach (var exceptionHandler in methodBody.ExceptionHandlers)
1001+
{
1002+
// Cecil represents "until the end of the method" with null. The generated outer
1003+
// fault handler extends the body, so existing regions must keep their original end.
1004+
exceptionHandler.TryEnd ??= faultStart;
1005+
exceptionHandler.HandlerEnd ??= faultStart;
1006+
}
1007+
1008+
if (epilogueStart is not null)
1009+
{
1010+
foreach (var returnInstruction in returnInstructions)
1011+
{
1012+
if (returnVariable is not null)
1013+
{
1014+
returnInstruction.OpCode = OpCodes.Stloc;
1015+
returnInstruction.Operand = returnVariable;
1016+
instructions.Insert(instructions.IndexOf(returnInstruction) + 1, Instruction.Create(OpCodes.Leave, epilogueStart));
1017+
}
1018+
else
1019+
{
1020+
returnInstruction.OpCode = OpCodes.Leave;
1021+
returnInstruction.Operand = epilogueStart;
1022+
}
1023+
}
1024+
}
1025+
1026+
instructions.Add(faultStart);
1027+
instructions.Add(faultDispose);
1028+
instructions.Add(faultEnd);
1029+
if (epilogueStart is not null)
1030+
{
1031+
instructions.Add(epilogueStart);
1032+
instructions.Add(Instruction.Create(OpCodes.Call, probeDisposeMethod));
1033+
if (returnVariable is not null)
1034+
{
1035+
instructions.Add(Instruction.Create(OpCodes.Ldloc, returnVariable));
1036+
}
1037+
1038+
instructions.Add(Instruction.Create(OpCodes.Ret));
1039+
}
1040+
1041+
methodBody.ExceptionHandlers.Add(
1042+
new ExceptionHandler(ExceptionHandlerType.Fault)
1043+
{
1044+
TryStart = protectedStart,
1045+
TryEnd = faultStart,
1046+
HandlerStart = faultStart,
1047+
HandlerEnd = epilogueStart,
1048+
});
1049+
}
1050+
9581051
private static void RemoveShortOpCodes(Instruction instruction)
9591052
{
9601053
if (instruction.OpCode == OpCodes.Br_S) { instruction.OpCode = OpCodes.Br; }

tracer/src/Datadog.Trace.Coverage.collector/InProcCoverageCollector.cs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System;
77
using System.IO;
8+
using Datadog.Trace.Ci;
89
using Datadog.Trace.Ci.Coverage;
910
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection;
1011
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollector.InProcDataCollector;
@@ -38,6 +39,7 @@ public class InProcCoverageCollector : InProcDataCollection
3839
{
3940
private const string OutputPathKey = "OutputPath";
4041
private string? _outputPathValue = null;
42+
private StandaloneCoverageReconciliation? _standaloneReconciliation;
4143

4244
/// <summary>
4345
/// Initialize inproc coverage collector
@@ -60,7 +62,23 @@ public void TestSessionStart(TestSessionStartArgs testSessionStartArgs)
6062

6163
if (CoverageReporter.Handler is DefaultWithGlobalCoverageEventHandler coverageHandler)
6264
{
63-
coverageHandler.RegisterCollectorOutputDirectory(_outputPathValue ?? Environment.CurrentDirectory);
65+
var outputDirectory = _outputPathValue ?? Environment.CurrentDirectory;
66+
if (coverageHandler.RegisterCollectorOutputDirectory(outputDirectory))
67+
{
68+
var coordinatorDirectory = outputDirectory;
69+
foreach (var registration in coverageHandler.OutputRegistrations)
70+
{
71+
if (registration.IsCoordinator)
72+
{
73+
coordinatorDirectory = registration.Directory;
74+
break;
75+
}
76+
}
77+
78+
_standaloneReconciliation = StandaloneCoverageReconciliation.TryCreate(
79+
coordinatorDirectory,
80+
TestOptimization.Instance.RunId);
81+
}
6482
}
6583
}
6684

@@ -86,6 +104,40 @@ public void TestCaseEnd(TestCaseEndArgs testCaseEndArgs)
86104
/// <param name="testSessionEndArgs">Test session end arguments</param>
87105
public void TestSessionEnd(TestSessionEndArgs testSessionEndArgs)
88106
{
89-
CoverageReporter.FinalizeGlobalCoverage();
107+
var standaloneReconciliation = _standaloneReconciliation;
108+
_standaloneReconciliation = null;
109+
if (standaloneReconciliation is null)
110+
{
111+
CoverageReporter.FinalizeGlobalCoverage();
112+
return;
113+
}
114+
115+
var completionRegistered = false;
116+
try
117+
{
118+
CoverageReporter.FinalizeGlobalCoverage(
119+
complete =>
120+
{
121+
try
122+
{
123+
if (complete)
124+
{
125+
standaloneReconciliation.TryPublish();
126+
}
127+
}
128+
finally
129+
{
130+
standaloneReconciliation.Dispose();
131+
}
132+
});
133+
completionRegistered = true;
134+
}
135+
finally
136+
{
137+
if (!completionRegistered)
138+
{
139+
standaloneReconciliation.Dispose();
140+
}
141+
}
90142
}
91143
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// <copyright file="StandaloneCoverageReconciliation.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#nullable enable
7+
8+
using System;
9+
using System.IO;
10+
using System.Threading;
11+
using Datadog.Trace.Ci;
12+
using Datadog.Trace.Ci.Coverage;
13+
14+
namespace Datadog.Trace.Coverage.Collector;
15+
16+
/// <summary>
17+
/// Owns reconciliation when the in-process collector runs without an enclosing Datadog test command.
18+
/// </summary>
19+
internal sealed class StandaloneCoverageReconciliation : IDisposable
20+
{
21+
private readonly string _directory;
22+
private FileStream? _activityStream;
23+
private GlobalCoverageReconciliationAuthority? _authority;
24+
private int _publicationStarted;
25+
26+
private StandaloneCoverageReconciliation(
27+
string directory,
28+
FileStream activityStream,
29+
GlobalCoverageReconciliationAuthority? authority)
30+
{
31+
_directory = directory;
32+
_activityStream = activityStream;
33+
_authority = authority;
34+
}
35+
36+
public static StandaloneCoverageReconciliation? TryCreate(string directory, string runId)
37+
{
38+
FileStream? activityStream = null;
39+
try
40+
{
41+
var canonicalDirectory = Path.GetFullPath(directory);
42+
activityStream = new FileStream(
43+
Path.Combine(canonicalDirectory, GlobalCoverageProtocol.ReconciliationLockFileName),
44+
FileMode.OpenOrCreate,
45+
FileAccess.Read,
46+
FileShare.Read);
47+
var authority = GlobalCoverageReconciliationAuthority.TryCreate(
48+
canonicalDirectory,
49+
GlobalCoverageProtocol.GetRunToken(runId),
50+
GlobalCoverageProtocol.CollectorClaimKind);
51+
52+
var reconciliation = new StandaloneCoverageReconciliation(canonicalDirectory, activityStream, authority);
53+
activityStream = null;
54+
return reconciliation;
55+
}
56+
catch (Exception ex)
57+
{
58+
TestOptimization.Instance.Log.Warning(ex, "Global coverage collector could not acquire standalone reconciliation ownership.");
59+
return null;
60+
}
61+
finally
62+
{
63+
activityStream?.Dispose();
64+
}
65+
}
66+
67+
public bool TryPublish()
68+
{
69+
if (Interlocked.CompareExchange(ref _publicationStarted, 1, 0) != 0)
70+
{
71+
return false;
72+
}
73+
74+
Interlocked.Exchange(ref _activityStream, null)?.Dispose();
75+
var authority = Interlocked.Exchange(ref _authority, null);
76+
77+
GlobalCoverageReconciliationLease? lease = null;
78+
try
79+
{
80+
var outputPath = Path.Combine(
81+
_directory,
82+
$"session-coverage-{DateTime.UtcNow:yyyy-MM-dd_HH_mm_ss_fffffff}-{Guid.NewGuid():N}.json");
83+
if (!global::CoverageUtils.TryReadAndCombine(_directory, outputPath, authority, out var coverage, out lease) ||
84+
coverage is null)
85+
{
86+
return false;
87+
}
88+
89+
var writer = new GlobalCoverageArtifactWriter();
90+
using var stagedOutput = writer.StageReplace(outputPath, coverage);
91+
lease!.Complete(stagedOutput.Commit);
92+
return true;
93+
}
94+
catch (Exception ex)
95+
{
96+
TestOptimization.Instance.Log.Warning(ex, "Global coverage collector could not publish the standalone coverage result.");
97+
return false;
98+
}
99+
finally
100+
{
101+
if (lease is null)
102+
{
103+
authority?.Dispose();
104+
}
105+
106+
lease?.Dispose();
107+
}
108+
}
109+
110+
public void Dispose()
111+
{
112+
Interlocked.Exchange(ref _activityStream, null)?.Dispose();
113+
Interlocked.Exchange(ref _authority, null)?.Dispose();
114+
}
115+
}

0 commit comments

Comments
 (0)