From 26e2e20840fc2eaefa0fee92e9b8c1113e9a7a5d Mon Sep 17 00:00:00 2001 From: Julian Krings <47589149+CrazyDev05@users.noreply.github.com> Date: Mon, 14 Apr 2025 21:26:39 +0200 Subject: [PATCH] Feat/pregen cache (#1190) Add cache to pregen to skip already generated chunks faster --- .../com/volmit/iris/core/IrisSettings.java | 1 + .../iris/core/commands/CommandPregen.java | 6 +- .../volmit/iris/core/gui/PregeneratorJob.java | 24 +- .../core/pregenerator/IrisPregenerator.java | 104 ++++++--- .../core/pregenerator/PregenListener.java | 8 +- .../core/pregenerator/cache/PregenCache.java | 70 ++++++ .../pregenerator/cache/PregenCacheImpl.java | 215 ++++++++++++++++++ .../pregenerator/cache/SynchronizedCache.java | 48 ++++ .../methods/CachedPregenMethod.java | 86 +++++++ .../iris/core/service/GlobalCacheSVC.java | 104 +++++++++ .../volmit/iris/core/tools/IrisToolbelt.java | 14 +- .../volmit/iris/util/parallel/MultiBurst.java | 34 +-- 12 files changed, 651 insertions(+), 63 deletions(-) create mode 100644 core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCache.java create mode 100644 core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCacheImpl.java create mode 100644 core/src/main/java/com/volmit/iris/core/pregenerator/cache/SynchronizedCache.java create mode 100644 core/src/main/java/com/volmit/iris/core/pregenerator/methods/CachedPregenMethod.java create mode 100644 core/src/main/java/com/volmit/iris/core/service/GlobalCacheSVC.java diff --git a/core/src/main/java/com/volmit/iris/core/IrisSettings.java b/core/src/main/java/com/volmit/iris/core/IrisSettings.java index 48161dadf..cc96233c0 100644 --- a/core/src/main/java/com/volmit/iris/core/IrisSettings.java +++ b/core/src/main/java/com/volmit/iris/core/IrisSettings.java @@ -130,6 +130,7 @@ public class IrisSettings { public boolean markerEntitySpawningSystem = true; public boolean effectSystem = true; public boolean worldEditWandCUI = true; + public boolean globalPregenCache = true; } @Data diff --git a/core/src/main/java/com/volmit/iris/core/commands/CommandPregen.java b/core/src/main/java/com/volmit/iris/core/commands/CommandPregen.java index 6d7bc42a6..2de5cef25 100644 --- a/core/src/main/java/com/volmit/iris/core/commands/CommandPregen.java +++ b/core/src/main/java/com/volmit/iris/core/commands/CommandPregen.java @@ -39,7 +39,9 @@ public class CommandPregen implements DecreeExecutor { @Param(description = "The world to pregen", contextual = true) World world, @Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0") - Vector center + Vector center, + @Param(description = "Open the Iris pregen gui", defaultValue = "true") + boolean gui ) { try { if (sender().isPlayer() && access() == null) { @@ -50,7 +52,7 @@ public class CommandPregen implements DecreeExecutor { IrisToolbelt.pregenerate(PregenTask .builder() .center(new Position2(center.getBlockX(), center.getBlockZ())) - .gui(true) + .gui(gui) .radiusX(radius) .radiusZ(radius) .build(), world); diff --git a/core/src/main/java/com/volmit/iris/core/gui/PregeneratorJob.java b/core/src/main/java/com/volmit/iris/core/gui/PregeneratorJob.java index 63b8919db..e12ca46a6 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PregeneratorJob.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PregeneratorJob.java @@ -40,6 +40,8 @@ import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.image.BufferedImage; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; @@ -64,6 +66,7 @@ public class PregeneratorJob implements PregenListener { private final Position2 max; private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1)); private final Engine engine; + private final ExecutorService service; private JFrame frame; private PregenRenderer renderer; private int rgc = 0; @@ -96,6 +99,7 @@ public class PregeneratorJob implements PregenListener { }, "Iris Pregenerator"); t.setPriority(Thread.MIN_PRIORITY); t.start(); + service = Executors.newVirtualThreadPerTaskExecutor(); } public static boolean shutdownInstance() { @@ -219,10 +223,10 @@ public class PregeneratorJob implements PregenListener { } @Override - public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, int generated, int totalChunks, int chunksRemaining, long eta, long elapsed, String method) { + public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) { info = new String[]{ (paused() ? "PAUSED" : (saving ? "Saving... " : "Generating")) + " " + Form.f(generated) + " of " + Form.f(totalChunks) + " (" + Form.pc(percent, 0) + " Complete)", - "Speed: " + Form.f(chunksPerSecond, 0) + " Chunks/s, " + Form.f(regionsPerMinute, 1) + " Regions/m, " + Form.f(chunksPerMinute, 0) + " Chunks/m", + "Speed: " + (cached ? "Cached " : "") + Form.f(chunksPerSecond, 0) + " Chunks/s, " + Form.f(regionsPerMinute, 1) + " Regions/m, " + Form.f(chunksPerMinute, 0) + " Chunks/m", Form.duration(eta, 2) + " Remaining " + " (" + Form.duration(elapsed, 2) + " Elapsed)", "Generation Method: " + method, "Memory: " + Form.memSize(monitor.getUsedBytes(), 2) + " (" + Form.pc(monitor.getUsagePercent(), 0) + ") Pressure: " + Form.memSize(monitor.getPressure(), 0) + "/s", @@ -240,13 +244,16 @@ public class PregeneratorJob implements PregenListener { } @Override - public void onChunkGenerated(int x, int z) { - if (engine != null) { - draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8)); - return; - } + public void onChunkGenerated(int x, int z, boolean cached) { + if (renderer == null || frame == null || !frame.isVisible()) return; + service.submit(() -> { + if (engine != null) { + draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8)); + return; + } - draw(x, z, COLOR_GENERATED); + draw(x, z, COLOR_GENERATED); + }); } @Override @@ -306,6 +313,7 @@ public class PregeneratorJob implements PregenListener { close(); instance = null; whenDone.forEach(Runnable::run); + service.shutdownNow(); } @Override diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/IrisPregenerator.java b/core/src/main/java/com/volmit/iris/core/pregenerator/IrisPregenerator.java index 57aa8b0eb..3201bebf4 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/IrisPregenerator.java +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/IrisPregenerator.java @@ -31,28 +31,33 @@ import com.volmit.iris.util.math.RollingSequence; import com.volmit.iris.util.scheduling.ChronoLatch; import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.Looper; +import com.volmit.iris.util.scheduling.PrecisionStopwatch; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; public class IrisPregenerator { + private static final double INVALID = 9223372036854775807d; private final PregenTask task; private final PregeneratorMethod generator; private final PregenListener listener; private final Looper ticker; private final AtomicBoolean paused; private final AtomicBoolean shutdown; + private final RollingSequence cachedPerSecond; private final RollingSequence chunksPerSecond; private final RollingSequence chunksPerMinute; private final RollingSequence regionsPerMinute; private final KList chunksPerSecondHistory; - private static AtomicInteger generated; - private final AtomicInteger generatedLast; - private final AtomicInteger generatedLastMinute; - private static AtomicInteger totalChunks; + private final AtomicLong generated; + private final AtomicLong generatedLast; + private final AtomicLong generatedLastMinute; + private final AtomicLong cached; + private final AtomicLong cachedLast; + private final AtomicLong cachedLastMinute; + private final AtomicLong totalChunks; private final AtomicLong startTime; private final ChronoLatch minuteLatch; private final AtomicReference currentGeneratorMethod; @@ -74,46 +79,71 @@ public class IrisPregenerator { net = new KSet<>(); currentGeneratorMethod = new AtomicReference<>("Void"); minuteLatch = new ChronoLatch(60000, false); + cachedPerSecond = new RollingSequence(5); chunksPerSecond = new RollingSequence(10); chunksPerMinute = new RollingSequence(10); regionsPerMinute = new RollingSequence(10); chunksPerSecondHistory = new KList<>(); - generated = new AtomicInteger(0); - generatedLast = new AtomicInteger(0); - generatedLastMinute = new AtomicInteger(0); - totalChunks = new AtomicInteger(0); + generated = new AtomicLong(0); + generatedLast = new AtomicLong(0); + generatedLastMinute = new AtomicLong(0); + cached = new AtomicLong(); + cachedLast = new AtomicLong(0); + cachedLastMinute = new AtomicLong(0); + totalChunks = new AtomicLong(0); task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet()); startTime = new AtomicLong(M.ms()); ticker = new Looper() { @Override protected long loop() { long eta = computeETA(); - int secondGenerated = generated.get() - generatedLast.get(); - generatedLast.set(generated.get()); - chunksPerSecond.put(secondGenerated); - chunksPerSecondHistory.add(secondGenerated); - if (minuteLatch.flip()) { - int minuteGenerated = generated.get() - generatedLastMinute.get(); - generatedLastMinute.set(generated.get()); - chunksPerMinute.put(minuteGenerated); - regionsPerMinute.put((double) minuteGenerated / 1024D); + long secondCached = cached.get() - cachedLast.get(); + cachedLast.set(cached.get()); + cachedPerSecond.put(secondCached); + + long secondGenerated = generated.get() - generatedLast.get() - secondCached; + generatedLast.set(generated.get()); + if (secondCached == 0 || secondGenerated != 0) { + chunksPerSecond.put(secondGenerated); + chunksPerSecondHistory.add((int) secondGenerated); } - listener.onTick(chunksPerSecond.getAverage(), chunksPerMinute.getAverage(), + if (minuteLatch.flip()) { + long minuteCached = cached.get() - cachedLastMinute.get(); + cachedLastMinute.set(cached.get()); + + long minuteGenerated = generated.get() - generatedLastMinute.get() - minuteCached; + generatedLastMinute.set(generated.get()); + if (minuteCached == 0 || minuteGenerated != 0) { + chunksPerMinute.put(minuteGenerated); + regionsPerMinute.put((double) minuteGenerated / 1024D); + } + } + boolean cached = cachedPerSecond.getAverage() != 0; + + listener.onTick( + cached ? cachedPerSecond.getAverage() : chunksPerSecond.getAverage(), + chunksPerMinute.getAverage(), regionsPerMinute.getAverage(), - (double) generated.get() / (double) totalChunks.get(), - generated.get(), totalChunks.get(), - totalChunks.get() - generated.get(), - eta, M.ms() - startTime.get(), currentGeneratorMethod.get()); + (double) generated.get() / (double) totalChunks.get(), generated.get(), + totalChunks.get(), + totalChunks.get() - generated.get(), eta, M.ms() - startTime.get(), currentGeneratorMethod.get(), + cached); if (cl.flip()) { double percentage = ((double) generated.get() / (double) totalChunks.get()) * 100; - if (!IrisPackBenchmarking.benchmarkInProgress) { - Iris.info("Pregen: " + Form.f(generated.get()) + " of " + Form.f(totalChunks.get()) + " (%.0f%%) " + Form.f((int) chunksPerSecond.getAverage()) + "/s ETA: " + Form.duration(eta, 2), percentage); - } else { - Iris.info("Benchmarking: " + Form.f(generated.get()) + " of " + Form.f(totalChunks.get()) + " (%.0f%%) " + Form.f((int) chunksPerSecond.getAverage()) + "/s ETA: " + Form.duration(eta, 2), percentage); - } + + Iris.info("%s: %s of %s (%.0f%%), %s/s ETA: %s", + IrisPackBenchmarking.benchmarkInProgress ? "Benchmarking" : "Pregen", + Form.f(generated.get()), + Form.f(totalChunks.get()), + percentage, + cached ? + "Cached " + Form.f((int) cachedPerSecond.getAverage()) : + Form.f((int) chunksPerSecond.getAverage()), + Form.duration(eta, 2) + ); } return 1000; } @@ -121,12 +151,13 @@ public class IrisPregenerator { } private long computeETA() { - return (long) (totalChunks.get() > 1024 ? // Generated chunks exceed 1/8th of total? + double d = (long) (totalChunks.get() > 1024 ? // Generated chunks exceed 1/8th of total? // If yes, use smooth function (which gets more accurate over time since its less sensitive to outliers) - ((totalChunks.get() - generated.get()) * ((double) (M.ms() - startTime.get()) / (double) generated.get())) : + ((totalChunks.get() - generated.get() - cached.get()) * ((double) (M.ms() - startTime.get()) / ((double) generated.get() - cached.get()))) : // If no, use quick function (which is less accurate over time but responds better to the initial delay) - ((totalChunks.get() - generated.get()) / chunksPerSecond.getAverage()) * 1000 + ((totalChunks.get() - generated.get() - cached.get()) / chunksPerSecond.getAverage()) * 1000 ); + return Double.isFinite(d) && d != INVALID ? (long) d : 0; } @@ -138,8 +169,10 @@ public class IrisPregenerator { init(); ticker.start(); checkRegions(); + var p = PrecisionStopwatch.start(); task.iterateRegions((x, z) -> visitRegion(x, z, true)); task.iterateRegions((x, z) -> visitRegion(x, z, false)); + Iris.info("Pregen took " + Form.duration((long) p.getMilliseconds())); shutdown(); if (!IrisPackBenchmarking.benchmarkInProgress) { Iris.info(C.IRIS + "Pregen stopped."); @@ -234,8 +267,8 @@ public class IrisPregenerator { private PregenListener listenify(PregenListener listener) { return new PregenListener() { @Override - public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, int generated, int totalChunks, int chunksRemaining, long eta, long elapsed, String method) { - listener.onTick(chunksPerSecond, chunksPerMinute, regionsPerMinute, percent, generated, totalChunks, chunksRemaining, eta, elapsed, method); + public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) { + listener.onTick(chunksPerSecond, chunksPerMinute, regionsPerMinute, percent, generated, totalChunks, chunksRemaining, eta, elapsed, method, cached); } @Override @@ -244,9 +277,10 @@ public class IrisPregenerator { } @Override - public void onChunkGenerated(int x, int z) { - listener.onChunkGenerated(x, z); + public void onChunkGenerated(int x, int z, boolean c) { + listener.onChunkGenerated(x, z, c); generated.addAndGet(1); + if (c) cached.addAndGet(1); } @Override diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/PregenListener.java b/core/src/main/java/com/volmit/iris/core/pregenerator/PregenListener.java index fb3ab3952..6f5d83194 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/PregenListener.java +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/PregenListener.java @@ -19,11 +19,15 @@ package com.volmit.iris.core.pregenerator; public interface PregenListener { - void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, int generated, int totalChunks, int chunksRemaining, long eta, long elapsed, String method); + void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached); void onChunkGenerating(int x, int z); - void onChunkGenerated(int x, int z); + default void onChunkGenerated(int x, int z) { + onChunkGenerated(x, z, false); + } + + void onChunkGenerated(int x, int z, boolean cached); void onRegionGenerated(int x, int z); 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 new file mode 100644 index 000000000..dff3840cd --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCache.java @@ -0,0 +1,70 @@ +package com.volmit.iris.core.pregenerator.cache; + +import com.volmit.iris.util.documentation.ChunkCoordinates; +import com.volmit.iris.util.documentation.RegionCoordinates; + +import java.io.File; + +public interface PregenCache { + default boolean isThreadSafe() { + return false; + } + + @ChunkCoordinates + boolean isChunkCached(int x, int z); + + @RegionCoordinates + boolean isRegionCached(int x, int z); + + @ChunkCoordinates + void cacheChunk(int x, int z); + + @RegionCoordinates + void cacheRegion(int x, int z); + + void write(); + + static PregenCache create(File directory) { + if (directory == null) return EMPTY; + return new PregenCacheImpl(directory); + } + + default PregenCache sync() { + if (isThreadSafe()) return this; + return new SynchronizedCache(this); + } + + PregenCache EMPTY = new PregenCache() { + @Override + public boolean isThreadSafe() { + return true; + } + + @Override + public boolean isChunkCached(int x, int z) { + return false; + } + + @Override + public boolean isRegionCached(int x, int z) { + return false; + } + + @Override + public void cacheChunk(int x, int z) { + + } + + @Override + public void cacheRegion(int x, int z) { + + } + + @Override + public void write() { + + } + }; + + +} 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 new file mode 100644 index 000000000..bf48e06e9 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/cache/PregenCacheImpl.java @@ -0,0 +1,215 @@ +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.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.parallel.HyperLock; +import lombok.RequiredArgsConstructor; +import net.jpountz.lz4.LZ4BlockInputStream; +import net.jpountz.lz4.LZ4BlockOutputStream; +import org.jetbrains.annotations.Nullable; + +import javax.annotation.concurrent.NotThreadSafe; +import java.io.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +@NotThreadSafe +@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) + .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); + 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 (var out = new DataOutputStream(new LZ4BlockOutputStream(new FileOutputStream(file)))) { + plate.write(out); + } catch (IOException e) { + Iris.error("Failed to write pregen cache " + file); + 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 new file mode 100644 index 000000000..52f3b7774 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/cache/SynchronizedCache.java @@ -0,0 +1,48 @@ +package com.volmit.iris.core.pregenerator.cache; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +class SynchronizedCache implements PregenCache { + private final PregenCache cache; + + @Override + public boolean isThreadSafe() { + return true; + } + + @Override + public boolean isChunkCached(int x, int z) { + synchronized (cache) { + return cache.isChunkCached(x, z); + } + } + + @Override + public boolean isRegionCached(int x, int z) { + synchronized (cache) { + return cache.isRegionCached(x, z); + } + } + + @Override + public void cacheChunk(int x, int z) { + synchronized (cache) { + cache.cacheChunk(x, z); + } + } + + @Override + public void cacheRegion(int x, int z) { + synchronized (cache) { + cache.cacheRegion(x, z); + } + } + + @Override + public void write() { + synchronized (cache) { + cache.write(); + } + } +} diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/methods/CachedPregenMethod.java b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/CachedPregenMethod.java new file mode 100644 index 000000000..91c4ddb87 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/methods/CachedPregenMethod.java @@ -0,0 +1,86 @@ +package com.volmit.iris.core.pregenerator.methods; + +import com.volmit.iris.Iris; +import com.volmit.iris.core.pregenerator.PregenListener; +import com.volmit.iris.core.pregenerator.PregeneratorMethod; +import com.volmit.iris.core.pregenerator.cache.PregenCache; +import com.volmit.iris.core.service.GlobalCacheSVC; +import com.volmit.iris.util.mantle.Mantle; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class CachedPregenMethod implements PregeneratorMethod { + private final PregeneratorMethod method; + private final PregenCache cache; + + public CachedPregenMethod(PregeneratorMethod method, String worldName) { + this.method = method; + var cache = Iris.service(GlobalCacheSVC.class).get(worldName); + if (cache == null) { + Iris.debug("Could not find existing cache for " + worldName + " creating fallback"); + cache = GlobalCacheSVC.createDefault(worldName); + } + this.cache = cache; + } + + @Override + public void init() { + method.init(); + } + + @Override + public void close() { + method.close(); + cache.write(); + } + + @Override + public void save() { + method.save(); + cache.write(); + } + + @Override + public boolean supportsRegions(int x, int z, PregenListener listener) { + return cache.isRegionCached(x, z) || method.supportsRegions(x, z, listener); + } + + @Override + public String getMethod(int x, int z) { + return method.getMethod(x, z); + } + + @Override + public void generateRegion(int x, int z, PregenListener listener) { + if (cache.isRegionCached(x, z)) { + listener.onRegionGenerated(x, z); + + int rX = x << 5, rZ = z << 5; + for (int cX = 0; cX < 32; cX++) { + for (int cZ = 0; cZ < 32; cZ++) { + listener.onChunkGenerated(rX + cX, rZ + cZ, true); + listener.onChunkCleaned(rX + cX, rZ + cZ); + } + } + return; + } + method.generateRegion(x, z, listener); + cache.cacheRegion(x, z); + } + + @Override + public void generateChunk(int x, int z, PregenListener listener) { + if (cache.isChunkCached(x, z)) { + listener.onChunkGenerated(x, z, true); + listener.onChunkCleaned(x, z); + return; + } + method.generateChunk(x, z, listener); + cache.cacheChunk(x, z); + } + + @Override + public Mantle getMantle() { + return method.getMantle(); + } +} 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 new file mode 100644 index 000000000..32d6af184 --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/service/GlobalCacheSVC.java @@ -0,0 +1,104 @@ +package com.volmit.iris.core.service; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.volmit.iris.core.IrisSettings; +import com.volmit.iris.core.pregenerator.cache.PregenCache; +import com.volmit.iris.util.collection.KMap; +import com.volmit.iris.util.plugin.IrisService; +import lombok.NonNull; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.WorldInitEvent; +import org.bukkit.event.world.WorldUnloadEvent; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.function.Function; + +public class GlobalCacheSVC implements IrisService { + private static final Cache REFERENCE_CACHE = Caffeine.newBuilder().weakValues().build(); + private final KMap globalCache = new KMap<>(); + private transient boolean lastState; + + @Override + public void onEnable() { + lastState = !IrisSettings.get().getWorld().isGlobalPregenCache(); + if (lastState) return; + Bukkit.getWorlds().forEach(this::createCache); + } + + @Override + public void onDisable() { + globalCache.values().forEach(PregenCache::write); + } + + @Nullable + public PregenCache get(@NonNull World world) { + return globalCache.get(world.getName()); + } + + @Nullable + public PregenCache get(@NonNull String world) { + return globalCache.get(world); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void on(WorldInitEvent event) { + if (isDisabled()) return; + createCache(event.getWorld()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void on(WorldUnloadEvent event) { + var cache = globalCache.remove(event.getWorld().getName()); + if (cache == null) return; + cache.write(); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void on(ChunkLoadEvent event) { + var cache = get(event.getWorld()); + if (cache == null) return; + cache.cacheChunk(event.getChunk().getX(), event.getChunk().getZ()); + } + + private void createCache(World world) { + globalCache.computeIfAbsent(world.getName(), GlobalCacheSVC::createDefault); + } + + private boolean isDisabled() { + boolean conf = IrisSettings.get().getWorld().isGlobalPregenCache(); + if (lastState != conf) + return lastState; + + if (conf) { + Bukkit.getWorlds().forEach(this::createCache); + } else { + globalCache.values().removeIf(cache -> { + cache.write(); + return true; + }); + } + + return lastState = !conf; + } + + + @NonNull + public static PregenCache createCache(@NonNull String worldName, @NonNull Function provider) { + return REFERENCE_CACHE.get(worldName, provider); + } + + @NonNull + public static PregenCache createDefault(@NonNull String worldName) { + return createCache(worldName, GlobalCacheSVC::createDefault0); + } + + private static PregenCache createDefault0(String worldName) { + return PregenCache.create(new File(Bukkit.getWorldContainer(), String.join(File.separator, worldName, "iris", "pregen"))).sync(); + } +} diff --git a/core/src/main/java/com/volmit/iris/core/tools/IrisToolbelt.java b/core/src/main/java/com/volmit/iris/core/tools/IrisToolbelt.java index ad552ee81..15b3dc8d3 100644 --- a/core/src/main/java/com/volmit/iris/core/tools/IrisToolbelt.java +++ b/core/src/main/java/com/volmit/iris/core/tools/IrisToolbelt.java @@ -24,6 +24,7 @@ import com.volmit.iris.core.gui.PregeneratorJob; import com.volmit.iris.core.loader.IrisData; import com.volmit.iris.core.pregenerator.PregenTask; import com.volmit.iris.core.pregenerator.PregeneratorMethod; +import com.volmit.iris.core.pregenerator.methods.CachedPregenMethod; import com.volmit.iris.core.pregenerator.methods.HybridPregenMethod; import com.volmit.iris.core.service.StudioSVC; import com.volmit.iris.engine.framework.Engine; @@ -141,7 +142,18 @@ public class IrisToolbelt { * @return the pregenerator job (already started) */ public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine) { - return new PregeneratorJob(task, method, engine); + return pregenerate(task, method, engine, true); + } + + /** + * Start a pregenerator task + * + * @param task the scheduled task + * @param method the method to execute the task + * @return the pregenerator job (already started) + */ + public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine, boolean cached) { + return new PregeneratorJob(task, cached && engine != null ? new CachedPregenMethod(method, engine.getWorld().name()) : method, engine); } /** diff --git a/core/src/main/java/com/volmit/iris/util/parallel/MultiBurst.java b/core/src/main/java/com/volmit/iris/util/parallel/MultiBurst.java index c23aee9d3..8a71ac48c 100644 --- a/core/src/main/java/com/volmit/iris/util/parallel/MultiBurst.java +++ b/core/src/main/java/com/volmit/iris/util/parallel/MultiBurst.java @@ -221,27 +221,31 @@ public class MultiBurst implements ExecutorService { public void close() { if (service != null) { - service.shutdown(); - PrecisionStopwatch p = PrecisionStopwatch.start(); - try { - while (!service.awaitTermination(1, TimeUnit.SECONDS)) { - Iris.info("Still waiting to shutdown burster..."); - if (p.getMilliseconds() > 7000) { - Iris.warn("Forcing Shutdown..."); + close(service); + } + } - try { - service.shutdownNow(); - } catch (Throwable e) { + public static void close(ExecutorService service) { + service.shutdown(); + PrecisionStopwatch p = PrecisionStopwatch.start(); + try { + while (!service.awaitTermination(1, TimeUnit.SECONDS)) { + Iris.info("Still waiting to shutdown burster..."); + if (p.getMilliseconds() > 7000) { + Iris.warn("Forcing Shutdown..."); - } + try { + service.shutdownNow(); + } catch (Throwable e) { - break; } + + break; } - } catch (Throwable e) { - e.printStackTrace(); - Iris.reportError(e); } + } catch (Throwable e) { + e.printStackTrace(); + Iris.reportError(e); } } }