]> git.basschouten.com Git - openhab-addons.git/commitdiff
[jsscripting] Improve performance & reduce memory usage (#14113)
authorFlorian Hotze <florianh_dev@icloud.com>
Fri, 30 Dec 2022 22:34:35 +0000 (23:34 +0100)
committerGitHub <noreply@github.com>
Fri, 30 Dec 2022 22:34:35 +0000 (23:34 +0100)
* [jsscripting] Share org.graalvm.polyglot.Engine across all OpenhabGraalJSScriptEngine instances

See https://github.com/oracle/graaljs/issues/121#issuecomment-880056648, it is not required to have one engine per GraalJSScriptEngine.

This might improve performance a bit on less powerful systems (Raspberry Pi) and decreases heap usage:
With 5 GraalJS UI scripts, heap usage is now below 100 MB. Before this change, it was over 100 MB.

* [jsscripting] Extend debug logging
* [jsscripting] Cache `@jsscripting-globals.js` across all engines

Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java

index 7c1c8704408fc35fc3793f487d900b39ed3dc709..51d393827cab5dbd8e4845d1175e31ebdc94aac9 100644 (file)
@@ -16,6 +16,8 @@ import static org.openhab.core.automation.module.script.ScriptEngineFactory.*;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
 import java.nio.channels.SeekableByteChannel;
 import java.nio.file.AccessMode;
 import java.nio.file.FileSystems;
@@ -42,6 +44,7 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.graalvm.polyglot.Context;
 import org.graalvm.polyglot.Engine;
 import org.graalvm.polyglot.HostAccess;
+import org.graalvm.polyglot.Source;
 import org.graalvm.polyglot.Value;
 import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem;
 import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel;
@@ -67,10 +70,23 @@ public class OpenhabGraalJSScriptEngine
         extends InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable<GraalJSScriptEngine> {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
-    private static final String GLOBAL_REQUIRE = "require(\"@jsscripting-globals\");";
+    private static Source GLOBAL_SOURCE;
+
+    static {
+        try {
+            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);
+        }
+    }
+
     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");
+    /** Shared Polyglot {@link Engine} across all instances of {@link OpenhabGraalJSScriptEngine} */
+    private static final Engine ENGINE = Engine.newBuilder().allowExperimentalOptions(true)
+            .option("engine.WarnInterpreterOnly", "false").build();
     /** Provides unlimited host access as well as custom translations from JS to Java Objects */
     private static final HostAccess HOST_ACCESS = HostAccess.newBuilder(HostAccess.ALL)
             // Translate JS-Joda ZonedDateTime to java.time.ZonedDateTime
@@ -95,7 +111,7 @@ public class OpenhabGraalJSScriptEngine
     private @Nullable Consumer<String> scriptDependencyListener;
 
     private boolean initialized = false;
-    private final String globalScript;
+    private final String injectionCode;
 
     /**
      * Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script
@@ -103,14 +119,12 @@ public class OpenhabGraalJSScriptEngine
      */
     public OpenhabGraalJSScriptEngine(@Nullable String injectionCode, JSScriptServiceUtil jsScriptServiceUtil) {
         super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
-        this.globalScript = GLOBAL_REQUIRE + (injectionCode != null ? injectionCode : "");
+        this.injectionCode = (injectionCode != null ? injectionCode : "");
         this.jsRuntimeFeatures = jsScriptServiceUtil.getJSRuntimeFeatures(lock);
 
         LOGGER.debug("Initializing GraalJS script engine...");
 
-        delegate = GraalJSScriptEngine.create(
-                Engine.newBuilder().allowExperimentalOptions(true).option("engine.WarnInterpreterOnly", "false")
-                        .build(),
+        delegate = GraalJSScriptEngine.create(ENGINE,
                 Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
                         .allowHostAccess(HOST_ACCESS).option("js.commonjs-require-cwd", JSDependencyTracker.LIB_PATH)
                         .option("js.nashorn-compat", "true") // Enable Nashorn compat mode as openhab-js relies on
@@ -230,7 +244,10 @@ public class OpenhabGraalJSScriptEngine
         initialized = true;
 
         try {
-            eval(globalScript);
+            LOGGER.debug("Evaluating global script...");
+            delegate.getPolyglotContext().eval(GLOBAL_SOURCE);
+            eval(injectionCode);
+            LOGGER.debug("Successfully initialized GraalJS script engine.");
         } catch (ScriptException e) {
             LOGGER.error("Could not inject global script", e);
         }
@@ -273,4 +290,18 @@ public class OpenhabGraalJSScriptEngine
     private String nodeFileToResource(Path path) {
         return "/" + path.subpath(0, path.getNameCount()).toString().replace('\\', '/');
     }
+
+    /**
+     * @param fileName filename relative to the resources folder
+     * @return file as {@link InputStreamReader}
+     */
+    private static Reader getFileAsReader(String fileName) {
+        InputStream ioStream = OpenhabGraalJSScriptEngine.class.getClassLoader().getResourceAsStream(fileName);
+
+        if (ioStream == null) {
+            throw new IllegalArgumentException(fileName + " not found");
+        }
+
+        return new InputStreamReader(ioStream);
+    }
 }