How make Java 8 Nashorn fast?

KajMagnus picture KajMagnus · Oct 25, 2014 · Viewed 11.2k times · Source

I'm using Java 8 Nashorn to render CommonMark to HTML server side. If I compile and cache and reuse a CompiledScript, a certain page takes 5 minutes to render. However, if I instead use eval, and cache and reuse the script engine, rendering the same page takes 3 seconds.

Why is CompiledScript so slow? (sample code follows)

What's a good approach for running Javascript code in Nashorn, over and over again as quickly as possible? And avoiding compiling the Javascript code more than once?

This is the server side Scala code snippet that calls Nashorn in a way that takes 5 minutes: (when run 200 times; I'm compiling many comments from CommonMark to HTML.) (This code is based on this blog article.)

if (engine == null) {
  val script = scala.io.Source.fromFile("public/res/remarkable.min.js").mkString
  engine = new js.ScriptEngineManager(null).getEngineByName("nashorn")
  compiledScript = engine.asInstanceOf[js.Compilable].compile(s"""
    var global = this;
    $script;
    remarkable = new Remarkable({});
    remarkable.render(__source__);""");
}
engine.put("__source__", "**bold**")
val htmlText = compiledScript.eval()

Edit Note that the $script above is reevaluated 200 times. I did test a version that evaluated it only once, but apparently then I wrote some bug, because the only-once version wasn't faster than 5 minutes, although it should have been one of the fastest ones, see Halfbit's answer. Here's the fast version:

...
val newCompiledScript = newEngine.asInstanceOf[js.Compilable].compile(s"""
  var global;
  var remarkable;
  if (!remarkable) {
    global = this;
    $script;
    remarkable = new Remarkable({});
  }
  remarkable.render(__source__);""")
...

/Edit

Whereas this takes 2.7 seconds: (when run 200 times)

if (engine == null) {
  engine = new js.ScriptEngineManager(null).getEngineByName("nashorn")
  engine.eval("var global = this;")
  engine.eval(new jio.FileReader("public/res/remarkable.min.js"))
  engine.eval("remarkable = new Remarkable({});")
}
engine.put("source", "**bold**")
val htmlText = engine.eval("remarkable.render(source)")

I would actually have guessed that the CompiledScript version (the topmost snippet) would have been faster. Anyway, I suppose I'll have to cache the rendered HTML server side.

(Linux Mint 17 & Java 8 u20)

Update:

I just noticed that using invokeFunction at the end instead of eval is almost twice as fast, takes only 1.7 seconds. This is roughly as fast as my Java 7 version that used Javascript code compiled by Rhino to Java bytecode (as a separate and complicated step in the build process). Perhaps this is as fast as it can get?

if (engine == null) {
  engine = new js.ScriptEngineManager(null).getEngineByName("nashorn")
  engine.eval("var global = this;")
  engine.eval(new jio.FileReader("public/res/remarkable.min.js"))
  engine.eval("remarkable = new Remarkable({});")
  engine.eval(
    "function renderCommonMark(source) { return remarkable.render(source); }")
}
val htmlText = engine.asInstanceOf[js.Invocable].invokeFunction(
                                       "renderCommonMark", "**bold1**")

Answer

halfbit picture halfbit · Oct 26, 2014

The variant of your code which uses CompiledScript seems to re-evaluate remarkable.min.js 200 times - while your eval based version does this once. This explains the huge difference in runtimes.

With just the remarkable.render(__source__) precompiled, the CompiledScript based variant is slightly faster than the eval and invokeFunction based ones (on my machine, Oracle Java 8u25).