Как определить основную причину вялой производительности Nashorn или узкого места с помощью предварительно скомпилированных скриптов - PullRequest
1 голос
/ 23 марта 2019

У меня медлительное выступление в Нэшорне, которое я не могу объяснить, по какой причине.Я подробно опишу мои настройки и то, как я пытался их отладить.

Аппаратное обеспечение: довольно приличное серверное оборудование (эпоха 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, поэтому трудно понять, на что тратится время в каждом инструменте, на который я смотрел.

...