diff --git a/build.sbt b/build.sbt index 0a39e4703154..070cfa3e30bc 100644 --- a/build.sbt +++ b/build.sbt @@ -2667,7 +2667,8 @@ lazy val `runtime-test-instruments` = lazy val runtime = (project in file("engine/runtime")) .enablePlugins(JPMSPlugin) .settings( - frgaalJavaCompilerSetting, + // Needed for `java.lang.Foreign`. + customFrgaalJavaCompilerSettings("24"), scalaModuleDependencySetting, mixedJavaScalaProjectSetting, annotationProcSetting, @@ -2793,7 +2794,7 @@ lazy val `runtime-integration-tests` = .enablePlugins(JPMSPlugin) .enablePlugins(PackageListPlugin) .settings( - frgaalJavaCompilerSetting, + customFrgaalJavaCompilerSettings("24"), annotationProcSetting, commands += WithDebugCommand.withDebug, libraryDependencies ++= GraalVM.modules ++ GraalVM.langsPkgs ++ GraalVM.insightPkgs ++ logbackPkg ++ helidon ++ slf4jApi ++ Seq( diff --git a/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/native-image.properties b/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/native-image.properties index 9e58138f6461..702ab7bd7b4d 100644 --- a/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/native-image.properties +++ b/engine/runner/src/main/resources/META-INF/native-image/org/enso/runner/native-image.properties @@ -1 +1,2 @@ -Args=--features=org.enso.runner.EnsoLibraryFeature +Args = --features=org.enso.runner.EnsoLibraryFeature \ + -H:+ForeignAPISupport diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheTests.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheTests.java new file mode 100644 index 000000000000..47aad90ffbf4 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheTests.java @@ -0,0 +1,267 @@ +package org.enso.interpreter.caches; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.fail; + +import com.oracle.truffle.api.TruffleLogger; +import java.io.IOException; +import java.lang.foreign.Arena; +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.logging.Level; +import org.enso.interpreter.caches.Cache.Roots; +import org.enso.interpreter.caches.Cache.Spi; +import org.enso.interpreter.runtime.EnsoContext; +import org.enso.test.utils.ContextUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public final class CacheTests { + + @Rule public final TemporaryFolder tempFolder = new TemporaryFolder(); + @Rule public final ContextUtils ctx = ContextUtils.createDefault(); + private final Random random = new Random(42); + + @Test + public void cacheCanBeSaved_ToLocalCacheRoot() throws IOException { + var cacheRoots = createCacheRoots(); + var ensoCtx = ctx.ensoContext(); + var spi = new CacheSpi(cacheRoots); + var cache = Cache.create(spi, Level.FINE, "testCache", false, false); + var ret = cache.save(new CachedData(), ensoCtx, false); + assertThat("was saved to local cache root", ret, is(cacheRoots.localCacheRoot())); + var localCacheFile = + cacheRoots.localCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.DATA_SUFFIX); + assertThat("local cache file was created", localCacheFile.exists(), is(true)); + } + + @Test + public void globalCacheIsPreferred() throws IOException { + var cacheRoots = createCacheRoots(); + var ensoCtx = ctx.ensoContext(); + var spi = new CacheSpi(cacheRoots); + var cache = Cache.create(spi, Level.FINE, "testCache", false, false); + var ret = cache.save(new CachedData(), ensoCtx, true); + assertThat("was saved to global cache root", ret, is(cacheRoots.globalCacheRoot())); + var globalCacheFile = + cacheRoots.globalCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.DATA_SUFFIX); + assertThat("global cache file was created", globalCacheFile.exists(), is(true)); + } + + @Test + public void memoryArenaIsClosed_AfterCacheSave() throws IOException { + var ensoCtx = ctx.ensoContext(); + var cacheRoots = createCacheRoots(); + var spi = new CacheSpi(cacheRoots); + var bigData = randomBytes(10 * 1024 * 1024); + saveToLocalRoot(bigData, cacheRoots); + + var memoryArena = new AtomicReference(); + Supplier arenaSupplier = + () -> { + memoryArena.set(Arena.ofConfined()); + return memoryArena.get(); + }; + var cache = Cache.create(spi, Level.FINE, "testCache", false, false, arenaSupplier); + var loaded = cache.load(ensoCtx); + assertThat("was loaded", loaded.isPresent(), is(true)); + assertThat("New arena was created", memoryArena.get(), is(notNullValue())); + + var ret = cache.save(new CachedData(), ensoCtx, false); + assertThat("was saved", ret, is(notNullValue())); + assertThat( + "Memory arena is closed after cache save", memoryArena.get().scope().isAlive(), is(false)); + } + + @Test + public void cacheCannotBeLoaded_WithoutMetadataOnDisk() throws IOException { + var ensoCtx = ctx.ensoContext(); + var cacheRoots = createCacheRoots(); + var localCacheFile = + cacheRoots.localCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.DATA_SUFFIX); + // Saving only data and no metadata + try (var os = localCacheFile.newOutputStream()) { + os.write(new byte[] {42}); + } + var spi = new CacheSpi(cacheRoots); + var cache = Cache.create(spi, Level.FINE, "testCache", false, false); + var loaded = cache.load(ensoCtx); + assertThat("Cannot be loaded without metadata on disk", loaded.isPresent(), is(false)); + } + + @Test + public void byteBufferIsClosed_AfterCacheIsInvalidated() throws IOException { + var ensoCtx = ctx.ensoContext(); + var cacheRoots = createCacheRoots(); + var bigData = randomBytes(10 * 1024 * 1024); + saveToLocalRoot(bigData, cacheRoots); + var spi = new CacheSpi(cacheRoots); + var cache = Cache.create(spi, Level.FINE, "testCache", false, false); + var loaded = cache.load(ensoCtx); + var deserializeBuffer = spi.deserializeBuffer; + assertThat("was loaded", loaded.isPresent(), is(true)); + + cache.invalidate(ensoCtx); + + try { + deserializeBuffer.get(); + fail("Expected IllegalStateException - cannot read from byte buffer anymore"); + } catch (IllegalStateException e) { + assertThat(e.getMessage(), containsString("closed")); + } + } + + @Test + public void byteBufferIsValid_AfterCacheLoad() throws IOException { + var ensoCtx = ctx.ensoContext(); + var cacheRoots = createCacheRoots(); + var bigData = randomBytes(10 * 1024 * 1024); + saveToLocalRoot(bigData, cacheRoots); + var spi = new CacheSpi(cacheRoots); + var cache = Cache.create(spi, Level.FINE, "testCache", false, false); + var loaded = cache.load(ensoCtx); + assertThat("was loaded", loaded.isPresent(), is(true)); + + var deserializeBuffer = spi.deserializeBuffer; + var firstByte = deserializeBuffer.get(); + assertThat("byte buffer is still readable", firstByte, is(bigData[0])); + } + + @Test + public void byteBufferIsReadonly() throws IOException { + var ensoCtx = ctx.ensoContext(); + var cacheRoots = createCacheRoots(); + var bigData = randomBytes(10 * 1024 * 1024); + saveToLocalRoot(bigData, cacheRoots); + var spi = new CacheSpi(cacheRoots); + var cache = Cache.create(spi, Level.FINE, "testCache", false, false); + var loaded = cache.load(ensoCtx); + assertThat("was loaded", loaded.isPresent(), is(true)); + + var deserializeBuffer = spi.deserializeBuffer; + try { + deserializeBuffer.put((byte) 42); + fail("Expected ReadOnlyBufferException"); + } catch (java.nio.ReadOnlyBufferException e) { + // expected + } + } + + private Roots createCacheRoots() throws IOException { + var cacheRootDirPath = tempFolder.newFolder("cacheRoot").toPath(); + var localCacheDir = cacheRootDirPath.resolve("local"); + var globalCacheDir = cacheRootDirPath.resolve("global"); + localCacheDir.toFile().mkdir(); + globalCacheDir.toFile().mkdir(); + var ensoCtx = ctx.ensoContext(); + return new Roots( + ensoCtx.getTruffleFile(localCacheDir.toFile()), + ensoCtx.getTruffleFile(globalCacheDir.toFile())); + } + + /** Saves data as well as empty metadata on the disk. */ + private static void saveToLocalRoot(byte[] data, Roots cacheRoots) throws IOException { + var localCacheFile = + cacheRoots.localCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.DATA_SUFFIX); + var localMetadataFile = + cacheRoots.localCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.METADATA_SUFFIX); + try (var os = localCacheFile.newOutputStream()) { + os.write(data); + } + try (var os = localMetadataFile.newOutputStream()) { + os.write(42); + } + } + + private byte[] randomBytes(int size) { + byte[] bytes = new byte[size]; + random.nextBytes(bytes); + return bytes; + } + + private static final class CachedData {} + + private static final class Metadata {} + + private static final class CacheSpi implements Spi { + public static final String DATA_SUFFIX = ".test.data"; + public static final String METADATA_SUFFIX = ".test.metadata"; + public static final String ENTRY_NAME = "test-entry"; + + private final Roots cacheRoots; + private ByteBuffer deserializeBuffer; + + private CacheSpi(Roots cacheRoots) { + this.cacheRoots = cacheRoots; + } + + @Override + public CachedData deserialize( + EnsoContext context, ByteBuffer data, Metadata meta, TruffleLogger logger) { + deserializeBuffer = data; + return new CachedData(); + } + + @Override + public byte[] serialize(EnsoContext context, CachedData entry) { + return new byte[] {42}; + } + + @Override + public byte[] metadata(String sourceDigest, String blobDigest, CachedData entry) { + return new byte[0]; + } + + @Override + public Metadata metadataFromBytes(byte[] bytes, TruffleLogger logger) { + return new Metadata(); + } + + @Override + public Optional computeDigest(CachedData entry, TruffleLogger logger) { + return Optional.of("42"); + } + + @Override + public Optional computeDigestFromSource(EnsoContext context, TruffleLogger logger) { + throw new AssertionError("should not be called"); + } + + @Override + public Optional getCacheRoots(EnsoContext context) { + return Optional.of(cacheRoots); + } + + @Override + public String entryName() { + return ENTRY_NAME; + } + + @Override + public String dataSuffix() { + return DATA_SUFFIX; + } + + @Override + public String metadataSuffix() { + return METADATA_SUFFIX; + } + + @Override + public String sourceHash(Metadata meta) { + return "42"; + } + + @Override + public String blobHash(Metadata meta) { + return "42"; + } + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/SaveAndLoadCacheTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/SaveAndLoadCacheTest.java new file mode 100644 index 000000000000..aa35137ef8c9 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/SaveAndLoadCacheTest.java @@ -0,0 +1,221 @@ +package org.enso.interpreter.caches; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import org.enso.common.RuntimeOptions; +import org.enso.editions.LibraryName; +import org.enso.polyglot.PolyglotContext; +import org.enso.test.utils.ContextUtils; +import org.enso.test.utils.ProjectUtils; +import org.graalvm.polyglot.Value; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class SaveAndLoadCacheTest { + private static final String CACHE_LOGGER_NAME = "enso.org.enso.interpreter.caches.Cache"; + + @Rule public final TemporaryFolder tmpFolder = new TemporaryFolder(); + private final List collectedLogs = new ArrayList<>(); + private final LogHandler logHandler = new LogHandler(); + + private final class LogHandler extends Handler { + @Override + public void publish(LogRecord record) { + if (record.getLoggerName().equals(CACHE_LOGGER_NAME)) { + collectedLogs.add(record); + } + } + + @Override + public void flush() {} + + @Override + public void close() {} + } + + @After + public void teardown() { + collectedLogs.clear(); + } + + @Test + public void compilationSavesSuggestionsAndImportExportCache() throws Exception { + var projDir = tmpFolder.newFolder("Proj").toPath(); + ProjectUtils.createProject("Proj", """ + method = + 42 + """, projDir); + var libName = LibraryName.apply("local", "Proj"); + try (var ctx = projCtx(projDir)) { + compileAndAssertCreatedCaches(ctx, libName); + } + } + + @Test + public void cachesAreLoaded_AfterProjectIsCompiled() throws IOException { + var projDir = tmpFolder.newFolder("Proj").toPath(); + ProjectUtils.createProject("Proj", """ + main = + 42 + """, projDir); + var libName = LibraryName.apply("local", "Proj"); + + // First, compile the project + try (var ctx = projCtx(projDir)) { + compileAndAssertCreatedCaches(ctx, libName); + } + + // Second, run the project. Caches should be loaded. + try (var ctx = projCtx(projDir)) { + var res = runMain(ctx, projDir); + assertThat("execution is OK", res.asInt(), is(42)); + assertContainsLog( + "load bindings cache", collectedLogs, log -> isLoadBindingsLog(log, libName)); + } + } + + @Test + public void bindingCachesOfBigProject_AreMmapped() throws IOException { + var projDir = tmpFolder.newFolder("Proj").toPath(); + var mainSrc = createBigSource(6_000); + ProjectUtils.createProject("Proj", mainSrc, projDir); + var libName = LibraryName.apply("local", "Proj"); + + int savedBindingsCacheSize; + try (var ctx = projCtx(projDir)) { + compileAndAssertCreatedCaches(ctx, libName); + var saveBindingsLog = + collectedLogs.stream() + .filter(log -> isSaveBindingsLog(log, libName)) + .findFirst() + .orElseThrow(() -> new AssertionError("No save bindings log found")); + savedBindingsCacheSize = (int) saveBindingsLog.getParameters()[2]; + var savedMb = savedBindingsCacheSize / 1024 / 1024; + assertThat("binding cache is at least 10MB", savedMb > 10, is(true)); + } + + // Run after compilation. Bindings cache should be mmapped. + try (var ctx = projCtx(projDir)) { + var res = runMain(ctx, projDir); + assertThat("execution is OK", res.asInt(), is(42)); + var bindingsMmappedLog = + collectedLogs.stream() + .filter( + log -> { + var msg = log.getMessage(); + return msg.contains("Cache") && msg.contains("mmapped"); + }) + .findFirst() + .orElseThrow(() -> new AssertionError("No load bindings log found")); + var loadedBytes = (long) bindingsMmappedLog.getParameters()[1]; + assertThat( + "Loaded same data as previously saved", (int) loadedBytes, is(savedBindingsCacheSize)); + } + } + + private ContextUtils projCtx(Path projDir) { + return ContextUtils.newBuilder() + .withModifiedContext( + bldr -> + bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false") + .option(RuntimeOptions.USE_GLOBAL_IR_CACHE_LOCATION, "false") + .option(RuntimeOptions.LOG_LEVEL, "FINEST") + .logHandler(logHandler)) + .withProjectRoot(projDir) + .build(); + } + + /** + * Compiles the project and asserts that suggestions and import/export (binding) caches were + * created (saved). + */ + private void compileAndAssertCreatedCaches(ContextUtils ctx, LibraryName libName) { + var polyCtx = new PolyglotContext(ctx.context()); + polyCtx.getTopScope().compile(true); + assertThat("Some logs collected", collectedLogs.isEmpty(), is(false)); + assertContainsLog( + "save suggestions cache", collectedLogs, log -> isSaveSuggestionsLog(log, libName)); + assertContainsLog( + "save import/export cache", collectedLogs, log -> isSaveBindingsLog(log, libName)); + } + + private static void assertContainsLog( + String descr, List messages, Predicate predicate) { + var hasItem = messages.stream().anyMatch(predicate); + if (!hasItem) { + throw new AssertionError("Expected to find message: " + descr + " in " + messages); + } + } + + private static boolean hasParams(LogRecord log) { + return log.getParameters() != null && log.getParameters().length > 0; + } + + private static boolean isSaveSuggestionsLog(LogRecord log, LibraryName libName) { + if (log.getMessage().contains("Written cache") && hasParams(log)) { + if (log.getParameters()[0] instanceof String param) { + return param.equals("Suggestions(" + libName + ")"); + } + } + return false; + } + + private static boolean isSaveBindingsLog(LogRecord log, LibraryName libName) { + if (log.getMessage().contains("Written cache") && hasParams(log)) { + if (log.getParameters()[0] instanceof String param) { + return param.equals(libName.toString()); + } + } + return false; + } + + private static boolean isLoadBindingsLog(LogRecord log, LibraryName libName) { + if (log.getMessage().contains("Loaded cache") && hasParams(log)) { + if (log.getParameters()[0] instanceof String param) { + return param.equals(libName.toString()); + } + } + return false; + } + + private static Value runMain(ContextUtils ctx, Path projDir) { + var polyCtx = new PolyglotContext(ctx.context()); + var mainSrcPath = projDir.resolve("src").resolve("Main.enso"); + if (!mainSrcPath.toFile().exists()) { + throw new IllegalArgumentException("Main module not found in " + projDir); + } + var mainMod = polyCtx.evalModule(mainSrcPath.toFile()); + var assocMainModType = mainMod.getAssociatedType(); + var mainMethod = mainMod.getMethod(assocMainModType, "main").get(); + var res = mainMethod.execute(); + return res; + } + + /** Creates executable big source file. */ + private static String createBigSource(int methodCount) { + var sb = new StringBuilder(); + sb.append(""" + method_0 = + 42 + """); + for (var i = 1; i < methodCount; i++) { + sb.append("\n"); + sb.append("method_").append(i).append(" = \n"); + sb.append(" method_0"); + sb.append("\n"); + } + sb.append("main = \n"); + sb.append(" ").append("method_").append(methodCount - 1).append("\n"); + return sb.toString(); + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/caches/Cache.java b/engine/runtime/src/main/java/org/enso/interpreter/caches/Cache.java index f562be67f887..99ed103c8461 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/caches/Cache.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/caches/Cache.java @@ -6,14 +6,16 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; -import java.io.RandomAccessFile; +import java.lang.foreign.Arena; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Optional; +import java.util.function.Supplier; import java.util.logging.Level; import org.enso.interpreter.runtime.EnsoContext; import org.enso.logger.masking.MaskedPath; @@ -43,6 +45,15 @@ public final class Cache { */ private final boolean needsSourceDigestVerification; + /** + * Large cache files will be {@link FileChannel#map(MapMode, long, long, Arena) mmapped} using a + * newly created arena via this supplier. Whenever a cache is loaded or saved, the previous arena + * will be closed, which will invalidate all byte buffers associated with that arena. + */ + private final Supplier memoryArenaSupplier; + + private Arena memoryArena; + /** * Flag indicating if the de-serialization process should compute the hash of the stored cache and * compare it with the stored metadata entry. @@ -61,16 +72,18 @@ public final class Cache { * compute the hash of the stored cache and compare it with the stored metadata entry. */ private Cache( - Cache.Spi spi, + Spi spi, Level logLevel, String logName, boolean needsSourceDigestVerification, - boolean needsDataDigestVerification) { + boolean needsDataDigestVerification, + Supplier memoryArenaSupplier) { this.spi = spi; this.logLevel = logLevel; this.logName = logName; this.needsDataDigestVerification = needsDataDigestVerification; this.needsSourceDigestVerification = needsSourceDigestVerification; + this.memoryArenaSupplier = memoryArenaSupplier; } /** @@ -86,13 +99,34 @@ private Cache( * compute the hash of the stored cache and compare it with the stored metadata entry. */ static Cache create( - Cache.Spi spi, + Spi spi, Level logLevel, String logName, boolean needsSourceDigestVerification, boolean needsDataDigestVerification) { return new Cache<>( - spi, logLevel, logName, needsSourceDigestVerification, needsDataDigestVerification); + spi, + logLevel, + logName, + needsSourceDigestVerification, + needsDataDigestVerification, + Arena::ofShared); + } + + static Cache create( + Spi spi, + Level logLevel, + String logName, + boolean needsSourceDigestVerification, + boolean needsDataDigestVerification, + Supplier memoryArenaSupplier) { + return new Cache<>( + spi, + logLevel, + logName, + needsSourceDigestVerification, + needsDataDigestVerification, + memoryArenaSupplier); } /** @@ -139,14 +173,12 @@ private boolean saveCacheTo( TruffleFile metadataFile = getCacheMetadataPath(cacheRoot); TruffleFile parentPath = cacheDataFile.getParent(); + closeMemoryArena(); if (writeBytesTo(cacheDataFile, bytesToWrite) && writeBytesTo(metadataFile, metadataBytes)) { logger.log( logLevel, - "Written cache data [" - + logName - + "] to [" - + toMaskedPath(parentPath).applyMasking() - + "]."); + "Written cache data [{0}] to [{1}] of size [{2}].", + new Object[] {logName, toMaskedPath(parentPath).applyMasking(), bytesToWrite.length}); return true; } else { // Clean up after ourselves if it fails. @@ -186,7 +218,7 @@ public final Optional load(EnsoContext context) { logLevel, "Using cache for [" + logName - + " at location [" + + "] at location [" + toMaskedPath(root).applyMasking() + "]."); return Optional.of(cache); @@ -234,9 +266,20 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge ByteBuffer blobBytes; var threeMbs = 3 * 1024 * 1024; if (file.exists() && file.length() > threeMbs) { - logger.log(Level.FINEST, "Cache file " + file + " mmapped with " + file.length() + " size"); - var raf = new RandomAccessFile(file, "r"); - blobBytes = raf.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); + logger.log( + Level.FINEST, + "Cache file {0} mmapped with {1} size", + new Object[] {file, file.length()}); + closeMemoryArena(); + memoryArena = memoryArenaSupplier.get(); + try (var chan = FileChannel.open(file.toPath())) { + assert memoryArena.scope().isAlive(); + var memSegment = chan.map(MapMode.READ_ONLY, 0, file.length(), memoryArena); + blobBytes = memSegment.asByteBuffer(); + } catch (IOException e) { + logger.log(Level.SEVERE, "Failed to mmap cache file " + file, e); + throw e; + } } else { blobBytes = ByteBuffer.wrap(dataPath.readAllBytes()); } @@ -278,6 +321,16 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge } } + /** + * Close any previous arena and creates a new one. Closing the previous arena invalidates all byte + * buffers associated with it. + */ + private void closeMemoryArena() { + if (memoryArena != null && memoryArena.scope().isAlive()) { + memoryArena.close(); + } + } + /** * Read metadata representation from the provided location * @@ -366,6 +419,7 @@ public final void invalidate(EnsoContext context) { for (var root : spi.getCacheRoots(context)) { invalidateCache(root, logger); } + closeMemoryArena(); } }