Performance of the compiled vs. interpreted javascript in java7 / Rhino

Xtra Coder picture Xtra Coder · Jan 26, 2013 · Viewed 15.2k times · Source

I have a problem with performance of Rhino javascript engine in Java7, shortly - my script (that parses and compiles texts) runs in Chrome around 50-100 times quicker than the same in Java7 Rhino script engine.

I was trying to find the way to improve situation and have found that Rhino supports compilation of scripts. I tried doing it with my scripts and actually did not see any improvement. Finally - i ended up with a dummy short test suite where i do not see any difference in performance between compiled and interpreted versions. Please let me know what I'd doing wrong.

Note: some sources mention that Rhino engine runs compiled script roughly 1.6 slower than the "same" code written directly in Java. Not sure if "compilation of script" used in this sample the is same one which is supposed there.

Test java class is below and sample result I'm getting from it on my machine ...

Results

     Running via com.sun.script.javascript.RhinoScriptEngine@c50443 ... 
      time: 886ms, chars: 38890, sum: 2046720
      time: 760ms, chars: 38890, sum: 2046720
      time: 725ms, chars: 38890, sum: 2046720
      time: 765ms, chars: 38890, sum: 2046720
      time: 742ms, chars: 38890, sum: 2046720
       ... 3918ms


     Running via com.sun.script.javascript.RhinoCompiledScript@b5c292 @ com.sun.script.javascript.RhinoScriptEngine@f92ab0 ... 
      time: 813ms, chars: 38890, sum: 2046720
      time: 805ms, chars: 38890, sum: 2046720
      time: 812ms, chars: 38890, sum: 2046720
      time: 834ms, chars: 38890, sum: 2046720
      time: 807ms, chars: 38890, sum: 2046720
       ... 4101ms

Update after comment from Anon-Micro:

After wrapping invocation of the JavaScript eval() and compile() in test class into ...

import sun.org.mozilla.javascript.internal.Context;
try {
    Context cx = Context.enter();

    cx.setOptimizationLevel(9);
    cx.setLanguageVersion(170);

    ...
}
finally {
    Context.exit();
}

result changed signigicantly - from average 1.8 (in new version of test class) sec to ~150msec. However instance of the doTest() function extracted from ScriptEngine loaded via (CompiledScript = Compilable.compile()).eval(Bindings) -> Bindings.get("doTest") still says it is sun.org.mozilla.javascript.internal.InterpretedFunction and its performance is slightly worse (around 10%) than version of JS loaded from pre-compiled bytecode (by Rhino 1.7r4) - so i'm still not sure what is actually happening behind the scene.

1800ms - ScriptEngine.eval(), Optimization Level = default(-1?)
1758ms - CompiledScript, Optimization Level = default(-1?)
 165ms - ScriptEngine.eval(), Optimization Level = 9
 132ms - CompiledScript, Optimization Level = 9
 116ms - compiled by Rhino 1.7r4 into bytecode class

PS: sun.org.mozilla.javascript.internal.Context within internal sun's package looks to be a weird design for me - 'internal' denotes this class is assumed not to be used by developers and therefor there is not 'certified' way to manipulate optimization level of JS evaluator in Java 7.

Test class (updated, doTestCompiled is loaded from external *.class)

import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.SimpleScriptContext;
import sun.org.mozilla.javascript.internal.Context;
import sun.org.mozilla.javascript.internal.Scriptable;
import sun.org.mozilla.javascript.internal.Function;

public class RhinoPerfTest4 {

    final static ScriptEngineManager scm = new ScriptEngineManager();
    final static String TEST_SCRIPT1 =
            "function doTest() {\n"
            + "    var scale = 5000, i, a = [], str, l, sum = 0,\n"
            + "        start = (new Date()).getTime(), end;\n"
            + "    for( i = 0; i < scale; i++ )\n"
            + "        a.push(\"\" + i);\n"
            + "    str = a.join(\"\");\n"
            + "    l = str.length;\n"
            + "    for( i = 0; i < l; i++ ) {\n"
            + "        var c = str.charCodeAt(i);\n"
            + "        if( c > 0)\n"
            + "            sum += c;\n"
            + "    }\n"
            + "    end = (new Date()).getTime();\n"
            + "\n"
            + "    // print(\" time: \" + (end - start) "
            + "          + \"ms, chars: \" + l "
            + "          + \", sum: \" + sum + \"\\n\");\n"
            + "}\n";
    final static String TEST_SCRIPT2 =
            "function doTest() {\n"
            + "    var a = [], i;\n"
            + "    for( i = 0; i < 500; i++ ) a.push(1);\n"
            + "}\n";

    static class TestSet {

        public int nCycles;
        public String script;

        public TestSet(int nCycles, String script) {
            this.nCycles = nCycles;
            this.script = script;
        }
    }
    static TestSet set1 = new TestSet(5, TEST_SCRIPT1);
    static TestSet set2 = new TestSet(500, TEST_SCRIPT2);

    public static void main(String[] args) throws Exception {
        ScriptEngine se;
        int i;
        long ts, te;
        TestSet set = set1;
        Object noArgs[] = new Object[]{};

        try {
            org.mozilla.javascript.Context mctx = org.mozilla.javascript.Context.enter();

            se = scm.getEngineByExtension("js");
            doTestCompiled doTestPreCompiled = new doTestCompiled();
            org.mozilla.javascript.Scriptable scope = mctx.initStandardObjects();

            doTestPreCompiled.call(mctx, scope, scope, null);
            org.mozilla.javascript.Function doTest = 
                    (org.mozilla.javascript.Function)scope.get("doTest", null);

            for( int nHotSpot = 0; nHotSpot < 5; nHotSpot++ ) {
                if( nHotSpot > 0 )
                    Thread.sleep(500);

                ts = System.currentTimeMillis();
                for( i = 0; i < set.nCycles; i++ ) {
                    doTest.call(mctx, scope, null, null);
                }
                te = System.currentTimeMillis();
                System.out.println("  " + nHotSpot + ": " + (te - ts) + "ms");
            }
        }
        finally {
            org.mozilla.javascript.Context.exit();
        }


        for( int nOpt = 0; nOpt < 2; nOpt++ ) {
            if( nOpt > 0 )
                Thread.sleep(500);

            Context cx = null;

            try {
                System.out.println("Cycle: " + nOpt);

                cx = Context.enter();
                if( nOpt > 0 ) {
                    System.out.println("OptLevel: " + 9);
                    cx.setOptimizationLevel(9);
                    cx.setLanguageVersion(170);
                }

                se = scm.getEngineByExtension("js");
                se.eval(set.script);
                System.out.println("\nRunning via " + se + " ... ");

                Invocable invocable = (Invocable) se;

                for( int nHotSpot = 0; nHotSpot < 5; nHotSpot++ ) {
                    if( nHotSpot > 0 )
                        Thread.sleep(500);

                    ts = System.currentTimeMillis();
                    for( i = 0; i < set.nCycles; i++ ) {
                        invocable.invokeFunction("doTest", noArgs);
                    }
                    te = System.currentTimeMillis();
                    System.out.println("  " + nHotSpot + ": " + (te - ts) + "ms");
                }

                se = scm.getEngineByExtension("js");
                Compilable cse = (Compilable) se;
                CompiledScript cs = cse.compile(set.script/* + "(doTest())"*/);
                Scriptable scope = cx.initStandardObjects();

                ScriptContext scriptContext = new SimpleScriptContext();
                Bindings vars = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);

                cs.eval(vars);

                Object odoTest = scriptContext.getAttribute("doTest");
                Function doTest = (Function) vars.get("doTest");

                System.out.println("\nRunning via " + cs + " @ " + se + " ... ");

                for( int nHotSpot = 0; nHotSpot < 5; nHotSpot++ ) {
                    if( nHotSpot > 0 )
                        Thread.sleep(500);

                    ts = System.currentTimeMillis();
                    for( i = 0; i < set.nCycles; i++ ) {
                        doTest.call(cx, scope, null, noArgs);
                    }
                    te = System.currentTimeMillis();
                    System.out.println("  " + nHotSpot + ": " + (te - ts) + "ms");
                }

            }
            finally {
                if( cx != null )
                    Context.exit();
            }
        }
    }
}

Answer

Anon-Micro picture Anon-Micro · Mar 10, 2013

The Rhino engine is actually capable of compiling scripts into bytecode 'in-process', so you don't need to run the tool to generate .class files first. You only need to set 'optimisation level' and the engine will automatically pre-compile the script before executing it. One way to override the optimisation level is with the VM argument -Drhino.opt.level. Set this to anything between 0 and 9 and run your original test program and you should see better performance.

This is the same optimisation setting used by the compiling tool you mentioned, by the way. https://developer.mozilla.org/en-US/docs/Rhino/Optimization

For total control of optimisation level and javascript version within your program, you can do the following. However you lose some of the trimmings that RhinoScriptEngine class (which is just a environment wrapper and not the javascript engine) provides. One such trimming is the 'print' function which is actually injected by said wrapper. For test purposes you can just replace 'print' with 'java.lang.System.out.print'.

    int optimisationLevel = 3;
    int languageVersion = Context.VERSION_1_7;

    try {
        Context cx = Context.enter();
        cx.setOptimizationLevel(optimisationLevel);
        cx.setLanguageVersion(languageVersion);

        ImporterTopLevel scope = new ImporterTopLevel(cx);
        cx.evaluateString(scope, TEST_SCRIPT1, "doTest", 1, null);

        for (int i = 0; i < 10; i++)
            cx.evaluateString(scope, "doTest();", "", 1, null);

    } finally {
        Context.exit();
    }

You mentioned the following:

Note: some sources mention that Rhino engine runs compiled script roughly 1.6 slower than the "same" code written directly in Java. Not sure if "compilation of script" used in this sample the is same one which is supposed there.

I'd be interested in the source that reported this, My fibonacci function in java takes about 1/30 the time as the compiled js implementation. But perhaps I'm missing something.