[jsscripting] Cache openhab-js injection to improve performance (#14135)
* [jsscripting] Extend comments for wraprequire * [jsscripting] Enable openhab-js caching to improve performance On my dev system (which I guess is much more powerful than most openHAB servers), cached openhab-js injection takes 100-200 ms. openhab-js injection from file system takes about 1000 ms. * [jsscripting] Update configuration language * [jsscripting] Upgrade openhab-js version to 3.2.1 for required webpack changes Documentation updates will follow in another PR to keep this one clean. Signed-off-by: Florian Hotze <florianh_dev@icloud.com>pull/14141/head
parent
12bd7c99c3
commit
3c669ad77a
|
@ -24,7 +24,7 @@
|
||||||
</bnd.importpackage>
|
</bnd.importpackage>
|
||||||
<graal.version>22.0.0.2</graal.version> <!-- DO NOT UPGRADE: 22.0.0.2 is the latest version working on armv7l / OpenJDK 11.0.16 & armv7l / Zulu 17.0.5+8 -->
|
<graal.version>22.0.0.2</graal.version> <!-- DO NOT UPGRADE: 22.0.0.2 is the latest version working on armv7l / OpenJDK 11.0.16 & armv7l / Zulu 17.0.5+8 -->
|
||||||
<oh.version>${project.version}</oh.version>
|
<oh.version>${project.version}</oh.version>
|
||||||
<ohjs.version>openhab@3.1.2</ohjs.version>
|
<ohjs.version>openhab@3.2.1</ohjs.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -45,7 +45,7 @@ import com.oracle.truffle.js.scriptengine.GraalJSEngineFactory;
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
|
public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
|
||||||
private static final String CFG_INJECTION_ENABLED = "injectionEnabled";
|
private static final String CFG_INJECTION_ENABLED = "injectionEnabled";
|
||||||
private static final String INJECTION_CODE = "Object.assign(this, require('openhab'));";
|
private static final String CFG_USE_INCLUDED_LIBRARY = "useIncludedLibrary";
|
||||||
|
|
||||||
private static final GraalJSEngineFactory factory = new GraalJSEngineFactory();
|
private static final GraalJSEngineFactory factory = new GraalJSEngineFactory();
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectionEnabled = true;
|
private boolean injectionEnabled = true;
|
||||||
|
private boolean useIncludedLibrary = true;
|
||||||
|
|
||||||
private final JSScriptServiceUtil jsScriptServiceUtil;
|
private final JSScriptServiceUtil jsScriptServiceUtil;
|
||||||
private final JSDependencyTracker jsDependencyTracker;
|
private final JSDependencyTracker jsDependencyTracker;
|
||||||
|
@ -89,7 +90,7 @@ public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new DebuggingGraalScriptEngine<>(
|
return new DebuggingGraalScriptEngine<>(
|
||||||
new OpenhabGraalJSScriptEngine(injectionEnabled ? INJECTION_CODE : null, jsScriptServiceUtil));
|
new OpenhabGraalJSScriptEngine(injectionEnabled, useIncludedLibrary, jsScriptServiceUtil));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -100,6 +101,8 @@ public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
|
||||||
@Modified
|
@Modified
|
||||||
protected void modified(Map<String, ?> config) {
|
protected void modified(Map<String, ?> config) {
|
||||||
Object injectionEnabled = config.get(CFG_INJECTION_ENABLED);
|
Object injectionEnabled = config.get(CFG_INJECTION_ENABLED);
|
||||||
this.injectionEnabled = injectionEnabled == null || (Boolean) injectionEnabled;
|
this.injectionEnabled = injectionEnabled == null || (boolean) injectionEnabled;
|
||||||
|
Object useIncludedLibrary = config.get(CFG_USE_INCLUDED_LIBRARY);
|
||||||
|
this.useIncludedLibrary = useIncludedLibrary == null || (boolean) useIncludedLibrary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
|
||||||
* @author Dan Cunningham - Script injections
|
* @author Dan Cunningham - Script injections
|
||||||
* @author Florian Hotze - Create lock object for multi-thread synchronization; Inject the {@link JSRuntimeFeatures}
|
* @author Florian Hotze - Create lock object for multi-thread synchronization; Inject the {@link JSRuntimeFeatures}
|
||||||
* into the JS context; Fix memory leak caused by HostObject by making HostAccess reference static; Switch to
|
* into the JS context; Fix memory leak caused by HostObject by making HostAccess reference static; Switch to
|
||||||
* {@link Lock} for multi-thread synchronization
|
* {@link Lock} for multi-thread synchronization; globals & openhab-js injection code caching
|
||||||
*/
|
*/
|
||||||
public class OpenhabGraalJSScriptEngine
|
public class OpenhabGraalJSScriptEngine
|
||||||
extends InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable<GraalJSScriptEngine> {
|
extends InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable<GraalJSScriptEngine> {
|
||||||
|
@ -77,10 +77,23 @@ public class OpenhabGraalJSScriptEngine
|
||||||
GLOBAL_SOURCE = Source.newBuilder("js", getFileAsReader("node_modules/@jsscripting-globals.js"),
|
GLOBAL_SOURCE = Source.newBuilder("js", getFileAsReader("node_modules/@jsscripting-globals.js"),
|
||||||
"@jsscripting-globals.js").cached(true).build();
|
"@jsscripting-globals.js").cached(true).build();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.error("Failed to load global script", e);
|
throw new RuntimeException("Failed to load @jsscripting-globals.js", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Source OPENHAB_JS_SOURCE;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
OPENHAB_JS_SOURCE = Source
|
||||||
|
.newBuilder("js", getFileAsReader("node_modules/@openhab-globals.js"), "@openhab-globals.js")
|
||||||
|
.cached(true).build();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to load @openhab-globals.js", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static String OPENHAB_JS_INJECTION_CODE = "Object.assign(this, require('openhab'));";
|
||||||
|
|
||||||
private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__";
|
private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__";
|
||||||
/** Final CommonJS search path for our library */
|
/** Final CommonJS search path for our library */
|
||||||
private static final Path NODE_DIR = Paths.get("node_modules");
|
private static final Path NODE_DIR = Paths.get("node_modules");
|
||||||
|
@ -111,15 +124,18 @@ public class OpenhabGraalJSScriptEngine
|
||||||
private @Nullable Consumer<String> scriptDependencyListener;
|
private @Nullable Consumer<String> scriptDependencyListener;
|
||||||
|
|
||||||
private boolean initialized = false;
|
private boolean initialized = false;
|
||||||
private final String injectionCode;
|
private final boolean injectionEnabled;
|
||||||
|
private final boolean useIncludedLibrary;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script
|
* Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script
|
||||||
* lifecycle and provides hooks for scripts to do so too.
|
* lifecycle and provides hooks for scripts to do so too.
|
||||||
*/
|
*/
|
||||||
public OpenhabGraalJSScriptEngine(@Nullable String injectionCode, JSScriptServiceUtil jsScriptServiceUtil) {
|
public OpenhabGraalJSScriptEngine(boolean injectionEnabled, boolean useIncludedLibrary,
|
||||||
|
JSScriptServiceUtil jsScriptServiceUtil) {
|
||||||
super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
|
super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
|
||||||
this.injectionCode = (injectionCode != null ? injectionCode : "");
|
this.injectionEnabled = injectionEnabled;
|
||||||
|
this.useIncludedLibrary = useIncludedLibrary;
|
||||||
this.jsRuntimeFeatures = jsScriptServiceUtil.getJSRuntimeFeatures(lock);
|
this.jsRuntimeFeatures = jsScriptServiceUtil.getJSRuntimeFeatures(lock);
|
||||||
|
|
||||||
LOGGER.debug("Initializing GraalJS script engine...");
|
LOGGER.debug("Initializing GraalJS script engine...");
|
||||||
|
@ -229,13 +245,14 @@ public class OpenhabGraalJSScriptEngine
|
||||||
ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider(
|
ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider(
|
||||||
scriptExtensionAccessor, lock);
|
scriptExtensionAccessor, lock);
|
||||||
|
|
||||||
|
// Wrap the "require" function to also allow loading modules from the ScriptExtensionModuleProvider
|
||||||
Function<Function<Object[], Object>, Function<String, Object>> wrapRequireFn = originalRequireFn -> moduleName -> scriptExtensionModuleProvider
|
Function<Function<Object[], Object>, Function<String, Object>> wrapRequireFn = originalRequireFn -> moduleName -> scriptExtensionModuleProvider
|
||||||
.locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName)
|
.locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName)
|
||||||
.map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName }));
|
.map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName }));
|
||||||
|
|
||||||
delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn);
|
delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn);
|
||||||
// Injections into the JS runtime
|
|
||||||
delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));
|
delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));
|
||||||
|
|
||||||
|
// Injections into the JS runtime
|
||||||
jsRuntimeFeatures.getFeatures().forEach((key, obj) -> {
|
jsRuntimeFeatures.getFeatures().forEach((key, obj) -> {
|
||||||
LOGGER.debug("Injecting {} into the JS runtime...", key);
|
LOGGER.debug("Injecting {} into the JS runtime...", key);
|
||||||
delegate.put(key, obj);
|
delegate.put(key, obj);
|
||||||
|
@ -244,9 +261,17 @@ public class OpenhabGraalJSScriptEngine
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
LOGGER.debug("Evaluating global script...");
|
LOGGER.debug("Evaluating cached global script...");
|
||||||
delegate.getPolyglotContext().eval(GLOBAL_SOURCE);
|
delegate.getPolyglotContext().eval(GLOBAL_SOURCE);
|
||||||
eval(injectionCode);
|
if (this.injectionEnabled) {
|
||||||
|
if (this.useIncludedLibrary) {
|
||||||
|
LOGGER.debug("Evaluating cached openhab-js injection...");
|
||||||
|
delegate.getPolyglotContext().eval(OPENHAB_JS_SOURCE);
|
||||||
|
} else {
|
||||||
|
LOGGER.debug("Evaluating openhab-js injection from the file system...");
|
||||||
|
eval(OPENHAB_JS_INJECTION_CODE);
|
||||||
|
}
|
||||||
|
}
|
||||||
LOGGER.debug("Successfully initialized GraalJS script engine.");
|
LOGGER.debug("Successfully initialized GraalJS script engine.");
|
||||||
} catch (ScriptException e) {
|
} catch (ScriptException e) {
|
||||||
LOGGER.error("Could not inject global script", e);
|
LOGGER.error("Could not inject global script", e);
|
||||||
|
@ -295,11 +320,11 @@ public class OpenhabGraalJSScriptEngine
|
||||||
* @param fileName filename relative to the resources folder
|
* @param fileName filename relative to the resources folder
|
||||||
* @return file as {@link InputStreamReader}
|
* @return file as {@link InputStreamReader}
|
||||||
*/
|
*/
|
||||||
private static Reader getFileAsReader(String fileName) {
|
private static Reader getFileAsReader(String fileName) throws IOException {
|
||||||
InputStream ioStream = OpenhabGraalJSScriptEngine.class.getClassLoader().getResourceAsStream(fileName);
|
InputStream ioStream = OpenhabGraalJSScriptEngine.class.getClassLoader().getResourceAsStream(fileName);
|
||||||
|
|
||||||
if (ioStream == null) {
|
if (ioStream == null) {
|
||||||
throw new IllegalArgumentException(fileName + " not found");
|
throw new IOException(fileName + " not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new InputStreamReader(ioStream);
|
return new InputStreamReader(ioStream);
|
||||||
|
|
|
@ -27,7 +27,7 @@ import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
|
||||||
import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager;
|
import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class providing script extensions via CommonJS modules.
|
* Class providing script extensions via CommonJS modules (with module name `@runtime`).
|
||||||
*
|
*
|
||||||
* @author Jonathan Gilbert - Initial contribution
|
* @author Jonathan Gilbert - Initial contribution
|
||||||
* @author Florian Hotze - Pass in lock object for multi-thread synchronization; Switch to {@link Lock} for multi-thread
|
* @author Florian Hotze - Pass in lock object for multi-thread synchronization; Switch to {@link Lock} for multi-thread
|
||||||
|
|
|
@ -7,8 +7,9 @@
|
||||||
<config-description uri="automation:jsscripting">
|
<config-description uri="automation:jsscripting">
|
||||||
<parameter name="injectionEnabled" type="boolean" required="true">
|
<parameter name="injectionEnabled" type="boolean" required="true">
|
||||||
<label>Use Built-in Global Variables</label>
|
<label>Use Built-in Global Variables</label>
|
||||||
<description><![CDATA[ Import all variables from the OH scripting library into all rules for common services like items, things, actions, log, etc... <br>
|
<description><![CDATA[
|
||||||
If disabled, the OH scripting library can be imported manually using "<i>require('openhab')</i>"
|
Import all variables from the openHAB JavaScript library into all rules for common services like items, things, actions, log, etc... <br>
|
||||||
|
If disabled, the openHAB JavaScript library can be imported manually using "<i>require('openhab')</i>"
|
||||||
]]></description>
|
]]></description>
|
||||||
<options>
|
<options>
|
||||||
<option value="true">Use Built-in Variables</option>
|
<option value="true">Use Built-in Variables</option>
|
||||||
|
@ -16,5 +17,17 @@
|
||||||
</options>
|
</options>
|
||||||
<default>true</default>
|
<default>true</default>
|
||||||
</parameter>
|
</parameter>
|
||||||
|
<parameter name="useIncludedLibrary" type="boolean" required="true">
|
||||||
|
<label>Use Included openHAB JavaScript Library</label>
|
||||||
|
<description><![CDATA[
|
||||||
|
Use the included openHAB JavaScript library for optimal performance.<br>
|
||||||
|
Disable this option to allow loading the library from the local user configuration directory "automation/js/node_modules". Using a user provided version of the library may increase script loading times, especially on less powerful systems.
|
||||||
|
]]></description>
|
||||||
|
<options>
|
||||||
|
<option value="true">Use Included Library</option>
|
||||||
|
<option value="false">Do Not Use Included Library</option>
|
||||||
|
</options>
|
||||||
|
<default>true</default>
|
||||||
|
</parameter>
|
||||||
</config-description>
|
</config-description>
|
||||||
</config-description:config-descriptions>
|
</config-description:config-descriptions>
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
automation.config.jsscripting.injectionEnabled.label = Use Built-in Global Variables
|
automation.config.jsscripting.injectionEnabled.label = Use Built-in Global Variables
|
||||||
automation.config.jsscripting.injectionEnabled.description = Import all variables from the OH scripting library into all rules for common services like items, things, actions, log, etc... <br> If disabled, the OH scripting library can be imported manually using "<i>require('openhab')</i>"
|
automation.config.jsscripting.injectionEnabled.description = Import all variables from the openHAB JavaScript library into all rules for common services like items, things, actions, log, etc... <br> If disabled, the openHAB JavaScript library can be imported manually using "<i>require('openhab')</i>"
|
||||||
automation.config.jsscripting.injectionEnabled.option.true = Use Built-in Variables
|
automation.config.jsscripting.injectionEnabled.option.true = Use Built-in Variables
|
||||||
automation.config.jsscripting.injectionEnabled.option.false = Do Not Use Built-in Variables
|
automation.config.jsscripting.injectionEnabled.option.false = Do Not Use Built-in Variables
|
||||||
|
automation.config.jsscripting.useIncludedLibrary.label = Use Included openHAB JavaScript Library
|
||||||
|
automation.config.jsscripting.useIncludedLibrary.description = Use the included openHAB JavaScript library for optimal performance.<br> Disable this option to allow loading the library from the local user configuration directory "automation/js/node_modules". Using a user provided version of the library may increase script loading times, especially on less powerful systems.
|
||||||
|
automation.config.jsscripting.useIncludedLibrary.option.true = Use Included Library
|
||||||
|
automation.config.jsscripting.useIncludedLibrary.option.false = Do Not Use Included Library
|
||||||
|
|
||||||
# service
|
# service
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue