Dynamic Code

I am dealing with a problem where I have to compute the value of boolean expressions like the below:

(c1 && c2) || (c1 && c2) || (c1 && c3) || (c1 && c3)

c1, c2. c3 are boolean. The expression can be any arbitrary expression provided as a text by user, the number of argument i.e. I am using java, so to evaluate this dynamic expression I can use the script engine. Another wild idea is to use ANTR4, which I am not going in use. I would evaluate two options in the terms of speed performance.

1. Using scala's code generation technique.

2. Use Script engine like Nashorn. I could also try Scala script engine.


Create a scala project with the following dependency

name := "CodeGenerator"
version := "0.1"
scalaVersion := "2.11.12"
libraryDependencies += "org.scala-lang" % "scala-compiler" % "2.11.12"


Define a class - Compiler (!not a good name). I would call this call from Java to compile a dynamic expression.

import scala.reflect.runtime.universe._
import scala.tools.reflect.ToolBox

import scala.collection.JavaConverters._

class Compiler {

  def compile[A](code: String): (Map[String, Any]) => A = {
    val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
    val tree = tb.parse(
      s"""
         |def wrapper(context: Map[String, Any]): Any = {
         |  $code
         |}
         |wrapper _
      """.stripMargin)
    val f = tb.compile(tree)
    val wrapper = f()
    wrapper.asInstanceOf[Map[String, Any] => A]
  }

  def convertArgs(args: java.util.Map[String, Object]) ={
    args.asScala.toMap
  }

}

Build a jar out of this project and use it in the next part. Let's call the output jar ScalaCompiler.jar.


Create a new java project and add ScalaCompiler.jar as dependency.

Here is the full code of the java class to test the above scala Compiler class.


import jdk.nashorn.api.scripting.ScriptObjectMirror;
import scala.Function1;
import scala.collection.immutable.Map;

import javax.script.*;
import java.util.*;

public class CompileTestApp {


    private static List<java.util.Map<String, Object>> generateRandomValues(){
        final Random random = new Random();
        final List<java.util.Map<String, Object>> result = new ArrayList<>();
        for(int i=0;i<1000*1000*10;++i) {
            java.util.Map<String, Object> map = new java.util.HashMap<>();
            map.put("c1", random.nextBoolean());
            map.put("c2", random.nextBoolean());
            map.put("c3", random.nextBoolean());
            result.add(map);
        }
        return result;
    }

    public static void testScala(List<java.util.Map<String, Object>> maps){
        com.avalanchio.codegen.Compiler compiler = new com.avalanchio.codegen.Compiler();

        final List<scala.collection.immutable.Map<String, Object>> scalaMaps = new ArrayList<>();
        for (java.util.Map<String, Object> map : maps) {
            scalaMaps.add(compiler.convertArgs(map));
        }

        String expression = "(c1 && c2) || (c1 && c2) || (c1 && c3) || (c1 && c3)";
        for(int i=1;i<=3;++i) {
            expression = expression.replaceAll("c" + i, "context(\"c" + i + "\").asInstanceOf[Boolean]");
        }
        final long millis = System.currentTimeMillis();
        final Function1<Map<String, Object>, Object> compiled = compiler.compile(expression);
        System.out.println("Scala compile time: " + (System.currentTimeMillis() - millis));

        final long start = System.currentTimeMillis();
        for (scala.collection.immutable.Map<String, Object> scalaMap : scalaMaps) {
            final Object apply = compiled.apply(scalaMap);
        }
        final long duration = System.currentTimeMillis() - start;
        System.out.println("Scala duration: " + duration);
    }

    public static void testNashorn(List<java.util.Map<String, Object>> maps) throws ScriptException {
        ScriptEngine engine = new ScriptEngineManager().getEngineByName( "Nashorn" );
        final Compilable compilable = (Compilable) engine;
        String javaScript = "function evalBoolean(arg){" +
                "var c1 = arg.get('c1');" +
                "var c2 = arg.get('c2');" +
                "var c3 = arg.get('c3');" +
                "return (c1 && c2) || (c1 && c2) || (c1 && c3) || (c1 && c3)" +
                "};";
        javaScript += "function(){return {'evalBoolean': evalBoolean}}";
        final CompiledScript compiledScript = compilable.compile(javaScript);
        final ScriptObjectMirror scriptObjectMirror = (ScriptObjectMirror) compiledScript.eval();

        final ScriptObjectMirror functionTable = (ScriptObjectMirror) scriptObjectMirror.call(null);
        final String[] functionNames = functionTable.getOwnKeys(true);
        System.out.println( "Function names: " + Arrays.toString( functionNames ) );

        final long start = System.currentTimeMillis();
        for (java.util.Map<String, Object> map : maps) {
            final Object o = functionTable.callMember("evalBoolean", map);
        }
        System.out.println("Nashorn: " + (System.currentTimeMillis() - start));
    }

    public static void main(String... args) throws ScriptException {
        final List<java.util.Map<String, Object>> maps = generateRandomValues();
        testScala(maps);
        testNashorn(maps);
    }

}


Here is the outcome. For 10 million set of values for (c1, c2, c3), scala option ~13 times faster that nashorn option.

Scala compile time: 63969

Scala duration: 743 ms

Nashorn duration: 10153 ms


Future work:

Rather than creating a seperate scala project, scala code could be used using Scala script engine. Test the performance using Scala script engine. Similar test to be performance for jython engine.