Quantcast
Channel: ClearScript
Viewing all articles
Browse latest Browse all 2297

New Post: MemoryLeak despite disposing of V8 Script Engines

0
0
Greetings! I could really use some help figuring out how to handle this. I've spent a lot of time trying to isolate the problem. I suspect I have multiple problems, actually.

My scenario is what I need to have a user-defined script hook into an event I expose. The event can be raised at any time in the future, so it's going to be a long-running script that sits around. I pass the event a managed object (a kind of event args) that contains information about the event.

The memory usage was continuously climbing as events were raised.

Reading other discussions here, I gather that passing managed objects to scripts can cause some tricky memory management issues. You suggested disposing of them when they are done being used. I can't really do that easily, though. The object that is passed to script contains methods that the script can use to get access to even more objects. To be able to clean them all up, I'd have to have an event-scoped list of objects that were passed in so that I could easily dispose them later, which would be possible, but difficult.

So instead of doing that, I keep track of how often I call the event. After every 100 calls, or so, I will completely dispose of the V8 Engine and the script, recompile it, and re-evalulate it. I figured whatever memory use issues I have should be covered by that, so no need to dispose the host objects directly. Or so I thought.

Even with that, memory continues to climb. I've created a reproduction app to demonstrate:
    class Program
    {
        static void Main(string[] args)
        {
            for (var i = 0; i < 50; i++)
            {
                Console.WriteLine("Iteration " + i + ". Memory = " + Process.GetCurrentProcess().PrivateMemorySize64.ToString("n0"));

                var engine = new Microsoft.ClearScript.V8.V8ScriptEngine();
                var host = new HostObject();
                engine.AddHostObject("host", host);

                var compiledScript = engine.Compile(@"host.onEvent(function() {});");
                engine.Execute(compiledScript);

                for (var j = 0; j < 50000; j++)
                {
                    Raise(host);
                }

                engine.Dispose();
                GC.Collect();
            }

            Console.WriteLine("Done. Memory: " + Process.GetCurrentProcess().PrivateMemorySize64.ToString("n0"));
            Console.ReadLine();
        }

        private static void Raise(HostObject host)
        {
            host.RaiseEvent(new SomeEventArgs());
        }
    }

    public class HostObject
    {
        private dynamic _callback;

        public void onEvent(dynamic callback)
        {
            _callback = callback;
        }

        internal void RaiseEvent(SomeEventArgs args)
        {
            _callback(args);
        }
    }

    public class SomeEventArgs
    {
        public object getThing()
        {
            return new Thing();
        }
    }

    public class Thing
    {
    }
So it's just a loop that continues to raise events. After 50,000 events have been raised, it disposes of the V8 engine and forces GC.Collect. Then it starts over. Each iteration it spits out the private memory use. The output looks like this:
Iteration 0. Memory = 27,127,808
Iteration 1. Memory = 48,844,800
Iteration 2. Memory = 50,040,832
Iteration 3. Memory = 54,665,216
Iteration 4. Memory = 61,140,992
Iteration 5. Memory = 59,023,360
Iteration 6. Memory = 60,551,168
Iteration 7. Memory = 65,290,240
Iteration 8. Memory = 63,279,104
Iteration 9. Memory = 68,558,848
Iteration 10. Memory = 66,580,480
Iteration 11. Memory = 72,167,424
Iteration 12. Memory = 69,926,912
Iteration 13. Memory = 68,534,272
Iteration 14. Memory = 75,706,368
Iteration 15. Memory = 71,839,744
Iteration 16. Memory = 79,609,856
Iteration 17. Memory = 79,757,312
Iteration 18. Memory = 76,496,896
Iteration 19. Memory = 83,943,424
Iteration 20. Memory = 79,884,288
Iteration 21. Memory = 87,781,376
Iteration 22. Memory = 87,670,784
Iteration 23. Memory = 84,738,048
Iteration 24. Memory = 91,852,800
Iteration 25. Memory = 88,100,864
Iteration 26. Memory = 95,801,344
Iteration 27. Memory = 95,752,192
Iteration 28. Memory = 92,495,872
Iteration 29. Memory = 100,003,840
Iteration 30. Memory = 95,817,728
Iteration 31. Memory = 103,780,352
Iteration 32. Memory = 104,042,496
Iteration 33. Memory = 100,737,024
Iteration 34. Memory = 107,982,848
Iteration 35. Memory = 104,165,376
Iteration 36. Memory = 111,931,392
Iteration 37. Memory = 112,066,560
Iteration 38. Memory = 108,818,432
Iteration 39. Memory = 116,264,960
Iteration 40. Memory = 112,078,848
Iteration 41. Memory = 120,107,008
Iteration 42. Memory = 119,910,400
Iteration 43. Memory = 117,051,392
Iteration 44. Memory = 124,166,144
Iteration 45. Memory = 120,414,208
Iteration 46. Memory = 128,114,688
Iteration 47. Memory = 128,196,608
Iteration 48. Memory = 125,067,264
Iteration 49. Memory = 132,448,256
Done. Memory: 128,397,312
Using windbg I looked at the heap. There's a single dictionary in memory that seems to grow forever:

000007fe938e5848 1 1206920 System.Runtime.CompilerServices.ConditionalWeakTable2+Entry[[System.Object, mscorlib],[System.Collections.Generic.List1[[System.WeakReference, mscorlib]], mscorlib]][]

I presume this is some kind of weakreference dictionary that keeps track of proxy objects. It contains 75,000 elements in this example.

0:000> !do 0000000012805c40
Name: System.Runtime.CompilerServices.ConditionalWeakTable2+Entry[[System.Object, mscorlib],[System.Collections.Generic.List1[[System.WeakReference, mscorlib]], mscorlib]][]
MethodTable: 000007fe938e5848
EEClass: 000007fe938e57b8
Size: 1206920(0x126a88) bytes
Array: Rank 1, Number of elements 75431, Type VALUETYPE
Fields:
None

To be fair, while this does seem to reproduce a problem, I'm not even sure that it's my actual problem. I think I have multiple. My actual program is leaking memory a lot faster than I can reproduce with this sample app. That may just be because the host objects I'm passing in are larger though. I could perhaps use some advice or guidelines on what to do or what not to do for the scenario I'm in.

Viewing all articles
Browse latest Browse all 2297