Skip to content

Commit 5770ca1

Browse files
authored
[Dynamic Instrumentation] DEBUG-5101 line probe two segment fallback (#8543)
## Summary of changes - Update `LineProbeResolver` to support line probe source-file fallback matches when only the last 2 path segments match (down from 4). - Keep the existing strict-top-score binding rule, so fallback only binds when a single qualified candidate has the strictly highest matching-trailing-segment score across all currently loaded symbolicated assemblies. - Preserve bound diagnostics for fallback resolutions by reporting `FallbackTrailingSuffixMatch` together with the matched trailing-segment count. - Extend `LineProbeResolverTest` coverage for 2-segment fallback binding and for picking the highest-scoring candidate when incidental low-scoring matches exist. ## Reason for change - `DEBUG-5101` requires line probes to resolve when the configured source path differs from the PDB path except for the trailing directory and filename (for example `unknown-prefix/Controllers/HomeController.cs` against a PDB path ending in `Controllers/HomeController.cs`). - Raising the matching reach without weakening safety is the goal: the existing top-score ambiguity check (across assemblies and within each assembly) is sufficient to keep wrong-assembly binding out, so we don't need to add a new uniqueness restriction on top of it. ## Implementation details - Lower `MinTrailingSegmentsForFallbackMatch` from 4 to 2 so resolver fallback can match scenarios like `unknown-prefix/Controllers/File.cs` against a loaded symbolicated assembly path ending in `Controllers/File.cs`. - Encapsulate the cross-assembly qualified-candidate counter inside `BestFallbackMatchSelection` (replacing the standalone `ref int qualifiedFallbackMatchCount` parameter on `TrackClosestPathMatch`) and expose it as `QualifiedMatchCount` so the diagnostics path keeps the same information. - Preserve the binding rule: bind to the candidate with the strictly highest matching-trailing-segment score, reject when the top score is tied across assemblies or when the top-scoring assembly is itself internally ambiguous (`HasAmbiguousBestMatch`). - Keep diagnostics explicit for successful fallback binds by continuing to emit `LineProbePathMatchType.FallbackTrailingSuffixMatch` together with the trailing-segment count used to resolve the probe. - The resolver change stays in warm-path debugger resolution logic and only adds integer bookkeeping on top of the existing assembly scan; no new allocations on this path. ## Test coverage - Ran: - `LineProbeResolverTest` - Added coverage for: - 2-segment fallback binding (`FallbackMatchBindsWhenOnlyTwoTrailingSegmentsMatch`) - binding to the highest-scoring qualified candidate when incidental lower-scoring matches exist (`BestFallbackMatchSelectionBindsHighestScoreOverIncidentalLowScoringCandidates`) - binding when a single qualified candidate is tracked (`BestFallbackMatchSelectionBindsWhenSingleQualifiedCandidateIsTracked`)
1 parent ea167be commit 5770ca1

2 files changed

Lines changed: 61 additions & 8 deletions

File tree

tracer/src/Datadog.Trace/Debugger/LineProbeResolver.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace Datadog.Trace.Debugger
2222
{
2323
internal sealed class LineProbeResolver : ILineProbeResolver
2424
{
25-
private const int MinTrailingSegmentsForFallbackMatch = 4;
25+
private const int MinTrailingSegmentsForFallbackMatch = 2;
2626
private const int MaxSameFileNameExamples = 3;
2727
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor<LineProbeResolver>();
2828
private readonly ImmutableHashSet<string> _thirdPartyDetectionExcludes;
@@ -101,7 +101,6 @@ private static void TrackClosestPathMatch(
101101
bool includeExamplePaths,
102102
ref int sameFileNameMatchCount,
103103
ref int bestMatchingTrailingSegments,
104-
ref int qualifiedFallbackMatchCount,
105104
ref List<string>? sameFileNameMatches)
106105
{
107106
if (result.ExampleSameFileNamePath is null)
@@ -120,8 +119,6 @@ private static void TrackClosestPathMatch(
120119
{
121120
bestMatchingTrailingSegments = result.BestMatchingTrailingSegments;
122121
}
123-
124-
qualifiedFallbackMatchCount += result.QualifiedMatchCount;
125122
}
126123

127124
private static LineProbeResolutionDiagnostics BuildMinimalDiagnostics(string probeFile, int? probeLine, string probeId)
@@ -210,7 +207,6 @@ private bool TryFindAssemblyContainingFile(string probeFilePath, LineProbeDiagno
210207
var symbolicatedAssemblyCount = 0;
211208
var sameFileNameMatchCount = 0;
212209
var bestMatchingTrailingSegments = 0;
213-
var qualifiedFallbackMatchCount = 0;
214210

215211
foreach (var candidateAssembly in AppDomain.CurrentDomain.GetAssemblies())
216212
{
@@ -237,10 +233,9 @@ private bool TryFindAssemblyContainingFile(string probeFilePath, LineProbeDiagno
237233
includeDetailedDiagnostics,
238234
ref sameFileNameMatchCount,
239235
ref bestMatchingTrailingSegments,
240-
ref qualifiedFallbackMatchCount,
241236
ref sameFileNameMatches);
242237

243-
// Only bind when a single global fallback candidate has the best score.
238+
// Only bind when a single global fallback candidate has the strictly best score.
244239
// Assemblies with internally ambiguous fallback matches must still participate in that global tie.
245240
bestFallbackMatchSelection.Track(candidateAssembly, in closestPathMatch);
246241
}
@@ -257,7 +252,7 @@ private bool TryFindAssemblyContainingFile(string probeFilePath, LineProbeDiagno
257252
symbolicatedAssemblyCount,
258253
sameFileNameMatchCount,
259254
bestMatchingTrailingSegments,
260-
qualifiedFallbackMatchCount,
255+
bestFallbackMatchSelection.QualifiedMatchCount,
261256
bestFallbackMatchSelection.HasAmbiguousBestMatch,
262257
includeDetailedDiagnostics ? sameFileNameMatches?.ToArray() ?? [] : []);
263258
return false;
@@ -376,18 +371,22 @@ internal struct BestFallbackMatchSelection
376371
{
377372
private BestFallbackMatch? _bestMatch;
378373
private int _bestMatchingTrailingSegments;
374+
private int _qualifiedMatchCount;
379375

380376
public BestFallbackMatch? BestMatch => HasAmbiguousBestMatch ? null : _bestMatch;
381377

382378
public bool HasAmbiguousBestMatch { get; private set; }
383379

380+
public int QualifiedMatchCount => _qualifiedMatchCount;
381+
384382
public void Track(Assembly assembly, in ClosestPathBySuffixResult result)
385383
{
386384
if (result.QualifiedMatchCount == 0)
387385
{
388386
return;
389387
}
390388

389+
_qualifiedMatchCount += result.QualifiedMatchCount;
391390
var candidateScore = result.BestQualifiedMatchingTrailingSegments;
392391
if (candidateScore > _bestMatchingTrailingSegments)
393392
{

tracer/test/Datadog.Trace.Debugger.IntegrationTests/LineProbeResolverTest.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
44
// </copyright>
55

6+
using System;
67
using System.Linq;
78
using Datadog.Trace.Debugger.Configurations.Models;
89
using Datadog.Trace.Debugger.IntegrationTests.Helpers;
@@ -158,6 +159,19 @@ public void FallbackMatchBindsWhenOnlyLeadingSegmentsDiffer()
158159
loc.Should().NotBeNull();
159160
}
160161

162+
[Fact]
163+
public void FallbackMatchBindsWhenOnlyTwoTrailingSegmentsMatch()
164+
{
165+
_probeDefinition.Where.SourceFile = BuildProbePathWithUnknownPrefix(_probeDefinition.Where.SourceFile, trailingSegmentCount: 2);
166+
167+
var result = _lineProbeResolver.TryResolveLineProbe(_probeDefinition, out var loc);
168+
169+
result.Status.Should().Be(LiveProbeResolveStatus.Bound);
170+
result.Diagnostics.PathMatchType.Should().Be(LineProbePathMatchType.FallbackTrailingSuffixMatch);
171+
result.Diagnostics.MatchingTrailingSegments.Should().Be(2);
172+
loc.Should().NotBeNull();
173+
}
174+
161175
[Fact]
162176
public void FilePathLookupFindsClosestUniqueSuffixMatchForLinuxPaths()
163177
{
@@ -262,6 +276,19 @@ public void BestFallbackMatchSelectionRejectsTieWhenHighestScoreIsAlreadyAmbiguo
262276
selection.BestMatch.Should().BeNull();
263277
}
264278

279+
[Fact]
280+
public void BestFallbackMatchSelectionBindsWhenSingleQualifiedCandidateIsTracked()
281+
{
282+
var selection = new LineProbeResolver.BestFallbackMatchSelection();
283+
284+
selection.Track(typeof(LineProbeResolverTest).Assembly, CreateClosestPathBySuffixResult(@"/b/src/One/Shared/Feature/MyFile.cs", matchingTrailingSegments: 5, isAmbiguous: false));
285+
286+
selection.QualifiedMatchCount.Should().Be(1);
287+
selection.BestMatch.Should().NotBeNull();
288+
selection.BestMatch!.Value.Path.Should().Be(@"/b/src/One/Shared/Feature/MyFile.cs");
289+
selection.BestMatch!.Value.MatchingTrailingSegments.Should().Be(5);
290+
}
291+
265292
[Fact]
266293
public void BestFallbackMatchSelectionAllowsHigherUniqueScoreToOverrideEarlierAmbiguity()
267294
{
@@ -271,11 +298,27 @@ public void BestFallbackMatchSelectionAllowsHigherUniqueScoreToOverrideEarlierAm
271298
selection.Track(typeof(LineProbeResolverTest).Assembly, CreateClosestPathBySuffixResult(@"/b/src/One/Shared/Feature/MyFile.cs", matchingTrailingSegments: 5, isAmbiguous: false));
272299

273300
selection.HasAmbiguousBestMatch.Should().BeFalse();
301+
selection.QualifiedMatchCount.Should().Be(3);
274302
selection.BestMatch.Should().NotBeNull();
275303
selection.BestMatch!.Value.Path.Should().Be(@"/b/src/One/Shared/Feature/MyFile.cs");
276304
selection.BestMatch!.Value.MatchingTrailingSegments.Should().Be(5);
277305
}
278306

307+
[Fact]
308+
public void BestFallbackMatchSelectionBindsHighestScoreOverIncidentalLowScoringCandidates()
309+
{
310+
var selection = new LineProbeResolver.BestFallbackMatchSelection();
311+
312+
selection.Track(typeof(LineProbeResolverTest).Assembly, CreateClosestPathBySuffixResult(@"/a/src/MyProject/Controllers/HomeController.cs", matchingTrailingSegments: 6, isAmbiguous: false));
313+
selection.Track(typeof(string).Assembly, CreateClosestPathBySuffixResult(@"/vendor/Controllers/HomeController.cs", matchingTrailingSegments: 2, isAmbiguous: false));
314+
315+
selection.HasAmbiguousBestMatch.Should().BeFalse();
316+
selection.QualifiedMatchCount.Should().Be(2);
317+
selection.BestMatch.Should().NotBeNull();
318+
selection.BestMatch!.Value.Path.Should().Be(@"/a/src/MyProject/Controllers/HomeController.cs");
319+
selection.BestMatch!.Value.MatchingTrailingSegments.Should().Be(6);
320+
}
321+
279322
[Fact]
280323
public void OutOfBoundsLineProbeReturnsAnError()
281324
{
@@ -433,6 +476,17 @@ public void MinimalDiagnosticsOnUnboundResolutionKeepReasonButSkipDetailedFields
433476
loc.Should().BeNull();
434477
}
435478

479+
private static string BuildProbePathWithUnknownPrefix(string sourceFile, int trailingSegmentCount)
480+
{
481+
var segments = sourceFile.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
482+
if (segments.Length < trailingSegmentCount)
483+
{
484+
throw new InvalidOperationException($"Expected at least {trailingSegmentCount} path segments.");
485+
}
486+
487+
return $@"UnknownPrefix\{string.Join(@"\", segments.Skip(segments.Length - trailingSegmentCount))}";
488+
}
489+
436490
private static LineProbeResolver.ClosestPathBySuffixResult CreateClosestPathBySuffixResult(string path, int matchingTrailingSegments, bool isAmbiguous)
437491
{
438492
return new LineProbeResolver.ClosestPathBySuffixResult(

0 commit comments

Comments
 (0)