diff --git a/Microsoft.Azure.Cosmos/src/Linq/DocumentQueryEvaluator.cs b/Microsoft.Azure.Cosmos/src/Linq/DocumentQueryEvaluator.cs index dec354bd19..73632a2edd 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/DocumentQueryEvaluator.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/DocumentQueryEvaluator.cs @@ -109,7 +109,7 @@ private static LinqQueryOperation HandleAsSqlTransformExpression(MethodCallExpre { LambdaExpression lambdaExpression = (LambdaExpression)paramExpression; // Send the lambda expression through the partial evaluator. - return GetSqlQuerySpec(lambdaExpression.Compile().DynamicInvoke(null)); + return GetSqlQuerySpec(lambdaExpression.Compile(preferInterpretation: true).DynamicInvoke(null)); } else if (paramExpression.NodeType == ExpressionType.Constant) { @@ -119,7 +119,7 @@ private static LinqQueryOperation HandleAsSqlTransformExpression(MethodCallExpre else { LambdaExpression lamdaExpression = Expression.Lambda(paramExpression); - return GetSqlQuerySpec(lamdaExpression.Compile().DynamicInvoke(null)); + return GetSqlQuerySpec(lamdaExpression.Compile(preferInterpretation: true).DynamicInvoke(null)); } } diff --git a/Microsoft.Azure.Cosmos/src/Linq/GeometrySqlExpressionFactory.cs b/Microsoft.Azure.Cosmos/src/Linq/GeometrySqlExpressionFactory.cs index 610c3f1b5f..ecaeef42ff 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 = le.Compile(); + Func compiledExpression = le.Compile(preferInterpretation: true); geometry = compiledExpression(); } catch (Exception ex) diff --git a/Microsoft.Azure.Cosmos/src/Linq/SubtreeEvaluator.cs b/Microsoft.Azure.Cosmos/src/Linq/SubtreeEvaluator.cs index d4b86b7683..4d67fbd67d 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/SubtreeEvaluator.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/SubtreeEvaluator.cs @@ -117,7 +117,7 @@ private Expression EvaluateConstant(Expression expression) } LambdaExpression lambda = Expression.Lambda(expression); - Delegate function = lambda.Compile(); + Delegate function = lambda.Compile(preferInterpretation: true); return Expression.Constant(function.DynamicInvoke(null), expression.Type); } diff --git a/Microsoft.Azure.Cosmos/src/Linq/Utilities.cs b/Microsoft.Azure.Cosmos/src/Linq/Utilities.cs index ca39671f74..4b53ae4682 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 = lambda.Compile(); + Func func = lambda.Compile(preferInterpretation: true); 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 new file mode 100644 index 0000000000..fa67ff1ba5 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Linq/SubtreeEvaluatorBenchmark.cs @@ -0,0 +1,51 @@ +// ---------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// ---------------------------------------------------------------- + +namespace Microsoft.Azure.Cosmos.Linq +{ + using System; + using System.Linq.Expressions; + using BenchmarkDotNet.Attributes; + using BenchmarkDotNet.Diagnostics.Windows.Configs; + + /// + /// Benchmark comparing Expression.Compile() vs Compile(preferInterpretation: true) + /// in the context of CosmosDB LINQ-to-SQL query generation. + /// Validates fix for GitHub Issue #5487: Unbounded JIT/IL growth from Expression.Compile(). + /// + /// [MemoryDiagnoser] reports managed GC allocations (Gen0/Gen1/Allocated columns). + /// [NativeMemoryProfiler] uses ETW to track native memory allocations and leaks per method, + /// adding "Allocated native memory" and "Native memory leak" columns to the results table. + /// Note: NativeMemoryProfiler requires Windows and elevated (admin) privileges. + /// + [ShortRunJob] + [MemoryDiagnoser] + // [NativeMemoryProfiler] // Enable this line to include native memory profiling, requires Windows and admin privileges. + public class SubtreeEvaluatorBenchmark + { + private LambdaExpression lambda; + + [GlobalSetup] + public void Setup() + { + string status = "active"; + Expression> expr = () => status == "active"; + this.lambda = Expression.Lambda(expr.Body); + } + + [Benchmark(Baseline = true)] + public object Compile() + { + Delegate fn = this.lambda.Compile(); + return fn.DynamicInvoke(null); + } + + [Benchmark] + public object CompileWithInterpretation() + { + Delegate fn = this.lambda.Compile(preferInterpretation: true); + return fn.DynamicInvoke(null); + } + } +} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Microsoft.Azure.Cosmos.Performance.Tests.csproj b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Microsoft.Azure.Cosmos.Performance.Tests.csproj index 8ef2a239f5..ff80e2ffb5 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Microsoft.Azure.Cosmos.Performance.Tests.csproj +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Microsoft.Azure.Cosmos.Performance.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Linq/LinqCompileGuardTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Linq/LinqCompileGuardTests.cs new file mode 100644 index 0000000000..6e7393d20b --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Linq/LinqCompileGuardTests.cs @@ -0,0 +1,75 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos.Linq +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text.RegularExpressions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// Guards against introducing bare Expression.Compile() calls in the LINQ provider. + /// All Compile() calls must use Compile(preferInterpretation: true) to avoid native + /// memory leaks from DynamicMethod IL emission. + /// See: https://github.com/Azure/azure-cosmos-dotnet-v3/issues/5487 + /// + [TestClass] + public class LinqCompileGuardTests + { + // Matches .Compile() with no arguments — the problematic pattern + private static readonly Regex BareCompilePattern = new Regex( + @"\.Compile\(\s*\)", + RegexOptions.Compiled); + + // Matches .Compile(preferInterpretation: true) — the correct pattern + private static readonly Regex InterpretedCompilePattern = new Regex( + @"\.Compile\(\s*preferInterpretation\s*:\s*true\s*\)", + RegexOptions.Compiled); + + [TestMethod] + public void LinqSourceFiles_ShouldNotUseBareCompile() + { + string linqDirectory = Path.GetFullPath( + Path.Combine( + Directory.GetCurrentDirectory(), + "..", "..", "..", "..", "..", + "src", "Linq")); + + Assert.IsTrue( + Directory.Exists(linqDirectory), + $"LINQ source directory not found at: {linqDirectory}"); + + string[] sourceFiles = Directory.GetFiles(linqDirectory, "*.cs", SearchOption.AllDirectories); + Assert.IsTrue(sourceFiles.Length > 0, "No source files found in LINQ directory."); + + List violations = new List(); + + foreach (string file in sourceFiles) + { + string[] lines = File.ReadAllLines(file); + string fileName = Path.GetFileName(file); + + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i]; + + if (BareCompilePattern.IsMatch(line) && !InterpretedCompilePattern.IsMatch(line)) + { + violations.Add($" {fileName}:{i + 1} => {line.Trim()}"); + } + } + } + + Assert.AreEqual( + 0, + violations.Count, + $"Found bare .Compile() calls without preferInterpretation: true. " + + $"Use .Compile(preferInterpretation: true) to avoid native memory leaks " + + $"(see issue #5487):\n{string.Join("\n", violations)}"); + } + } +}