TL;DR: I may have identified my problem, which was my fault, but I'm not sure if it is expected behavior from ClearScript.
Alright I figured out why my thread count was continuously rising. You saying that it creates that optimization thread per isolate was what helped me find it.
Sometimes my javascript will call into a managed object and expect back a javascript array (I always try to avoid exposing .NET details to the scripts). On the managed side, I have a List<T>. It converts it into an Array by a ClearScript Evaluate("[]") call, then pushes each item into it. Well, I did that wrong. The Evaluate("[]") was being done on a NEW ScriptEngine rather than the Engine from the calling code. So the result was I was returning a js array created from one ScriptEngine back into code from another ScriptEngine.
Apparently when you do that, the object won't be cleaned up until disposing of the receiving engine.
I fixed that, and it's been a huge improvement. I'm still monitoring it for leaks though -- I'm not yet convinced I'm out of the woods. Some of that may just be the known problems you mentioned.
I'm also a bit confused by the results, because my app is very explicitly disposing of the script, engine, and runtime after every 200 calls. It even calls GC.Collect. But the thread count was still rising forever. This sample app sort of reproduces by problem, but I can't reproduce the inability to explicitly clean it up by disposing of things. Perhaps there's yet more subtlety to my app preventing that.
Sample app:
Alright I figured out why my thread count was continuously rising. You saying that it creates that optimization thread per isolate was what helped me find it.
Sometimes my javascript will call into a managed object and expect back a javascript array (I always try to avoid exposing .NET details to the scripts). On the managed side, I have a List<T>. It converts it into an Array by a ClearScript Evaluate("[]") call, then pushes each item into it. Well, I did that wrong. The Evaluate("[]") was being done on a NEW ScriptEngine rather than the Engine from the calling code. So the result was I was returning a js array created from one ScriptEngine back into code from another ScriptEngine.
Apparently when you do that, the object won't be cleaned up until disposing of the receiving engine.
I fixed that, and it's been a huge improvement. I'm still monitoring it for leaks though -- I'm not yet convinced I'm out of the woods. Some of that may just be the known problems you mentioned.
I'm also a bit confused by the results, because my app is very explicitly disposing of the script, engine, and runtime after every 200 calls. It even calls GC.Collect. But the thread count was still rising forever. This sample app sort of reproduces by problem, but I can't reproduce the inability to explicitly clean it up by disposing of things. Perhaps there's yet more subtlety to my app preventing that.
Sample app:
private const string _script = @"
project.onIssueExceptionStatsChanged(function(ev) {
var things = ev.getThings();
});
";
static void Main(string[] args)
{
var runtime = new V8Runtime();
var engine = runtime.CreateScriptEngine();
var compiledScript = runtime.Compile(_script);
var host = new HostObject();
engine.AddHostObject("project", host);
engine.Execute(compiledScript);
for (var i = 0; i < 10; i++)
{
var iteration = i;
Console.WriteLine("Iteration {0}. Threads = {1}. Memory = {2}.",
iteration,
Process.GetCurrentProcess().Threads.Count,
Process.GetCurrentProcess().PrivateMemorySize64.ToString("n0"));
for (var j = 0; j < 100; j++)
{
host.RaiseEvent(new SomeEventArgs());
}
}
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Done. Hit enter to Dispose. Threads: {0}. Memory: {1}",
Process.GetCurrentProcess().Threads.Count,
Process.GetCurrentProcess().PrivateMemorySize64.ToString("n0"));
Console.ReadLine();
compiledScript.Dispose();
engine.Dispose();
runtime.Dispose();
// need to do this explicitly or it may not get cleaned up yet...
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Done. Threads: {0}. Memory: {1}",
Process.GetCurrentProcess().Threads.Count,
Process.GetCurrentProcess().PrivateMemorySize64.ToString("n0"));
Console.ReadLine();
}
}
public class HostObject
{
private dynamic _callback;
public void onIssueExceptionStatsChanged(dynamic callback)
{
_callback = callback;
}
internal void RaiseEvent(SomeEventArgs args)
{
_callback(args);
}
}
public class SomeEventArgs
{
public object getThings()
{
dynamic list = new V8ScriptEngine().Evaluate("[]");
list.push("foo");
return list;
}
}
Output:Iteration 0. Threads = 5. Memory = 30,679,040.
Iteration 1. Threads = 105. Memory = 942,821,376.
Iteration 2. Threads = 205. Memory = 1,850,576,896.
Iteration 3. Threads = 305. Memory = 2,758,254,592.
Iteration 4. Threads = 405. Memory = 3,662,749,696.
Iteration 5. Threads = 505. Memory = 4,570,488,832.
Iteration 6. Threads = 605. Memory = 5,477,806,080.
Iteration 7. Threads = 705. Memory = 6,386,348,032.
Iteration 8. Threads = 808. Memory = 7,291,899,904.
Iteration 9. Threads = 908. Memory = 8,198,541,312.
Done. Hit enter to Dispose. Threads: 1008. Memory: 9,104,228,352
Done. Threads: 4. Memory: 38,354,944