Search code examples
c#assemblydelegatesconsole-applicationroslyn

Memory Leak caused by Roslyn, how do I contain it?


So I have this NET6.0 console application that acts as a sort of bridge between other apps for UDP messages.

To define how to send data out of my application have this profile system that consists in a dozen of files where the desired variables of each data structure are defined. At startup I read each file and for each data structure defined in the file I compose a string that represents the method I want to use to generate the custom string for each profile and then compile it using Roslyn to create a delegate for later use (via the Microsoft.CodeAnalysis.CSharp.Scripting NuGet package):

ScriptRunner<string> scriptToAdd = CSharpScript.Create<string>(toConvert, globalsType: WorldData.GetTypeByName(tempName)).CreateDelegate();

While it was a console application with few profiles it worked as intended, but now I had to give this application a web interface (I did it by creating a Blazor project and referencing the original project) and now at startup I get an Out Of Memory exception.

After some research I found out that Roslyn when compiling some code loads all the necessary assemblies but then it doesn't unload them.

The first thing I did was to verify that Roslyn was actually the culprit, so I skipped the delegate creation, it worked like a charm.

Next step was to find out if I could unload assemblies after Roslyn was done with them, I then discovered AssemblyLoadContext, but being out of my knowledge realm I enlisted the help of GitHub Copilot and it gave me this piece of code:

var context = new AssemblyLoadContext("ScriptContext", isCollectible: true);

try
{
    scriptToAdd = CSharpScript.Create<string>(
        toConvert,
        globalsType: WorldData.GetTypeByName(tempName),
        options: ScriptOptions.Default.WithReferences(
            context.LoadFromAssemblyPath(typeof(object).Assembly.Location)
        )
    ).CreateDelegate();
}
finally
{
    context.Unload();
}

As a relatively unexprienced developer this looked promising, I get that LoadFromAssemblyPath would need to be called multiple times to get every assembly needed, but I wanted to try this. Then I get this error:

System.IO.FileLoadException: 'Could not load file or assembly 'System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e'.'

At this point I am way out of my league and couldn't find anything more beside this article, but as I already said...way out of my league.

Thanks in advance to anyone that will try and help me. Let me know if and what more I could provide to help you help me.


Solution

  • First off, this is not a memory leak, it's the intended way Roslyn works. It trades off memory usage (by caching an insane amount of data as much and as often as it can) for speed, to allow near-real-time compilation, including user-defined analyzers and source generation, on every key press in VS. It works beautifully!

    I get an Out Of Memory exception

    First thing I'd check is if your application isn't running in 32-bit mode. .Net apps haven't been meant to run in 32-bit mode for over a decade now. You should never encounter out of memory exceptions in 64-bit mode.

    then it doesn't unload them

    You can't unload assemblies in general once they are loaded, because of the way the JIT works. At most you could have function calls to clean up data structures, but Roslyn is made for performance, so it manages its own memory and doesn't provide user-visible cleanup functions.

    With that out of the way, if you still want to decrease overall memory usage, you have two options really:

    1. Use Roslyn as an external process to create a .dll assembly out of your source code, then reference it and use it as you are now (with delegates). This way the caches would go away as the compiler process dies.
    2. Don't use Roslyn at all and directly emit IL instructions to memory. It's really not difficult, especially if you keep your generated code simple (for example, by creating helper functions to call if it turns out that you need to do processing on your fields).