From 06ae8582eb2b59eef3cdb00a5bf90b194f7ecfcc Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Wed, 27 Aug 2025 17:22:45 +0200 Subject: [PATCH 01/31] Test that second cache serialization fails --- .../interpreter/caches/CacheClosingTest.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java new file mode 100644 index 000000000000..d80483a792c7 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java @@ -0,0 +1,64 @@ +package org.enso.interpreter.caches; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import org.enso.common.RuntimeOptions; +import org.enso.compiler.context.CompilerContext; +import org.enso.polyglot.PolyglotContext; +import org.enso.test.utils.ContextUtils; +import org.enso.test.utils.ProjectUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class CacheClosingTest { + + @Rule public final TemporaryFolder tmpFolder = new TemporaryFolder(); + + @Test + public void cacheCannotBeSavedTwice() throws IOException { + var projDir = tmpFolder.newFolder("Proj").toPath(); + ProjectUtils.createProject("Proj", """ + main = + 42 + """, projDir); + try (var ctx = + ContextUtils.newBuilder() + .withModifiedContext(bldr -> bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false")) + .withProjectRoot(projDir) + .build()) { + var polyCtx = new PolyglotContext(ctx.context()); + polyCtx.getTopScope().compile(true); + var compilerCtx = ctx.ensoContext().getCompiler().context(); + var modOpt = ctx.ensoContext().getPackageRepository().getLoadedModule("local.Proj.Main"); + assertThat(modOpt.isDefined(), is(true)); + var mod = modOpt.get(); + boolean serialized = false; + try { + serialized = serialize(compilerCtx, ctx, mod); + } catch (ExecutionException | InterruptedException e) { + fail("First serialization should be OK"); + } + assertThat("First serialization should be OK", serialized, is(true)); + + try { + serialize(compilerCtx, ctx, mod); + fail("Second serialization should fail"); + } catch (ExecutionException | InterruptedException e) { + // OK + } + } + } + + private static boolean serialize( + CompilerContext compilerCtx, ContextUtils ctx, CompilerContext.Module mod) + throws ExecutionException, InterruptedException { + var serializeFut = + compilerCtx.serializeModule(ctx.ensoContext().getCompiler(), mod, false, false); + return serializeFut.get(); + } +} From ab0fc5e338ac7e6dcf9b00a5d21895a7c3a03dcc Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Wed, 27 Aug 2025 17:23:21 +0200 Subject: [PATCH 02/31] Cache uses native memory Arena --- build.sbt | 3 ++- .../org/enso/interpreter/caches/Cache.java | 20 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/build.sbt b/build.sbt index 40203e53a5e7..74b8a74a385b 100644 --- a/build.sbt +++ b/build.sbt @@ -2664,7 +2664,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, 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 c3c211f4e269..c3ca1e617b00 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,9 +6,10 @@ 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; @@ -42,6 +43,8 @@ public final class Cache { */ private final boolean needsSourceDigestVerification; + private final 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. @@ -60,7 +63,7 @@ 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, @@ -70,6 +73,7 @@ private Cache( this.logName = logName; this.needsDataDigestVerification = needsDataDigestVerification; this.needsSourceDigestVerification = needsSourceDigestVerification; + this.memoryArena = Arena.ofConfined(); } /** @@ -85,7 +89,7 @@ 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, @@ -158,6 +162,7 @@ private boolean saveCacheTo( + "] to [" + toMaskedPath(parentPath).applyMasking() + "]."); + memoryArena.close(); return true; } else { // Clean up after ourselves if it fails. @@ -260,8 +265,13 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge 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()); + try (var chan = FileChannel.open(file.toPath())) { + 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()); } From 883dc141d2bce6c98ea580ee9f3f95eef7e7ee0a Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 28 Aug 2025 15:06:45 +0200 Subject: [PATCH 03/31] Add optional CacheStatistics to EnsoContext --- .../java/org/enso/common/RuntimeOptions.java | 8 +++- .../org/enso/interpreter/EnsoLanguage.java | 7 ++- .../org/enso/interpreter/caches/Cache.java | 46 ++++++++++++++++++- .../enso/interpreter/caches/CacheEvent.java | 38 +++++++++++++++ .../interpreter/caches/CacheStatistics.java | 25 ++++++++++ .../interpreter/caches/ImportExportCache.java | 4 +- .../interpreter/caches/SuggestionsCache.java | 4 +- .../enso/interpreter/runtime/EnsoContext.java | 14 +++++- .../runtime/TruffleCompilerContext.java | 11 +++-- 9 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java create mode 100644 engine/runtime/src/main/java/org/enso/interpreter/caches/CacheStatistics.java diff --git a/engine/common/src/main/java/org/enso/common/RuntimeOptions.java b/engine/common/src/main/java/org/enso/common/RuntimeOptions.java index 66c2d9675bb6..71e229bb55db 100644 --- a/engine/common/src/main/java/org/enso/common/RuntimeOptions.java +++ b/engine/common/src/main/java/org/enso/common/RuntimeOptions.java @@ -153,6 +153,11 @@ private RuntimeOptions() {} OptionDescriptor.newBuilder(USE_GLOBAL_IR_CACHE_LOCATION_KEY, USE_GLOBAL_IR_CACHE_LOCATION) .build(); + public static final String ENABLE_CACHE_STATS = optionName("enableCacheCounters"); + public static final OptionKey ENABLE_CACHE_STATS_KEYS = new OptionKey<>(false); + public static final OptionDescriptor ENABLE_CACHE_STATS_DESCRIPTOR = + OptionDescriptor.newBuilder(ENABLE_CACHE_STATS_KEYS, ENABLE_CACHE_STATS).build(); + public static final String ENABLE_EXECUTION_TIMER = optionName("enableExecutionTimer"); /* Enables timer that counts down the execution time of expressions. */ @@ -201,7 +206,8 @@ private RuntimeOptions() {} WAIT_FOR_PENDING_SERIALIZATION_JOBS_DESCRIPTOR, USE_GLOBAL_IR_CACHE_LOCATION_DESCRIPTOR, ENABLE_EXECUTION_TIMER_DESCRIPTOR, - WARNINGS_LIMIT_DESCRIPTOR)); + WARNINGS_LIMIT_DESCRIPTOR, + ENABLE_CACHE_STATS_DESCRIPTOR)); /** * Canonicalizes the option name by prefixing it with the language name. diff --git a/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java b/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java index 990d7e03c7e3..97abab74baad 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java @@ -35,6 +35,7 @@ import org.enso.distribution.Environment; import org.enso.distribution.locking.LockManager; import org.enso.distribution.locking.ThreadSafeFileLockManager; +import org.enso.interpreter.caches.CacheStatistics; import org.enso.interpreter.node.EnsoRootNode; import org.enso.interpreter.node.ExpressionNode; import org.enso.interpreter.node.ProgramRootNode; @@ -168,13 +169,17 @@ protected EnsoContext createContext(Env env) { env.registerService(lockManager); } + boolean cacheStatsEnabled = env.getOptions().get(RuntimeOptions.ENABLE_CACHE_STATS_KEYS); + var cacheStats = cacheStatsEnabled ? CacheStatistics.create() : null; + boolean isExecutionTimerEnabled = env.getOptions().get(RuntimeOptions.ENABLE_EXECUTION_TIMER_KEY); Timer timer = isExecutionTimerEnabled ? new Timer.Nanosecond() : new Timer.Disabled(); env.registerService(timer); EnsoContext context = - new EnsoContext(this, env, notificationHandler, lockManager, distributionManager); + new EnsoContext( + this, env, notificationHandler, lockManager, distributionManager, cacheStats); env.registerService(context.getThreadManager()); return context; 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 c3ca1e617b00..297e9e21fed9 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 @@ -14,6 +14,7 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; 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; @@ -51,6 +52,8 @@ public final class Cache { */ private final boolean needsDataDigestVerification; + private final CacheStatistics cacheStatistics; + /** * Constructor for subclasses. * @@ -61,18 +64,21 @@ public final class Cache { * stored metadata entry. * @param needsDataDigestVerification Flag indicating if the de-serialization process should * compute the hash of the stored cache and compare it with the stored metadata entry. + * @param cacheStatistics Optional cache statistics collector */ private Cache( Spi spi, Level logLevel, String logName, boolean needsSourceDigestVerification, - boolean needsDataDigestVerification) { + boolean needsDataDigestVerification, + CacheStatistics cacheStatistics) { this.spi = spi; this.logLevel = logLevel; this.logName = logName; this.needsDataDigestVerification = needsDataDigestVerification; this.needsSourceDigestVerification = needsSourceDigestVerification; + this.cacheStatistics = cacheStatistics; this.memoryArena = Arena.ofConfined(); } @@ -95,7 +101,29 @@ static Cache create( boolean needsSourceDigestVerification, boolean needsDataDigestVerification) { return new Cache<>( - spi, logLevel, logName, needsSourceDigestVerification, needsDataDigestVerification); + spi, logLevel, logName, needsSourceDigestVerification, needsDataDigestVerification, null); + } + + /** + * Creates cache with statistics collection enabled. + * + * @see #create(Spi, Level, String, boolean, boolean) + */ + static Cache create( + Spi spi, + Level logLevel, + String logName, + boolean needsSourceDigestVerification, + boolean needsDataDigestVerification, + CacheStatistics cacheStats) { + assert cacheStats != null; + return new Cache<>( + spi, + logLevel, + logName, + needsSourceDigestVerification, + needsDataDigestVerification, + cacheStats); } /** @@ -162,6 +190,7 @@ private boolean saveCacheTo( + "] to [" + toMaskedPath(parentPath).applyMasking() + "]."); + recordCacheEvent(() -> new CacheEvent.Save(spi.entryName(), bytesToWrite.length)); memoryArena.close(); return true; } else { @@ -272,8 +301,12 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge logger.log(Level.SEVERE, "Failed to mmap cache file " + file, e); throw e; } + recordCacheEvent( + () -> new CacheEvent.MmapLoad(spi.entryName(), (int) file.length(), file.getPath())); } else { blobBytes = ByteBuffer.wrap(dataPath.readAllBytes()); + recordCacheEvent( + () -> new CacheEvent.FileLoad(spi.entryName(), (int) file.length(), file.getPath())); } boolean blobDigestValid = !needsDataDigestVerification @@ -289,6 +322,7 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge Level.FINEST, "Loaded cache for {0} with {1} bytes in {2} ms", new Object[] {logName, blobBytes.limit(), took}); + recordCacheEvent(() -> new CacheEvent.Deserialize(spi.entryName())); return cachedObject; } else { invalidateCache(cacheRoot, logger); @@ -313,6 +347,12 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge } } + private void recordCacheEvent(Supplier eventSupply) { + if (cacheStatistics != null) { + cacheStatistics.addEvent(eventSupply.get()); + } + } + /** * Read metadata representation from the provided location * @@ -361,6 +401,8 @@ private void invalidateCache(TruffleFile cacheRoot, TruffleLogger logger) { TruffleFile metadataFile = getCacheMetadataPath(cacheRoot); TruffleFile dataFile = getCacheDataPath(cacheRoot); + recordCacheEvent(() -> new CacheEvent.Invalidate(spi.entryName())); + doDeleteAt(cacheRoot, metadataFile, logger); doDeleteAt(cacheRoot, dataFile, logger); } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java b/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java new file mode 100644 index 000000000000..1ffe1a42e4ab --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java @@ -0,0 +1,38 @@ +package org.enso.interpreter.caches; + +import org.enso.interpreter.caches.Cache.Spi; + +public sealed interface CacheEvent { + + /** + * {@link Spi#entryName() entry name} of the cache. + * + * @return not null. + */ + String cacheName(); + + sealed interface Load extends CacheEvent { + + /** Size in bytes. */ + int size(); + + /** + * @return File path or file name. Not null. + */ + String file(); + } + + record Save(String cacheName, int size) implements CacheEvent {} + + /** Load by reading all bytes from a file. */ + record FileLoad(String cacheName, int size, String file) implements Load {} + + /** Load by mapping file to memory. */ + record MmapLoad(String cacheName, int size, String file) implements Load {} + + record Serialize(String cacheName) implements CacheEvent {} + + record Deserialize(String cacheName) implements CacheEvent {} + + record Invalidate(String cacheName) implements CacheEvent {} +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheStatistics.java b/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheStatistics.java new file mode 100644 index 000000000000..29f0639096bd --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheStatistics.java @@ -0,0 +1,25 @@ +package org.enso.interpreter.caches; + +import java.util.ArrayList; +import java.util.List; + +/** Utility class to keep track of cache-related statistics. */ +public final class CacheStatistics { + private CacheStatistics() {} + + private final List cacheEvents = new ArrayList<>(); + + public static CacheStatistics create() { + return new CacheStatistics(); + } + + public void addEvent(CacheEvent event) { + synchronized (cacheEvents) { + cacheEvents.add(event); + } + } + + public List getCacheEvents() { + return cacheEvents; + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/caches/ImportExportCache.java b/engine/runtime/src/main/java/org/enso/interpreter/caches/ImportExportCache.java index 3162ceff77d8..07cd0d820654 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/caches/ImportExportCache.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/caches/ImportExportCache.java @@ -33,9 +33,9 @@ private ImportExportCache(LibraryName libraryName) { } public static Cache create( - LibraryName libraryName) { + LibraryName libraryName, CacheStatistics cacheStats) { var impl = new ImportExportCache(libraryName); - return Cache.create(impl, Level.FINEST, libraryName.toString(), true, false); + return Cache.create(impl, Level.FINEST, libraryName.toString(), true, false, cacheStats); } @Override diff --git a/engine/runtime/src/main/java/org/enso/interpreter/caches/SuggestionsCache.java b/engine/runtime/src/main/java/org/enso/interpreter/caches/SuggestionsCache.java index a083db1d8b81..0516525e76ff 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/caches/SuggestionsCache.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/caches/SuggestionsCache.java @@ -41,10 +41,10 @@ private SuggestionsCache(LibraryName libraryName) { } public static Cache create( - LibraryName libraryName) { + LibraryName libraryName, CacheStatistics cacheStatistics) { var impl = new SuggestionsCache(libraryName); var logName = "Suggestions(" + libraryName + ")"; - return Cache.create(impl, Level.FINE, logName, true, false); + return Cache.create(impl, Level.FINE, logName, true, false, cacheStatistics); } @Override diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java index 16bf2b0f6c50..4f3e2743851f 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java @@ -47,6 +47,7 @@ import org.enso.editions.LibraryName; import org.enso.interpreter.EnsoLanguage; import org.enso.interpreter.OptionsHelper; +import org.enso.interpreter.caches.CacheStatistics; import org.enso.interpreter.runtime.builtin.Builtins; import org.enso.interpreter.runtime.data.Type; import org.enso.interpreter.runtime.data.atom.Atom; @@ -84,6 +85,7 @@ public final class EnsoContext { private final boolean isStaticAnalysisEnabled; private final boolean isHostClassLoading; private final boolean isGuestClassLoading; + private final CacheStatistics cacheStatistics; /** * Right now there is just a single polyglot Java system. */ @@ -123,18 +125,21 @@ public final class EnsoContext { * @param notificationHandler a handler for notifications * @param lockManager the lock manager instance * @param distributionManager a distribution manager + * @param cacheStatistics nullable */ public EnsoContext( EnsoLanguage language, Env environment, NotificationHandler notificationHandler, LockManager lockManager, - DistributionManager distributionManager) { + DistributionManager distributionManager, + CacheStatistics cacheStatistics) { this.language = language; this.environment = environment; this.out = new PrintStream(environment.out()); this.err = new PrintStream(environment.err()); this.in = environment.in(); + this.cacheStatistics = cacheStatistics; this.inReader = new BufferedReader(new InputStreamReader(environment.in())); var threadExecutors = new ThreadExecutors(environment, logger); var guestParallelism = getOption(RuntimeOptions.GUEST_PARALLELISM_KEY); @@ -860,6 +865,13 @@ public DefaultPackageRepository getPackageRepository() { return packageRepository; } + /** + * @return null if cache counters were {@link RuntimeOptions#ENABLE_CACHE_STATS_KEYS disabled}. + */ + public CacheStatistics getCacheStatistics() { + return cacheStatistics; + } + /** * Gets a logger for the specified class that is bound to this engine. Such logger may then be * safely used in threads defined in a thread-pool. diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/TruffleCompilerContext.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/TruffleCompilerContext.java index 65b976e2b21a..cd9fca8d5501 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/TruffleCompilerContext.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/TruffleCompilerContext.java @@ -45,6 +45,7 @@ import org.enso.editions.LibraryName; import org.enso.interpreter.CompilationAbortedException; import org.enso.interpreter.caches.Cache; +import org.enso.interpreter.caches.CacheStatistics; import org.enso.interpreter.caches.ImportExportCache; import org.enso.interpreter.caches.ImportExportCache.MapToBindings; import org.enso.interpreter.caches.ModuleCache; @@ -66,6 +67,7 @@ final class TruffleCompilerContext implements CompilerContext { private final TruffleLogger loggerSerializationManager; private final RuntimeStubsGenerator stubsGenerator; private final SerializationPool serializationPool; + private final CacheStatistics cacheStatistics; TruffleCompilerContext(EnsoContext context) { this.context = context; @@ -73,6 +75,7 @@ final class TruffleCompilerContext implements CompilerContext { this.loggerSerializationManager = context.getLogger(SerializationPool.class); this.serializationPool = new SerializationPool(this); this.stubsGenerator = new RuntimeStubsGenerator(context.getBuiltins()); + this.cacheStatistics = context.getCacheStatistics(); } @Override @@ -544,7 +547,7 @@ Callable doSerializeLibrary( boolean result = doSerializeLibrarySuggestions(compiler, libraryName, useGlobalCacheLocations); try { - var cache = ImportExportCache.create(libraryName); + var cache = ImportExportCache.create(libraryName, cacheStatistics); var file = saveCache(cache, bindingsCache, useGlobalCacheLocations); result &= file != null; } catch (Throwable e) { @@ -594,7 +597,7 @@ private boolean doSerializeLibrarySuggestions( .foreach(suggestions::add); var cachedSuggestions = new SuggestionsCache.CachedSuggestions(libraryName, suggestions); - var cache = SuggestionsCache.create(libraryName); + var cache = SuggestionsCache.create(libraryName, cacheStatistics); var file = saveCache(cache, cachedSuggestions, useGlobalCacheLocations); return file != null; } catch (Throwable e) { @@ -621,7 +624,7 @@ private scala.Option deserializeSuggestionsI return scala.Option.empty(); } else { pool.waitWhileSerializing(toQualifiedName(libraryName)); - var cache = SuggestionsCache.create(libraryName); + var cache = SuggestionsCache.create(libraryName, cacheStatistics); var loaded = loadCache(cache); if (loaded.isPresent()) { logSerializationManager(Level.FINE, "Restored suggestions for library [{0}].", libraryName); @@ -642,7 +645,7 @@ scala.Option deserializeLibraryBindings(Librar return scala.Option.empty(); } else { pool.waitWhileSerializing(toQualifiedName(libraryName)); - var cache = ImportExportCache.create(libraryName); + var cache = ImportExportCache.create(libraryName, cacheStatistics); var loaded = loadCache(cache); if (loaded.isPresent()) { logSerializationManager(Level.FINE, "Restored bindings for library [{0}].", libraryName); From 963afda358358c7153537f4d97c99217ca451de5 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 28 Aug 2025 15:51:37 +0200 Subject: [PATCH 04/31] Use logName instead of entryName --- .../src/main/java/org/enso/interpreter/caches/Cache.java | 8 ++++---- .../main/java/org/enso/interpreter/caches/CacheEvent.java | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) 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 297e9e21fed9..0b01015eeaaf 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 @@ -190,7 +190,7 @@ private boolean saveCacheTo( + "] to [" + toMaskedPath(parentPath).applyMasking() + "]."); - recordCacheEvent(() -> new CacheEvent.Save(spi.entryName(), bytesToWrite.length)); + recordCacheEvent(() -> new CacheEvent.Save(logName, bytesToWrite.length)); memoryArena.close(); return true; } else { @@ -302,11 +302,11 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge throw e; } recordCacheEvent( - () -> new CacheEvent.MmapLoad(spi.entryName(), (int) file.length(), file.getPath())); + () -> new CacheEvent.MmapLoad(logName, (int) file.length(), file.getPath())); } else { blobBytes = ByteBuffer.wrap(dataPath.readAllBytes()); recordCacheEvent( - () -> new CacheEvent.FileLoad(spi.entryName(), (int) file.length(), file.getPath())); + () -> new CacheEvent.FileLoad(logName, (int) file.length(), file.getPath())); } boolean blobDigestValid = !needsDataDigestVerification @@ -322,7 +322,7 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge Level.FINEST, "Loaded cache for {0} with {1} bytes in {2} ms", new Object[] {logName, blobBytes.limit(), took}); - recordCacheEvent(() -> new CacheEvent.Deserialize(spi.entryName())); + recordCacheEvent(() -> new CacheEvent.Deserialize(logName)); return cachedObject; } else { invalidateCache(cacheRoot, logger); diff --git a/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java b/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java index 1ffe1a42e4ab..6e57748b58ba 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java @@ -1,11 +1,9 @@ package org.enso.interpreter.caches; -import org.enso.interpreter.caches.Cache.Spi; - public sealed interface CacheEvent { /** - * {@link Spi#entryName() entry name} of the cache. + * Unique name for the cache. For example {@link Cache#logName}. * * @return not null. */ From 5a143306d7202608c16d1ae20ff94a74df6c7b1c Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 28 Aug 2025 15:51:56 +0200 Subject: [PATCH 05/31] Test save of suggestions and bindings caches --- .../interpreter/caches/CacheClosingTest.java | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java index d80483a792c7..83ae12cc4b83 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java @@ -2,12 +2,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.junit.Assert.fail; -import java.io.IOException; -import java.util.concurrent.ExecutionException; import org.enso.common.RuntimeOptions; -import org.enso.compiler.context.CompilerContext; +import org.enso.editions.LibraryName; import org.enso.polyglot.PolyglotContext; import org.enso.test.utils.ContextUtils; import org.enso.test.utils.ProjectUtils; @@ -20,7 +17,7 @@ public class CacheClosingTest { @Rule public final TemporaryFolder tmpFolder = new TemporaryFolder(); @Test - public void cacheCannotBeSavedTwice() throws IOException { + public void compilationSavesSuggestionsAndImportExportCache() throws Exception { var projDir = tmpFolder.newFolder("Proj").toPath(); ProjectUtils.createProject("Proj", """ main = @@ -28,37 +25,40 @@ public void cacheCannotBeSavedTwice() throws IOException { """, projDir); try (var ctx = ContextUtils.newBuilder() - .withModifiedContext(bldr -> bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false")) + .withModifiedContext( + bldr -> + bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false") + .option(RuntimeOptions.USE_GLOBAL_IR_CACHE_LOCATION, "false") + .option(RuntimeOptions.ENABLE_CACHE_STATS, "true")) .withProjectRoot(projDir) .build()) { var polyCtx = new PolyglotContext(ctx.context()); polyCtx.getTopScope().compile(true); - var compilerCtx = ctx.ensoContext().getCompiler().context(); - var modOpt = ctx.ensoContext().getPackageRepository().getLoadedModule("local.Proj.Main"); - assertThat(modOpt.isDefined(), is(true)); - var mod = modOpt.get(); - boolean serialized = false; - try { - serialized = serialize(compilerCtx, ctx, mod); - } catch (ExecutionException | InterruptedException e) { - fail("First serialization should be OK"); - } - assertThat("First serialization should be OK", serialized, is(true)); - - try { - serialize(compilerCtx, ctx, mod); - fail("Second serialization should fail"); - } catch (ExecutionException | InterruptedException e) { - // OK - } + var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); + var libName = LibraryName.apply("local", "Proj"); + var hasSaveSuggestionCacheEvent = + cacheEvents.stream() + .anyMatch(e -> isSuggestionCacheEvent(e, libName) && e instanceof CacheEvent.Save); + var hasSaveImportExportCacheEvent = + cacheEvents.stream() + .anyMatch(e -> isImportExportCacheEvent(e, libName) && e instanceof CacheEvent.Save); + assertThat( + "There should be a save event for SuggestionsCache", + hasSaveSuggestionCacheEvent, + is(true)); + assertThat( + "There should be a save event for ImportExportCache", + hasSaveImportExportCacheEvent, + is(true)); } } - private static boolean serialize( - CompilerContext compilerCtx, ContextUtils ctx, CompilerContext.Module mod) - throws ExecutionException, InterruptedException { - var serializeFut = - compilerCtx.serializeModule(ctx.ensoContext().getCompiler(), mod, false, false); - return serializeFut.get(); + private static boolean isSuggestionCacheEvent(CacheEvent event, LibraryName libName) { + return event.cacheName().contains("Suggestions") + && event.cacheName().contains(libName.toString()); + } + + private static boolean isImportExportCacheEvent(CacheEvent event, LibraryName libName) { + return libName.toString().equals(event.cacheName()); } } From 24b7c4d0c2d8b3b828dcd8dc107fdeb910a79040 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 28 Aug 2025 16:09:14 +0200 Subject: [PATCH 06/31] Use hamcrest matchers --- .../interpreter/caches/CacheClosingTest.java | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java index 83ae12cc4b83..fa17c9b25868 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java @@ -1,13 +1,20 @@ package org.enso.interpreter.caches; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; +import java.io.IOException; +import java.nio.file.Path; +import java.util.function.Predicate; 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.hamcrest.CustomMatcher; +import org.hamcrest.Matcher; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -20,7 +27,7 @@ public class CacheClosingTest { public void compilationSavesSuggestionsAndImportExportCache() throws Exception { var projDir = tmpFolder.newFolder("Proj").toPath(); ProjectUtils.createProject("Proj", """ - main = + method = 42 """, projDir); try (var ctx = @@ -36,6 +43,18 @@ public void compilationSavesSuggestionsAndImportExportCache() throws Exception { polyCtx.getTopScope().compile(true); var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); var libName = LibraryName.apply("local", "Proj"); + var saveSuggestionEventMatcher = + eventMatcher( + "save suggestions cache", + e -> isSuggestionCacheEvent(e, libName) && e instanceof CacheEvent.Save); + var saveBindingsCacheMatcher = + eventMatcher( + "save import/export cache", + e -> isImportExportCacheEvent(e, libName) && e instanceof CacheEvent.Save); + assertThat(cacheEvents, hasItem(saveSuggestionEventMatcher)); + assertThat(cacheEvents, hasItem(saveBindingsCacheMatcher)); + } + } var hasSaveSuggestionCacheEvent = cacheEvents.stream() .anyMatch(e -> isSuggestionCacheEvent(e, libName) && e instanceof CacheEvent.Save); @@ -61,4 +80,17 @@ private static boolean isSuggestionCacheEvent(CacheEvent event, LibraryName libN private static boolean isImportExportCacheEvent(CacheEvent event, LibraryName libName) { return libName.toString().equals(event.cacheName()); } + + private static Matcher eventMatcher(String descr, Predicate predicate) { + return new CustomMatcher<>(descr) { + @Override + public boolean matches(Object item) { + if (item instanceof CacheEvent event) { + return predicate.test(event); + } else { + return false; + } + } + }; + } } From 58b92e8eb8f282531f702cd2d40cd05b0e8273b1 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 28 Aug 2025 16:22:17 +0200 Subject: [PATCH 07/31] Add test for loading caches --- .../interpreter/caches/CacheClosingTest.java | 120 ++++++++++++------ 1 file changed, 79 insertions(+), 41 deletions(-) diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java index fa17c9b25868..578518a33481 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java @@ -1,11 +1,11 @@ package org.enso.interpreter.caches; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import java.io.IOException; import java.nio.file.Path; +import java.util.List; import java.util.function.Predicate; import org.enso.common.RuntimeOptions; import org.enso.editions.LibraryName; @@ -13,8 +13,6 @@ import org.enso.test.utils.ContextUtils; import org.enso.test.utils.ProjectUtils; import org.graalvm.polyglot.Value; -import org.hamcrest.CustomMatcher; -import org.hamcrest.Matcher; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -39,37 +37,82 @@ public void compilationSavesSuggestionsAndImportExportCache() throws Exception { .option(RuntimeOptions.ENABLE_CACHE_STATS, "true")) .withProjectRoot(projDir) .build()) { - var polyCtx = new PolyglotContext(ctx.context()); - polyCtx.getTopScope().compile(true); - var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); var libName = LibraryName.apply("local", "Proj"); - var saveSuggestionEventMatcher = - eventMatcher( - "save suggestions cache", - e -> isSuggestionCacheEvent(e, libName) && e instanceof CacheEvent.Save); - var saveBindingsCacheMatcher = - eventMatcher( - "save import/export cache", - e -> isImportExportCacheEvent(e, libName) && e instanceof CacheEvent.Save); - assertThat(cacheEvents, hasItem(saveSuggestionEventMatcher)); - assertThat(cacheEvents, hasItem(saveBindingsCacheMatcher)); + compileAndAssertCreatedCaches(ctx, libName); } } - var hasSaveSuggestionCacheEvent = - cacheEvents.stream() - .anyMatch(e -> isSuggestionCacheEvent(e, libName) && e instanceof CacheEvent.Save); - var hasSaveImportExportCacheEvent = - cacheEvents.stream() - .anyMatch(e -> isImportExportCacheEvent(e, libName) && e instanceof CacheEvent.Save); - assertThat( - "There should be a save event for SuggestionsCache", - hasSaveSuggestionCacheEvent, - is(true)); - assertThat( - "There should be a save event for ImportExportCache", - hasSaveImportExportCacheEvent, - is(true)); + + @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 = + ContextUtils.newBuilder() + .withModifiedContext( + bldr -> + bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false") + .option(RuntimeOptions.USE_GLOBAL_IR_CACHE_LOCATION, "false") + .option(RuntimeOptions.ENABLE_CACHE_STATS, "true")) + .withProjectRoot(projDir) + .build()) { + compileAndAssertCreatedCaches(ctx, libName); } + + // Second, run the project. Caches should be loaded. + try (var ctx = + ContextUtils.newBuilder() + .withModifiedContext( + bldr -> + bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false") + .option(RuntimeOptions.USE_GLOBAL_IR_CACHE_LOCATION, "false") + .option(RuntimeOptions.ENABLE_CACHE_STATS, "true")) + .withProjectRoot(projDir) + .build()) { + var res = runMain(ctx, projDir); + assertThat("execution is OK", res.asInt(), is(42)); + var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); + assertContainsEvent( + "load bindings cache", + cacheEvents, + e -> isImportExportCacheEvent(e, libName) && e instanceof CacheEvent.Load); + } + } + + /** + * Compiles the project and asserts that suggestions and import/export (binding) caches were + * created (saved). + */ + private static void compileAndAssertCreatedCaches(ContextUtils ctx, LibraryName libName) { + var polyCtx = new PolyglotContext(ctx.context()); + polyCtx.getTopScope().compile(true); + var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); + assertContainsEvent( + "save suggestions cache", + cacheEvents, + e -> isSuggestionCacheEvent(e, libName) && e instanceof CacheEvent.Save); + assertContainsEvent( + "save import/export cache", + cacheEvents, + e -> isImportExportCacheEvent(e, libName) && e instanceof CacheEvent.Save); + } + + 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; } private static boolean isSuggestionCacheEvent(CacheEvent event, LibraryName libName) { @@ -81,16 +124,11 @@ private static boolean isImportExportCacheEvent(CacheEvent event, LibraryName li return libName.toString().equals(event.cacheName()); } - private static Matcher eventMatcher(String descr, Predicate predicate) { - return new CustomMatcher<>(descr) { - @Override - public boolean matches(Object item) { - if (item instanceof CacheEvent event) { - return predicate.test(event); - } else { - return false; - } - } - }; + private static void assertContainsEvent( + String descr, List events, Predicate predicate) { + var hasItem = events.stream().anyMatch(predicate); + if (!hasItem) { + throw new AssertionError("Expected to find event: " + descr + " in " + events); + } } } From 822bf5db9b2f1fce4e2d424082165e4116e53edf Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 28 Aug 2025 16:22:44 +0200 Subject: [PATCH 08/31] Rename test --- .../caches/{CacheClosingTest.java => SaveAndLoadCacheTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/{CacheClosingTest.java => SaveAndLoadCacheTest.java} (99%) diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/SaveAndLoadCacheTest.java similarity index 99% rename from engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java rename to engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/SaveAndLoadCacheTest.java index 578518a33481..348aa769b18a 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheClosingTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/SaveAndLoadCacheTest.java @@ -17,7 +17,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -public class CacheClosingTest { +public class SaveAndLoadCacheTest { @Rule public final TemporaryFolder tmpFolder = new TemporaryFolder(); From e3a1c3617bc0b0f23b4d90443357783bf390edcb Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 28 Aug 2025 16:42:33 +0200 Subject: [PATCH 09/31] Test that loading bindings cache of big project should be mmapped --- .../caches/SaveAndLoadCacheTest.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) 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 index 348aa769b18a..61920545c258 100644 --- 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 @@ -2,6 +2,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import java.io.IOException; import java.nio.file.Path; @@ -84,6 +85,60 @@ public void cachesAreLoaded_AfterProjectIsCompiled() throws IOException { } } + @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 bindingsCacheSize; + try (var ctx = + ContextUtils.newBuilder() + .withModifiedContext( + bldr -> + bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false") + .option(RuntimeOptions.USE_GLOBAL_IR_CACHE_LOCATION, "false") + .option(RuntimeOptions.ENABLE_CACHE_STATS, "true")) + .withProjectRoot(projDir) + .build()) { + compileAndAssertCreatedCaches(ctx, libName); + var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); + assertThat(cacheEvents, is(notNullValue())); + var bindingCacheSave = + cacheEvents.stream() + .filter(e -> isImportExportCacheEvent(e, libName)) + .map(e -> (CacheEvent.Save) e) + .findFirst() + .orElseThrow(() -> new AssertionError("No binding cache events found")); + bindingsCacheSize = bindingCacheSave.size(); + var savedMb = bindingCacheSave.size() / 1024 / 1024; + assertThat("binding cache is at least 10MB", savedMb > 10, is(true)); + } + + // Run after compilation. Bindings cache should be mmapped. + try (var ctx = + ContextUtils.newBuilder() + .withModifiedContext( + bldr -> + bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false") + .option(RuntimeOptions.USE_GLOBAL_IR_CACHE_LOCATION, "false") + .option(RuntimeOptions.ENABLE_CACHE_STATS, "true")) + .withProjectRoot(projDir) + .build()) { + var res = runMain(ctx, projDir); + assertThat("execution is OK", res.asInt(), is(42)); + var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); + var mmapLoad = + cacheEvents.stream() + .filter(e -> e instanceof CacheEvent.MmapLoad) + .map(e -> (CacheEvent.MmapLoad) e) + .findFirst() + .orElseThrow(() -> new AssertionError("No mmap load events found")); + assertThat("Loaded same cached as previously saved", mmapLoad.size(), is(bindingsCacheSize)); + } + } + /** * Compiles the project and asserts that suggestions and import/export (binding) caches were * created (saved). @@ -131,4 +186,21 @@ private static void assertContainsEvent( throw new AssertionError("Expected to find event: " + descr + " in " + events); } } + + 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(); + } } From 54deef17997fd6caa435d75621864804f17ce8eb Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 28 Aug 2025 16:49:39 +0200 Subject: [PATCH 10/31] refact: extract method --- .../caches/SaveAndLoadCacheTest.java | 62 +++++-------------- 1 file changed, 17 insertions(+), 45 deletions(-) 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 index 61920545c258..abbb65b2bd19 100644 --- 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 @@ -29,15 +29,7 @@ public void compilationSavesSuggestionsAndImportExportCache() throws Exception { method = 42 """, projDir); - try (var ctx = - ContextUtils.newBuilder() - .withModifiedContext( - bldr -> - bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false") - .option(RuntimeOptions.USE_GLOBAL_IR_CACHE_LOCATION, "false") - .option(RuntimeOptions.ENABLE_CACHE_STATS, "true")) - .withProjectRoot(projDir) - .build()) { + try (var ctx = projCtx(projDir)) { var libName = LibraryName.apply("local", "Proj"); compileAndAssertCreatedCaches(ctx, libName); } @@ -53,28 +45,12 @@ public void cachesAreLoaded_AfterProjectIsCompiled() throws IOException { var libName = LibraryName.apply("local", "Proj"); // First, compile the project - try (var ctx = - ContextUtils.newBuilder() - .withModifiedContext( - bldr -> - bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false") - .option(RuntimeOptions.USE_GLOBAL_IR_CACHE_LOCATION, "false") - .option(RuntimeOptions.ENABLE_CACHE_STATS, "true")) - .withProjectRoot(projDir) - .build()) { + try (var ctx = projCtx(projDir)) { compileAndAssertCreatedCaches(ctx, libName); } // Second, run the project. Caches should be loaded. - try (var ctx = - ContextUtils.newBuilder() - .withModifiedContext( - bldr -> - bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false") - .option(RuntimeOptions.USE_GLOBAL_IR_CACHE_LOCATION, "false") - .option(RuntimeOptions.ENABLE_CACHE_STATS, "true")) - .withProjectRoot(projDir) - .build()) { + try (var ctx = projCtx(projDir)) { var res = runMain(ctx, projDir); assertThat("execution is OK", res.asInt(), is(42)); var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); @@ -93,15 +69,7 @@ public void bindingCachesOfBigProject_AreMmapped() throws IOException { var libName = LibraryName.apply("local", "Proj"); int bindingsCacheSize; - try (var ctx = - ContextUtils.newBuilder() - .withModifiedContext( - bldr -> - bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false") - .option(RuntimeOptions.USE_GLOBAL_IR_CACHE_LOCATION, "false") - .option(RuntimeOptions.ENABLE_CACHE_STATS, "true")) - .withProjectRoot(projDir) - .build()) { + try (var ctx = projCtx(projDir)) { compileAndAssertCreatedCaches(ctx, libName); var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); assertThat(cacheEvents, is(notNullValue())); @@ -117,15 +85,7 @@ public void bindingCachesOfBigProject_AreMmapped() throws IOException { } // Run after compilation. Bindings cache should be mmapped. - try (var ctx = - ContextUtils.newBuilder() - .withModifiedContext( - bldr -> - bldr.option(RuntimeOptions.DISABLE_IR_CACHES, "false") - .option(RuntimeOptions.USE_GLOBAL_IR_CACHE_LOCATION, "false") - .option(RuntimeOptions.ENABLE_CACHE_STATS, "true")) - .withProjectRoot(projDir) - .build()) { + try (var ctx = projCtx(projDir)) { var res = runMain(ctx, projDir); assertThat("execution is OK", res.asInt(), is(42)); var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); @@ -139,6 +99,17 @@ public void bindingCachesOfBigProject_AreMmapped() throws IOException { } } + private static 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.ENABLE_CACHE_STATS, "true")) + .withProjectRoot(projDir) + .build(); + } + /** * Compiles the project and asserts that suggestions and import/export (binding) caches were * created (saved). @@ -187,6 +158,7 @@ private static void assertContainsEvent( } } + /** Creates executable big source file. */ private static String createBigSource(int methodCount) { var sb = new StringBuilder(); sb.append(""" From cef388571f4d66b4b1b387848c71c90e10d37913 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 28 Aug 2025 17:52:35 +0200 Subject: [PATCH 11/31] Test that cache can be saved. --- .../enso/interpreter/caches/CacheTests.java | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheTests.java 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..73ae1286dc9b --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/caches/CacheTests.java @@ -0,0 +1,124 @@ +package org.enso.interpreter.caches; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import com.oracle.truffle.api.TruffleLogger; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Optional; +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(); + + @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((byte) 42), 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)); + } + + 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())); + } + + private record CachedData(byte data) {} + + 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 CacheSpi(Roots cacheRoots) { + this.cacheRoots = cacheRoots; + } + + @Override + public CachedData deserialize( + EnsoContext context, ByteBuffer data, Metadata meta, TruffleLogger logger) { + return new CachedData(data.get(0)); + } + + @Override + public byte[] serialize(EnsoContext context, CachedData entry) { + return new byte[] {entry.data}; + } + + @Override + public byte[] metadata(String sourceDigest, String blobDigest, CachedData entry) { + return new byte[0]; + } + + @Override + public Metadata metadataFromBytes(byte[] bytes, TruffleLogger logger) throws IOException { + return null; + } + + @Override + public Optional computeDigest(CachedData entry, TruffleLogger logger) { + return Optional.of(Byte.toString(entry.data)); + } + + @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"; + } + } +} From 04eaf5719521deaf9735bab81e187e5ba0071fa4 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 28 Aug 2025 17:54:11 +0200 Subject: [PATCH 12/31] Test global cache save preference --- .../org/enso/interpreter/caches/CacheTests.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 index 73ae1286dc9b..02e99d1bca0a 100644 --- 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 @@ -33,6 +33,19 @@ public void cacheCanBeSaved_ToLocalCacheRoot() throws IOException { 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((byte) 42), 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)); + } + private Roots createCacheRoots() throws IOException { var cacheRootDirPath = tempFolder.newFolder("cacheRoot").toPath(); var localCacheDir = cacheRootDirPath.resolve("local"); From 225beaa7d7ed047291f3874396de29b8f0600cf9 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 28 Aug 2025 18:55:07 +0200 Subject: [PATCH 13/31] More cache tests --- .../enso/interpreter/caches/CacheTests.java | 133 ++++++++++++++++-- 1 file changed, 125 insertions(+), 8 deletions(-) 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 index 02e99d1bca0a..36cb3928451d 100644 --- 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 @@ -2,11 +2,16 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import com.oracle.truffle.api.TruffleLogger; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Optional; +import java.util.Random; import java.util.logging.Level; import org.enso.interpreter.caches.Cache.Roots; import org.enso.interpreter.caches.Cache.Spi; @@ -17,8 +22,10 @@ import org.junit.rules.TemporaryFolder; public final class CacheTests { + @Rule public final TemporaryFolder tempFolder = new TemporaryFolder(); @Rule public final ContextUtils ctx = ContextUtils.createDefault(); + private static final Random random = new Random(42); @Test public void cacheCanBeSaved_ToLocalCacheRoot() throws IOException { @@ -26,7 +33,8 @@ public void cacheCanBeSaved_ToLocalCacheRoot() throws IOException { 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((byte) 42), ensoCtx, false); + var data = new CachedData(List.of((byte) 42)); + var ret = cache.save(data, ensoCtx, false); assertThat("was saved to local cache root", ret, is(cacheRoots.localCacheRoot())); var localCacheFile = cacheRoots.localCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.DATA_SUFFIX); @@ -39,13 +47,79 @@ public void globalCacheIsPreferred() throws IOException { 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((byte) 42), ensoCtx, true); + var data = new CachedData(List.of((byte) 42)); + var ret = cache.save(data, 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 cacheCannotBeLoadedViaSameInstance_AfterSave() 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 data = new CachedData(List.of((byte) 42)); + var ret = cache.save(data, ensoCtx, false); + assertThat("was saved", ret, is(notNullValue())); + var loaded = cache.load(ensoCtx); + assertThat( + "cache should be closed - unable to load after save via the same instance", + loaded.isEmpty(), + is(true)); + } + + @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); + var data = new CachedData(List.of((byte) 42)); + // Saving only data and no metadata + try (var os = localCacheFile.newOutputStream()) { + os.write(data.toPrimitive()); + } + 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_AfterCacheIsSaved() throws IOException { + var ensoCtx = ctx.ensoContext(); + var cacheRoots = createCacheRoots(); + var bigData = randomData(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); + assertThat( + "byte buffer got in deserialize is invalid", deserializeBuffer.hasRemaining(), is(false)); + } + + @Test + public void byteBufferIsValid_AfterCacheLoad() throws IOException { + var ensoCtx = ctx.ensoContext(); + var cacheRoots = createCacheRoots(); + var bigData = randomData(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; + assertThat("byte buffer is still readable", deserializeBuffer.hasRemaining(), is(true)); + } + private Roots createCacheRoots() throws IOException { var cacheRootDirPath = tempFolder.newFolder("cacheRoot").toPath(); var localCacheDir = cacheRootDirPath.resolve("local"); @@ -58,7 +132,43 @@ private Roots createCacheRoots() throws IOException { ensoCtx.getTruffleFile(globalCacheDir.toFile())); } - private record CachedData(byte data) {} + /** Saves data as well as empty metadata on the disk. */ + private static void saveToLocalRoot(CachedData 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.toPrimitive()); + } + try (var os = localMetadataFile.newOutputStream()) { + os.write(42); + } + } + + private static CachedData randomData(int size) { + byte[] primBytes = new byte[size]; + random.nextBytes(primBytes); + return CachedData.fromPrimitive(primBytes); + } + + private record CachedData(List bytes) { + private byte[] toPrimitive() { + byte[] primBytes = new byte[bytes.size()]; + for (int i = 0; i < bytes.size(); i++) { + primBytes[i] = bytes.get(i); + } + return primBytes; + } + + private static CachedData fromPrimitive(byte[] primBytes) { + var bytes = new ArrayList(primBytes.length); + for (var b : primBytes) { + bytes.add(b); + } + return new CachedData(bytes); + } + } private static final class Metadata {} @@ -68,6 +178,7 @@ private static final class CacheSpi implements Spi { public static final String ENTRY_NAME = "test-entry"; private final Roots cacheRoots; + private ByteBuffer deserializeBuffer; private CacheSpi(Roots cacheRoots) { this.cacheRoots = cacheRoots; @@ -76,12 +187,17 @@ private CacheSpi(Roots cacheRoots) { @Override public CachedData deserialize( EnsoContext context, ByteBuffer data, Metadata meta, TruffleLogger logger) { - return new CachedData(data.get(0)); + deserializeBuffer = data; + var bytes = new ArrayList(); + while (data.hasRemaining()) { + bytes.add(data.get()); + } + return new CachedData(bytes); } @Override public byte[] serialize(EnsoContext context, CachedData entry) { - return new byte[] {entry.data}; + return entry.toPrimitive(); } @Override @@ -90,13 +206,14 @@ public byte[] metadata(String sourceDigest, String blobDigest, CachedData entry) } @Override - public Metadata metadataFromBytes(byte[] bytes, TruffleLogger logger) throws IOException { - return null; + public Metadata metadataFromBytes(byte[] bytes, TruffleLogger logger) { + return new Metadata(); } @Override public Optional computeDigest(CachedData entry, TruffleLogger logger) { - return Optional.of(Byte.toString(entry.data)); + var hash = Arrays.hashCode(entry.toPrimitive()); + return Optional.of(Integer.toString(hash)); } @Override From 3e362b8d1dffa43d025a0224df8a84dddbe039f4 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 28 Aug 2025 19:00:24 +0200 Subject: [PATCH 14/31] Use only primitive bytes --- .../enso/interpreter/caches/CacheTests.java | 43 ++++++------------- 1 file changed, 13 insertions(+), 30 deletions(-) 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 index 36cb3928451d..68aabb8bb6c9 100644 --- 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 @@ -7,9 +7,7 @@ import com.oracle.truffle.api.TruffleLogger; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Optional; import java.util.Random; import java.util.logging.Level; @@ -33,7 +31,7 @@ public void cacheCanBeSaved_ToLocalCacheRoot() throws IOException { var ensoCtx = ctx.ensoContext(); var spi = new CacheSpi(cacheRoots); var cache = Cache.create(spi, Level.FINE, "testCache", false, false); - var data = new CachedData(List.of((byte) 42)); + var data = new CachedData(new byte[] {42}); var ret = cache.save(data, ensoCtx, false); assertThat("was saved to local cache root", ret, is(cacheRoots.localCacheRoot())); var localCacheFile = @@ -47,7 +45,7 @@ public void globalCacheIsPreferred() throws IOException { var ensoCtx = ctx.ensoContext(); var spi = new CacheSpi(cacheRoots); var cache = Cache.create(spi, Level.FINE, "testCache", false, false); - var data = new CachedData(List.of((byte) 42)); + var data = new CachedData(new byte[] {42}); var ret = cache.save(data, ensoCtx, true); assertThat("was saved to global cache root", ret, is(cacheRoots.globalCacheRoot())); var globalCacheFile = @@ -61,7 +59,7 @@ public void cacheCannotBeLoadedViaSameInstance_AfterSave() throws IOException { var ensoCtx = ctx.ensoContext(); var spi = new CacheSpi(cacheRoots); var cache = Cache.create(spi, Level.FINE, "testCache", false, false); - var data = new CachedData(List.of((byte) 42)); + var data = new CachedData(new byte[] {42}); var ret = cache.save(data, ensoCtx, false); assertThat("was saved", ret, is(notNullValue())); var loaded = cache.load(ensoCtx); @@ -77,10 +75,10 @@ public void cacheCannotBeLoaded_WithoutMetadataOnDisk() throws IOException { var cacheRoots = createCacheRoots(); var localCacheFile = cacheRoots.localCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.DATA_SUFFIX); - var data = new CachedData(List.of((byte) 42)); + var data = new CachedData(new byte[] {42}); // Saving only data and no metadata try (var os = localCacheFile.newOutputStream()) { - os.write(data.toPrimitive()); + os.write(data.bytes); } var spi = new CacheSpi(cacheRoots); var cache = Cache.create(spi, Level.FINE, "testCache", false, false); @@ -139,7 +137,7 @@ private static void saveToLocalRoot(CachedData data, Roots cacheRoots) throws IO var localMetadataFile = cacheRoots.localCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.METADATA_SUFFIX); try (var os = localCacheFile.newOutputStream()) { - os.write(data.toPrimitive()); + os.write(data.bytes); } try (var os = localMetadataFile.newOutputStream()) { os.write(42); @@ -149,26 +147,10 @@ private static void saveToLocalRoot(CachedData data, Roots cacheRoots) throws IO private static CachedData randomData(int size) { byte[] primBytes = new byte[size]; random.nextBytes(primBytes); - return CachedData.fromPrimitive(primBytes); + return new CachedData(primBytes); } - private record CachedData(List bytes) { - private byte[] toPrimitive() { - byte[] primBytes = new byte[bytes.size()]; - for (int i = 0; i < bytes.size(); i++) { - primBytes[i] = bytes.get(i); - } - return primBytes; - } - - private static CachedData fromPrimitive(byte[] primBytes) { - var bytes = new ArrayList(primBytes.length); - for (var b : primBytes) { - bytes.add(b); - } - return new CachedData(bytes); - } - } + private record CachedData(byte[] bytes) {} private static final class Metadata {} @@ -188,16 +170,17 @@ private CacheSpi(Roots cacheRoots) { public CachedData deserialize( EnsoContext context, ByteBuffer data, Metadata meta, TruffleLogger logger) { deserializeBuffer = data; - var bytes = new ArrayList(); + byte[] bytes = new byte[data.remaining()]; + int bytesIdx = 0; while (data.hasRemaining()) { - bytes.add(data.get()); + bytes[bytesIdx++] = data.get(); } return new CachedData(bytes); } @Override public byte[] serialize(EnsoContext context, CachedData entry) { - return entry.toPrimitive(); + return entry.bytes; } @Override @@ -212,7 +195,7 @@ public Metadata metadataFromBytes(byte[] bytes, TruffleLogger logger) { @Override public Optional computeDigest(CachedData entry, TruffleLogger logger) { - var hash = Arrays.hashCode(entry.toPrimitive()); + var hash = Arrays.hashCode(entry.bytes); return Optional.of(Integer.toString(hash)); } From f63d3a3c001133b17c08ac77c050e69190c3c442 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Fri, 29 Aug 2025 13:51:48 +0200 Subject: [PATCH 15/31] Revert "Use logName instead of entryName" This reverts commit 963afda358358c7153537f4d97c99217ca451de5. --- .../src/main/java/org/enso/interpreter/caches/Cache.java | 8 ++++---- .../main/java/org/enso/interpreter/caches/CacheEvent.java | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) 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 0b01015eeaaf..297e9e21fed9 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 @@ -190,7 +190,7 @@ private boolean saveCacheTo( + "] to [" + toMaskedPath(parentPath).applyMasking() + "]."); - recordCacheEvent(() -> new CacheEvent.Save(logName, bytesToWrite.length)); + recordCacheEvent(() -> new CacheEvent.Save(spi.entryName(), bytesToWrite.length)); memoryArena.close(); return true; } else { @@ -302,11 +302,11 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge throw e; } recordCacheEvent( - () -> new CacheEvent.MmapLoad(logName, (int) file.length(), file.getPath())); + () -> new CacheEvent.MmapLoad(spi.entryName(), (int) file.length(), file.getPath())); } else { blobBytes = ByteBuffer.wrap(dataPath.readAllBytes()); recordCacheEvent( - () -> new CacheEvent.FileLoad(logName, (int) file.length(), file.getPath())); + () -> new CacheEvent.FileLoad(spi.entryName(), (int) file.length(), file.getPath())); } boolean blobDigestValid = !needsDataDigestVerification @@ -322,7 +322,7 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge Level.FINEST, "Loaded cache for {0} with {1} bytes in {2} ms", new Object[] {logName, blobBytes.limit(), took}); - recordCacheEvent(() -> new CacheEvent.Deserialize(logName)); + recordCacheEvent(() -> new CacheEvent.Deserialize(spi.entryName())); return cachedObject; } else { invalidateCache(cacheRoot, logger); diff --git a/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java b/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java index 6e57748b58ba..1ffe1a42e4ab 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java @@ -1,9 +1,11 @@ package org.enso.interpreter.caches; +import org.enso.interpreter.caches.Cache.Spi; + public sealed interface CacheEvent { /** - * Unique name for the cache. For example {@link Cache#logName}. + * {@link Spi#entryName() entry name} of the cache. * * @return not null. */ From be049616ed46c687362ba50841801360721b005e Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Fri, 29 Aug 2025 13:51:50 +0200 Subject: [PATCH 16/31] Revert "Add optional CacheStatistics to EnsoContext" This reverts commit 883dc141d2bce6c98ea580ee9f3f95eef7e7ee0a. --- .../java/org/enso/common/RuntimeOptions.java | 8 +--- .../org/enso/interpreter/EnsoLanguage.java | 7 +-- .../org/enso/interpreter/caches/Cache.java | 46 +------------------ .../enso/interpreter/caches/CacheEvent.java | 38 --------------- .../interpreter/caches/CacheStatistics.java | 25 ---------- .../interpreter/caches/ImportExportCache.java | 4 +- .../interpreter/caches/SuggestionsCache.java | 4 +- .../enso/interpreter/runtime/EnsoContext.java | 14 +----- .../runtime/TruffleCompilerContext.java | 11 ++--- 9 files changed, 13 insertions(+), 144 deletions(-) delete mode 100644 engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java delete mode 100644 engine/runtime/src/main/java/org/enso/interpreter/caches/CacheStatistics.java diff --git a/engine/common/src/main/java/org/enso/common/RuntimeOptions.java b/engine/common/src/main/java/org/enso/common/RuntimeOptions.java index 71e229bb55db..66c2d9675bb6 100644 --- a/engine/common/src/main/java/org/enso/common/RuntimeOptions.java +++ b/engine/common/src/main/java/org/enso/common/RuntimeOptions.java @@ -153,11 +153,6 @@ private RuntimeOptions() {} OptionDescriptor.newBuilder(USE_GLOBAL_IR_CACHE_LOCATION_KEY, USE_GLOBAL_IR_CACHE_LOCATION) .build(); - public static final String ENABLE_CACHE_STATS = optionName("enableCacheCounters"); - public static final OptionKey ENABLE_CACHE_STATS_KEYS = new OptionKey<>(false); - public static final OptionDescriptor ENABLE_CACHE_STATS_DESCRIPTOR = - OptionDescriptor.newBuilder(ENABLE_CACHE_STATS_KEYS, ENABLE_CACHE_STATS).build(); - public static final String ENABLE_EXECUTION_TIMER = optionName("enableExecutionTimer"); /* Enables timer that counts down the execution time of expressions. */ @@ -206,8 +201,7 @@ private RuntimeOptions() {} WAIT_FOR_PENDING_SERIALIZATION_JOBS_DESCRIPTOR, USE_GLOBAL_IR_CACHE_LOCATION_DESCRIPTOR, ENABLE_EXECUTION_TIMER_DESCRIPTOR, - WARNINGS_LIMIT_DESCRIPTOR, - ENABLE_CACHE_STATS_DESCRIPTOR)); + WARNINGS_LIMIT_DESCRIPTOR)); /** * Canonicalizes the option name by prefixing it with the language name. diff --git a/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java b/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java index 97abab74baad..990d7e03c7e3 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/EnsoLanguage.java @@ -35,7 +35,6 @@ import org.enso.distribution.Environment; import org.enso.distribution.locking.LockManager; import org.enso.distribution.locking.ThreadSafeFileLockManager; -import org.enso.interpreter.caches.CacheStatistics; import org.enso.interpreter.node.EnsoRootNode; import org.enso.interpreter.node.ExpressionNode; import org.enso.interpreter.node.ProgramRootNode; @@ -169,17 +168,13 @@ protected EnsoContext createContext(Env env) { env.registerService(lockManager); } - boolean cacheStatsEnabled = env.getOptions().get(RuntimeOptions.ENABLE_CACHE_STATS_KEYS); - var cacheStats = cacheStatsEnabled ? CacheStatistics.create() : null; - boolean isExecutionTimerEnabled = env.getOptions().get(RuntimeOptions.ENABLE_EXECUTION_TIMER_KEY); Timer timer = isExecutionTimerEnabled ? new Timer.Nanosecond() : new Timer.Disabled(); env.registerService(timer); EnsoContext context = - new EnsoContext( - this, env, notificationHandler, lockManager, distributionManager, cacheStats); + new EnsoContext(this, env, notificationHandler, lockManager, distributionManager); env.registerService(context.getThreadManager()); return context; 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 297e9e21fed9..c3ca1e617b00 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 @@ -14,7 +14,6 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; 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; @@ -52,8 +51,6 @@ public final class Cache { */ private final boolean needsDataDigestVerification; - private final CacheStatistics cacheStatistics; - /** * Constructor for subclasses. * @@ -64,21 +61,18 @@ public final class Cache { * stored metadata entry. * @param needsDataDigestVerification Flag indicating if the de-serialization process should * compute the hash of the stored cache and compare it with the stored metadata entry. - * @param cacheStatistics Optional cache statistics collector */ private Cache( Spi spi, Level logLevel, String logName, boolean needsSourceDigestVerification, - boolean needsDataDigestVerification, - CacheStatistics cacheStatistics) { + boolean needsDataDigestVerification) { this.spi = spi; this.logLevel = logLevel; this.logName = logName; this.needsDataDigestVerification = needsDataDigestVerification; this.needsSourceDigestVerification = needsSourceDigestVerification; - this.cacheStatistics = cacheStatistics; this.memoryArena = Arena.ofConfined(); } @@ -101,29 +95,7 @@ static Cache create( boolean needsSourceDigestVerification, boolean needsDataDigestVerification) { return new Cache<>( - spi, logLevel, logName, needsSourceDigestVerification, needsDataDigestVerification, null); - } - - /** - * Creates cache with statistics collection enabled. - * - * @see #create(Spi, Level, String, boolean, boolean) - */ - static Cache create( - Spi spi, - Level logLevel, - String logName, - boolean needsSourceDigestVerification, - boolean needsDataDigestVerification, - CacheStatistics cacheStats) { - assert cacheStats != null; - return new Cache<>( - spi, - logLevel, - logName, - needsSourceDigestVerification, - needsDataDigestVerification, - cacheStats); + spi, logLevel, logName, needsSourceDigestVerification, needsDataDigestVerification); } /** @@ -190,7 +162,6 @@ private boolean saveCacheTo( + "] to [" + toMaskedPath(parentPath).applyMasking() + "]."); - recordCacheEvent(() -> new CacheEvent.Save(spi.entryName(), bytesToWrite.length)); memoryArena.close(); return true; } else { @@ -301,12 +272,8 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge logger.log(Level.SEVERE, "Failed to mmap cache file " + file, e); throw e; } - recordCacheEvent( - () -> new CacheEvent.MmapLoad(spi.entryName(), (int) file.length(), file.getPath())); } else { blobBytes = ByteBuffer.wrap(dataPath.readAllBytes()); - recordCacheEvent( - () -> new CacheEvent.FileLoad(spi.entryName(), (int) file.length(), file.getPath())); } boolean blobDigestValid = !needsDataDigestVerification @@ -322,7 +289,6 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge Level.FINEST, "Loaded cache for {0} with {1} bytes in {2} ms", new Object[] {logName, blobBytes.limit(), took}); - recordCacheEvent(() -> new CacheEvent.Deserialize(spi.entryName())); return cachedObject; } else { invalidateCache(cacheRoot, logger); @@ -347,12 +313,6 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge } } - private void recordCacheEvent(Supplier eventSupply) { - if (cacheStatistics != null) { - cacheStatistics.addEvent(eventSupply.get()); - } - } - /** * Read metadata representation from the provided location * @@ -401,8 +361,6 @@ private void invalidateCache(TruffleFile cacheRoot, TruffleLogger logger) { TruffleFile metadataFile = getCacheMetadataPath(cacheRoot); TruffleFile dataFile = getCacheDataPath(cacheRoot); - recordCacheEvent(() -> new CacheEvent.Invalidate(spi.entryName())); - doDeleteAt(cacheRoot, metadataFile, logger); doDeleteAt(cacheRoot, dataFile, logger); } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java b/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java deleted file mode 100644 index 1ffe1a42e4ab..000000000000 --- a/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheEvent.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.enso.interpreter.caches; - -import org.enso.interpreter.caches.Cache.Spi; - -public sealed interface CacheEvent { - - /** - * {@link Spi#entryName() entry name} of the cache. - * - * @return not null. - */ - String cacheName(); - - sealed interface Load extends CacheEvent { - - /** Size in bytes. */ - int size(); - - /** - * @return File path or file name. Not null. - */ - String file(); - } - - record Save(String cacheName, int size) implements CacheEvent {} - - /** Load by reading all bytes from a file. */ - record FileLoad(String cacheName, int size, String file) implements Load {} - - /** Load by mapping file to memory. */ - record MmapLoad(String cacheName, int size, String file) implements Load {} - - record Serialize(String cacheName) implements CacheEvent {} - - record Deserialize(String cacheName) implements CacheEvent {} - - record Invalidate(String cacheName) implements CacheEvent {} -} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheStatistics.java b/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheStatistics.java deleted file mode 100644 index 29f0639096bd..000000000000 --- a/engine/runtime/src/main/java/org/enso/interpreter/caches/CacheStatistics.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.enso.interpreter.caches; - -import java.util.ArrayList; -import java.util.List; - -/** Utility class to keep track of cache-related statistics. */ -public final class CacheStatistics { - private CacheStatistics() {} - - private final List cacheEvents = new ArrayList<>(); - - public static CacheStatistics create() { - return new CacheStatistics(); - } - - public void addEvent(CacheEvent event) { - synchronized (cacheEvents) { - cacheEvents.add(event); - } - } - - public List getCacheEvents() { - return cacheEvents; - } -} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/caches/ImportExportCache.java b/engine/runtime/src/main/java/org/enso/interpreter/caches/ImportExportCache.java index 07cd0d820654..3162ceff77d8 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/caches/ImportExportCache.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/caches/ImportExportCache.java @@ -33,9 +33,9 @@ private ImportExportCache(LibraryName libraryName) { } public static Cache create( - LibraryName libraryName, CacheStatistics cacheStats) { + LibraryName libraryName) { var impl = new ImportExportCache(libraryName); - return Cache.create(impl, Level.FINEST, libraryName.toString(), true, false, cacheStats); + return Cache.create(impl, Level.FINEST, libraryName.toString(), true, false); } @Override diff --git a/engine/runtime/src/main/java/org/enso/interpreter/caches/SuggestionsCache.java b/engine/runtime/src/main/java/org/enso/interpreter/caches/SuggestionsCache.java index 0516525e76ff..a083db1d8b81 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/caches/SuggestionsCache.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/caches/SuggestionsCache.java @@ -41,10 +41,10 @@ private SuggestionsCache(LibraryName libraryName) { } public static Cache create( - LibraryName libraryName, CacheStatistics cacheStatistics) { + LibraryName libraryName) { var impl = new SuggestionsCache(libraryName); var logName = "Suggestions(" + libraryName + ")"; - return Cache.create(impl, Level.FINE, logName, true, false, cacheStatistics); + return Cache.create(impl, Level.FINE, logName, true, false); } @Override diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java index 4f3e2743851f..16bf2b0f6c50 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java @@ -47,7 +47,6 @@ import org.enso.editions.LibraryName; import org.enso.interpreter.EnsoLanguage; import org.enso.interpreter.OptionsHelper; -import org.enso.interpreter.caches.CacheStatistics; import org.enso.interpreter.runtime.builtin.Builtins; import org.enso.interpreter.runtime.data.Type; import org.enso.interpreter.runtime.data.atom.Atom; @@ -85,7 +84,6 @@ public final class EnsoContext { private final boolean isStaticAnalysisEnabled; private final boolean isHostClassLoading; private final boolean isGuestClassLoading; - private final CacheStatistics cacheStatistics; /** * Right now there is just a single polyglot Java system. */ @@ -125,21 +123,18 @@ public final class EnsoContext { * @param notificationHandler a handler for notifications * @param lockManager the lock manager instance * @param distributionManager a distribution manager - * @param cacheStatistics nullable */ public EnsoContext( EnsoLanguage language, Env environment, NotificationHandler notificationHandler, LockManager lockManager, - DistributionManager distributionManager, - CacheStatistics cacheStatistics) { + DistributionManager distributionManager) { this.language = language; this.environment = environment; this.out = new PrintStream(environment.out()); this.err = new PrintStream(environment.err()); this.in = environment.in(); - this.cacheStatistics = cacheStatistics; this.inReader = new BufferedReader(new InputStreamReader(environment.in())); var threadExecutors = new ThreadExecutors(environment, logger); var guestParallelism = getOption(RuntimeOptions.GUEST_PARALLELISM_KEY); @@ -865,13 +860,6 @@ public DefaultPackageRepository getPackageRepository() { return packageRepository; } - /** - * @return null if cache counters were {@link RuntimeOptions#ENABLE_CACHE_STATS_KEYS disabled}. - */ - public CacheStatistics getCacheStatistics() { - return cacheStatistics; - } - /** * Gets a logger for the specified class that is bound to this engine. Such logger may then be * safely used in threads defined in a thread-pool. diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/TruffleCompilerContext.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/TruffleCompilerContext.java index cd9fca8d5501..65b976e2b21a 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/TruffleCompilerContext.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/TruffleCompilerContext.java @@ -45,7 +45,6 @@ import org.enso.editions.LibraryName; import org.enso.interpreter.CompilationAbortedException; import org.enso.interpreter.caches.Cache; -import org.enso.interpreter.caches.CacheStatistics; import org.enso.interpreter.caches.ImportExportCache; import org.enso.interpreter.caches.ImportExportCache.MapToBindings; import org.enso.interpreter.caches.ModuleCache; @@ -67,7 +66,6 @@ final class TruffleCompilerContext implements CompilerContext { private final TruffleLogger loggerSerializationManager; private final RuntimeStubsGenerator stubsGenerator; private final SerializationPool serializationPool; - private final CacheStatistics cacheStatistics; TruffleCompilerContext(EnsoContext context) { this.context = context; @@ -75,7 +73,6 @@ final class TruffleCompilerContext implements CompilerContext { this.loggerSerializationManager = context.getLogger(SerializationPool.class); this.serializationPool = new SerializationPool(this); this.stubsGenerator = new RuntimeStubsGenerator(context.getBuiltins()); - this.cacheStatistics = context.getCacheStatistics(); } @Override @@ -547,7 +544,7 @@ Callable doSerializeLibrary( boolean result = doSerializeLibrarySuggestions(compiler, libraryName, useGlobalCacheLocations); try { - var cache = ImportExportCache.create(libraryName, cacheStatistics); + var cache = ImportExportCache.create(libraryName); var file = saveCache(cache, bindingsCache, useGlobalCacheLocations); result &= file != null; } catch (Throwable e) { @@ -597,7 +594,7 @@ private boolean doSerializeLibrarySuggestions( .foreach(suggestions::add); var cachedSuggestions = new SuggestionsCache.CachedSuggestions(libraryName, suggestions); - var cache = SuggestionsCache.create(libraryName, cacheStatistics); + var cache = SuggestionsCache.create(libraryName); var file = saveCache(cache, cachedSuggestions, useGlobalCacheLocations); return file != null; } catch (Throwable e) { @@ -624,7 +621,7 @@ private scala.Option deserializeSuggestionsI return scala.Option.empty(); } else { pool.waitWhileSerializing(toQualifiedName(libraryName)); - var cache = SuggestionsCache.create(libraryName, cacheStatistics); + var cache = SuggestionsCache.create(libraryName); var loaded = loadCache(cache); if (loaded.isPresent()) { logSerializationManager(Level.FINE, "Restored suggestions for library [{0}].", libraryName); @@ -645,7 +642,7 @@ scala.Option deserializeLibraryBindings(Librar return scala.Option.empty(); } else { pool.waitWhileSerializing(toQualifiedName(libraryName)); - var cache = ImportExportCache.create(libraryName, cacheStatistics); + var cache = ImportExportCache.create(libraryName); var loaded = loadCache(cache); if (loaded.isPresent()) { logSerializationManager(Level.FINE, "Restored bindings for library [{0}].", libraryName); From ae1d6875f85b20a6c29f6650f4fd86b2581a2eb9 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Fri, 29 Aug 2025 15:04:12 +0200 Subject: [PATCH 17/31] Test caches by collecting logs --- .../caches/SaveAndLoadCacheTest.java | 149 +++++++++++------- .../org/enso/interpreter/caches/Cache.java | 16 +- 2 files changed, 104 insertions(+), 61 deletions(-) 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 index abbb65b2bd19..aa35137ef8c9 100644 --- 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 @@ -2,25 +2,51 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; 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 { @@ -29,8 +55,8 @@ public void compilationSavesSuggestionsAndImportExportCache() throws Exception { method = 42 """, projDir); + var libName = LibraryName.apply("local", "Proj"); try (var ctx = projCtx(projDir)) { - var libName = LibraryName.apply("local", "Proj"); compileAndAssertCreatedCaches(ctx, libName); } } @@ -53,11 +79,8 @@ public void cachesAreLoaded_AfterProjectIsCompiled() throws IOException { try (var ctx = projCtx(projDir)) { var res = runMain(ctx, projDir); assertThat("execution is OK", res.asInt(), is(42)); - var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); - assertContainsEvent( - "load bindings cache", - cacheEvents, - e -> isImportExportCacheEvent(e, libName) && e instanceof CacheEvent.Load); + assertContainsLog( + "load bindings cache", collectedLogs, log -> isLoadBindingsLog(log, libName)); } } @@ -68,19 +91,16 @@ public void bindingCachesOfBigProject_AreMmapped() throws IOException { ProjectUtils.createProject("Proj", mainSrc, projDir); var libName = LibraryName.apply("local", "Proj"); - int bindingsCacheSize; + int savedBindingsCacheSize; try (var ctx = projCtx(projDir)) { compileAndAssertCreatedCaches(ctx, libName); - var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); - assertThat(cacheEvents, is(notNullValue())); - var bindingCacheSave = - cacheEvents.stream() - .filter(e -> isImportExportCacheEvent(e, libName)) - .map(e -> (CacheEvent.Save) e) + var saveBindingsLog = + collectedLogs.stream() + .filter(log -> isSaveBindingsLog(log, libName)) .findFirst() - .orElseThrow(() -> new AssertionError("No binding cache events found")); - bindingsCacheSize = bindingCacheSave.size(); - var savedMb = bindingCacheSave.size() / 1024 / 1024; + .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)); } @@ -88,24 +108,29 @@ public void bindingCachesOfBigProject_AreMmapped() throws IOException { try (var ctx = projCtx(projDir)) { var res = runMain(ctx, projDir); assertThat("execution is OK", res.asInt(), is(42)); - var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); - var mmapLoad = - cacheEvents.stream() - .filter(e -> e instanceof CacheEvent.MmapLoad) - .map(e -> (CacheEvent.MmapLoad) e) + var bindingsMmappedLog = + collectedLogs.stream() + .filter( + log -> { + var msg = log.getMessage(); + return msg.contains("Cache") && msg.contains("mmapped"); + }) .findFirst() - .orElseThrow(() -> new AssertionError("No mmap load events found")); - assertThat("Loaded same cached as previously saved", mmapLoad.size(), is(bindingsCacheSize)); + .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 static ContextUtils projCtx(Path projDir) { + 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.ENABLE_CACHE_STATS, "true")) + .option(RuntimeOptions.LOG_LEVEL, "FINEST") + .logHandler(logHandler)) .withProjectRoot(projDir) .build(); } @@ -114,18 +139,53 @@ private static ContextUtils projCtx(Path projDir) { * Compiles the project and asserts that suggestions and import/export (binding) caches were * created (saved). */ - private static void compileAndAssertCreatedCaches(ContextUtils ctx, LibraryName libName) { + private void compileAndAssertCreatedCaches(ContextUtils ctx, LibraryName libName) { var polyCtx = new PolyglotContext(ctx.context()); polyCtx.getTopScope().compile(true); - var cacheEvents = ctx.ensoContext().getCacheStatistics().getCacheEvents(); - assertContainsEvent( - "save suggestions cache", - cacheEvents, - e -> isSuggestionCacheEvent(e, libName) && e instanceof CacheEvent.Save); - assertContainsEvent( - "save import/export cache", - cacheEvents, - e -> isImportExportCacheEvent(e, libName) && e instanceof CacheEvent.Save); + 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) { @@ -141,23 +201,6 @@ private static Value runMain(ContextUtils ctx, Path projDir) { return res; } - private static boolean isSuggestionCacheEvent(CacheEvent event, LibraryName libName) { - return event.cacheName().contains("Suggestions") - && event.cacheName().contains(libName.toString()); - } - - private static boolean isImportExportCacheEvent(CacheEvent event, LibraryName libName) { - return libName.toString().equals(event.cacheName()); - } - - private static void assertContainsEvent( - String descr, List events, Predicate predicate) { - var hasItem = events.stream().anyMatch(predicate); - if (!hasItem) { - throw new AssertionError("Expected to find event: " + descr + " in " + events); - } - } - /** Creates executable big source file. */ private static String createBigSource(int methodCount) { var sb = new StringBuilder(); 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 c3ca1e617b00..f693db2a1c88 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 @@ -157,11 +157,8 @@ private boolean saveCacheTo( 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}); memoryArena.close(); return true; } else { @@ -204,7 +201,7 @@ public final Optional load(EnsoContext context) { logLevel, "Using cache for [" + logName - + " at location [" + + "] at location [" + toMaskedPath(roots.globalCacheRoot()).applyMasking() + "]."); return Optional.of(globalCache); @@ -215,7 +212,7 @@ public final Optional load(EnsoContext context) { logLevel, "Using cache for [" + logName - + " at location [" + + "] at location [" + toMaskedPath(roots.localCacheRoot()).applyMasking() + "]."); return Optional.of(localCache); @@ -264,7 +261,10 @@ 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"); + logger.log( + Level.FINEST, + "Cache file {0} mmapped with {1} size", + new Object[] {file, file.length()}); try (var chan = FileChannel.open(file.toPath())) { var memSegment = chan.map(MapMode.READ_ONLY, 0, file.length(), memoryArena); blobBytes = memSegment.asByteBuffer(); From 4f01c2c4d7841f09d6003b8b0ed98cae18707ce4 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Fri, 29 Aug 2025 18:14:17 +0200 Subject: [PATCH 18/31] CachedData test class is an empty class. --- .../enso/interpreter/caches/CacheTests.java | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) 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 index 68aabb8bb6c9..7a519e71312c 100644 --- 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 @@ -7,14 +7,20 @@ import com.oracle.truffle.api.TruffleLogger; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Optional; import java.util.Random; +import java.util.logging.Handler; import java.util.logging.Level; +import java.util.logging.LogRecord; +import org.enso.common.RuntimeOptions; 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.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -31,8 +37,7 @@ public void cacheCanBeSaved_ToLocalCacheRoot() throws IOException { var ensoCtx = ctx.ensoContext(); var spi = new CacheSpi(cacheRoots); var cache = Cache.create(spi, Level.FINE, "testCache", false, false); - var data = new CachedData(new byte[] {42}); - var ret = cache.save(data, ensoCtx, 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); @@ -45,8 +50,7 @@ public void globalCacheIsPreferred() throws IOException { var ensoCtx = ctx.ensoContext(); var spi = new CacheSpi(cacheRoots); var cache = Cache.create(spi, Level.FINE, "testCache", false, false); - var data = new CachedData(new byte[] {42}); - var ret = cache.save(data, ensoCtx, true); + 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); @@ -59,8 +63,7 @@ public void cacheCannotBeLoadedViaSameInstance_AfterSave() throws IOException { var ensoCtx = ctx.ensoContext(); var spi = new CacheSpi(cacheRoots); var cache = Cache.create(spi, Level.FINE, "testCache", false, false); - var data = new CachedData(new byte[] {42}); - var ret = cache.save(data, ensoCtx, false); + var ret = cache.save(new CachedData(), ensoCtx, false); assertThat("was saved", ret, is(notNullValue())); var loaded = cache.load(ensoCtx); assertThat( @@ -75,10 +78,9 @@ public void cacheCannotBeLoaded_WithoutMetadataOnDisk() throws IOException { var cacheRoots = createCacheRoots(); var localCacheFile = cacheRoots.localCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.DATA_SUFFIX); - var data = new CachedData(new byte[] {42}); // Saving only data and no metadata try (var os = localCacheFile.newOutputStream()) { - os.write(data.bytes); + os.write(new byte[] { 42 }); } var spi = new CacheSpi(cacheRoots); var cache = Cache.create(spi, Level.FINE, "testCache", false, false); @@ -90,7 +92,7 @@ public void cacheCannotBeLoaded_WithoutMetadataOnDisk() throws IOException { public void byteBufferIsClosed_AfterCacheIsSaved() throws IOException { var ensoCtx = ctx.ensoContext(); var cacheRoots = createCacheRoots(); - var bigData = randomData(10 * 1024 * 1024); + var bigData = randomBytes(10 * 1024 * 1024); saveToLocalRoot(bigData, cacheRoots); var spi = new CacheSpi(cacheRoots); var cache = Cache.create(spi, Level.FINE, "testCache", false, false); @@ -107,7 +109,7 @@ public void byteBufferIsClosed_AfterCacheIsSaved() throws IOException { public void byteBufferIsValid_AfterCacheLoad() throws IOException { var ensoCtx = ctx.ensoContext(); var cacheRoots = createCacheRoots(); - var bigData = randomData(10 * 1024 * 1024); + var bigData = randomBytes(10 * 1024 * 1024); saveToLocalRoot(bigData, cacheRoots); var spi = new CacheSpi(cacheRoots); var cache = Cache.create(spi, Level.FINE, "testCache", false, false); @@ -131,26 +133,26 @@ private Roots createCacheRoots() throws IOException { } /** Saves data as well as empty metadata on the disk. */ - private static void saveToLocalRoot(CachedData data, Roots cacheRoots) throws IOException { + 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.bytes); + os.write(data); } try (var os = localMetadataFile.newOutputStream()) { os.write(42); } } - private static CachedData randomData(int size) { - byte[] primBytes = new byte[size]; - random.nextBytes(primBytes); - return new CachedData(primBytes); + private static byte[] randomBytes(int size) { + byte[] bytes = new byte[size]; + random.nextBytes(bytes); + return bytes; } - private record CachedData(byte[] bytes) {} + private static final class CachedData{} private static final class Metadata {} @@ -170,17 +172,12 @@ private CacheSpi(Roots cacheRoots) { public CachedData deserialize( EnsoContext context, ByteBuffer data, Metadata meta, TruffleLogger logger) { deserializeBuffer = data; - byte[] bytes = new byte[data.remaining()]; - int bytesIdx = 0; - while (data.hasRemaining()) { - bytes[bytesIdx++] = data.get(); - } - return new CachedData(bytes); + return new CachedData(); } @Override public byte[] serialize(EnsoContext context, CachedData entry) { - return entry.bytes; + return new byte[]{ 42 }; } @Override @@ -195,8 +192,7 @@ public Metadata metadataFromBytes(byte[] bytes, TruffleLogger logger) { @Override public Optional computeDigest(CachedData entry, TruffleLogger logger) { - var hash = Arrays.hashCode(entry.bytes); - return Optional.of(Integer.toString(hash)); + return Optional.of("42"); } @Override From 5bce4bce70f9eaa2203780112d0d7200539736f1 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Fri, 29 Aug 2025 18:51:13 +0200 Subject: [PATCH 19/31] Add memory arena validity assert --- .../runtime/src/main/java/org/enso/interpreter/caches/Cache.java | 1 + 1 file changed, 1 insertion(+) 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 f693db2a1c88..86d92eb7efea 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 @@ -266,6 +266,7 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge "Cache file {0} mmapped with {1} size", new Object[] {file, file.length()}); 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) { From 46b42e5941099dc9e85d220f06088c8f4b2d03ab Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Fri, 29 Aug 2025 18:52:21 +0200 Subject: [PATCH 20/31] First close memory arena and then writeBytesTo --- .../src/main/java/org/enso/interpreter/caches/Cache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 86d92eb7efea..bdb8d44056a4 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 @@ -154,12 +154,12 @@ private boolean saveCacheTo( TruffleFile metadataFile = getCacheMetadataPath(cacheRoot); TruffleFile parentPath = cacheDataFile.getParent(); + memoryArena.close(); if (writeBytesTo(cacheDataFile, bytesToWrite) && writeBytesTo(metadataFile, metadataBytes)) { logger.log( logLevel, "Written cache data [{0}] to [{1}] of size [{2}].", new Object[] {logName, toMaskedPath(parentPath).applyMasking(), bytesToWrite.length}); - memoryArena.close(); return true; } else { // Clean up after ourselves if it fails. From 86533504e74cfd64af8b2f5b72c096e596052fe6 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 1 Sep 2025 13:44:29 +0200 Subject: [PATCH 21/31] Memory arena must be shared. It is accessed from different threads in SerializationPool --- .../src/main/java/org/enso/interpreter/caches/Cache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bdb8d44056a4..45fe2668ee4c 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 @@ -73,7 +73,7 @@ private Cache( this.logName = logName; this.needsDataDigestVerification = needsDataDigestVerification; this.needsSourceDigestVerification = needsSourceDigestVerification; - this.memoryArena = Arena.ofConfined(); + this.memoryArena = Arena.ofShared(); } /** From 2756b6936f6abb3f60b4772bb3d5334cdebdafb8 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 1 Sep 2025 13:44:42 +0200 Subject: [PATCH 22/31] Cache.invalidate closes memory arena --- .../src/main/java/org/enso/interpreter/caches/Cache.java | 3 +++ 1 file changed, 3 insertions(+) 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 45fe2668ee4c..2267c376801e 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 @@ -405,6 +405,9 @@ public final void invalidate(EnsoContext context) { invalidateCache(roots.globalCacheRoot, logger); invalidateCache(roots.localCacheRoot, logger); }); + if (memoryArena.scope().isAlive()) { + memoryArena.close(); + } } } From 7cf44a9f224f53701b083e3325c6ef948c1f47f8 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 1 Sep 2025 13:55:55 +0200 Subject: [PATCH 23/31] Test that memory arena is closed after cache is saved --- build.sbt | 2 +- .../enso/interpreter/caches/CacheTests.java | 15 ++++++++------- .../org/enso/interpreter/caches/Cache.java | 18 +++++++++++++++--- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/build.sbt b/build.sbt index 74b8a74a385b..fd674159bdf5 100644 --- a/build.sbt +++ b/build.sbt @@ -2791,7 +2791,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/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 index 7a519e71312c..cda79561c1d4 100644 --- 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 @@ -6,6 +6,7 @@ import com.oracle.truffle.api.TruffleLogger; import java.io.IOException; +import java.lang.foreign.Arena; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -58,18 +59,18 @@ public void globalCacheIsPreferred() throws IOException { } @Test - public void cacheCannotBeLoadedViaSameInstance_AfterSave() throws IOException { - var cacheRoots = createCacheRoots(); + public void memoryArenaIsClosed_AfterCacheSave() throws IOException { var ensoCtx = ctx.ensoContext(); + var cacheRoots = createCacheRoots(); var spi = new CacheSpi(cacheRoots); - var cache = Cache.create(spi, Level.FINE, "testCache", false, false); + var memoryArena = Arena.ofConfined(); + var cache = Cache.create(spi, Level.FINE, "testCache", false, false, memoryArena); var ret = cache.save(new CachedData(), ensoCtx, false); assertThat("was saved", ret, is(notNullValue())); - var loaded = cache.load(ensoCtx); assertThat( - "cache should be closed - unable to load after save via the same instance", - loaded.isEmpty(), - is(true)); + "Memory arena is closed after cache save", + memoryArena.scope().isAlive(), + is(false)); } @Test 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 2267c376801e..4b8cee798772 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 @@ -67,13 +67,14 @@ private Cache( Level logLevel, String logName, boolean needsSourceDigestVerification, - boolean needsDataDigestVerification) { + boolean needsDataDigestVerification, + Arena memoryArena) { this.spi = spi; this.logLevel = logLevel; this.logName = logName; this.needsDataDigestVerification = needsDataDigestVerification; this.needsSourceDigestVerification = needsSourceDigestVerification; - this.memoryArena = Arena.ofShared(); + this.memoryArena = memoryArena; } /** @@ -95,7 +96,18 @@ static Cache create( 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, + Arena memoryArena) { + return new Cache<>( + spi, logLevel, logName, needsSourceDigestVerification, needsDataDigestVerification, memoryArena); } /** From a38623a5764fd2c3f359c25894dc43ea2aae214c Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 1 Sep 2025 13:56:43 +0200 Subject: [PATCH 24/31] Test that byteBuffer from deserialization is read only --- .../enso/interpreter/caches/CacheTests.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 index cda79561c1d4..20553cc54d5f 100644 --- 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 @@ -121,6 +121,26 @@ public void byteBufferIsValid_AfterCacheLoad() throws IOException { assertThat("byte buffer is still readable", deserializeBuffer.hasRemaining(), is(true)); } + @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"); From 74a9fc61bf91caf7fea80959d36f45a5ac83a75b Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 1 Sep 2025 13:57:09 +0200 Subject: [PATCH 25/31] readability is tested via ByteBuffer.get --- .../src/test/java/org/enso/interpreter/caches/CacheTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 20553cc54d5f..a5bb75ea761a 100644 --- 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 @@ -118,7 +118,8 @@ public void byteBufferIsValid_AfterCacheLoad() throws IOException { assertThat("was loaded", loaded.isPresent(), is(true)); var deserializeBuffer = spi.deserializeBuffer; - assertThat("byte buffer is still readable", deserializeBuffer.hasRemaining(), is(true)); + var firstByte = deserializeBuffer.get(); + assertThat("byte buffer is still readable", firstByte, is(bigData[0])); } @Test From e79df91f9b3429d757b8b8ac91b52e95275e62ea Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 1 Sep 2025 13:57:18 +0200 Subject: [PATCH 26/31] Random is not static field --- .../src/test/java/org/enso/interpreter/caches/CacheTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a5bb75ea761a..fc3ec9e8abe0 100644 --- 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 @@ -30,7 +30,7 @@ public final class CacheTests { @Rule public final TemporaryFolder tempFolder = new TemporaryFolder(); @Rule public final ContextUtils ctx = ContextUtils.createDefault(); - private static final Random random = new Random(42); + private final Random random = new Random(42); @Test public void cacheCanBeSaved_ToLocalCacheRoot() throws IOException { From 79ecc3c5b21bc22aa53297251837b7f845e2de41 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 1 Sep 2025 13:57:48 +0200 Subject: [PATCH 27/31] Test that byteBuffer is closed after cache is invalidated --- .../org/enso/interpreter/caches/CacheTests.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 index fc3ec9e8abe0..aa9e7e54b3e1 100644 --- 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 @@ -1,8 +1,11 @@ package org.enso.interpreter.caches; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.nullValue; 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; @@ -90,7 +93,7 @@ public void cacheCannotBeLoaded_WithoutMetadataOnDisk() throws IOException { } @Test - public void byteBufferIsClosed_AfterCacheIsSaved() throws IOException { + public void byteBufferIsClosed_AfterCacheIsInvalidated() throws IOException { var ensoCtx = ctx.ensoContext(); var cacheRoots = createCacheRoots(); var bigData = randomBytes(10 * 1024 * 1024); @@ -102,8 +105,13 @@ public void byteBufferIsClosed_AfterCacheIsSaved() throws IOException { assertThat("was loaded", loaded.isPresent(), is(true)); cache.invalidate(ensoCtx); - assertThat( - "byte buffer got in deserialize is invalid", deserializeBuffer.hasRemaining(), is(false)); + + try { + deserializeBuffer.get(); + fail("Expected IllegalStateException - cannot read from byte buffer anymore"); + } catch (IllegalStateException e) { + assertThat(e.getMessage(), containsString("closed")); + } } @Test From 00e3786d3a714864bfcfdf444446d86705e5fa36 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 1 Sep 2025 13:58:25 +0200 Subject: [PATCH 28/31] randomBytes is not static method --- .../src/test/java/org/enso/interpreter/caches/CacheTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index aa9e7e54b3e1..478cf1f23944 100644 --- 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 @@ -176,7 +176,7 @@ private static void saveToLocalRoot(byte[] data, Roots cacheRoots) throws IOExce } } - private static byte[] randomBytes(int size) { + private byte[] randomBytes(int size) { byte[] bytes = new byte[size]; random.nextBytes(bytes); return bytes; From b1bd76af54224e4bff44e813fadae5cca40f2114 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 1 Sep 2025 13:59:10 +0200 Subject: [PATCH 29/31] fmt --- .../enso/interpreter/caches/CacheTests.java | 19 ++++--------------- .../org/enso/interpreter/caches/Cache.java | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 17 deletions(-) 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 index 478cf1f23944..174cc9c319b9 100644 --- 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 @@ -1,7 +1,6 @@ package org.enso.interpreter.caches; import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -11,20 +10,13 @@ import java.io.IOException; import java.lang.foreign.Arena; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.Optional; import java.util.Random; -import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogRecord; -import org.enso.common.RuntimeOptions; 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.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -70,10 +62,7 @@ public void memoryArenaIsClosed_AfterCacheSave() throws IOException { var cache = Cache.create(spi, Level.FINE, "testCache", false, false, memoryArena); var ret = cache.save(new CachedData(), ensoCtx, false); assertThat("was saved", ret, is(notNullValue())); - assertThat( - "Memory arena is closed after cache save", - memoryArena.scope().isAlive(), - is(false)); + assertThat("Memory arena is closed after cache save", memoryArena.scope().isAlive(), is(false)); } @Test @@ -84,7 +73,7 @@ public void cacheCannotBeLoaded_WithoutMetadataOnDisk() throws IOException { 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 }); + os.write(new byte[] {42}); } var spi = new CacheSpi(cacheRoots); var cache = Cache.create(spi, Level.FINE, "testCache", false, false); @@ -182,7 +171,7 @@ private byte[] randomBytes(int size) { return bytes; } - private static final class CachedData{} + private static final class CachedData {} private static final class Metadata {} @@ -207,7 +196,7 @@ public CachedData deserialize( @Override public byte[] serialize(EnsoContext context, CachedData entry) { - return new byte[]{ 42 }; + return new byte[] {42}; } @Override 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 4b8cee798772..cac789750077 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 @@ -96,7 +96,12 @@ static Cache create( boolean needsSourceDigestVerification, boolean needsDataDigestVerification) { return new Cache<>( - spi, logLevel, logName, needsSourceDigestVerification, needsDataDigestVerification, Arena.ofShared()); + spi, + logLevel, + logName, + needsSourceDigestVerification, + needsDataDigestVerification, + Arena.ofShared()); } static Cache create( @@ -107,7 +112,12 @@ static Cache create( boolean needsDataDigestVerification, Arena memoryArena) { return new Cache<>( - spi, logLevel, logName, needsSourceDigestVerification, needsDataDigestVerification, memoryArena); + spi, + logLevel, + logName, + needsSourceDigestVerification, + needsDataDigestVerification, + memoryArena); } /** From c7cbcac1d83c3528e8a5fb01d563ff4e53dd9652 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Mon, 1 Sep 2025 14:49:50 +0200 Subject: [PATCH 30/31] Add `-H:+ForeignAPISupport` NI flag --- .../native-image/org/enso/runner/native-image.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 72478c03585ecfc79a41d8c27df902741a18cf42 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Wed, 3 Sep 2025 13:20:42 +0200 Subject: [PATCH 31/31] New Arena is created before mmap. Previous arena is closed both in load and save. --- .../enso/interpreter/caches/CacheTests.java | 21 ++++++++-- .../org/enso/interpreter/caches/Cache.java | 38 ++++++++++++++----- 2 files changed, 46 insertions(+), 13 deletions(-) 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 index 174cc9c319b9..47aad90ffbf4 100644 --- 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 @@ -12,6 +12,8 @@ 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; @@ -58,11 +60,24 @@ public void memoryArenaIsClosed_AfterCacheSave() throws IOException { var ensoCtx = ctx.ensoContext(); var cacheRoots = createCacheRoots(); var spi = new CacheSpi(cacheRoots); - var memoryArena = Arena.ofConfined(); - var cache = Cache.create(spi, Level.FINE, "testCache", false, false, memoryArena); + 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.scope().isAlive(), is(false)); + assertThat( + "Memory arena is closed after cache save", memoryArena.get().scope().isAlive(), is(false)); } @Test 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 cac789750077..f8d237761cda 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 @@ -14,6 +14,7 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; 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,7 +44,14 @@ public final class Cache { */ private final boolean needsSourceDigestVerification; - private final Arena memoryArena; + /** + * 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 @@ -68,13 +76,13 @@ private Cache( String logName, boolean needsSourceDigestVerification, boolean needsDataDigestVerification, - Arena memoryArena) { + Supplier memoryArenaSupplier) { this.spi = spi; this.logLevel = logLevel; this.logName = logName; this.needsDataDigestVerification = needsDataDigestVerification; this.needsSourceDigestVerification = needsSourceDigestVerification; - this.memoryArena = memoryArena; + this.memoryArenaSupplier = memoryArenaSupplier; } /** @@ -101,7 +109,7 @@ static Cache create( logName, needsSourceDigestVerification, needsDataDigestVerification, - Arena.ofShared()); + Arena::ofShared); } static Cache create( @@ -110,14 +118,14 @@ static Cache create( String logName, boolean needsSourceDigestVerification, boolean needsDataDigestVerification, - Arena memoryArena) { + Supplier memoryArenaSupplier) { return new Cache<>( spi, logLevel, logName, needsSourceDigestVerification, needsDataDigestVerification, - memoryArena); + memoryArenaSupplier); } /** @@ -176,7 +184,7 @@ private boolean saveCacheTo( TruffleFile metadataFile = getCacheMetadataPath(cacheRoot); TruffleFile parentPath = cacheDataFile.getParent(); - memoryArena.close(); + closeMemoryArena(); if (writeBytesTo(cacheDataFile, bytesToWrite) && writeBytesTo(metadataFile, metadataBytes)) { logger.log( logLevel, @@ -287,6 +295,8 @@ private T loadCacheFrom(TruffleFile cacheRoot, EnsoContext context, TruffleLogge 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); @@ -336,6 +346,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 * @@ -427,9 +447,7 @@ public final void invalidate(EnsoContext context) { invalidateCache(roots.globalCacheRoot, logger); invalidateCache(roots.localCacheRoot, logger); }); - if (memoryArena.scope().isAlive()) { - memoryArena.close(); - } + closeMemoryArena(); } }