Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#nullable enable

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Reflection;
using Datadog.Trace.Debugger.Caching;
Expand All @@ -23,6 +24,7 @@ internal class SpanCodeOrigin
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(SpanCodeOrigin));

private readonly ConcurrentAdaptiveCache<Assembly, AssemblyPdbInfo?> _assemblyPdbCache = new();
private readonly ConcurrentDictionary<Assembly, bool> _assemblySkipCache = new();
private readonly CodeOriginTags _tags;

internal SpanCodeOrigin(DebuggerSettings settings)
Expand Down Expand Up @@ -101,12 +103,14 @@ private void AddEntrySpanTags(Span span, Type type, MethodInfo method)
}

var assembly = type.Assembly;
if (AssemblyFilter.ShouldSkipAssembly(assembly, Settings.SymDbThirdPartyDetectionExcludes, Settings.SymDbThirdPartyDetectionIncludes))
if (ShouldSkipAssembly(assembly))
{
// use cache when this will be merged: https://github.com/DataDog/dd-trace-dotnet/pull/6093
return;
}

// Add code origin tags to entry span
// Adds 4 tags always (type, index, method, typename) + 3 tags if PDB available (file, line, column)
// Size: ~210-300 bytes without PDB, ~250-500 bytes with PDB
span.Tags.SetTag(_tags.Type, "entry");
span.Tags.SetTag(_tags.Index[0], "0");
span.Tags.SetTag(_tags.Method[0], methodName);
Expand All @@ -128,6 +132,21 @@ private void AddEntrySpanTags(Span span, Type type, MethodInfo method)

private DatadogMetadataReader.DatadogSequencePoint? GetPdbInfo(Assembly assembly, MethodInfo method)
{
// Design Decision: Read ALL endpoint sequence points upfront per assembly
//
// Current approach: Opens PDB once, reads all endpoint sequence points (~50-200 methods),
// closes immediately. One-time cost per assembly, then instant cache hits.
//
// Alternatives considered:
// - Lazy loading: Would reopen PDB repeatedly (expensive I/O, unpredictable latency spikes)
// - Keep PDB open: File handle leaks, resource limits, complex lifecycle management
// - Background/async: Race conditions, thundering herd, testing complexity
//
// Trade-off: Slightly higher first-request latency for simplicity, predictability, and no resource leaks.
// Memory cost is negligible: 50-200 endpoints × ~150 bytes = 7.5-30 KB per assembly.
//
// Note: Will revisit if profiling shows significant performance impact.

var pdbInfo = _assemblyPdbCache.GetOrAdd(
assembly,
asm =>
Expand All @@ -142,7 +161,8 @@ private void AddEntrySpanTags(Span span, Type type, MethodInfo method)
{
var endpointMethodTokens = EndpointDetector.GetEndpointMethodTokens(reader);

// Build dictionary of sequence points only for endpoint methods
// Build dictionary of sequence points for ALL detected endpoint methods in one pass
// This avoids reopening the PDB file on subsequent endpoint calls
var builder = ImmutableDictionary.CreateBuilder<int, DatadogMetadataReader.DatadogSequencePoint?>();

foreach (var token in endpointMethodTokens)
Expand All @@ -155,6 +175,8 @@ private void AddEntrySpanTags(Span span, Type type, MethodInfo method)
catch (Exception ex)
{
Log.Error(ex, "Failed to get sequence point for method token {Token} in assembly {AssemblyName}", property0: token, asm.FullName);
// Add null to dictionary to avoid retrying on every call
builder.Add(token, null);
}
}

Expand Down Expand Up @@ -247,9 +269,8 @@ private int PopulateUserFrames(FrameInfo[] frames)
continue;
}

if (AssemblyFilter.ShouldSkipAssembly(assembly, Settings.ThirdPartyDetectionExcludes, Settings.ThirdPartyDetectionIncludes))
if (ShouldSkipAssembly(assembly))
{
// use cache when this will be merged: https://github.com/DataDog/dd-trace-dotnet/pull/6093
continue;
}

Expand All @@ -259,6 +280,16 @@ private int PopulateUserFrames(FrameInfo[] frames)
return count;
}

private bool ShouldSkipAssembly(Assembly assembly)
{
return _assemblySkipCache.GetOrAdd(
assembly,
asm => AssemblyFilter.ShouldSkipAssembly(
asm,
Settings.ThirdPartyDetectionExcludes,
Settings.ThirdPartyDetectionIncludes));
}

private readonly record struct FrameInfo(int FrameIndex, StackFrame Frame);

private sealed class AssemblyPdbInfo(ImmutableDictionary<int, DatadogMetadataReader.DatadogSequencePoint?> sequencePoints)
Expand All @@ -267,7 +298,7 @@ private sealed class AssemblyPdbInfo(ImmutableDictionary<int, DatadogMetadataRea
}

/// <summary>
/// avoid string concatenations and reduce GC pressure in hot path
/// Avoid string concatenations and reduce GC pressure in hot path
/// </summary>
internal class CodeOriginTags
{
Expand Down
Loading