]> git.basschouten.com Git - openhab-addons.git/commitdiff
[jsscripting] Cache openhab-js injection to improve performance (#14135)
authorFlorian Hotze <florianh_dev@icloud.com>
Mon, 2 Jan 2023 19:41:35 +0000 (20:41 +0100)
committerGitHub <noreply@github.com>
Mon, 2 Jan 2023 19:41:35 +0000 (20:41 +0100)
* [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>
bundles/org.openhab.automation.jsscripting/pom.xml
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java
bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/config/config.xml
bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/i18n/jsscripting.properties

index 79c4fe899491e6fa5c64c51e4e9cbae9dc8ed72d..f51ed442e47e6c67fdeb4672a8ebdfc7592740eb 100644 (file)
@@ -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>
index 769601056d1719503ab55e267f73a7958db21b3e..ebcd660a5056fe5ee5328f2f49c87008dccb82bb 100644 (file)
@@ -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<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;
     }
 }
index 51d393827cab5dbd8e4845d1175e31ebdc94aac9..b9a460147038cc92c559256b874379a1244e0d74 100644 (file)
@@ -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<GraalJSScriptEngine> {
@@ -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<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...");
@@ -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<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);
@@ -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);
index da131c27fe323ad295bb87fb2e3303fc8c1c6fb8..5384097a4b6064ff4f7d014a045dbe8097b76c29 100644 (file)
@@ -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
index 3348680be3321f0108730d6bcfe4233aa03bd67f..0d6230c76ed0caaed4a51d12a6aeb395d24adaf8 100644 (file)
@@ -7,8 +7,9 @@
        <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>
                        </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>
index 17580ae61c910f0a06564e4c21d51fc49cd15d7e..f9fb7c9d786b74f9bb50a5b277d44e4248bab9ba 100644 (file)
@@ -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