sebastienros / jint

Javascript Interpreter for .NET
BSD 2-Clause "Simplified" License
4.13k stars 561 forks source link

Performance Concerns #64

Closed atom0s closed 6 years ago

atom0s commented 10 years ago

I have just started looking into Jint after having very poor performance with NLua with a project of mine. After implementing Jint in its place I am seeing similar results in performance though. I'm fairly certain this is due to the conversions between C# -> scripting engine and back.

My project consists of extending a plugin system to allow plugins to be written in a scripting language and not require compiling. This also allows for unloading/reloading of plugin scripts instead of having to hard-restart the server since .NET DLLs cannot be unloaded (unless they are ran in separate AppDomains etc.)

One of the tests I was using to check the performance is on the games Update call. This is called constantly as it is the games loop handler. From within my scripts callback for this handler, I loop the player list and do some checks and such on the objects like this:

function GameUpdate()
{
    for (var x = 0; x < SomeObject.SubArray.length - 1; x++) {
        if (SomeObject.SubArray[x] != undefined && SomeObject.SubArray[x].Active == true)
        {
            print(SomeObject.SubArray[x].Name);
        }
    }
}

SomeObject is a static object on the .NET side. Its SubArray is an array (255 static sized) array of another object type.

So in this case I am looping the 255 objects every frame of the server. If this is coded in C# itself, there is no performance drops noticeable. But with Jint, the game server is unplayable from the lag this produces.

(The lag is there even without the print, that was just to give an example. So it is not an output slowdown etc.)

My question would be is there something I am doing wrong or "bad" in a sense of how I am using Jint? Is there a better method to approach this with to prevent the lag?

flts commented 10 years ago

I don't know how your C# code looks like right now but instantiating the Engine object inside the Update call affects performance. You should instantiate the Engine object only ones. Another possible improvement could be the following code (I haven't tested this yet, so there could be possible side effects, but the Javascript script will only be parsed once which also takes some time).

// Code not tested but should give you a basic idea

private readonly JavaScriptParser _parser = new JavaScriptParser();
private Engine _engine;
private Program _program;

/// Call at the start of the application.
private void InitiateEngine()
{
    // can also pass Options to the Engine object.
    _engine = new Engine();
}

/// Only call once, not during the Update call
private void ParseScript()
{
    _program = _parser.Parse(somescript);
}

private void UpdateCall()
{
    _engine.Execute(_program);
}

Another possible improvement for the Javascript script.

function GameUpdate()
{
    for (var x = 0; x < SomeObject.SubArray.length - 1; x++) {
        var obj = SomeObject.SubArray[x];
        if (obj != undefined && obj.Active == true)
        {
            print(obj.Name);
        }
    }
}

You have to keep in mind that there will be a performance hit every time you go from Javascript to .NET and the other way. .NET function calls are not yet cached so they have to be looked up every time you call them from Javascript. Perhaps you could start with an empty script and pass that in the Update call to see if there is any lag. Then add the for loop and see if there is any lag, Next add the if undefined check and check for lag. Then the Active == true check and see if there is any lag and last add the print.

atom0s commented 10 years ago

Yeah at the moment the above is similar to how it is setup internally. Scripts are only executed when the addon is loaded. (Or reloaded but that is a manual occurrence.) The parser is only invoked once, and the events are raised via the Engine::Execute method.

When the addon is initialized I create a new engine instance for that specific addon and bind some objects to it. I execute the script associated with that addon once, and then I can use Execute afterward to continue calling functions within that script object:

    // Create the Javascript engine..
    this.JEngine = new Engine(cfg => cfg.AllowClr());
    if (this.JEngine == null)
    {
        this.State = AddonState.Error;
        return false;
    }

    // Bind various objects and functions..
    this.JEngine.SetValue("print", new Action<object>(Console.WriteLine));
    this.JEngine.SetValue("TShock", new Dictionary<string, object>()
        {
            { "Players", TShock.Players }
        });

    // Execute the script..
    var addonPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Addons", name, name + ".js");
    var script = File.ReadAllText(addonPath);
    this.JEngine.Execute(script);

Afterward, I have a function for the addon to invoke any function which looks like this:

        public void InvokeEvent(string name)
        {
            try
            {
                if (this.JEngine == null || this.State == AddonState.Error)
                    return;

                var func = this.JEngine.GetValue(name);
                if (func.IsUndefined())
                    return;

                func.Invoke();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                this.State = AddonState.Error;
            }
        }

So in the end the script is only loaded once until the user requests that it is reparsed by reloading it. Otherwise it will not be reloaded at all.

There is about ~30 events that the plugin subscribes to that will land up calling various events inside of the Javascripts. The Update example I provided above has a Pre/Post event call setup so those are two events alone that are constantly being called. As well as a few others that are called very frequently, while others are less frequent.

ghost commented 10 years ago

This also allows for unloading/reloading of plugin scripts instead of having to hard-restart the server since .NET DLLs cannot be unloaded (unless they are ran in separate AppDomains etc.)

This may not help the issue at hand, but is information that could be of use.

Actually, @atom0s, there is a way around this. I am using a system to load DLLs as plugins in my current game, plugins can be reloaded at any time while the game is running.

Anyway, here's an example if you want to load both the DLL and the PDB -

byte[] dllBytes = File.ReadAllBytes(dllPath);
byte[] pdbBytes = File.ReadAllBytes(pdbPath);
Assembly asm = Assembly.Load(dllBytes, pdbBytes);

And then, here is all I do to load the plugins (though there is a little more, some JSON loading, loading other classes, etc.)

//Activate all "Plugin" classes within the assembly. Register each as a plugin.
for (int i = 0; i < Assemblies.Count; i++)
{
foreach (Type t in Assemblies[i].GetTypes())
{
    if (t.IsSubclassOf(typeof(Plugin)))
    {
        Plugins.Add(Activator.CreateInstance(t) as Plugin);
        break;
    }
}

}

As we are activating from a byte array, that is loaded in the memory, the DLL itself does not actually get "locked".

(I actually came here because I was considering using Jint for my dev console)

atom0s commented 10 years ago

@WillHuxtable loading the plugins are not the issue with the lag or performance. It is with invoking events across the .NET to Javascript bounds as well as using objects between the two languages. There is a massive performance hit on anything that is constantly called or used in excess since there is a constant back and forth conversion.

ghost commented 10 years ago

Yes, that is why I suggested loading DLLs. The performance should be better, once you get past the slow-ness of reflection (though it may well be faster than transferring to/from JS)

Loading the DLL as a byte array means that you can change the DLL, allowing you to reload at run time, meaning you may not actually have to use this. Then, you can be running proper .NET, perhaps avoiding this performance slowdown.

Though, to be fair, what I said doesn't really relate much to the issue, but may help you/others in the future.

Oh, and your Terraria stuff looks pretty cool, @atom0s!

atom0s commented 10 years ago

I have written a plugin system already that uses separate AppDomains for each module so that they can be fully unloaded at runtime and so on.

The issue still lies in the middle-man layer of the .NET < - > JS transforming of the managed objects though, there is such a huge hit in performance for anything that is called and used constantly. Granted, I don't think there is much that can be done to prevent the slow down given that the object data is dynamic and has to be rebuilt each time its used. Jint is definitely awesome for smaller scale things that are invoked on a minimal use base though. I would love to use it in a more intensive process but ultimately the performance hit is too much. This is also the case for other scripting languages like Lua in .NET as well though.

fatalerrorx commented 10 years ago

Problem is that the MethodInfo PropertyDescriptor is not cached in ObjectWrapper.

Very easy to do.

sebastienros commented 10 years ago

@fatalerrorx before you try anything, there is a PR which might fix that, I have submitted some change request before I can pull it.

sebastienros commented 6 years ago

I am closing this one as this should not be an issue anymore.