Memory leak

Nov 23, 2013 at 2:53 AM
I'm having a problem where I can't get an object passed to javascript to be garbage collected.
I've included sample code below.
It demonstrates that calling JavascriptContext.Collect and GC.Collect can not clean up the reference to HeavyObject.
Besides the WeakReference indication you can open task manager and see that before you press a key for the first time it uses 100 megs of memory.
Once you press a key the JavascriptContext is disposed, the WeakReference reports that the object is dead, and task manager shows the memory usage dropped to a couple of megs.

Am I doing something wrong here? Or is there no way to clean up objects that were passed to noesis.javascript.net without disposing the entire context?

using System;
using Noesis.Javascript;

namespace NoesisMemoryLeakTest
{
class Program
{
    static void Main(string[] args)
    {
        WeakReference wr;
        using (var js = new JavascriptContext())
        {
            wr = AddHeavyInScopedMethod(js);
            JavascriptContext.Collect();
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            GC.WaitForPendingFinalizers();
            // and again just in case the order the collections happened matters
            JavascriptContext.Collect();
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            GC.WaitForPendingFinalizers();
            Console.WriteLine(wr.IsAlive); // prints true
            Console.ReadKey();
        } // end using javascript context
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        Console.WriteLine(wr.IsAlive); // prints false
        Console.ReadKey();
    }

    static WeakReference AddHeavyInScopedMethod(JavascriptContext js)
    { // by adding the heavy object here and returning a WeakReference we ensure that C# holds no strong references to the HeavyObject
        var heavy = new HeavyObject();
        var wr = new WeakReference(heavy);
        js.SetParameter("heavy", heavy);
        js.Run("heavy = null;");
        return wr;
    }
} // end Program class
public class HeavyObject
{
    const int ArraySize = 100000000;
    public byte[] Buffer = new byte[ArraySize];

    // constructor
    public HeavyObject()
    {
        for (int i=0; i < ArraySize; i++)
            Buffer[i] = (byte)(i % 256);
    }
} // end HeavyObject class
}
Developer
Nov 24, 2013 at 10:24 PM
I don't know why it wouldn't work, but you might want to examine the v8 method that JavascriptContext.Collect() invokes. It's something to do with idleness and I don't know what guarantees it makes about freeing objects.
Nov 24, 2013 at 11:15 PM
Hi Oliver, thanks for the response.

I looked at the JavascriptContext.Collect method and it consists of this one line:
while(!v8::V8::IdleNotification()) {};
Part of the comments for IdleNotification are:
  • Optional notification that the embedder is idle.
  • V8 uses the notification to reduce memory footprint.
  • This call can be used repeatedly if the embedder remains idle.
  • Returns true if the embedder should stop calling IdleNotification
  • until real work has been done. This indicates that V8 has done
  • as much cleanup as it will be able to do.
So the Collect method is calling IdleNotification until V8 says there is no more cleanup that it can do.
That seems like a reasonable guarantee that V8 has cleaned up all the objects it can.

Also as I've been looking through the code I noticed that the JavascriptExternal class maintains this member variable:
System::Runtime::InteropServices::GCHandle mObjectHandle;
I assume that is as good as a strong reference in C#.
As I look in JavascriptContext I find that it has this member variable:
vector<JavascriptExternal*>* mExternals;
And it only deletes the JavascriptExternal objects upon JavascriptContext destruction.

Based on that alone I'd think it is possible that V8 is cleaning up it's objects but the underlying JavascriptExternal objects never get cleaned up until the context is disposed, which in turn stops C# objects from getting collected due to the GCHandle held by the JavascriptExternal objects.

Also the following url indicates that we should be maintaining V8 weak handles that provide us with callbacks to know when it cleans up an object so we can properly dispose of the underlying External object:
http://create.tpsitulsa.com/wiki/V8/Persistent_Handles

There are no places in the noesis source code where MakeWeak() is called which makes me really think this could be the missing piece.

I've noticed you often respond to questions about noesis.javascript.net.
What is your relation to the project if you don't mind me asking?

I'm going to see if I can implement the V8 weak references stuff, but neither C++ CLI or native C++ are my strong points.
If there's an active maintainer I'd love to work with them to solve the problem.

Thanks for your help.
Ian Reed
Developer
Nov 25, 2013 at 3:14 AM
Hi Ian,

I am one of the maintainers, but I am not particularly active - I mainly submit fixes I need. I had forgotten about mExternals, Without having put a lot of thought into it, I think that using weak references is a good idea. Make sure you use the GitHub repository for your code, and be aware that one of the regression tests is reliably failling (an Issue is lodged).


Oliver