RazorEngine icon indicating copy to clipboard operation
RazorEngine copied to clipboard

Critical nemory leaks in RazorEngine on server applications since not unloading modules from orphan AppDomain

Open ChameleonRed opened this issue 7 years ago • 5 comments

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: obraz

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();
        }
    }
}

ChameleonRed avatar Jan 09 '18 11:01 ChameleonRed

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;
        }
    }
}

ChameleonRed avatar Jan 09 '18 11:01 ChameleonRed

This never got fixed?

alelavoie avatar Apr 17 '18 00:04 alelavoie

This issue is still occuring. Even when calling IDispose the generated assembly is not remoed from memory, causing a leak.

jkonecki avatar Dec 28 '18 16:12 jkonecki

I’m dealing with the same issue.

HomeroLara avatar Oct 09 '20 21:10 HomeroLara

This issue is still exist.

meysambagheri96 avatar Feb 15 '23 14:02 meysambagheri96