如何通过预编译的脚本确定Nashorn性能缓慢或瓶颈的根本原因

问题描述 投票:1回答:1

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

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

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

我的实现是:多个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。

这是创建ScriptEngine并使用“ 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脚本调用Utility.js函数,该函数本质上是“执行另一个脚本” ,所有堆栈跟踪似乎都是从此嵌套调用到另一个脚本的。在我的设置中,它将使用相同的线程,并使用新上下文再次使用该线程中的相同引擎。我认为这与以前相同,不需要进一步编译?

我已经看过的相关文章:What is the difference between anonymous and inline functions in JavaScript?Nashorn inefficiency

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

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

java performance nashorn
1个回答
0
投票

我在具有16个VCPU VM的2018年老式硬件上看到类似的问题。

我们的症状是:

  • 最多可以将每个服务器的负载增加到15个模拟繁忙用户
  • 此时,吞吐量达到峰值,CPU约为16 VCPU的50%
  • 然后我们增加到50个模拟的繁忙用户,在此期间响应时间减少,吞吐量保持平稳,并且CPU使用率保持在50%左右
  • 我们看不到应用程序中的任何限制或约束
  • ThreadDumps和配置文件显示了Nashorn,线程评估和setCallSiteTargetNormal中的大量活动

希望这对某人有帮助-如果有人有见识,请回复。

© www.soinside.com 2019 - 2024. All rights reserved.