diff --git a/tracer/src/Datadog.Trace/Debugger/Expressions/ExpressionCacheKey.cs b/tracer/src/Datadog.Trace/Debugger/Expressions/ExpressionCacheKey.cs
new file mode 100644
index 000000000000..0cf028ac4c31
--- /dev/null
+++ b/tracer/src/Datadog.Trace/Debugger/Expressions/ExpressionCacheKey.cs
@@ -0,0 +1,126 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#nullable enable
+using System;
+
+namespace Datadog.Trace.Debugger.Expressions;
+
+///
+/// Cache key for compiled expressions. Includes thisType, returnType, and all member RUNTIME types
+/// to ensure we recompile when any type changes (polymorphic calls).
+///
+internal readonly struct ExpressionCacheKey : IEquatable
+{
+ private readonly int _hashCode;
+ private readonly Type?[]? _memberRuntimeTypes;
+
+ public ExpressionCacheKey(Type thisType, Type? returnType, ScopeMember[] members)
+ {
+ ThisType = thisType;
+ ReturnType = returnType;
+
+ // Capture RUNTIME types of members (Value.GetType()), not declared types
+ // This is critical for polymorphic scenarios where declared type is Object/base
+ // but actual values are different concrete types
+ var hash = new HashCode();
+ hash.Add(thisType);
+ hash.Add(returnType);
+
+ if (members != null && members.Length > 0)
+ {
+ var types = new Type?[members.Length];
+ var count = 0;
+ foreach (var member in members)
+ {
+ if (member.Type == null)
+ {
+ break; // End of valid members
+ }
+
+ // Use runtime type if value exists, else declared type
+ var runtimeType = member.Value?.GetType() ?? member.Type;
+ types[count++] = runtimeType;
+ hash.Add(runtimeType);
+ }
+
+ // Trim array to actual count
+ if (count > 0)
+ {
+ if (count < types.Length)
+ {
+ Array.Resize(ref types, count);
+ }
+
+ _memberRuntimeTypes = types;
+ }
+ else
+ {
+ _memberRuntimeTypes = null;
+ }
+ }
+ else
+ {
+ _memberRuntimeTypes = null;
+ }
+
+ _hashCode = hash.ToHashCode();
+ }
+
+ public Type ThisType { get; }
+
+ public Type? ReturnType { get; }
+
+ public override int GetHashCode() => _hashCode;
+
+ public override bool Equals(object? obj) => obj is ExpressionCacheKey other && Equals(other);
+
+ public bool Equals(ExpressionCacheKey other)
+ {
+ // Fast path: hash mismatch means definitely not equal
+ if (_hashCode != other._hashCode)
+ {
+ return false;
+ }
+
+ // Must have same ThisType
+ if (ThisType != other.ThisType)
+ {
+ return false;
+ }
+
+ // Must have same ReturnType
+ if (ReturnType != other.ReturnType)
+ {
+ return false;
+ }
+
+ // Must have same member types (compare actual types, not just hash)
+ if (_memberRuntimeTypes == null && other._memberRuntimeTypes == null)
+ {
+ return true;
+ }
+
+ if (_memberRuntimeTypes == null || other._memberRuntimeTypes == null)
+ {
+ return false;
+ }
+
+ if (_memberRuntimeTypes.Length != other._memberRuntimeTypes.Length)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < _memberRuntimeTypes.Length; i++)
+ {
+ if (_memberRuntimeTypes[i] != other._memberRuntimeTypes[i])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionEvaluator.cs b/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionEvaluator.cs
index df4129d6f36a..a48567f2b173 100644
--- a/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionEvaluator.cs
+++ b/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionEvaluator.cs
@@ -1,13 +1,13 @@
-//
+//
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
//
#nullable enable
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq.Expressions;
-using System.Threading;
using Datadog.Trace.Debugger.Configurations.Models;
using Datadog.Trace.Debugger.Models;
using Datadog.Trace.Logging;
@@ -20,15 +20,14 @@ internal sealed class ProbeExpressionEvaluator
{
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(ProbeExpressionEvaluator));
- private Lazy[]?>? _compiledTemplates;
-
- private Lazy?>? _compiledCondition;
-
- private Lazy?>? _compiledMetric;
-
- private Lazy, KeyValuePair[]>[]>[]?>? _compiledDecorations;
-
- private int _expressionsCompiled;
+ // Per-type caching using ConcurrentDictionary.
+ // Cache key includes thisType AND member types to handle polymorphic args/locals.
+ // When same method is called with different arg types (e.g., Stream declared but sometimes LimitedInputStream, sometimes MemoryStream),
+ // each combination gets its own compiled expression.
+ private readonly ConcurrentDictionary[]?> _compiledTemplatesByType = new();
+ private readonly ConcurrentDictionary?> _compiledConditionByType = new();
+ private readonly ConcurrentDictionary?> _compiledMetricByType = new();
+ private readonly ConcurrentDictionary, KeyValuePair[]>[]>[]?> _compiledDecorationsByType = new();
internal ProbeExpressionEvaluator(
DebuggerExpression?[]? templates,
@@ -43,24 +42,34 @@ internal ProbeExpressionEvaluator(
}
///
- /// Gets CompiledTemplates, for use in "DebuggerExpressionLanguageTests"
+ /// Gets CompiledTemplates for the first cached type, for use in "DebuggerExpressionLanguageTests"
///
internal CompiledExpression[]? CompiledTemplates
{
get
{
- return _compiledTemplates?.Value;
+ foreach (var kvp in _compiledTemplatesByType)
+ {
+ return kvp.Value;
+ }
+
+ return null;
}
}
///
- /// Gets CompiledCondition, for use in "DebuggerExpressionLanguageTests"
+ /// Gets CompiledCondition for the first cached type, for use in "DebuggerExpressionLanguageTests"
///
internal CompiledExpression? CompiledCondition
{
get
{
- return _compiledCondition?.Value;
+ foreach (var kvp in _compiledConditionByType)
+ {
+ return kvp.Value;
+ }
+
+ return null;
}
}
@@ -68,7 +77,12 @@ internal CompiledExpression? CompiledMetric
{
get
{
- return _compiledMetric?.Value;
+ foreach (var kvp in _compiledMetricByType)
+ {
+ return kvp.Value;
+ }
+
+ return null;
}
}
@@ -76,7 +90,12 @@ internal CompiledExpression? CompiledMetric
{
get
{
- return _compiledDecorations?.Value;
+ foreach (var kvp in _compiledDecorationsByType)
+ {
+ return kvp.Value;
+ }
+
+ return null;
}
}
@@ -90,31 +109,63 @@ internal CompiledExpression? CompiledMetric
internal ExpressionEvaluationResult Evaluate(MethodScopeMembers scopeMembers)
{
- if (Interlocked.CompareExchange(ref _expressionsCompiled, 1, 0) == 0)
- {
- Interlocked.CompareExchange(ref _compiledTemplates, new Lazy[]?>(() => CompileTemplates(scopeMembers)), null);
- Interlocked.CompareExchange(ref _compiledCondition, new Lazy?>(() => CompileCondition(scopeMembers)), null);
- Interlocked.CompareExchange(ref _compiledMetric, new Lazy?>(() => CompileMetric(scopeMembers)), null);
- Interlocked.CompareExchange(ref _compiledDecorations, new Lazy, KeyValuePair[]>[]>[]?>(() => CompileDecorations(scopeMembers)), null);
- }
+ // Use the RUNTIME type (Value.GetType()) for per-type caching, not the declared type (Type).
+ // The declared type can differ from runtime type when:
+ // - Method is on a base class/interface but called on derived type
+ // - Parameter is declared as base type but actual value is derived
+ // Without this, expressions compiled for declared type would fail to cast actual values.
+ var invocationTarget = scopeMembers.InvocationTarget;
+
+ // CRITICAL: Always use Value.GetType() for runtime type. If Value is null,
+ // we use the declared Type, but this can cause cache collisions when different
+ // runtime types share the same declared type (e.g., all inherit from Object).
+ var thisRuntimeType = invocationTarget.Value?.GetType();
+ var thisDeclaredType = invocationTarget.Type;
+ var thisType = thisRuntimeType ?? thisDeclaredType ?? typeof(object);
+
+ // Also get return runtime type - critical for methods returning different types
+ var returnValue = scopeMembers.Return;
+ var returnRuntimeType = returnValue.Value?.GetType() ?? returnValue.Type;
+
+ // Cache key includes thisType, returnType, AND member RUNTIME types to handle polymorphic scenarios.
+ // This ensures we recompile when any type changes between invocations.
+ var cacheKey = new ExpressionCacheKey(thisType, returnRuntimeType, scopeMembers.Members);
ExpressionEvaluationResult result = default;
- EvaluateTemplates(ref result, scopeMembers);
- EvaluateCondition(ref result, scopeMembers);
- EvaluateMetric(ref result, scopeMembers);
- EvaluateSpanDecorations(ref result, scopeMembers);
+ EvaluateTemplates(ref result, scopeMembers, cacheKey);
+ EvaluateCondition(ref result, scopeMembers, cacheKey);
+ EvaluateMetric(ref result, scopeMembers, cacheKey);
+ EvaluateSpanDecorations(ref result, scopeMembers, cacheKey);
return result;
}
- private void EvaluateTemplates(ref ExpressionEvaluationResult result, MethodScopeMembers scopeMembers)
+ private void EvaluateTemplates(ref ExpressionEvaluationResult result, MethodScopeMembers scopeMembers, ExpressionCacheKey cacheKey)
{
var resultBuilder = StringBuilderCache.Acquire();
try
{
- EnsureNotNull(_compiledTemplates);
+ if (Templates == null || cacheKey.ThisType == null)
+ {
+ return;
+ }
- var compiledExpressions = _compiledTemplates?.Value;
- if (compiledExpressions == null || Templates == null)
+ // Get or create compiled expressions for this specific type combination
+ if (!_compiledTemplatesByType.TryGetValue(cacheKey, out var compiledExpressions))
+ {
+ if (Log.IsEnabled(LogEventLevel.Debug))
+ {
+ Log.Debug(
+ "Template cache MISS. Compiling for ThisType={ThisType}, Hash={Hash}, CacheSize={CacheSize}",
+ property0: cacheKey.ThisType.FullName,
+ property1: cacheKey.GetHashCode(),
+ property2: _compiledConditionByType.Count);
+ }
+
+ compiledExpressions = CompileTemplates(scopeMembers, cacheKey.ThisType);
+ _compiledTemplatesByType.TryAdd(cacheKey, compiledExpressions);
+ }
+
+ if (compiledExpressions == null)
{
return;
}
@@ -165,19 +216,40 @@ private void EvaluateTemplates(ref ExpressionEvaluationResult result, MethodScop
}
}
- private void EvaluateCondition(ref ExpressionEvaluationResult result, MethodScopeMembers scopeMembers)
+ private void EvaluateCondition(ref ExpressionEvaluationResult result, MethodScopeMembers scopeMembers, ExpressionCacheKey cacheKey)
{
+ if (Condition == null || cacheKey.ThisType == null)
+ {
+ return;
+ }
+
CompiledExpression compiledExpression = default;
+
try
{
- EnsureNotNull(_compiledCondition);
+ // Get or create compiled condition for this specific type combination.
+ if (!_compiledConditionByType.TryGetValue(cacheKey, out var cached))
+ {
+ if (Log.IsEnabled(LogEventLevel.Debug))
+ {
+ Log.Debug(
+ "Condition cache MISS. Compiling for ThisType={ThisType}, Hash={Hash}, CacheSize={CacheSize}",
+ property0: cacheKey.ThisType.FullName,
+ property1: cacheKey.GetHashCode(),
+ property2: _compiledConditionByType.Count);
+ }
+
+ var compiled = CompileCondition(scopeMembers, cacheKey.ThisType);
+ _compiledConditionByType.TryAdd(cacheKey, compiled);
+ cached = compiled;
+ }
- if (_compiledCondition?.Value == null)
+ if (!cached.HasValue)
{
return;
}
- compiledExpression = _compiledCondition.Value.Value;
+ compiledExpression = cached.Value;
var condition = compiledExpression.Delegate(scopeMembers.InvocationTarget, scopeMembers.Return, scopeMembers.Duration, scopeMembers.Exception, scopeMembers.Members);
result.Condition = condition;
if (compiledExpression.Errors != null)
@@ -192,19 +264,39 @@ private void EvaluateCondition(ref ExpressionEvaluationResult result, MethodScop
}
}
- private void EvaluateMetric(ref ExpressionEvaluationResult result, MethodScopeMembers scopeMembers)
+ private void EvaluateMetric(ref ExpressionEvaluationResult result, MethodScopeMembers scopeMembers, ExpressionCacheKey cacheKey)
{
+ if (Metric == null || cacheKey.ThisType == null)
+ {
+ return;
+ }
+
CompiledExpression compiledExpression = default;
try
{
- EnsureNotNull(_compiledMetric);
+ // Get or create compiled metric for this specific type combination
+ if (!_compiledMetricByType.TryGetValue(cacheKey, out var cached))
+ {
+ if (Log.IsEnabled(LogEventLevel.Debug))
+ {
+ Log.Debug(
+ "Metric cache MISS. Compiling for ThisType={ThisType}, Hash={Hash}, CacheSize={CacheSize}",
+ property0: cacheKey.ThisType.FullName,
+ property1: cacheKey.GetHashCode(),
+ property2: _compiledConditionByType.Count);
+ }
+
+ var compiled = CompileMetric(scopeMembers, cacheKey.ThisType);
+ _compiledMetricByType.TryAdd(cacheKey, compiled);
+ cached = compiled;
+ }
- if (_compiledMetric?.Value == null)
+ if (!cached.HasValue)
{
return;
}
- compiledExpression = _compiledMetric.Value.Value;
+ compiledExpression = cached.Value;
var metric = compiledExpression.Delegate(scopeMembers.InvocationTarget, scopeMembers.Return, scopeMembers.Duration, scopeMembers.Exception, scopeMembers.Members);
result.Metric = metric;
if (compiledExpression.Errors != null)
@@ -218,17 +310,26 @@ private void EvaluateMetric(ref ExpressionEvaluationResult result, MethodScopeMe
}
}
- private void EvaluateSpanDecorations(ref ExpressionEvaluationResult result, MethodScopeMembers scopeMembers)
+ private void EvaluateSpanDecorations(ref ExpressionEvaluationResult result, MethodScopeMembers scopeMembers, ExpressionCacheKey cacheKey)
{
+ if (SpanDecorations == null || cacheKey.ThisType == null)
+ {
+ return;
+ }
+
var decorations = new List();
try
{
- EnsureNotNull(_compiledDecorations);
+ // Get or create compiled decorations for this specific type combination
+ if (!_compiledDecorationsByType.TryGetValue(cacheKey, out var compiledDecorations))
+ {
+ compiledDecorations = CompileDecorations(scopeMembers, cacheKey.ThisType);
+ _compiledDecorationsByType.TryAdd(cacheKey, compiledDecorations);
+ }
- var compiledDecorations = _compiledDecorations?.Value;
if (compiledDecorations == null)
{
- Log.Debug($"{nameof(ProbeExpressionEvaluator)}.{nameof(EvaluateSpanDecorations)}: {nameof(_compiledDecorations.Value)} is null");
+ Log.Debug($"{nameof(ProbeExpressionEvaluator)}.{nameof(EvaluateSpanDecorations)}: compiled decorations is null");
return;
}
@@ -342,7 +443,7 @@ private void EvaluateSpanDecorations(ref ExpressionEvaluationResult result, Meth
}
}
- private CompiledExpression[]? CompileTemplates(MethodScopeMembers scopeMembers)
+ private CompiledExpression[]? CompileTemplates(MethodScopeMembers scopeMembers, Type thisType)
{
if (Templates == null)
{
@@ -371,7 +472,7 @@ private void EvaluateSpanDecorations(ref ExpressionEvaluationResult result, Meth
return compiledExpressions;
}
- private CompiledExpression? CompileCondition(MethodScopeMembers scopeMembers)
+ private CompiledExpression? CompileCondition(MethodScopeMembers scopeMembers, Type thisType)
{
if (!Condition.HasValue)
{
@@ -381,7 +482,7 @@ private void EvaluateSpanDecorations(ref ExpressionEvaluationResult result, Meth
return ProbeExpressionParser.ParseExpression(Condition.Value.Json, scopeMembers);
}
- private CompiledExpression? CompileMetric(MethodScopeMembers scopeMembers)
+ private CompiledExpression? CompileMetric(MethodScopeMembers scopeMembers, Type thisType)
{
if (!Metric.HasValue)
{
@@ -391,7 +492,7 @@ private void EvaluateSpanDecorations(ref ExpressionEvaluationResult result, Meth
return ProbeExpressionParser.ParseExpression(Metric?.Json, scopeMembers);
}
- private KeyValuePair, KeyValuePair[]>[]>[]? CompileDecorations(MethodScopeMembers scopeMembers)
+ private KeyValuePair, KeyValuePair[]>[]>[]? CompileDecorations(MethodScopeMembers scopeMembers, Type thisType)
{
if (SpanDecorations == null)
{
@@ -460,21 +561,6 @@ private void EvaluateSpanDecorations(ref ExpressionEvaluationResult result, Meth
return compiledExpressions;
}
- private void EnsureNotNull(T? value)
- where T : class
- {
- if (value != null)
- {
- return;
- }
-
- var sw = new SpinWait();
- while (Volatile.Read(ref value) == null)
- {
- sw.SpinOnce();
- }
- }
-
private bool? IsLiteral(DebuggerExpression? expression)
{
if (expression is null)
@@ -507,7 +593,6 @@ private bool IsExpression(CompiledExpression expression)
private void HandleException(ref ExpressionEvaluationResult result, CompiledExpression compiledExpression, Exception e)
{
- Log.Information(e, "Failed to parse probe expression: {Expression}", compiledExpression.RawExpression);
result.Errors ??= new List();
if (compiledExpression.Errors != null)
{