diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 8e4894bd4..b695ade78 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -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 { diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCache.java b/core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCache.java index dff3840cd..e085975c2 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCache.java +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCache.java @@ -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) {} }; diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCacheImpl.java b/core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCacheImpl.java deleted file mode 100644 index d124b25bc..000000000 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCacheImpl.java +++ /dev/null @@ -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 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 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 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) {} -} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/cache/SynchronizedCache.java b/core/src/main/java/com/volmit/iris/core/pregenerator/cache/SynchronizedCache.java index 52f3b7774..18efefa43 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/cache/SynchronizedCache.java +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/cache/SynchronizedCache.java @@ -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); + } + } } diff --git a/core/src/main/java/com/volmit/iris/core/service/GlobalCacheSVC.java b/core/src/main/java/com/volmit/iris/core/service/GlobalCacheSVC.java index ceeedbee7..4d6e2db76 100644 --- a/core/src/main/java/com/volmit/iris/core/service/GlobalCacheSVC.java +++ b/core/src/main/java/com/volmit/iris/core/service/GlobalCacheSVC.java @@ -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 REFERENCE_CACHE = Caffeine.newBuilder() - .executor(KCache.EXECUTOR) - .scheduler(Scheduler.systemScheduler()) - .weakValues() - .build(); + private static final KMap> REFERENCE_CACHE = new KMap<>(); private final KMap 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 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 diff --git a/core/src/main/kotlin/com/volmit/iris/core/pregenerator/cache/PregenCacheImpl.kt b/core/src/main/kotlin/com/volmit/iris/core/pregenerator/cache/PregenCacheImpl.kt new file mode 100644 index 000000000..ba8575102 --- /dev/null +++ b/core/src/main/kotlin/com/volmit/iris/core/pregenerator/cache/PregenCacheImpl.kt @@ -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, 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? = 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) }) + } + } +} \ No newline at end of file