首页 > 解决方案 > 如何使用预编译脚本确定 Nashorn 性能缓慢或瓶颈的根本原因

问题描述

我在 Nashorn 的表现有些迟缓,我无法真正解释原因。我将详细说明我的设置是什么以及我是如何尝试调试它的。

硬件:相当不错的服务器硬件('13 时代 - 12 核 Xeon,2.1GHz)。64GB DDR3 内存。

软件:Oracle JDK8(最新的 64 位)(预先分配给 JVM 的 40GB RAM)。

我的实现是:多个 Nashorn ScriptEngine 实例,每个实例都有一个预编译的“utility.js”,它提供了一些用户定义的脚本可以使用的辅助函数。

我有一个 ScriptEngine 对象池,所有这些对象都已准备好与已针对它们编译的 utility.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(以及上面提到的所有预编译的东西)将需要很长时间才能执行,同时显示很少的 CPU 使用(总共 8-10%)并且在 jmc/jvisualvm 中没有重大阻塞。线程将显示他们已经阻塞了相当多的数量(计数明智),但是切片太小了,我在点击线程时永远看不到它们。

大多数时候,当我单击线程时,它们都显示它们位于 MethodHandleNatives.setCallSiteTargetNormal 中。

我尝试了几件事: 1. 单一引擎,不同的上下文。即使它都是预编译的,我也可以在我的线程之间看到阻塞。线程会等待(因为它们应该),然后再根据我的判断调用各个字节码片段。这不是一个可行的解决方案。

  1. 尝试在用户脚本中内联一堆函数(大多数但不是全部),这仍然没有增加 CPU 使用率,并且大多数线程仍在 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());
    }
}

我对此的期望是,如果我的线程池耗尽了机器上的可用资源并显示出低性能,那么 CPU 使用率将是 100%,但相反我看到的是低 CPU 和低性能 :( 我不能非常确定我在这里出错的地方,或者为什么它这么慢而没有任何明显的资源消耗。

在从 JVisualVM 获取堆栈跟踪时,我刚刚注意到的一件事是,我的所有线程似乎都出现了这种情况:我允许用户定义的 Java 脚本调用实用程序.js 函数,该函数本质上是“执行另一个脚本”,即堆栈跟踪似乎都是从这个嵌套调用到另一个脚本。在我的设置中,它将再次使用相同的线程和来自线程的相同引擎以及新的上下文。我认为这将与以前相同并且不需要进一步编译?

我已经看过的相关文章: JavaScript 中的匿名函数和内联函数有什么区别?Nashorn 效率低下

编辑:深入研究这一点,主要是当 eval() 从编译脚本内部发生时,但并非总是如此,关于特定情况的某些事情必须使它无法在不调用 setTarget() 的情况下直接重新调用,这最终会花费更多时间。

有趣的是,当线程对本机 JVM 方法进行这些调用时,它们并没有显示它们正在阻塞,因此很难看出我查看过的每个工具的时间都花在了哪里。

标签: javaperformancenashorn

解决方案


推荐阅读