RazorEngine
RazorEngine copied to clipboard
Critical nemory leaks in RazorEngine on server applications since not unloading modules from orphan AppDomain
I tested RazorEngine and found that it can not be used in server solution since critical bug generating memory leaks.
Each creation AppDomain generates memory leak since Razor not unload AppDoman but should do it.
Here is problem explained and leaks traced:

Here is test code (with use marshal and serialized) - comment line var rendered_template = Engine.Razor.RunCompile(parameters.template_source, parameters.template_name, null, expando); to see or not to see memory leaks - it is RazorEngine impact:
using RazorEngine;
using RazorEngine.Templating;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Dynamic;
namespace razor_engine.s04
{
[Serializable]
class RazorEngineParameters
{
public string template_name { get; set; }
public string template_source { get; set; }
public Dictionary<string, object> variables { get; set; }
}
class RazorEngineProxy : MarshalByRefObject
{
public string render(RazorEngineParameters parameters)
{
// Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
var expando = new ExpandoObject();
var expando_collection = (ICollection<KeyValuePair<string, object>>)expando;
foreach (var item in parameters.variables)
{
expando_collection.Add(item);
}
var rendered_template = Engine.Razor.RunCompile(parameters.template_source, parameters.template_name, null, expando);
//parameters.Dispose();
//return rendered_template;
return "Good!";
}
}
class Program
{
static void Test(int iterations)
{
int i;
var app_domain_setup = new AppDomainSetup();
app_domain_setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
var local_app_domain = AppDomain.CreateDomain("local_domain");
var assembly_name = typeof(RazorEngineProxy).Assembly.CodeBase;
//var assembly_name = typeof(RazorEngineProxy).Assembly.FullName;
var type_name = typeof(RazorEngineProxy).FullName;
var razor_engine_proxy = (RazorEngineProxy)local_app_domain.CreateInstanceFromAndUnwrap(assembly_name, type_name);
string template = "Hello @Model.name!";
for (i = 0; i < iterations; i++)
{
var variables = new Dictionary<string, object>
{
{ "name", i.ToString() }
};
var parameters = new RazorEngineParameters
{
template_name = "template_1",
template_source = template,
variables = variables
};
var result = razor_engine_proxy.render(parameters);
}
//razor_engine_proxy.Dispose();
AppDomain.Unload(local_app_domain);
}
static void Main(string[] args)
{
int ITERATIONS = 10000;
var stopper = new Stopwatch();
GC.RegisterForFullGCNotification(99, 99);
GC.Collect();
GC.WaitForFullGCComplete(1000);
Console.WriteLine(GC.GetTotalMemory(true).ToString());
stopper.Start();
Test(ITERATIONS);
stopper.Stop();
Console.WriteLine(stopper.Elapsed);
GC.Collect();
GC.WaitForFullGCComplete(1000);
Console.WriteLine(GC.GetTotalMemory(true).ToString());
stopper.Start();
Test(ITERATIONS);
stopper.Stop();
Console.WriteLine(stopper.Elapsed);
GC.Collect();
GC.WaitForFullGCComplete(1000);
Console.WriteLine(GC.GetTotalMemory(true).ToString());
stopper.Start();
Test(ITERATIONS);
stopper.Stop();
Console.WriteLine(stopper.Elapsed);
GC.Collect();
GC.WaitForFullGCComplete(1000);
Console.WriteLine(GC.GetTotalMemory(true).ToString());
stopper.Start();
Test(ITERATIONS);
stopper.Stop();
Console.WriteLine(stopper.Elapsed);
GC.Collect();
GC.WaitForFullGCComplete(1000);
Console.WriteLine(GC.GetTotalMemory(true).ToString());
Console.ReadLine();
}
}
}
Same thing is when I use CrossAppDomainObject and *.Dispose().
using RazorEngine;
using RazorEngine.Templating;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Dynamic;
namespace razor_engine.s04
{
// [Serializable]
class RazorEngineParameters : cs_local_remoting.CrossAppDomainObject
{
public string template_name { get; set; }
public string template_source { get; set; }
public Dictionary<string, object> variables { get; set; }
}
class RazorEngineProxy : MarshalByRefObject
{
public string render(RazorEngineParameters parameters)
{
// Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
var expando = new ExpandoObject();
var expando_collection = (ICollection<KeyValuePair<string, object>>)expando;
foreach (var item in parameters.variables)
{
expando_collection.Add(item);
}
var rendered_template = Engine.Razor.RunCompile(parameters.template_source, parameters.template_name, null, expando);
parameters.Dispose();
//return rendered_template;
return "Good!";
}
}
class Program
{
static void Test(int iterations)
{
int i;
var app_domain_setup = new AppDomainSetup();
app_domain_setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
var local_app_domain = AppDomain.CreateDomain("local_domain");
var assembly_name = typeof(RazorEngineProxy).Assembly.CodeBase;
//var assembly_name = typeof(RazorEngineProxy).Assembly.FullName;
var type_name = typeof(RazorEngineProxy).FullName;
var razor_engine_proxy = (RazorEngineProxy)local_app_domain.CreateInstanceFromAndUnwrap(assembly_name, type_name);
string template = "Hello @Model.name!";
for (i = 0; i < iterations; i++)
{
var variables = new Dictionary<string, object>
{
{ "name", i.ToString() }
};
var parameters = new RazorEngineParameters
{
template_name = "template_1",
template_source = template,
variables = variables
};
var result = razor_engine_proxy.render(parameters);
}
//razor_engine_proxy.Dispose();
AppDomain.Unload(local_app_domain);
}
static void Main(string[] args)
{
int ITERATIONS = 10000;
var stopper = new Stopwatch();
GC.RegisterForFullGCNotification(99, 99);
GC.Collect();
GC.WaitForFullGCComplete(1000);
Console.WriteLine(GC.GetTotalMemory(true).ToString());
stopper.Start();
Test(ITERATIONS);
stopper.Stop();
Console.WriteLine(stopper.Elapsed);
GC.Collect();
GC.WaitForFullGCComplete(1000);
Console.WriteLine(GC.GetTotalMemory(true).ToString());
stopper.Start();
Test(ITERATIONS);
stopper.Stop();
Console.WriteLine(stopper.Elapsed);
GC.Collect();
GC.WaitForFullGCComplete(1000);
Console.WriteLine(GC.GetTotalMemory(true).ToString());
stopper.Start();
Test(ITERATIONS);
stopper.Stop();
Console.WriteLine(stopper.Elapsed);
GC.Collect();
GC.WaitForFullGCComplete(1000);
Console.WriteLine(GC.GetTotalMemory(true).ToString());
stopper.Start();
Test(ITERATIONS);
stopper.Stop();
Console.WriteLine(stopper.Elapsed);
GC.Collect();
GC.WaitForFullGCComplete(1000);
Console.WriteLine(GC.GetTotalMemory(true).ToString());
Console.ReadLine();
}
}
}
Here is missed code for CrossDomainAppObject:
using System;
using System.Runtime.Remoting;
using System.Security;
namespace cs_local_remoting
{
/// <summary>
/// Enables access to objects across application domain boundaries.
/// This type differs from <see cref="MarshalByRefObject"/> by ensuring that the
/// service lifetime is managed deterministically by the consumer.
/// </summary>
public abstract class CrossAppDomainObject : MarshalByRefObject, IDisposable
{
/// <summary>
/// Cleans up the <see cref="CrossAppDomainObject"/> instance.
/// </summary>
~CrossAppDomainObject()
{
Dispose(false);
}
/// <summary>
/// Disconnects the remoting channel(s) of this object and all nested objects.
/// </summary>
[SecuritySafeCritical]
private void Disconnect()
{
RemotingServices.Disconnect(this);
}
/// <summary>
/// initializes the lifetime service for the current instance.
/// </summary>
/// <returns>null</returns>
[SecurityCritical]
public sealed override object InitializeLifetimeService()
{
//
// Returning null designates an infinite non-expiring lease.
// We must therefore ensure that RemotingServices.Disconnect() is called when
// it's no longer needed otherwise there will be a memory leak.
//
return null;
}
/// <summary>
/// Disposes the current instance.
/// </summary>
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
private bool disposed;
/// <summary>
/// Disposes the current instance via the disposable pattern.
/// </summary>
/// <param name="disposing">true when Dispose() was called manually.</param>
protected virtual void Dispose(bool disposing)
{
if (disposed)
return;
Disconnect();
disposed = true;
}
}
}
This never got fixed?
This issue is still occuring.
Even when calling IDispose the generated assembly is not remoed from memory, causing a leak.
I’m dealing with the same issue.
This issue is still exist.