From 3c669ad77a743ec0d5306334bd058b3dff7bb6a5 Mon Sep 17 00:00:00 2001 From: Florian Hotze Date: Mon, 2 Jan 2023 20:41:35 +0100 Subject: [PATCH] [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 --- .../pom.xml | 2 +- .../internal/GraalJSScriptEngineFactory.java | 9 ++-- .../internal/OpenhabGraalJSScriptEngine.java | 47 ++++++++++++++----- .../ScriptExtensionModuleProvider.java | 2 +- .../main/resources/OH-INF/config/config.xml | 17 ++++++- .../OH-INF/i18n/jsscripting.properties | 6 ++- 6 files changed, 64 insertions(+), 19 deletions(-) diff --git a/bundles/org.openhab.automation.jsscripting/pom.xml b/bundles/org.openhab.automation.jsscripting/pom.xml index 79c4fe8994..f51ed442e4 100644 --- a/bundles/org.openhab.automation.jsscripting/pom.xml +++ b/bundles/org.openhab.automation.jsscripting/pom.xml @@ -24,7 +24,7 @@ 22.0.0.2 ${project.version} - openhab@3.1.2 + openhab@3.2.1 diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java index 769601056d..ebcd660a50 100644 --- a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java @@ -45,7 +45,7 @@ import com.oracle.truffle.js.scriptengine.GraalJSEngineFactory; @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(); @@ -61,6 +61,7 @@ public final class GraalJSScriptEngineFactory implements ScriptEngineFactory { } private boolean injectionEnabled = true; + private boolean useIncludedLibrary = true; private final JSScriptServiceUtil jsScriptServiceUtil; private final JSDependencyTracker jsDependencyTracker; @@ -89,7 +90,7 @@ public final class GraalJSScriptEngineFactory implements ScriptEngineFactory { return null; } return new DebuggingGraalScriptEngine<>( - new OpenhabGraalJSScriptEngine(injectionEnabled ? INJECTION_CODE : null, jsScriptServiceUtil)); + new OpenhabGraalJSScriptEngine(injectionEnabled, useIncludedLibrary, jsScriptServiceUtil)); } @Override @@ -100,6 +101,8 @@ public final class GraalJSScriptEngineFactory implements ScriptEngineFactory { @Modified protected void modified(Map 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; } } diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java index 51d393827c..b9a4601470 100644 --- a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java @@ -64,7 +64,7 @@ import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine; * @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 { @@ -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"); @@ -111,15 +124,18 @@ public class OpenhabGraalJSScriptEngine private @Nullable Consumer 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..."); @@ -229,13 +245,14 @@ public class OpenhabGraalJSScriptEngine ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider( scriptExtensionAccessor, lock); + // Wrap the "require" function to also allow loading modules from the ScriptExtensionModuleProvider Function, Function> 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) 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); @@ -244,9 +261,17 @@ public class OpenhabGraalJSScriptEngine 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); @@ -295,11 +320,11 @@ public class OpenhabGraalJSScriptEngine * @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); diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java index da131c27fe..5384097a4b 100644 --- a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java @@ -27,7 +27,7 @@ import org.openhab.core.automation.module.script.ScriptExtensionAccessor; 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 diff --git a/bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/config/config.xml index 3348680be3..0d6230c76e 100644 --- a/bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/config/config.xml @@ -7,8 +7,9 @@ - - If disabled, the OH scripting library can be imported manually using "require('openhab')" + + If disabled, the openHAB JavaScript library can be imported manually using "require('openhab')" ]]> @@ -16,5 +17,17 @@ true + + + + 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. + ]]> + + + + + true + diff --git a/bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/i18n/jsscripting.properties b/bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/i18n/jsscripting.properties index 17580ae61c..f9fb7c9d78 100644 --- a/bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/i18n/jsscripting.properties +++ b/bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/i18n/jsscripting.properties @@ -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...
If disabled, the OH scripting library can be imported manually using "require('openhab')" +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...
If disabled, the openHAB JavaScript library can be imported manually using "require('openhab')" 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.
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 -- 2.47.3