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.