memory leak when passing javascript object to managed code

Jul 22, 2015 at 10:19 PM
Edited Jul 22, 2015 at 10:22 PM
I am seeing a memory leak when passing a javascript object to a managed code function. I am using the latest nuget clearscript V8. Please see below for the code that reproduces this issue.

Program.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.ClearScript;
using Microsoft.ClearScript.V8;

namespace ClearScriptMemoryLeak
{
    public class Program
    {
        protected V8ScriptEngine m_engine;
        public Program(V8ScriptEngine engine)
        {
            m_engine = engine;
        }

        public void doSomething(dynamic foo)
        {
        }

        public void garbageCollect()
        {
            V8RuntimeHeapInfo beforeInfo = m_engine.GetRuntimeHeapInfo();
            
            m_engine.CollectGarbage(true);
            V8RuntimeHeapInfo afterInfo = m_engine.GetRuntimeHeapInfo();

            Console.WriteLine("garbageCollect: memstats (before/after): used: " + beforeInfo.UsedHeapSize + "/" + afterInfo.UsedHeapSize);
        }

        public static String loadScript(String filePath)
        {
            StringBuilder builder = new StringBuilder();
            FileInfo fileInfo = new FileInfo(filePath);

            using (StreamReader sr = fileInfo.OpenText())
            {
                String s = "";
                while ((s = sr.ReadLine()) != null)
                {
                    builder.AppendLine(s);
                }
            }
            return builder.ToString();
        }

        static void Main(string[] args)
        {
            V8ScriptEngine engine = new V8ScriptEngine(V8ScriptEngineFlags.EnableDebugging);
            Program p = new Program(engine);
            
            engine.AddHostObject("nativeBridge", p);

            engine.Execute(loadScript(args[0]));
        }
    }
}
MemLeak.js
var count = 0;
var obj;
while (true) {
    if (++count % 10000 == 0) {
        nativeBridge.garbageCollect();
    }
    obj = {
        a: "b",
        c: "d"
    };
    nativeBridge.doSomething(obj);
}
Running the above javascript will eventually crash with the following exception:
garbageCollect: memstats (before/after): used: 227850008/227529964
garbageCollect: memstats (before/after): used: 228050008/227729964
garbageCollect: memstats (before/after): used: 228250008/227929964
garbageCollect: memstats (before/after): used: 228450008/228129964

Unhandled Exception: Microsoft.ClearScript.ScriptEngineException: Error: Exception of type 'System.OutOfMemoryException' was thrown. ---> System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.
   at Microsoft.ClearScript.HostItem.AdjustInvokeFlags(BindingFlags& invokeFlags)
   at Microsoft.ClearScript.HostItem.InvokeMember(String name, BindingFlags invokeFlags, Object[] args, Object[] bindArgs, CultureInfo culture, Boolean bypassTunneling, Boolean& isCacheable)
   at Microsoft.ClearScript.HostMethod.TryInvoke(IHostInvokeContext context, BindingFlags invokeFlags, Object[] args, Object[] bindArgs, Object& result)
   at Microsoft.ClearScript.Util.InvokeHelpers.TryInvokeObject(IHostInvokeContext context, Object target, BindingFlags invokeFlags, Object[] args, Object[] bindArgs, Boolean tryDynamic, Object& result)
   at Microsoft.ClearScript.HostItem.InvokeHostMember(String name, BindingFlags invokeFlags, Object[] args, Object[] bindArgs, CultureInfo culture, Boolean& isCacheable)
   at Microsoft.ClearScript.HostItem.InvokeMember(String name, BindingFlags invokeFlags, Object[] args, Object[] bindArgs, CultureInfo culture, Boolean bypassTunneling, Boolean& isCacheable)
   at Microsoft.ClearScript.HostItem.<>c__DisplayClass4b.<InvokeReflectMember>b__4a()
   at Microsoft.ClearScript.ScriptEngine.HostInvoke[T](Func`1 func)
   at Microsoft.ClearScript.HostItem.HostInvoke[T](Func`1 func)
   at Microsoft.ClearScript.HostItem.InvokeReflectMember(String name, BindingFlags invokeFlags, Object[] wrappedArgs, CultureInfo culture, String[] namedParams, Boolean& isCacheable)
   at Microsoft.ClearScript.HostItem.System.Reflection.IReflect.InvokeMember(String name, BindingFlags invokeFlags, Binder binder, Object invokeTarget, Object[] wrappedArgs, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParams)
   at Microsoft.ClearScript.HostItem.Microsoft.ClearScript.Util.IDynamic.Invoke(Object[] args, Boolean asConstructor)
   at HostObjectHelpers.Invoke(V8Value* , Void* pvObject, vector<V8Value\,std::allocator<V8Value> >* args, Boolean asConstructor)
   --- End of inner exception stack trace ---
   at V8Exception.ThrowScriptEngineException(V8Exception* )
   at Microsoft.ClearScript.V8.V8ContextProxyImpl.Execute(String gcDocumentName, String gcCode, Boolean evaluate, Boolean discard)
   at Microsoft.ClearScript.V8.V8ScriptEngine.<>c__DisplayClass1b.<Execute>b__19()
   at Microsoft.ClearScript.ScriptEngine.ScriptInvoke[T](Func`1 func)
   at Microsoft.ClearScript.V8.V8ScriptEngine.BaseScriptInvoke[T](Func`1 func)
   at Microsoft.ClearScript.V8.V8ScriptEngine.<>c__DisplayClass25`1.<ScriptInvoke>b__24()
   at Microsoft.ClearScript.V8.?A0x792c8756.InvokeAction(Void* pvActionRef)
   at Microsoft.ClearScript.V8.V8ContextProxyImpl.InvokeWithLock(Action gcAction)
   at Microsoft.ClearScript.V8.V8ScriptEngine.ScriptInvoke[T](Func`1 func)
   at Microsoft.ClearScript.V8.V8ScriptEngine.Execute(String documentName, String code, Boolean evaluate, Boolean discard)
   at Microsoft.ClearScript.ScriptEngine.Execute(String code)
If I remove the 'nativeBridge.doSomething(obj);' line, then there is no memory leak. Likewise, if I move the "obj = {...}" statement outside of the while loop, there is no leak.

Am I doing something wrong with this code? If not, is there any workaround for this issue?

thanks,
Tom
Coordinator
Jul 23, 2015 at 2:19 AM
Edited Jul 23, 2015 at 2:29 AM
Hi Tom,

Unfortunately memory management can get tricky with long-running scripts.

When you pass a script object into managed code, ClearScript creates a managed proxy for it. The proxy holds a strong reference to its target, so the script object can't be garbage-collected until the proxy is disposed or finalized.

Therefore one way to avoid the leak is to dispose the proxy when you're done with it:
public void doSomething(dynamic foo) {
    var disposable = foo as IDisposable;
    if (disposable != null) {
        disposable.Dispose();
    }
}
Another possibility is to invoke managed garbage collection and finalization before calling ScriptEngine.CollectGarbage():
public void garbageCollect() {
    GC.Collect();
    GC.WaitForPendingFinalizers();
    // [...]
    m_engine.CollectGarbage(true);
    // [...]
}
An unfortunate wrinkle with this approach is that you can no longer call garbageCollect() from script code. Proxy finalization requires help from the script engine, which is locked during script execution, so the GC.WaitForPendingFinalizers() call causes a deadlock. Instead, you'll have to restructure your code like this:
engine.Execute(@"var count = 0");
while (true) {
    engine.Execute(@"
        while (true) {
            if (++count % 10000 == 0) {
                break;
            }
            obj = {
                a: ""b"",
                c: ""d""
            };
            nativeBridge.doSomething(obj);
        }
    ");
    p.garbageCollect();
}
Thanks, and good luck!
Jul 23, 2015 at 4:46 PM
The first solution you mentioned should work for me. Thanks for the prompt and detailed reply!

-Tom
Coordinator
Jul 24, 2015 at 2:17 PM
Hi Tom,

We've looked into this some more and found a serious bug. We're testing a fix that'll make explicit disposal of script items unnecessary, eliminate the GC.WaitForPendingFinalizers() deadlock, and lessen the need to invoke garbage collection manually.

Thanks for bringing it to our attention!