Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
06ae858
Test that second cache serialization fails
Akirathan Aug 27, 2025
ab0fc5e
Cache uses native memory Arena
Akirathan Aug 27, 2025
883dc14
Add optional CacheStatistics to EnsoContext
Akirathan Aug 28, 2025
963afda
Use logName instead of entryName
Akirathan Aug 28, 2025
5a14330
Test save of suggestions and bindings caches
Akirathan Aug 28, 2025
24b7c4d
Use hamcrest matchers
Akirathan Aug 28, 2025
58b92e8
Add test for loading caches
Akirathan Aug 28, 2025
822bf5d
Rename test
Akirathan Aug 28, 2025
e3a1c36
Test that loading bindings cache of big project should be mmapped
Akirathan Aug 28, 2025
54deef1
refact: extract method
Akirathan Aug 28, 2025
cef3885
Test that cache can be saved.
Akirathan Aug 28, 2025
04eaf57
Test global cache save preference
Akirathan Aug 28, 2025
225beaa
More cache tests
Akirathan Aug 28, 2025
3e362b8
Use only primitive bytes
Akirathan Aug 28, 2025
f63d3a3
Revert "Use logName instead of entryName"
Akirathan Aug 29, 2025
be04961
Revert "Add optional CacheStatistics to EnsoContext"
Akirathan Aug 29, 2025
ae1d687
Test caches by collecting logs
Akirathan Aug 29, 2025
4f01c2c
CachedData test class is an empty class.
Akirathan Aug 29, 2025
5bce4bc
Add memory arena validity assert
Akirathan Aug 29, 2025
46b42e5
First close memory arena and then writeBytesTo
Akirathan Aug 29, 2025
8653350
Memory arena must be shared.
Akirathan Sep 1, 2025
2756b69
Cache.invalidate closes memory arena
Akirathan Sep 1, 2025
7cf44a9
Test that memory arena is closed after cache is saved
Akirathan Sep 1, 2025
a38623a
Test that byteBuffer from deserialization is read only
Akirathan Sep 1, 2025
74a9fc6
readability is tested via ByteBuffer.get
Akirathan Sep 1, 2025
e79df91
Random is not static field
Akirathan Sep 1, 2025
79ecc3c
Test that byteBuffer is closed after cache is invalidated
Akirathan Sep 1, 2025
00e3786
randomBytes is not static method
Akirathan Sep 1, 2025
b1bd76a
fmt
Akirathan Sep 1, 2025
c7cbcac
Add `-H:+ForeignAPISupport` NI flag
Akirathan Sep 1, 2025
72478c0
New Arena is created before mmap.
Akirathan Sep 3, 2025
1de26ec
Merge branch 'develop' into wip/akirathan/13816-bindings-via-arena
Akirathan Sep 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2667,7 +2667,8 @@ lazy val `runtime-test-instruments` =
lazy val runtime = (project in file("engine/runtime"))
.enablePlugins(JPMSPlugin)
.settings(
frgaalJavaCompilerSetting,
// Needed for `java.lang.Foreign`.
customFrgaalJavaCompilerSettings("24"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I was afraid that this would be needed.
  • I wanted to suggest to create a separate runtime-caches module:
    • move all the cache related code there
    • invoke it with requires (from runtime)/provides pattern
    • if the runtime-caches module is missing, then runtime would just avoid loading caches
  • Because such a change would prevent usage of runtime inside of IGV/VSCode on LTS JDK (e.g. JDK 21)
  • but we don't need runtime in there anyway
    • the plans we have count with usage of runtime-compiler at max
    • and runtime-compiler can still run on JDK21, maybe even JDK17

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked in #13898

scalaModuleDependencySetting,
mixedJavaScalaProjectSetting,
annotationProcSetting,
Expand Down Expand Up @@ -2793,7 +2794,7 @@ lazy val `runtime-integration-tests` =
.enablePlugins(JPMSPlugin)
.enablePlugins(PackageListPlugin)
.settings(
frgaalJavaCompilerSetting,
customFrgaalJavaCompilerSettings("24"),
annotationProcSetting,
commands += WithDebugCommand.withDebug,
libraryDependencies ++= GraalVM.modules ++ GraalVM.langsPkgs ++ GraalVM.insightPkgs ++ logbackPkg ++ helidon ++ slf4jApi ++ Seq(
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
Args=--features=org.enso.runner.EnsoLibraryFeature
Args = --features=org.enso.runner.EnsoLibraryFeature \
-H:+ForeignAPISupport
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
package org.enso.interpreter.caches;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.fail;

import com.oracle.truffle.api.TruffleLogger;
import java.io.IOException;
import java.lang.foreign.Arena;
import java.nio.ByteBuffer;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.logging.Level;
import org.enso.interpreter.caches.Cache.Roots;
import org.enso.interpreter.caches.Cache.Spi;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.test.utils.ContextUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

public final class CacheTests {

@Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
@Rule public final ContextUtils ctx = ContextUtils.createDefault();
private final Random random = new Random(42);

@Test
public void cacheCanBeSaved_ToLocalCacheRoot() throws IOException {
var cacheRoots = createCacheRoots();
var ensoCtx = ctx.ensoContext();
var spi = new CacheSpi(cacheRoots);
var cache = Cache.create(spi, Level.FINE, "testCache", false, false);
var ret = cache.save(new CachedData(), ensoCtx, false);
assertThat("was saved to local cache root", ret, is(cacheRoots.localCacheRoot()));
var localCacheFile =
cacheRoots.localCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.DATA_SUFFIX);
assertThat("local cache file was created", localCacheFile.exists(), is(true));
}

@Test
public void globalCacheIsPreferred() throws IOException {
var cacheRoots = createCacheRoots();
var ensoCtx = ctx.ensoContext();
var spi = new CacheSpi(cacheRoots);
var cache = Cache.create(spi, Level.FINE, "testCache", false, false);
var ret = cache.save(new CachedData(), ensoCtx, true);
assertThat("was saved to global cache root", ret, is(cacheRoots.globalCacheRoot()));
var globalCacheFile =
cacheRoots.globalCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.DATA_SUFFIX);
assertThat("global cache file was created", globalCacheFile.exists(), is(true));
}

@Test
public void memoryArenaIsClosed_AfterCacheSave() throws IOException {
var ensoCtx = ctx.ensoContext();
var cacheRoots = createCacheRoots();
var spi = new CacheSpi(cacheRoots);
var bigData = randomBytes(10 * 1024 * 1024);
saveToLocalRoot(bigData, cacheRoots);

var memoryArena = new AtomicReference<Arena>();
Supplier<Arena> arenaSupplier =
() -> {
memoryArena.set(Arena.ofConfined());
return memoryArena.get();
};
var cache = Cache.create(spi, Level.FINE, "testCache", false, false, arenaSupplier);
var loaded = cache.load(ensoCtx);
assertThat("was loaded", loaded.isPresent(), is(true));
assertThat("New arena was created", memoryArena.get(), is(notNullValue()));

var ret = cache.save(new CachedData(), ensoCtx, false);
assertThat("was saved", ret, is(notNullValue()));
assertThat(
"Memory arena is closed after cache save", memoryArena.get().scope().isAlive(), is(false));
}

@Test
public void cacheCannotBeLoaded_WithoutMetadataOnDisk() throws IOException {
var ensoCtx = ctx.ensoContext();
var cacheRoots = createCacheRoots();
var localCacheFile =
cacheRoots.localCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.DATA_SUFFIX);
// Saving only data and no metadata
try (var os = localCacheFile.newOutputStream()) {
os.write(new byte[] {42});
}
var spi = new CacheSpi(cacheRoots);
var cache = Cache.create(spi, Level.FINE, "testCache", false, false);
var loaded = cache.load(ensoCtx);
assertThat("Cannot be loaded without metadata on disk", loaded.isPresent(), is(false));
}

@Test
public void byteBufferIsClosed_AfterCacheIsInvalidated() throws IOException {
var ensoCtx = ctx.ensoContext();
var cacheRoots = createCacheRoots();
var bigData = randomBytes(10 * 1024 * 1024);
saveToLocalRoot(bigData, cacheRoots);
var spi = new CacheSpi(cacheRoots);
var cache = Cache.create(spi, Level.FINE, "testCache", false, false);
var loaded = cache.load(ensoCtx);
var deserializeBuffer = spi.deserializeBuffer;
assertThat("was loaded", loaded.isPresent(), is(true));

cache.invalidate(ensoCtx);

try {
deserializeBuffer.get();
fail("Expected IllegalStateException - cannot read from byte buffer anymore");
} catch (IllegalStateException e) {
assertThat(e.getMessage(), containsString("closed"));
}
}

@Test
public void byteBufferIsValid_AfterCacheLoad() throws IOException {
var ensoCtx = ctx.ensoContext();
var cacheRoots = createCacheRoots();
var bigData = randomBytes(10 * 1024 * 1024);
saveToLocalRoot(bigData, cacheRoots);
var spi = new CacheSpi(cacheRoots);
var cache = Cache.create(spi, Level.FINE, "testCache", false, false);
var loaded = cache.load(ensoCtx);
assertThat("was loaded", loaded.isPresent(), is(true));

var deserializeBuffer = spi.deserializeBuffer;
var firstByte = deserializeBuffer.get();
assertThat("byte buffer is still readable", firstByte, is(bigData[0]));
}

@Test
public void byteBufferIsReadonly() throws IOException {
var ensoCtx = ctx.ensoContext();
var cacheRoots = createCacheRoots();
var bigData = randomBytes(10 * 1024 * 1024);
saveToLocalRoot(bigData, cacheRoots);
var spi = new CacheSpi(cacheRoots);
var cache = Cache.create(spi, Level.FINE, "testCache", false, false);
var loaded = cache.load(ensoCtx);
assertThat("was loaded", loaded.isPresent(), is(true));

var deserializeBuffer = spi.deserializeBuffer;
try {
deserializeBuffer.put((byte) 42);
fail("Expected ReadOnlyBufferException");
} catch (java.nio.ReadOnlyBufferException e) {
// expected
}
}

private Roots createCacheRoots() throws IOException {
var cacheRootDirPath = tempFolder.newFolder("cacheRoot").toPath();
var localCacheDir = cacheRootDirPath.resolve("local");
var globalCacheDir = cacheRootDirPath.resolve("global");
localCacheDir.toFile().mkdir();
globalCacheDir.toFile().mkdir();
var ensoCtx = ctx.ensoContext();
return new Roots(
ensoCtx.getTruffleFile(localCacheDir.toFile()),
ensoCtx.getTruffleFile(globalCacheDir.toFile()));
}

/** Saves data as well as empty metadata on the disk. */
private static void saveToLocalRoot(byte[] data, Roots cacheRoots) throws IOException {
var localCacheFile =
cacheRoots.localCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.DATA_SUFFIX);
var localMetadataFile =
cacheRoots.localCacheRoot().resolve(CacheSpi.ENTRY_NAME + CacheSpi.METADATA_SUFFIX);
try (var os = localCacheFile.newOutputStream()) {
os.write(data);
}
try (var os = localMetadataFile.newOutputStream()) {
os.write(42);
}
}

private byte[] randomBytes(int size) {
byte[] bytes = new byte[size];
random.nextBytes(bytes);
return bytes;
}

private static final class CachedData {}

private static final class Metadata {}

private static final class CacheSpi implements Spi<CachedData, Metadata> {
public static final String DATA_SUFFIX = ".test.data";
public static final String METADATA_SUFFIX = ".test.metadata";
public static final String ENTRY_NAME = "test-entry";

private final Roots cacheRoots;
private ByteBuffer deserializeBuffer;

private CacheSpi(Roots cacheRoots) {
this.cacheRoots = cacheRoots;
}

@Override
public CachedData deserialize(
EnsoContext context, ByteBuffer data, Metadata meta, TruffleLogger logger) {
deserializeBuffer = data;
return new CachedData();
}

@Override
public byte[] serialize(EnsoContext context, CachedData entry) {
return new byte[] {42};
}

@Override
public byte[] metadata(String sourceDigest, String blobDigest, CachedData entry) {
return new byte[0];
}

@Override
public Metadata metadataFromBytes(byte[] bytes, TruffleLogger logger) {
return new Metadata();
}

@Override
public Optional<String> computeDigest(CachedData entry, TruffleLogger logger) {
return Optional.of("42");
}

@Override
public Optional<String> computeDigestFromSource(EnsoContext context, TruffleLogger logger) {
throw new AssertionError("should not be called");
}

@Override
public Optional<Roots> 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";
}
}
}
Loading
Loading