runtime: Memory leak on unix systems using dynamic code compilation

Memory leak on unix systems using dynamic code compilation

General

I am experiencing a rather strange behaviour that caused me hours of debugging to nail it down to the dynamic code generation.

If I compile a piece of code using the rosyln compiler from within C# everything runs well on windows. When I add the MetaDataReferences the assemblies are loaded into memory, the assembly is compiled, written to the file system and the allocated memory freed by the GC.

If I run the same code on a Unix system though (Debian, Ubuntu 18.04, Alpine - doesn’t matter) the memory allocated by the MetaDataReferences is only freed if I do not invoke collection.Emit. When I call the method the process is leaking huge amounts of memory and I do not know why. I searched the roslyn source and couldn’t find a clue as to why this is happening, thus I do not know if this is a Roslyn or dotnet GC related issue (if it is any and not me not realizing my mistakes)

For reference, in our production environment each call to our generation method is leaking up to 250mb of ram. Since I cannot share that source here, I provide a sample code that on my Ubuntu machine is left with over 200mb while at the same time it is consuming only 50 mb on windows (just every referenced assembly loaded)

namespace NetCoreRazor
{
    using Microsoft.CodeAnalysis;
    using Microsoft.CodeAnalysis.CSharp;
    using System;
    using System.IO;
    using System.Linq;

    class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 100; i++)
            {
                DynCompile();
            }

            GC.Collect();

            Console.WriteLine("Done");
            Console.ReadLine();
        }

        private static void DynCompile()
        {
            string myProgram = @"using System;
            namespace DynProgram
            {
                public class DynClass
                {
                    public static void WriteToConsole() { Console.WriteLine(""Hello from Dyn""); }
                }
            }";

            var assemblies = AppDomain.CurrentDomain.GetAssemblies()
                   .Where(a => !a.IsDynamic && File.Exists(a.Location))
                   .GroupBy(a => a.GetName().Name)
                   .Select(grp => 
                        grp.First(y => y.GetName().Version == grp.Max(x => x.GetName().Version)))
                   .Select(a => MetadataReference.CreateFromFile(a.Location));

            var syntaxTree = CSharpSyntaxTree.ParseText(myProgram);
            var compile = CSharpCompilation.Create("dyn_compile");

            compile = compile.AddSyntaxTrees(syntaxTree)
                .AddReferences(assemblies)
                .WithOptions(
                    new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
                    .WithPlatform(Platform.AnyCpu));

            using (var myAssembly = File.Open(Path.GetTempFileName(), FileMode.Create, FileAccess.ReadWrite))
            {
                compile.Emit(myAssembly);
            }
        }
    }
}

NET Core 2.2.6

It took me so long to nail it down because the memory dumps weren’t really helpfull. If I’d dump our production system the dump would be 3.5 Gb large. Analysing it with lldb and sos would yield only a heap sice of 200 mb on the other side. That is the sice of DumpHeap and EEHeap combined… so there are 3.2 GB seemingly nothing.

King regards

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 28 (25 by maintainers)

Most upvoted comments

@agocke and @Maoni0 - PTAL?