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) {