java - 如何使用预编译脚本确定 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. 单一引擎,不同的上下文。即使它都是预编译的,我也可以在我的线程之间看到阻塞。线程会等待(因为它们应该),然后再根据我的判断调用各个字节码片段。这不是一个可行的解决方案。
- 尝试在用户脚本中内联一堆函数(大多数但不是全部),这仍然没有增加 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 方法进行这些调用时,它们并没有显示它们正在阻塞,因此很难看出我查看过的每个工具的时间都花在了哪里。
解决方案
推荐阅读
- html - 你能把一个块引用放在另一个块引用中吗?
- x86 - 8086 复位向量高于 20 位,总线为 20 位
- c++ - 多线程中是否可能读取对象的一半大小?
- javascript - create a session using php and ajax
- task - freertos subpriority of xSemaphoreTake() within same priority tasks
- r - MLR: Extracting the names of the covariates with non-zero coefficients in CoxBoost
- null - Impala - replace NULL to zero for entire table
- linux - 如何在 GNU/Linux cli 上使用 find regex 排除新行(回车)?
- reactjs - 使用 Switch 标签后 React-Router 不起作用
- python - 字符串比较的时间复杂度