diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionCompileHelper.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionCompileHelper.cs
index 2f7e6f1315..69fbf0f929 100644
--- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionCompileHelper.cs
+++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionCompileHelper.cs
@@ -44,6 +44,22 @@ public static Delegate CompileLambda(LambdaExpression lambda)
return lambda.Compile();
}
+ ///
+ /// Compiles a typed lambda while preserving the delegate type at the call site.
+ /// This allows callers to use the shared interpretation-aware compile path
+ /// without repeating delegate casts in each LINQ helper.
+ ///
+ public static TDelegate CompileLambda(Expression lambda)
+ where TDelegate : Delegate
+ {
+ if (lambda == null)
+ {
+ throw new ArgumentNullException(nameof(lambda));
+ }
+
+ return (TDelegate)(object)ExpressionCompileHelper.CompileLambda((LambdaExpression)lambda);
+ }
+
private static Func CreateInterpretedCompile()
{
MethodInfo compileWithPreference = typeof(LambdaExpression)
diff --git a/Microsoft.Azure.Cosmos/src/Linq/GeometrySqlExpressionFactory.cs b/Microsoft.Azure.Cosmos/src/Linq/GeometrySqlExpressionFactory.cs
index 397eb72a0a..0acbdea7d3 100644
--- a/Microsoft.Azure.Cosmos/src/Linq/GeometrySqlExpressionFactory.cs
+++ b/Microsoft.Azure.Cosmos/src/Linq/GeometrySqlExpressionFactory.cs
@@ -43,7 +43,7 @@ public static SqlScalarExpression Construct(Expression geometryExpression)
try
{
Expression> le = Expression.Lambda>(geometryExpression);
- Func compiledExpression = (Func)ExpressionCompileHelper.CompileLambda(le);
+ Func compiledExpression = ExpressionCompileHelper.CompileLambda(le);
geometry = compiledExpression();
}
catch (Exception ex)
diff --git a/Microsoft.Azure.Cosmos/src/Linq/Utilities.cs b/Microsoft.Azure.Cosmos/src/Linq/Utilities.cs
index c4142e1b5a..ee134380cf 100644
--- a/Microsoft.Azure.Cosmos/src/Linq/Utilities.cs
+++ b/Microsoft.Azure.Cosmos/src/Linq/Utilities.cs
@@ -102,7 +102,7 @@ public override object EvalBoxed(Expression expr)
public T Eval(Expression expr)
{
Expression> lambda = Expression.Lambda>(expr);
- Func func = (Func)ExpressionCompileHelper.CompileLambda(lambda);
+ Func func = ExpressionCompileHelper.CompileLambda(lambda);
return func();
}
}
diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Linq/SubtreeEvaluatorBenchmark.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Linq/SubtreeEvaluatorBenchmark.cs
index c3f51fc2e0..4468dd5fdd 100644
--- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Linq/SubtreeEvaluatorBenchmark.cs
+++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Linq/SubtreeEvaluatorBenchmark.cs
@@ -6,15 +6,33 @@ namespace Microsoft.Azure.Cosmos.Linq
{
using System;
using System.Collections.Generic;
+ using System.Diagnostics;
using System.Linq.Expressions;
using BenchmarkDotNet.Attributes;
///
/// Benchmark measuring SubtreeEvaluator constant evaluation performance.
- /// Validates fix for GitHub Issue #5487: Unbounded JIT/IL growth from Expression.Compile()
+ /// Validates fix for GitHub Issue #5487: Unbounded JIT/IL growth from Expression.Compile().
+ ///
+ /// CompileBaseline uses the original Expression.Compile() path which emits DynamicMethod IL
+ /// into native memory on every call – this memory is never reclaimed, causing an unbounded leak.
+ /// EvaluateWithFix uses the full SubtreeEvaluator code path with Compile(preferInterpretation: true),
+ /// which avoids IL emission entirely.
+ ///
+ /// Each benchmark method runs an inner loop of OperationsPerInvoke iterations so that
+ /// per-invocation time exceeds BenchmarkDotNet's 100 ms floor and the native-memory leak
+ /// in the baseline accumulates enough to be clearly visible in the GlobalCleanup output.
+ ///
+ /// [MemoryDiagnoser] reports managed GC allocations per operation. The native-memory leak
+ /// (the real issue) is not visible through MemoryDiagnoser but is observable via process private
+ /// bytes – the GlobalCleanup prints that value for manual inspection.
///
+ [MemoryDiagnoser]
public class SubtreeEvaluatorBenchmark
{
+ private const int OperationsPerInvoke = 2000;
+
+ private long privateMemoryBeforeBytes;
private Expression expression;
private LambdaExpression lambda;
private SubtreeEvaluator evaluator;
@@ -27,21 +45,54 @@ public void Setup()
this.expression = expr.Body;
this.lambda = Expression.Lambda(this.expression);
this.evaluator = new SubtreeEvaluator(new HashSet { this.expression });
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ using Process process = Process.GetCurrentProcess();
+ process.Refresh();
+ this.privateMemoryBeforeBytes = process.PrivateMemorySize64;
}
- [Benchmark(Baseline = true)]
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ using Process process = Process.GetCurrentProcess();
+ process.Refresh();
+ long delta = process.PrivateMemorySize64 - this.privateMemoryBeforeBytes;
+ Console.WriteLine($"[NativeMemory] Private bytes delta: {delta:N0} bytes");
+ }
+
+ [Benchmark(Baseline = true, OperationsPerInvoke = OperationsPerInvoke)]
public object CompileBaseline()
{
- // Baseline: duplicates old code path that emits DynamicMethod with IL per call
- Delegate function = this.lambda.Compile();
- return function.DynamicInvoke(null);
+ // Baseline: duplicates the original code path that emits DynamicMethod IL per call.
+ object result = null;
+ for (int i = 0; i < OperationsPerInvoke; i++)
+ {
+ Delegate function = this.lambda.Compile();
+ result = function.DynamicInvoke(null);
+ }
+
+ return result;
}
- [Benchmark]
+ [Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
public Expression EvaluateWithFix()
{
- // Measures actual SubtreeEvaluator code path with the preferInterpretation fix
- return this.evaluator.Evaluate(this.expression);
+ // Measures the full SubtreeEvaluator code path with the interpretation-based fix.
+ Expression result = null;
+ for (int i = 0; i < OperationsPerInvoke; i++)
+ {
+ result = this.evaluator.Evaluate(this.expression);
+ }
+
+ return result;
}
}
}