9
0
mirror of https://github.com/VolmitSoftware/Iris.git synced 2025-12-19 15:09:18 +00:00

improve pregen cache

This commit is contained in:
Julian Krings
2025-12-01 16:01:54 +01:00
parent b5ab4968ba
commit 123708601f
6 changed files with 281 additions and 245 deletions

View File

@@ -142,6 +142,7 @@ slimJar {
relocate("com.google.inject", "$lib.guice")
relocate("org.dom4j", "$lib.dom4j")
relocate("org.jaxen", "$lib.jaxen")
relocate("com.github.benmanes.caffeine", "$lib.caffeine")
}
tasks {

View File

@@ -24,9 +24,11 @@ public interface PregenCache {
void write();
void trim(long unloadDuration);
static PregenCache create(File directory) {
if (directory == null) return EMPTY;
return new PregenCacheImpl(directory);
return new PregenCacheImpl(directory, 16);
}
default PregenCache sync() {
@@ -51,19 +53,16 @@ public interface PregenCache {
}
@Override
public void cacheChunk(int x, int z) {
}
public void cacheChunk(int x, int z) {}
@Override
public void cacheRegion(int x, int z) {
}
public void cacheRegion(int x, int z) {}
@Override
public void write() {
public void write() {}
}
@Override
public void trim(long unloadDuration) {}
};

View File

@@ -1,220 +0,0 @@
package com.volmit.iris.core.pregenerator.cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.Scheduler;
import com.volmit.iris.Iris;
import com.volmit.iris.util.data.KCache;
import com.volmit.iris.util.data.Varint;
import com.volmit.iris.util.documentation.ChunkCoordinates;
import com.volmit.iris.util.documentation.RegionCoordinates;
import com.volmit.iris.util.io.IO;
import com.volmit.iris.util.parallel.HyperLock;
import lombok.RequiredArgsConstructor;
import net.jpountz.lz4.LZ4BlockInputStream;
import net.jpountz.lz4.LZ4BlockOutputStream;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
@RequiredArgsConstructor
class PregenCacheImpl implements PregenCache {
private static final int SIZE = 32;
private final File directory;
private final HyperLock hyperLock = new HyperLock(SIZE * 2, true);
private final LoadingCache<Pos, Plate> cache = Caffeine.newBuilder()
.expireAfterAccess(10, TimeUnit.SECONDS)
.executor(KCache.EXECUTOR)
.scheduler(Scheduler.systemScheduler())
.maximumSize(SIZE)
.removalListener(this::onRemoval)
.evictionListener(this::onRemoval)
.build(this::load);
@ChunkCoordinates
public boolean isChunkCached(int x, int z) {
var plate = cache.get(new Pos(x >> 10, z >> 10));
if (plate == null) return false;
return plate.isCached((x >> 5) & 31, (z >> 5) & 31, r -> r.isCached(x & 31, z & 31));
}
@RegionCoordinates
public boolean isRegionCached(int x, int z) {
var plate = cache.get(new Pos(x >> 5, z >> 5));
if (plate == null) return false;
return plate.isCached(x & 31, z & 31, Region::isCached);
}
@ChunkCoordinates
public void cacheChunk(int x, int z) {
var plate = cache.get(new Pos(x >> 10, z >> 10));
plate.cache((x >> 5) & 31, (z >> 5) & 31, r -> r.cache(x & 31, z & 31));
}
@RegionCoordinates
public void cacheRegion(int x, int z) {
var plate = cache.get(new Pos(x >> 5, z >> 5));
plate.cache(x & 31, z & 31, Region::cache);
}
public void write() {
cache.asMap().values().forEach(this::write);
}
private Plate load(Pos key) {
hyperLock.lock(key.x, key.z);
try {
File file = fileForPlate(key);
if (!file.exists()) return new Plate(key);
try (var in = new DataInputStream(new LZ4BlockInputStream(new FileInputStream(file)))) {
return new Plate(key, in);
} catch (IOException e){
Iris.error("Failed to read pregen cache " + file);
Iris.reportError(e);
e.printStackTrace();
return new Plate(key);
}
} finally {
hyperLock.unlock(key.x, key.z);
}
}
private void write(Plate plate) {
hyperLock.lock(plate.pos.x, plate.pos.z);
try {
File file = fileForPlate(plate.pos);
try {
IO.write(file, out -> new DataOutputStream(new LZ4BlockOutputStream(out)), plate::write);
} catch (IOException e) {
Iris.error("Failed to write pregen cache " + file);
Iris.reportError(e);
e.printStackTrace();
}
} finally {
hyperLock.unlock(plate.pos.x, plate.pos.z);
}
}
private void onRemoval(@Nullable Pos key, @Nullable Plate plate, RemovalCause cause) {
if (plate == null) return;
write(plate);
}
private File fileForPlate(Pos pos) {
if (!directory.exists() && !directory.mkdirs())
throw new IllegalStateException("Cannot create directory: " + directory.getAbsolutePath());
return new File(directory, "c." + pos.x + "." + pos.z + ".lz4b");
}
private static class Plate {
private final Pos pos;
private short count;
private Region[] regions;
public Plate(Pos pos) {
this.pos = pos;
count = 0;
regions = new Region[1024];
}
public Plate(Pos pos, DataInput in) throws IOException {
this.pos = pos;
count = (short) Varint.readSignedVarInt(in);
if (count == 1024) return;
regions = new Region[1024];
for (int i = 0; i < 1024; i++) {
if (in.readBoolean()) continue;
regions[i] = new Region(in);
}
}
public boolean isCached(int x, int z, Predicate<Region> predicate) {
if (count == 1024) return true;
Region region = regions[x * 32 + z];
if (region == null) return false;
return predicate.test(region);
}
public void cache(int x, int z, Predicate<Region> predicate) {
if (count == 1024) return;
Region region = regions[x * 32 + z];
if (region == null) regions[x * 32 + z] = region = new Region();
if (predicate.test(region)) count++;
}
public void write(DataOutput out) throws IOException {
Varint.writeSignedVarInt(count, out);
if (count == 1024) return;
for (Region region : regions) {
out.writeBoolean(region == null);
if (region == null) continue;
region.write(out);
}
}
}
private static class Region {
private short count;
private long[] words;
public Region() {
count = 0;
words = new long[64];
}
public Region(DataInput in) throws IOException {
count = (short) Varint.readSignedVarInt(in);
if (count == 1024) return;
words = new long[64];
for (int i = 0; i < 64; i++) {
words[i] = Varint.readUnsignedVarLong(in);
}
}
public boolean cache() {
if (count == 1024) return false;
count = 1024;
words = null;
return true;
}
public boolean cache(int x, int z) {
if (count == 1024) return false;
int i = x * 32 + z;
int w = i >> 6;
long b = 1L << (i & 63);
var cur = (words[w] & b) != 0;
if (cur) return false;
if (++count == 1024) {
words = null;
return true;
} else words[w] |= b;
return false;
}
public boolean isCached() {
return count == 1024;
}
public boolean isCached(int x, int z) {
int i = x * 32 + z;
return count == 1024 || (words[i >> 6] & 1L << (i & 63)) != 0;
}
public void write(DataOutput out) throws IOException {
Varint.writeSignedVarInt(count, out);
if (isCached()) return;
for (long word : words) {
Varint.writeUnsignedVarLong(word, out);
}
}
}
private record Pos(int x, int z) {}
}

View File

@@ -1,11 +1,6 @@
package com.volmit.iris.core.pregenerator.cache;
import lombok.AllArgsConstructor;
@AllArgsConstructor
class SynchronizedCache implements PregenCache {
private final PregenCache cache;
record SynchronizedCache(PregenCache cache) implements PregenCache {
@Override
public boolean isThreadSafe() {
return true;
@@ -45,4 +40,11 @@ class SynchronizedCache implements PregenCache {
cache.write();
}
}
@Override
public void trim(long unloadDuration) {
synchronized (cache) {
cache.trim(unloadDuration);
}
}
}

View File

@@ -1,13 +1,11 @@
package com.volmit.iris.core.service;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Scheduler;
import com.volmit.iris.core.IrisSettings;
import com.volmit.iris.core.pregenerator.cache.PregenCache;
import com.volmit.iris.core.tools.IrisToolbelt;
import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.data.KCache;
import com.volmit.iris.util.plugin.IrisService;
import com.volmit.iris.util.scheduling.Looper;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.World;
@@ -19,21 +17,33 @@ import org.bukkit.event.world.WorldUnloadEvent;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.function.Function;
public class GlobalCacheSVC implements IrisService {
private static final Cache<String, PregenCache> REFERENCE_CACHE = Caffeine.newBuilder()
.executor(KCache.EXECUTOR)
.scheduler(Scheduler.systemScheduler())
.weakValues()
.build();
private static final KMap<String, Reference<PregenCache>> REFERENCE_CACHE = new KMap<>();
private final KMap<String, PregenCache> globalCache = new KMap<>();
private transient boolean lastState;
private static boolean disabled = true;
private Looper trimmer;
@Override
public void onEnable() {
disabled = false;
trimmer = new Looper() {
@Override
protected long loop() {
var it = REFERENCE_CACHE.values().iterator();
while (it.hasNext()) {
var cache = it.next().get();
if (cache == null) it.remove();
else cache.trim(10_000);
}
return disabled ? -1 : 2_000;
}
};
trimmer.start();
lastState = !IrisSettings.get().getWorld().isGlobalPregenCache();
if (lastState) return;
Bukkit.getWorlds().forEach(this::createCache);
@@ -42,6 +52,9 @@ public class GlobalCacheSVC implements IrisService {
@Override
public void onDisable() {
disabled = true;
try {
trimmer.join();
} catch (InterruptedException ignored) {}
globalCache.qclear((world, cache) -> cache.write());
}
@@ -76,6 +89,7 @@ public class GlobalCacheSVC implements IrisService {
}
private void createCache(World world) {
if (!IrisToolbelt.isIrisWorld(world)) return;
globalCache.computeIfAbsent(world.getName(), GlobalCacheSVC::createDefault);
}
@@ -99,7 +113,15 @@ public class GlobalCacheSVC implements IrisService {
@NonNull
public static PregenCache createCache(@NonNull String worldName, @NonNull Function<String, PregenCache> provider) {
return REFERENCE_CACHE.get(worldName, provider);
PregenCache[] holder = new PregenCache[1];
REFERENCE_CACHE.compute(worldName, (name, ref) -> {
if (ref != null) {
if ((holder[0] = ref.get()) != null)
return ref;
}
return new WeakReference<>(holder[0] = provider.apply(worldName));
});
return holder[0];
}
@NonNull

View File

@@ -0,0 +1,232 @@
package com.volmit.iris.core.pregenerator.cache
import com.volmit.iris.Iris
import com.volmit.iris.util.data.Varint
import com.volmit.iris.util.documentation.ChunkCoordinates
import com.volmit.iris.util.documentation.RegionCoordinates
import com.volmit.iris.util.io.IO
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.jpountz.lz4.LZ4BlockInputStream
import net.jpountz.lz4.LZ4BlockOutputStream
import java.io.*
class PregenCacheImpl(
private val directory: File,
private val maxSize: Int
) : PregenCache {
private val cache = Object2ObjectLinkedOpenHashMap<Pair<Int, Int>, Plate>()
@ChunkCoordinates
override fun isChunkCached(x: Int, z: Int): Boolean {
return this[x shr 10, z shr 10].isCached(
(x shr 5) and 31,
(z shr 5) and 31
) { isCached(x and 31, z and 31) }
}
@RegionCoordinates
override fun isRegionCached(x: Int, z: Int): Boolean {
return this[x shr 5, z shr 5].isCached(
x and 31,
z and 31,
Region::isCached
)
}
@ChunkCoordinates
override fun cacheChunk(x: Int, z: Int) {
this[x shr 10, z shr 10].cache(
(x shr 5) and 31,
(z shr 5) and 31
) { cache(x and 31, z and 31) }
}
@RegionCoordinates
override fun cacheRegion(x: Int, z: Int) {
this[x shr 5, z shr 5].cache(
x and 31,
z and 31,
Region::cache
)
}
override fun write() {
if (cache.isEmpty()) return
runBlocking {
for (plate in cache.values) {
if (!plate.dirty) continue
launch(dispatcher) {
writePlate(plate)
}
}
}
}
override fun trim(unloadDuration: Long) {
if (cache.isEmpty()) return
val threshold = System.currentTimeMillis() - unloadDuration
runBlocking {
val it = cache.values.iterator()
while (it.hasNext()) {
val plate = it.next()
if (plate.lastAccess < threshold) it.remove()
launch(dispatcher) {
writePlate(plate)
}
}
}
}
private operator fun get(x: Int, z: Int): Plate {
val key = x to z
val plate = cache.getAndMoveToFirst(key)
if (plate != null) return plate
return readPlate(x, z).also {
cache.putAndMoveToFirst(key, it)
runBlocking {
while (cache.size > maxSize) {
val plate = cache.removeLast()
launch(dispatcher) {
writePlate(plate)
}
}
}
}
}
private fun readPlate(x: Int, z: Int): Plate {
val file = fileForPlate(x, z)
if (!file.exists()) return Plate(x, z)
try {
DataInputStream(LZ4BlockInputStream(file.inputStream())).use {
return readPlate(x, z, it)
}
} catch (e: IOException) {
Iris.error("Failed to read pregen cache $file")
e.printStackTrace()
Iris.reportError(e)
}
return Plate(x, z)
}
private fun writePlate(plate: Plate) {
if (!plate.dirty) return
val file = fileForPlate(plate.x, plate.z)
try {
IO.write(file, { DataOutputStream(LZ4BlockOutputStream(it)) }, plate::write)
plate.dirty = false
} catch (e: IOException) {
Iris.error("Failed to write preen cache $file")
e.printStackTrace()
Iris.reportError(e)
}
}
private fun fileForPlate(x: Int, z: Int): File {
check(!(!directory.exists() && !directory.mkdirs())) { "Cannot create directory: " + directory.absolutePath }
return File(directory, "c.$x.$z.lz4b")
}
private class Plate(
val x: Int,
val z: Int,
private var count: Short = 0,
private var regions: Array<Region?>? = arrayOfNulls(1024)
) {
var dirty: Boolean = false
var lastAccess: Long = System.currentTimeMillis()
fun cache(x: Int, z: Int, predicate: Region.() -> Boolean): Boolean {
lastAccess = System.currentTimeMillis()
if (count == SIZE) return false
val region = regions!!.run { this[x * 32 + z] ?: Region().also { this[x * 32 + z] = it } }
if (!region.predicate()) return false
if (++count == SIZE) regions = null
dirty = true
return true
}
fun isCached(x: Int, z: Int, predicate: Region.() -> Boolean): Boolean {
lastAccess = System.currentTimeMillis()
if (count == SIZE) return true
val region = regions!![x * 32 + z] ?: return false
return region.predicate()
}
fun write(dos: DataOutput) {
Varint.writeSignedVarInt(count.toInt(), dos)
regions?.forEach {
dos.writeBoolean(it == null)
it?.write(dos)
}
}
}
private class Region(
private var count: Short = 0,
private var words: LongArray? = LongArray(64)
) {
fun cache(): Boolean {
if (count == SIZE) return false
count = SIZE
words = null
return true
}
fun cache(x: Int, z: Int): Boolean {
if (count == SIZE) return false
val words = words ?: return false
val i = x * 32 + z
val w = i shr 6
val b = 1L shl (i and 63)
val cur = (words[w] and b) != 0L
if (cur) return false
if (++count == SIZE) {
this.words = null
return true
} else {
words[w] = words[w] or b
return false
}
}
fun isCached(): Boolean = count == SIZE
fun isCached(x: Int, z: Int): Boolean {
val i = x * 32 + z
return count == SIZE || (words!![i shr 6] and (1L shl (i and 63))) != 0L
}
@Throws(IOException::class)
fun write(dos: DataOutput) {
Varint.writeSignedVarInt(count.toInt(), dos)
words?.forEach { Varint.writeUnsignedVarLong(it, dos) }
}
}
companion object {
private val dispatcher = Dispatchers.IO.limitedParallelism(4)
private const val SIZE: Short = 1024
@Throws(IOException::class)
private fun readPlate(x: Int, z: Int, din: DataInput): Plate {
val count = Varint.readSignedVarInt(din)
if (count == 1024) return Plate(x, z, SIZE, null)
return Plate(x, z, count.toShort(), Array(1024) {
if (din.readBoolean()) null
else readRegion(din)
})
}
@Throws(IOException::class)
private fun readRegion(din: DataInput): Region {
val count = Varint.readSignedVarInt(din)
return if (count == 1024) Region(SIZE, null)
else Region(count.toShort(), LongArray(64) { Varint.readUnsignedVarLong(din) })
}
}
}