Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[jsscripting] Cache openhab-js injection to improve performance #14135

Merged
merged 8 commits into from
Jan 2, 2023
2 changes: 1 addition & 1 deletion bundles/org.openhab.automation.jsscripting/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
</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 -->
<oh.version>${project.version}</oh.version>
<ohjs.version>openhab@3.1.2</ohjs.version>
<ohjs.version>openhab@3.2.1</ohjs.version>
</properties>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
@NonNullByDefault
public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
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();

Expand All @@ -61,6 +61,7 @@ private static List<String> createScriptTypes() {
}

private boolean injectionEnabled = true;
private boolean useIncludedLibrary = true;

private final JSScriptServiceUtil jsScriptServiceUtil;
private final JSDependencyTracker jsDependencyTracker;
Expand Down Expand Up @@ -89,7 +90,7 @@ public void scopeValues(ScriptEngine scriptEngine, Map<String, Object> scopeValu
return null;
}
return new DebuggingGraalScriptEngine<>(
new OpenhabGraalJSScriptEngine(injectionEnabled ? INJECTION_CODE : null, jsScriptServiceUtil));
new OpenhabGraalJSScriptEngine(injectionEnabled, useIncludedLibrary, jsScriptServiceUtil));
}

@Override
Expand All @@ -100,6 +101,8 @@ public void scopeValues(ScriptEngine scriptEngine, Map<String, Object> scopeValu
@Modified
protected void modified(Map<String, ?> config) {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
* @author Dan Cunningham - Script injections
* @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
* {@link Lock} for multi-thread synchronization
* {@link Lock} for multi-thread synchronization; globals & openhab-js injection code caching
*/
public class OpenhabGraalJSScriptEngine
extends InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable<GraalJSScriptEngine> {
Expand All @@ -77,10 +77,23 @@ public class OpenhabGraalJSScriptEngine
GLOBAL_SOURCE = Source.newBuilder("js", getFileAsReader("node_modules/@jsscripting-globals.js"),
"@jsscripting-globals.js").cached(true).build();
} 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__";
/** Final CommonJS search path for our library */
private static final Path NODE_DIR = Paths.get("node_modules");
Expand Down Expand Up @@ -111,15 +124,18 @@ public class OpenhabGraalJSScriptEngine
private @Nullable Consumer<String> scriptDependencyListener;

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
* 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
this.injectionCode = (injectionCode != null ? injectionCode : "");
this.injectionEnabled = injectionEnabled;
this.useIncludedLibrary = useIncludedLibrary;
this.jsRuntimeFeatures = jsScriptServiceUtil.getJSRuntimeFeatures(lock);

LOGGER.debug("Initializing GraalJS script engine...");
Expand Down Expand Up @@ -229,13 +245,14 @@ protected void beforeInvocation() {
ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider(
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
.locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName)
.map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName }));

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")));

// Injections into the JS runtime
jsRuntimeFeatures.getFeatures().forEach((key, obj) -> {
LOGGER.debug("Injecting {} into the JS runtime...", key);
delegate.put(key, obj);
Expand All @@ -244,9 +261,17 @@ protected void beforeInvocation() {
initialized = true;

try {
LOGGER.debug("Evaluating global script...");
LOGGER.debug("Evaluating cached global script...");
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.");
} catch (ScriptException e) {
LOGGER.error("Could not inject global script", e);
Expand Down Expand Up @@ -295,11 +320,11 @@ private String nodeFileToResource(Path path) {
* @param fileName filename relative to the resources folder
* @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);

if (ioStream == null) {
throw new IllegalArgumentException(fileName + " not found");
throw new IOException(fileName + " not found");
}

return new InputStreamReader(ioStream);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
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 Florian Hotze - Pass in lock object for multi-thread synchronization; Switch to {@link Lock} for multi-thread
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,27 @@
<config-description uri="automation:jsscripting">
<parameter name="injectionEnabled" type="boolean" required="true">
<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>
If disabled, the OH scripting library can be imported manually using "<i>require('openhab')</i>"
<description><![CDATA[
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>
<options>
<option value="true">Use Built-in Variables</option>
<option value="false">Do Not Use Built-in Variables</option>
</options>
<default>true</default>
</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-descriptions>
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
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.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

Expand Down