Size pregen backpressure from measured plates and post-GC heap

The heap gate and the engine service's keep-alive scaling read heap usage
right after the last collection instead of the moment's usage, which G1
lets climb near the maximum on a healthy heap. The plate budget now comes
from a measured 38 MB per plate on a 1088 block world instead of an
estimate that assumed 136 MB there, and is enforced as is rather than at
twice its value. Pregen evicts only the least recently used plates down to
the budget instead of every idle one, so it no longer reloads the plates
it is still generating around.

Model used: Claude Opus 5.5
This commit is contained in:
piexel committed 2026-10-15 13:45:47 +02:00
1 parent 7f34bb917f
commit d6d6fd031c
4 files changed
+67 -35

No files matched your search

@@ -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() {
@@ -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<MemoryPoolMXBean> 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() {
@@ -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;
@@ -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");