Thursday, 23 January 2014

Memory Leak Free Roslyn ScriptEngine

Following on from my recent post regarding scripting with Roslyn, I have witnessed memory leaks whilst using the ScriptEngine class.  Although cool, it appears Roslyn is yet to make use of collectible assemblies which means that every expression call will generate and load a new assembly.  Once loaded this assembly will be persisted into ram and will live for the life of your application.  It appears this happens for every session.Execute method called.

To prevent this from happening you can make use of Application Domains (AppDomains).  Application Domains provide an isolated execution environment for code.  Every application runs within a domain and has the capability of spawning further domains.  AppDomains provide the capability to load resources and unload resources whilst preventing memory leaks.  All assemblies loaded and created inside of an isolated domain are destroyed when the domain is unloaded.  This has been around since .NET 1.0 and is an amazing asset when managing dynamic assembles and isolation.  It also provides memory isolation and is perfect for destroying heavy isolated code.

Below is a further elaboration on my previous sample application which shows the AppDomain approach.  My application no longer leaks memory so long as the application domain is unloaded.  This approach does have some limitations as all objects are required to be serializable.  Hope you find this helpful.

using Roslyn.Scripting.CSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace RoslynMemFree
{
public class Program
{
static void Main(string[] args)
{
var domain = AppDomain.CreateDomain("Rosyln");
var roslyn = (Executor)domain.CreateInstanceAndUnwrap(typeof(Program).Assembly.FullName, typeof(Executor).FullName);

using (var file = File.OpenWrite(@"C:\temp\roslyn.csv"))
using (var streamw = new StreamWriter(file))
{
for (var i = 0; i < 100; i++)
{
var url = @"?Item1={VisitId}&Item2={TrustNumber}&Item3={VisitId.Substring(0,1)}&StartDate={System.Web.HttpUtility.UrlEncode(DateTime.Now.AddDays(-5).ToString())}&Item4={(1==0?""No"":""Yes"")}";
var item = new PatientVisitEntity { VisitId = "V123456", TrustNumber = "P123456" };
var output = roslyn.Execute(item, url);
Console.WriteLine(output);

Console.WriteLine(GC.GetTotalMemory(true));
}
}

AppDomain.Unload(domain);

Console.WriteLine(GC.GetTotalMemory(true));
while (Console.ReadLine().ToUpper() != "EXIT")
{
Console.WriteLine(GC.GetTotalMemory(true));
}
}

[Serializable]
public class PatientVisitEntity
{
public string VisitId { get; set; }

public string TrustNumber { get; set; }
}

[Serializable]
public class Executor : MarshalByRefObject
{
public override object InitializeLifetimeService()
{
return null;
}

public string Execute(dynamic t, string exp)
{
var engine = new ScriptEngine();

var session = engine.CreateSession(t);
session.AddReference(t.GetType().Assembly);
session.AddReference("System.Web");
session.ImportNamespace("System");
session.ImportNamespace("System.Web");

var expressions = System.Text.RegularExpressions.Regex.Matches(exp, @"\{.*?\}");
var outputUrl = exp;

foreach (var func in expressions)
{
var f = func.ToString();
var realExp = f.Substring(1, f.Length - 2);
var output = session.Execute(realExp).ToString();

outputUrl = outputUrl.Replace(f, output);
}

return outputUrl;
}
}
}
}



And the memory output from the console.  You will see memory grows as with every call to Execute until I unload the new AppDomain.


image

No comments:

Post a Comment