Skip to content
Closed
Show file tree
Hide file tree
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
16 changes: 16 additions & 0 deletions Microsoft.Azure.Cosmos/src/Linq/ExpressionCompileHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ public static Delegate CompileLambda(LambdaExpression lambda)
return lambda.Compile();
}

/// <summary>
/// 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.
/// </summary>
public static TDelegate CompileLambda<TDelegate>(Expression<TDelegate> lambda)
where TDelegate : Delegate
{
if (lambda == null)
{
throw new ArgumentNullException(nameof(lambda));
}

return (TDelegate)(object)ExpressionCompileHelper.CompileLambda((LambdaExpression)lambda);
}

private static Func<LambdaExpression, Delegate> CreateInterpretedCompile()
{
MethodInfo compileWithPreference = typeof(LambdaExpression)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static SqlScalarExpression Construct(Expression geometryExpression)
try
{
Expression<Func<Geometry>> le = Expression.Lambda<Func<Geometry>>(geometryExpression);
Func<Geometry> compiledExpression = (Func<Geometry>)ExpressionCompileHelper.CompileLambda(le);
Func<Geometry> compiledExpression = ExpressionCompileHelper.CompileLambda(le);
geometry = compiledExpression();
}
catch (Exception ex)
Expand Down
2 changes: 1 addition & 1 deletion Microsoft.Azure.Cosmos/src/Linq/Utilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public override object EvalBoxed(Expression expr)
public T Eval(Expression expr)
{
Expression<Func<T>> lambda = Expression.Lambda<Func<T>>(expr);
Func<T> func = (Func<T>)ExpressionCompileHelper.CompileLambda(lambda);
Func<T> func = ExpressionCompileHelper.CompileLambda(lambda);
return func();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// 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.
/// </summary>
[MemoryDiagnoser]
public class SubtreeEvaluatorBenchmark
{
private const int OperationsPerInvoke = 2000;

private long privateMemoryBeforeBytes;
private Expression expression;
private LambdaExpression lambda;
private SubtreeEvaluator evaluator;
Expand All @@ -27,21 +45,54 @@ public void Setup()
this.expression = expr.Body;
this.lambda = Expression.Lambda(this.expression);
this.evaluator = new SubtreeEvaluator(new HashSet<Expression> { 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;
}
}
}
Loading