У меня медлительное выступление в Нэшорне, которое я не могу объяснить, по какой причине.Я подробно опишу мои настройки и то, как я пытался их отладить.
Аппаратное обеспечение: довольно приличное серверное оборудование (эпоха 13 года - 12-ядерный Xeon, 2,1 ГГц).64 ГБ ОЗУ DDR3.
Программное обеспечение: Oracle JDK8 (последняя 64-разрядная версия) (40 ГБ ОЗУ предварительно выделено для JVM).
Моя реализация: несколько экземпляров Nashorn ScriptEngine, каждый с предустановленнымскомпилированный "utility.js", который предоставляет некоторые вспомогательные функции, которые могут использовать пользовательские сценарии.
У меня есть пул объектов ScriptEngine, готовых к работе с уже скомпилированной утилитой .js и распределителем потоковкоторый будет раскручивать потоки до установленного предела.Каждый поток будет захватывать предварительно выделенный ScriptEngine и пересылать ему пользовательский JS с использованием нового контекста и выполнять его / сохранять где-либо перед возвратом ScriptEngine в пул.Все это прекрасно работает, и если мой пользовательский сценарий довольно прост (одна функция), он невероятно быстр.
Однако большинство пользовательских сценариев довольно большие и имеют вид:
function myFunc() {
myFunc1();
myFunc2();
... (you get the picture, they define and call a lot of functions!)
myFunc100();
}
function myFunc1() {
// do something simple here
}
Когдаработать параллельно, скажем, с 25 потоками одновременно, и каждый со своим собственным ScriptEngine (и всеми выше скомпилированными объектами, упомянутыми выше) будет выполняться очень долго, пока не будет загружен процессор (всего 8-10%) инет серьезных блокировок в jmc / jvisualvm.Потоки покажут, что они заблокировали изрядное количество (в пересчете на счет), но срезы настолько малы, что я никогда их не увижу при просмотре потоков.
Большую часть времени, когда я нажимаю на темывсе они показывают, что находятся в MethodHandleNatives.setCallSiteTargetNormal.
Я пробовал несколько вещей: 1. Один движок, разные контексты.Я мог видеть блокировку между моими потоками, хотя все это было предварительно скомпилировано.Потоки будут ждать (как они должны), прежде чем вызвать отдельные фрагменты байт-кода из того, что я мог сказать.Это не жизнеспособное решение.
Попытка встроить ряд функций (большинство, но не все) в пользовательские сценарии, это все еще не увеличивало загрузку ЦП, и большинство потоков все еще находились в MethodHandleNatives.setCallSiteTargetNormal.Даже встроенные функции все еще, кажется, направляются в MethodHandleNatives.setCallSiteTargetNormal, если я проверял следы стека.
Вот как я создаю ScriptEngines и предварительно наполняю их «utility.js» (код, гдеЯ вставляю их в пул, поэтому для краткости я его опускаю):
/**
* Creates a PreCompiledScriptEngine which will contain a ScriptEngine + Pre-compiled utility.js
*/
private PreCompiledScriptEngine createScriptEngine() {
String source = new Scanner(this.getClass().getClassLoader().getResourceAsStream(UTILITY_SCRIPT)).useDelimiter("\\Z").next();
try {
totalEngines.getAndAdd(1);
ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine();
return new PreCompiledScriptEngine(engine, ((Compilable) engine).compile(source));
}
catch (ScriptException e) {
Logger.error(e);
}
return null;
}
/**
* Small helper class to group a ScriptEngine and a CompiledScript (of utility.js) together
*/
public class PreCompiledScriptEngine {
private ScriptEngine scriptEngine;
private CompiledScript compiledScript;
PreCompiledScriptEngine(ScriptEngine scriptEngine, CompiledScript compiledScript) {
this.scriptEngine = scriptEngine;
this.compiledScript = compiledScript;
}
public ScriptEngine getScriptEngine() {
return scriptEngine;
}
/**
* This method will return the utility.js compiled runtime against our engine.
*
* @return CompiledScript version of utility.js
*/
public CompiledScript getCompiledScript() {
return compiledScript;
}
}
И вот как я выполняю специфичный для пользователя JavaScript:
public Object executeUserScript(String script, String scriptFunction, Object[] parameters) {
try {
// Create a brand new context
PreCompiledScriptEngine preCompiledScriptEngine = obtainFromMyScriptEnginePool();
ScriptEngine engine = preCompiledScriptEngine.getScriptEngine();
ScriptContext context = new SimpleScriptContext();
context.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
// Evaluate the pre-compiled utility.js in our new context
preCompiledScriptEngine.getCompiledScript().eval(context);
// Evaluate the specific user script in this context too
engine.eval(script, context);
//get the JS function the user wants to call
JSObject jsObject = (JSObject) context.getAttribute(scriptFunction, ScriptContext.ENGINE_SCOPE);
// Call the JS function with the parameters
return jsObject.call(null, parameters);
}
catch (ScriptException e) {
Logger.error("generated", e);
throw new RuntimeException(e.getMessage());
}
}
Что я ожидаю от этого, так этоэта загрузка процессора составила бы 100%, если бы мой пул потоков исчерпал ресурсы, доступные на машине, и показал низкую производительность, но вместо этого я вижу низкую загрузку процессора и низкую производительность :( Я не могу точно определить, где я ошибаюсьздесь или почему он такой медленный без очевидной потери ресурсов.
Одна вещь, которую я только что заметил, собираясь получить трассировку стека из JVisualVM, заключается в том, что все мои потоки, по-видимому, демонстрируют этот сценарий: я разрешаю определенному пользователемJava Script для вызова функции utility.js, которая по сути «выполняет другой скрипт», стек отслеживает всеГруша должна быть из этого вложенного вызова в другой скрипт.В моей настройке он будет использовать тот же поток и тот же движок из потока снова с новым контекстом.Я думаю, что это будет так же, как и раньше, и не потребует дальнейшей компиляции?
Статьи по теме, на которые я уже смотрел: В чем разница между анонимными и встроенными функциями в JavaScript? и Нэшорн неэффективность
Редактировать: Если углубиться в это, то в основном это происходит, когда eval () происходит изнутри скомпилированного скрипта, но не всегда, что-то в конкретных случаях должно делать этоневозможно просто вызвать напрямую без вызова setTarget (), что в итоге занимает больше времени.
Интересно то, что потоки не показывают, что они блокируют, когда они делают эти вызовы родным методам JVM, поэтому трудно понять, на что тратится время в каждом инструменте, на который я смотрел.