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; } } }