Lock-free volume reads

- Structure volume caches read through concurrent maps and publish only current builds
- Natural terrain receipts reuse a per-thread deflater with the platform gzip framing
- Tests pin receipt and saved biome bytes across reused compressors
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-22 07:29:53 -04:00
1 parent 0fe2363feb
commit 04f2798c06
4 files changed
+142 -68

No files matched your search

@@ -12,15 +12,14 @@ import art.arcane.volmlib.util.cache.CacheKey;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.BooleanSupplier;
import java.util.function.IntConsumer; import java.util.function.IntConsumer;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -36,7 +35,6 @@ public final class NativeStructureVolumeIndex {
private static final int ORIGIN_REACH_CHUNKS = StructureOwnershipRecordView.MAX_REFERENCE_DISTANCE_CHUNKS; private static final int ORIGIN_REACH_CHUNKS = StructureOwnershipRecordView.MAX_REFERENCE_DISTANCE_CHUNKS;
private static final int MAX_CACHED_ORIGIN_CHUNKS = 16_384; private static final int MAX_CACHED_ORIGIN_CHUNKS = 16_384;
private static final int MAX_CACHED_QUERY_CHUNKS = 4_096; private static final int MAX_CACHED_QUERY_CHUNKS = 4_096;
private static final int CACHE_STRIPES = 32;
private static final Map<Engine, NativeStructureVolumeIndex> INDEXES = private static final Map<Engine, NativeStructureVolumeIndex> INDEXES =
Collections.synchronizedMap(new WeakHashMap<>()); Collections.synchronizedMap(new WeakHashMap<>());
@@ -223,12 +221,9 @@ public final class NativeStructureVolumeIndex {
RuntimeChunkKey key, RuntimeChunkKey key,
Supplier<KList<NativeStructureVolume>> loader Supplier<KList<NativeStructureVolume>> loader
) { ) {
Map<RuntimeChunkKey, KList<NativeStructureVolume>> cache = volumes.stripe(key); KList<NativeStructureVolume> hit = volumes.get(key);
synchronized (cache) { if (hit != null) {
KList<NativeStructureVolume> hit = cache.get(key); return hit;
if (hit != null) {
return hit;
}
} }
CompletableFuture<KList<NativeStructureVolume>> future = new CompletableFuture<>(); CompletableFuture<KList<NativeStructureVolume>> future = new CompletableFuture<>();
@@ -239,22 +234,16 @@ public final class NativeStructureVolumeIndex {
// Close the check-then-claim window: a racer whose cache check missed before the // Close the check-then-claim window: a racer whose cache check missed before the
// previous builder cached could claim the build slot after it retired and rebuild. // previous builder cached could claim the build slot after it retired and rebuild.
synchronized (cache) { KList<NativeStructureVolume> published = volumes.get(key);
KList<NativeStructureVolume> published = cache.get(key); if (published != null) {
if (published != null) { future.complete(published);
future.complete(published); builds.remove(key, future);
builds.remove(key, future); return published;
return published;
}
} }
try { try {
KList<NativeStructureVolume> built = loader.get(); KList<NativeStructureVolume> built = loader.get();
synchronized (cache) { volumes.publish(key, built, () -> builds.get(key) == future);
if (builds.get(key) == future) {
cache.put(key, built);
}
}
future.complete(built); future.complete(built);
return built; return built;
} catch (RuntimeException | Error error) { } catch (RuntimeException | Error error) {
@@ -301,47 +290,44 @@ public final class NativeStructureVolumeIndex {
} }
} }
private static Map<RuntimeChunkKey, KList<NativeStructureVolume>> lru(int capacity) {
return new LinkedHashMap<>(16, 0.75F, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<RuntimeChunkKey, KList<NativeStructureVolume>> eldest
) {
return size() > capacity;
}
};
}
private static final class VolumeCache { private static final class VolumeCache {
private final List<Map<RuntimeChunkKey, KList<NativeStructureVolume>>> stripes; private final int capacity;
private final ConcurrentHashMap<RuntimeChunkKey, KList<NativeStructureVolume>> entries = new ConcurrentHashMap<>();
private final ConcurrentLinkedQueue<RuntimeChunkKey> insertionOrder = new ConcurrentLinkedQueue<>();
private VolumeCache(int capacity) { private VolumeCache(int capacity) {
int stripeCapacity = Math.max(1, capacity / CACHE_STRIPES); this.capacity = capacity;
List<Map<RuntimeChunkKey, KList<NativeStructureVolume>>> created = new ArrayList<>(CACHE_STRIPES);
for (int stripe = 0; stripe < CACHE_STRIPES; stripe++) {
created.add(lru(stripeCapacity));
}
stripes = List.copyOf(created);
}
private Map<RuntimeChunkKey, KList<NativeStructureVolume>> stripe(RuntimeChunkKey key) {
int hash = key.hashCode();
return stripes.get((hash ^ (hash >>> 16)) & (CACHE_STRIPES - 1));
} }
private KList<NativeStructureVolume> get(RuntimeChunkKey key) { private KList<NativeStructureVolume> get(RuntimeChunkKey key) {
Map<RuntimeChunkKey, KList<NativeStructureVolume>> cache = stripe(key); return entries.get(key);
synchronized (cache) { }
return cache.get(key);
private void publish(RuntimeChunkKey key, KList<NativeStructureVolume> volumes, BooleanSupplier current) {
boolean[] inserted = new boolean[1];
entries.compute(key, (ignored, previous) -> {
if (!current.getAsBoolean()) {
return previous;
}
inserted[0] = previous == null;
return volumes;
});
if (!inserted[0]) {
return;
}
insertionOrder.add(key);
while (entries.size() > capacity) {
RuntimeChunkKey eldest = insertionOrder.poll();
if (eldest == null) {
return;
}
entries.remove(eldest);
} }
} }
private void evictRuntime(int runtimeId) { private void evictRuntime(int runtimeId) {
for (Map<RuntimeChunkKey, KList<NativeStructureVolume>> cache : stripes) { entries.keySet().removeIf(key -> key.runtimeId() == runtimeId);
synchronized (cache) { insertionOrder.removeIf(key -> key.runtimeId() == runtimeId);
cache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
}
}
} }
} }
@@ -6,11 +6,16 @@ import java.io.ByteArrayOutputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.OptionalInt; import java.util.OptionalInt;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream; import java.util.zip.GZIPOutputStream;
@@ -18,6 +23,8 @@ public final class NativeTerrainReceipt {
private static final int MAGIC = 0x4952544E; private static final int MAGIC = 0x4952544E;
private static final int VERSION = 1; private static final int VERSION = 1;
private static final int MAXIMUM_BYTES = 64 * 1024 * 1024; private static final int MAXIMUM_BYTES = 64 * 1024 * 1024;
private static final ThreadLocal<Deflater> DEFLATERS =
ThreadLocal.withInitial(() -> new Deflater(Deflater.DEFAULT_COMPRESSION, true));
private NativeTerrainReceipt() { private NativeTerrainReceipt() {
} }
@@ -25,7 +32,9 @@ public final class NativeTerrainReceipt {
public static byte[] encode(SavedTerrainChunk chunk, long activationId, String epochId) throws IOException { public static byte[] encode(SavedTerrainChunk chunk, long activationId, String epochId) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(); ByteArrayOutputStream bytes = new ByteArrayOutputStream();
UtfCache strings = new UtfCache(); UtfCache strings = new UtfCache();
try (DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new GZIPOutputStream(bytes)))) { Deflater deflater = DEFLATERS.get();
deflater.reset();
try (DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new GzipStream(bytes, deflater)))) {
output.writeInt(MAGIC); output.writeInt(MAGIC);
output.writeInt(VERSION); output.writeInt(VERSION);
output.writeInt(chunk.chunkX()); output.writeInt(chunk.chunkX());
@@ -196,6 +205,60 @@ public final class NativeTerrainReceipt {
public record Decoded(SavedTerrainChunk terrain, long activationId, String epochId) { public record Decoded(SavedTerrainChunk terrain, long activationId, String epochId) {
} }
private static final class GzipStream extends DeflaterOutputStream {
private static final int BUFFER_BYTES = 512;
private static final int TRAILER_BYTES = 8;
private static final byte[] HEADER = gzipHeader();
private final CRC32 checksum = new CRC32();
private GzipStream(OutputStream out, Deflater deflater) throws IOException {
super(out, deflater, BUFFER_BYTES);
out.write(HEADER);
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
super.write(buffer, offset, length);
checksum.update(buffer, offset, length);
}
@Override
public void finish() throws IOException {
if (def.finished()) {
return;
}
def.finish();
while (!def.finished()) {
int length = def.deflate(buf, 0, buf.length);
if (length > 0) {
out.write(buf, 0, length);
}
}
byte[] trailer = new byte[TRAILER_BYTES];
writeLittleEndian(trailer, 0, (int) checksum.getValue());
writeLittleEndian(trailer, 4, (int) def.getBytesRead());
out.write(trailer);
}
private static void writeLittleEndian(byte[] target, int offset, int value) {
target[offset] = (byte) value;
target[offset + 1] = (byte) (value >>> 8);
target[offset + 2] = (byte) (value >>> 16);
target[offset + 3] = (byte) (value >>> 24);
}
private static byte[] gzipHeader() {
ByteArrayOutputStream empty = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(empty)) {
gzip.finish();
} catch (IOException failure) {
throw new IllegalStateException("Unable to resolve the platform gzip header", failure);
}
return Arrays.copyOf(empty.toByteArray(), 10);
}
}
private static final class UtfCache { private static final class UtfCache {
private static final int MAXIMUM_ENTRIES = 256; private static final int MAXIMUM_ENTRIES = 256;
private static final int MAXIMUM_ENCODED_BYTES = 64 * 1024; private static final int MAXIMUM_ENCODED_BYTES = 64 * 1024;
@@ -60,6 +60,15 @@ public class NativeTerrainReceiptTest {
} }
} }
@Test
public void repeatedEncodingOnOneThreadKeepsLegacyBytes() throws Exception {
for (int round = 0; round < 6; round++) {
SavedTerrainChunk chunk = chunk(round % 2 == 0);
String epoch = "epoch-" + round;
assertArrayEquals(legacyEncode(chunk, round, epoch), NativeTerrainReceipt.encode(chunk, round, epoch));
}
}
@Test @Test
public void saturatedStringCachesPreserveModifiedUtfBytes() throws Exception { public void saturatedStringCachesPreserveModifiedUtfBytes() throws Exception {
for (int padding : new int[]{0, 300}) { for (int padding : new int[]{0, 300}) {
@@ -60,6 +60,16 @@ public class SavedBiomeStoreWriteTest {
assertArrayEquals(expectedRegion(), Files.readAllBytes(regionPath(root))); assertArrayEquals(expectedRegion(), Files.readAllBytes(regionPath(root)));
} }
@Test
public void consecutiveRecordsKeepIndependentlyCompressedBytes() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
for (int x = 0; x < 4; x++) {
store.claimAndPersist(chunk(x, 3L));
}
assertArrayEquals(expectedRegion(4), Files.readAllBytes(regionPath(root)));
}
@Test @Test
public void relaxedDurabilityPreservesRecordsWithoutForcingEachAppend() throws Exception { public void relaxedDurabilityPreservesRecordsWithoutForcingEachAppend() throws Exception {
String previousMode = System.getProperty(Durability.MODE_PROPERTY); String previousMode = System.getProperty(Durability.MODE_PROPERTY);
@@ -379,6 +389,10 @@ public class SavedBiomeStoreWriteTest {
} }
private static byte[] expectedRegion() throws IOException { private static byte[] expectedRegion() throws IOException {
return expectedRegion(1);
}
private static byte[] expectedRegion(int records) throws IOException {
ByteArrayOutputStream raw = new ByteArrayOutputStream(); ByteArrayOutputStream raw = new ByteArrayOutputStream();
DataOutputStream data = new DataOutputStream(raw); DataOutputStream data = new DataOutputStream(raw);
data.writeLong(3L); data.writeLong(3L);
@@ -392,17 +406,6 @@ public class SavedBiomeStoreWriteTest {
data.writeInt(1); data.writeInt(1);
data.write(new byte[]{0, 0, 1, (byte) 0x80, 3, 0}); data.write(new byte[]{0, 0, 1, (byte) 0x80, 3, 0});
data.write(new byte[256]); data.write(new byte[256]);
ByteArrayOutputStream body = new ByteArrayOutputStream();
DataOutputStream coordinates = new DataOutputStream(body);
coordinates.writeInt(0);
coordinates.writeInt(0);
coordinates.writeInt(raw.size());
Deflater deflater = new Deflater(Deflater.BEST_SPEED);
try (DeflaterOutputStream compressed = new DeflaterOutputStream(coordinates, deflater)) {
raw.writeTo(compressed);
} finally {
deflater.end();
}
ByteArrayOutputStream region = new ByteArrayOutputStream(); ByteArrayOutputStream region = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(region); DataOutputStream output = new DataOutputStream(region);
output.writeInt(0x4942494F); output.writeInt(0x4942494F);
@@ -410,9 +413,22 @@ public class SavedBiomeStoreWriteTest {
output.writeInt(0); output.writeInt(0);
output.writeInt(0); output.writeInt(0);
output.writeInt(checksum(region.toByteArray())); output.writeInt(checksum(region.toByteArray()));
output.writeInt(body.size()); for (int x = 0; x < records; x++) {
output.write(body.toByteArray()); ByteArrayOutputStream body = new ByteArrayOutputStream();
output.writeInt(checksum(body.toByteArray())); DataOutputStream coordinates = new DataOutputStream(body);
coordinates.writeInt(x);
coordinates.writeInt(0);
coordinates.writeInt(raw.size());
Deflater deflater = new Deflater(Deflater.BEST_SPEED);
try (DeflaterOutputStream compressed = new DeflaterOutputStream(coordinates, deflater)) {
raw.writeTo(compressed);
} finally {
deflater.end();
}
output.writeInt(body.size());
output.write(body.toByteArray());
output.writeInt(checksum(body.toByteArray()));
}
return region.toByteArray(); return region.toByteArray();
} }