Как я могу использовать ScriptContext (или иным образом улучшить производительность)?
У меня есть решение ETL доморощенного. Слой преобразования определяется в файле конфигурации в скриплетах JavaScript, интерпретируемых движком Java Nashorn.
Я сталкиваюсь с проблемами производительности. Возможно, с этим ничего не поделаешь, но я надеюсь, что кто-то может найти проблему с тем, как я использую Nashorn, что помогает. Процесс многопоточный.
Я создаю один статический ScriptEngine, который используется только для создания объектов CompiledScript.
private static ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
Я компилирую скриптлеты, которые будут повторно выполняться для каждой записи, в объекты CompiledScript.
public static CompiledScript compile(Reader reader) throws ScriptException {
return ((Compilable) engine).compile(reader);
}
Есть две стандартные библиотеки JavaScript, которые также компилируются с использованием этого метода.
Для каждой записи создается ScriptContext, добавляются стандартные библиотеки, а значения записи устанавливаются как привязки.
public static ScriptContext getContext(List<CompiledScript> libs, Map<String, ? extends Object> variables) throws ScriptException {
SimpleScriptContext context = new SimpleScriptContext();
Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE);
for (CompiledScript lib : libs) {
lib.eval(context);
}
for (Entry<String, ? extends Object> variable : variables.entrySet()) {
bindings.put("$" + variable.getKey(), variable.getValue());
}
return context;
}
Затем контекст записи используется для преобразования записи и оценки фильтров, все с использованием CompiledScripts.
public static String evalToString(CompiledScript script, ScriptContext context) throws ScriptException {
return script.eval(context).toString();
}
Фактическое выполнение CompiledScripts против ScriptContext очень быстро, однако инициализация ScriptContexts очень медленная. К сожалению, по крайней мере, насколько я понимаю, это должно быть сделано для набора привязок. Если запись соответствует фильтру, то мне придется повторно перестроить контекст для той же записи, на этот раз с некоторыми дополнительными привязками из сопоставленного фильтра.
Кажется очень неэффективным повторное выполнение двух стандартных библиотек каждый раз, когда я создаю ScriptContext, однако я не нашел никакого поточного способа клонирования ScriptContext после того, как эти библиотеки были выполнены, но до добавления привязок. Также кажется очень неэффективным повторное выполнение двух стандартных библиотек и повторное присоединение всех привязок из записи, если она соответствует фильтру, но, опять же, я не нашел никакого поточного способа клонирования ScriptContext записи, чтобы добавить другую привязку к ней, не изменяя также оригинал.
Согласно jvisualvm, большая часть времени моей программы проводится в
jdk.internal.dynalink.support.AbstractRelinkableCallSite.initialize() (70%)
jdk.internal.dynalink.ChainedCallSite.relinkInternal() (14%)
Я был бы признателен за любую информацию о Nashorn, которая может помочь повысить производительность для этого варианта использования. Спасибо.
1 ответ
Мне удалось добиться успеха с помощью ThreadLocal, чтобы избежать перекрестных разговоров. Это запускает 1 000 000 тестов для обнаружения перекрестных помех и не находит ни одного. Это изменение означает, что я создаю ~4 объекта ScriptContext вместо примерно 8 000 000.
package com.foo;
import java.util.UUID;
import java.util.stream.Stream;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
public class Bar {
private static ScriptEngine engine;
private static CompiledScript lib;
private static CompiledScript script;
// Use ThreadLocal context to avoid cross-talk
private static ThreadLocal<ScriptContext> context;
static {
try {
engine = new ScriptEngineManager().getEngineByName("JavaScript");
lib = ((Compilable) engine)
.compile("var firstChar = function(value) {return value.charAt(0);};");
script = ((Compilable) engine).compile("firstChar(myVar)");
context = ThreadLocal.withInitial(() -> initContext(lib));
} catch (ScriptException e) {
e.printStackTrace();
}
}
// A function to initialize a ScriptContext with a base library
private static ScriptContext initContext(CompiledScript lib) {
ScriptContext context = new SimpleScriptContext();
try {
lib.eval(context);
} catch (ScriptException e) {
e.printStackTrace();
}
return context;
}
// A function to set the variable binding, evaluate the script, and catch
// the exception inside a lambda
private static String runScript(CompiledScript script,
ScriptContext context, String uuid) {
Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.put("myVar", uuid);
String result = null;
try {
result = ((String) script.eval(context));
} catch (ScriptException e) {
e.printStackTrace();
}
return result;
}
// The driver function which generates a UUID, uses Nashorn to get the 1st
// char, uses Java to get the 1st char, compares them and prints mismatches.
// Theoretically if there was cross-talk, the variable binding might change
// between the evaluation of the CompiledScript and the java charAt.
public static void main(String[] args) {
Stream.generate(UUID::randomUUID)
.map(uuid -> uuid.toString())
.limit(1000000)
.parallel()
.map(uuid -> runScript(script, context.get(), uuid)
+ uuid.charAt(0))
.filter(s -> !s.substring(0, 1).equals(s.substring(1, 2)))
.forEach(System.out::println);
}
}