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 cfd830827..1ab105d43 100644 --- a/core/src/main/java/com/volmit/iris/core/IrisSettings.java +++ b/core/src/main/java/com/volmit/iris/core/IrisSettings.java @@ -147,10 +147,10 @@ public class IrisSettings { @Data public static class IrisSettingsPregen { - private static final int REFERENCE_WORLD_HEIGHT = 384; private static final int MIN_RESIDENT_TECTONIC_PLATES = 16; private static final double MANTLE_HEAP_FRACTION = 0.6D; - private static final int REFERENCE_PLATE_MEGABYTES = 48; + private static final int MEASURED_PLATE_MEGABYTES = 38; + private static final int MEASURED_WORLD_HEIGHT = 1088; public boolean useCacheByDefault = true; public boolean useHighPriority = false; @@ -162,12 +162,11 @@ public class IrisSettings { public int mantleBackpressureTimeoutMs = 60_000; public int getEffectiveResidentTectonicPlates(int worldHeight) { - int normalizedHeight = Math.max(1, worldHeight); - int heightScaledCap = (int) Math.round((double) maxResidentTectonicPlates * REFERENCE_WORLD_HEIGHT / normalizedHeight); - double plateMegabytes = (double) REFERENCE_PLATE_MEGABYTES * normalizedHeight / REFERENCE_WORLD_HEIGHT; - int heapBudgetCap = (int) Math.floor(MANTLE_HEAP_FRACTION * getHardware.getProcessMemory() / plateMegabytes); - int effective = Math.min(heightScaledCap, heapBudgetCap); - return Math.max(MIN_RESIDENT_TECTONIC_PLATES, Math.min(maxResidentTectonicPlates, effective)); + // Mantle chunks only hold the sections something was written to, so shorter worlds keep the measured size. + double heightScale = Math.max(1D, (double) worldHeight / MEASURED_WORLD_HEIGHT); + double plateMegabytes = MEASURED_PLATE_MEGABYTES * heightScale; + int heapBudget = (int) Math.floor(MANTLE_HEAP_FRACTION * getHardware.getProcessMemory() / plateMegabytes); + return Math.max(MIN_RESIDENT_TECTONIC_PLATES, Math.min(maxResidentTectonicPlates, heapBudget)); } public int getMantleBackpressureWaitMs() { diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/MantleHeapPressure.java b/core/src/main/java/com/volmit/iris/core/pregenerator/MantleHeapPressure.java index 629e7d9d2..1b1b616ff 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/MantleHeapPressure.java +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/MantleHeapPressure.java @@ -5,7 +5,10 @@ import com.volmit.iris.Iris; import javax.management.MBeanServer; import javax.management.ObjectName; import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryType; import java.lang.management.MemoryUsage; +import java.util.List; import java.util.function.BiConsumer; import java.util.function.DoubleConsumer; import java.util.function.LongSupplier; @@ -15,6 +18,9 @@ public final class MantleHeapPressure { private static final double LOW_WATER = 0.82D; private static final long MAXIMUM_HYSTERESIS_MS = 60_000L; private static final ObjectName DIAGNOSTIC_COMMAND = diagnosticCommandName(); + private static final List HEAP_POOLS = ManagementFactory.getMemoryPoolMXBeans().stream() + .filter(pool -> pool.getType() == MemoryType.HEAP) + .toList(); private static final PanicGcReclaimer PANIC_RECLAIMER = new PanicGcReclaimer( new PanicGcPolicy(10_000L, 60_000L, 60_000L, 15 * 60_000L), new PanicGcActions( @@ -37,19 +43,19 @@ public final class MantleHeapPressure { private MantleHeapPressure() { } + // Usage between collections includes garbage, which G1 lets climb close to the maximum on a healthy heap. public static double usedFraction() { - MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); - long max = heap.getMax(); - if (max > 0L) { - return (double) heap.getUsed() / (double) max; - } - - Runtime runtime = Runtime.getRuntime(); - long runtimeMax = runtime.maxMemory(); - if (runtimeMax <= 0L) { + long max = Runtime.getRuntime().maxMemory(); + if (max <= 0L || max == Long.MAX_VALUE) { return 0.0D; } - return (double) (runtime.totalMemory() - runtime.freeMemory()) / (double) runtimeMax; + + long usedAfterGc = 0L; + for (MemoryPoolMXBean pool : HEAP_POOLS) { + MemoryUsage afterGc = pool.getCollectionUsage(); + usedAfterGc += afterGc == null ? pool.getUsage().getUsed() : afterGc.getUsed(); + } + return (double) usedAfterGc / (double) max; } public static boolean overHighWater() { diff --git a/core/src/main/java/com/volmit/iris/core/pregenerator/PregenMantleBackpressure.java b/core/src/main/java/com/volmit/iris/core/pregenerator/PregenMantleBackpressure.java index 2d000496e..fa0854c6b 100644 --- a/core/src/main/java/com/volmit/iris/core/pregenerator/PregenMantleBackpressure.java +++ b/core/src/main/java/com/volmit/iris/core/pregenerator/PregenMantleBackpressure.java @@ -36,27 +36,26 @@ public final class PregenMantleBackpressure { } private void enforcePlateBudget(Mantle mantle, int plateBudget) { - int hardCap = plateBudget * 2; long waitStart = M.ms(); long lastLog = waitStart; - while (mantle.getLoadedRegionCount() > hardCap && !isCancelled()) { - int freed = evictIdlePlates(mantle); + while (mantle.getLoadedRegionCount() > plateBudget && !isCancelled()) { + int freed = evictOldestPlates(mantle, plateBudget); int resident = mantle.getLoadedRegionCount(); - if (resident <= hardCap) { + if (resident <= plateBudget) { return; } long elapsed = M.ms() - waitStart; if (elapsed >= timeoutMs) { - Iris.warn("Pregen mantle backpressure waited %dms with %d tectonic plates resident (hard cap %d); " + Iris.warn("Pregen mantle backpressure waited %dms with %d tectonic plates resident (budget %d); " + "continuing to avoid a deadlock. Raise pregen.maxResidentTectonicPlates if this keeps happening.", - timeoutMs, resident, hardCap); + timeoutMs, resident, plateBudget); return; } if (M.ms() - lastLog >= PROGRESS_LOG_INTERVAL_MS) { lastLog = M.ms(); - Iris.info("Pregen mantle backpressure: %d tectonic plates resident (hard cap %d), freed %d last pass, waited %dms.", - resident, hardCap, freed, elapsed); + Iris.info("Pregen mantle backpressure: %d tectonic plates resident (budget %d), freed %d last pass, waited %dms.", + resident, plateBudget, freed, elapsed); } if (!pause()) { return; @@ -68,20 +67,18 @@ public final class PregenMantleBackpressure { long waitStart = M.ms(); long lastLog = waitStart; while (MantleHeapPressure.overHighWater() && !isCancelled()) { - if (mantle.getLoadedRegionCount() > plateBudget) { - evictIdlePlates(mantle); - } + evictOldestPlates(mantle, plateBudget); MantleHeapPressure.requestPanicReclaim(); long elapsed = M.ms() - waitStart; if (elapsed >= timeoutMs) { - Iris.warn("Pregen heap pressure wait exceeded %dms at %d%% heap; continuing to avoid a deadlock. " + Iris.warn("Pregen heap pressure wait exceeded %dms at %d%% heap after GC; continuing to avoid a deadlock. " + "Give the server more memory if this keeps happening.", timeoutMs, heapPercent()); return; } if (M.ms() - lastLog >= PROGRESS_LOG_INTERVAL_MS) { lastLog = M.ms(); - Iris.info("Pregen heap pressure: paused at %d%% heap with %d tectonic plates resident, waiting for headroom.", + Iris.info("Pregen heap pressure: paused at %d%% heap after GC with %d tectonic plates resident, waiting for headroom.", heapPercent(), mantle.getLoadedRegionCount()); } if (!pause()) { @@ -90,10 +87,9 @@ public final class PregenMantleBackpressure { } } - private int evictIdlePlates(Mantle mantle) { + private int evictOldestPlates(Mantle mantle, int plateBudget) { try { - mantle.trim(0L, 0); - return mantle.unloadTectonicPlate(0); + return mantle.unloadLeastRecentlyUsed(plateBudget); } catch (Throwable e) { Iris.reportError(e); return 0; diff --git a/core/src/main/java/com/volmit/iris/util/mantle/Mantle.java b/core/src/main/java/com/volmit/iris/util/mantle/Mantle.java index 068e131aa..2822138d1 100644 --- a/core/src/main/java/com/volmit/iris/util/mantle/Mantle.java +++ b/core/src/main/java/com/volmit/iris/util/mantle/Mantle.java @@ -49,6 +49,7 @@ import lombok.Getter; import org.bukkit.Chunk; import java.io.*; +import java.util.Map; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -63,6 +64,7 @@ import java.util.function.Supplier; public class Mantle { private static final int LOCK_SIZE = Short.MAX_VALUE; + private static final long MINIMUM_IDLE_DURATION_MS = 500L; private final File dataFolder; @Getter @@ -487,7 +489,7 @@ public class Mantle { } double idleDuration = baseIdleDuration; - double minimumIdleDuration = Math.max(500D, Math.min(baseIdleDuration, 4000D)); + double minimumIdleDuration = Math.max(MINIMUM_IDLE_DURATION_MS, Math.min(baseIdleDuration, 4000D)); if (loadedRegions.size() > tectonicLimit) { // todo update this correctly and maybe do something when its above a 100% idleDuration = Math.max(idleDuration - (1000 * (((loadedRegions.size() - tectonicLimit) / (double) tectonicLimit) * 100) * 0.4), minimumIdleDuration); @@ -516,6 +518,35 @@ public class Mantle { } } + /** + * Unlike {@link #trim}, keeps the plates around the chunks being generated, so they are not saved only to be + * loaded again. + */ + public synchronized int unloadLeastRecentlyUsed(int tectonicLimit) { + if (closed.get()) { + throw new RuntimeException("The Mantle is closed"); + } + + int excess = loadedRegions.size() - tectonicLimit; + if (excess <= 0) { + return 0; + } + + ioTrim.acquireUninterruptibly(LOCK_SIZE); + try { + long idleSince = M.ms() - MINIMUM_IDLE_DURATION_MS; + lastUse.entrySet().stream() + .filter(entry -> entry.getValue() < idleSince) + .sorted(Map.Entry.comparingByValue()) + .limit(excess) + .forEach(entry -> toUnload.add(entry.getKey())); + adjustedIdleDuration.set(MINIMUM_IDLE_DURATION_MS); + } finally { + ioTrim.release(LOCK_SIZE); + } + return unloadTectonicPlate(tectonicLimit); + } + public synchronized int unloadTectonicPlate(int tectonicLimit) { if (closed.get()) { throw new RuntimeException("The Mantle is closed");