javascriptjavascriptingjshell

How do I get better performance out of JShell?


Background:

Since Nashorn is being dropped in JDK15, I am looking for an alternative for an application I am working on. I am currently only using for the dynamic execution of some user-definable formatting snippets within a java swing desktop application.

I don't particularly want to add another library dependency to my app (like rhino). I would be willing to use nashorn as an additional dependency if it is available (this would save me from having to rewrite code, and ensure compatibility with existing js snippets). I haven't seen that it is available anywhere except as something that was related to minecraft.

I won't switch to Graal vm.

The problem:

I was considering using JShell (though not javascript, much of the formatting code is very similar), but the performance is abysmal the way I am calling it:

try(JShell js = JShell.create())
{
    js.eval("public int add(int a, int b) { return a + b; }");
    for(int i = 0; i < 100; i++)
    {
        List<SnippetEvent> eval = js.eval("add(5,6)");
        eval.forEach(se -> {
            System.out.println(se.value());
        });
    }
}

The for loop in the that code is taking ~6 seconds to run (compared to ~11 microseconds in nashorn). This will not be fast enough for my application.


Solution

  • I stumbled across the same performance issue today and I think I found a solution. It boils down to the following four steps:

    1. starting the JShell inside the currently running JVM using the LocalExecutionControlProvider such that they share the same class loader
    2. defining a static field inside a class that serves as a "shared variable" between the JShell and our "normal" Java code
    3. overwriting the static field inside the JShell with a newly constructed lambda function
    4. storing the reference to the constructed function in a variable inside our "normal" Java code (necessary if we want to construct multiple functions via this approach)

    The following code snippet implements this approach for your example. On my machine, it takes approx. 350ms for the initial function construction via JShell and approx. 50ms for the 10_000 subsequent function calls.

    package app;
    
    import jdk.jshell.JShell;
    import jdk.jshell.execution.LocalExecutionControlProvider;
    import org.junit.jupiter.api.Test;
    
    import java.util.function.BiFunction;
    
    public class Debug {
    
        public static BiFunction<Integer, Integer, Integer> function = null;
    
        @Test
        public void debug() {
            JShell jShell = JShell.builder()
                    .in(System.in).out(System.out).err(System.err)
                    .executionEngine(new LocalExecutionControlProvider(), null)
                    .build();
            jShell.eval("app.Debug.function = (a,b) -> a+b;");
            BiFunction<Integer, Integer, Integer> theFunction = Debug.function;
            for (int a = 0; a < 100; a++) {
                for (int b = 0; b < 100; b++) {
                    assert theFunction.apply(a, b) == a + b;
                }
            }
        }
    }