From 0052dfec39102a6d19735dd648d168ea33fbdfd9 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 13:34:33 -0400 Subject: [PATCH 1/3] Trimmed mantle section writes - The section adapter writes without re-trimming; the chunk writer trims right before --- .../art/arcane/iris/generation/runtime/IrisEngineMantle.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngineMantle.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngineMantle.java index 82e8c324c..d16c9eae1 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngineMantle.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngineMantle.java @@ -335,7 +335,7 @@ public class IrisEngineMantle implements EngineMantle { @Override public void writeSection(Matter section, java.io.DataOutputStream dos) throws IOException { - section.writeDos(dos); + section.writeTrimmedDos(dos); } @Override From 2930ca40fb41fb8eeaa020b0cb35c50800506310 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 13:34:33 -0400 Subject: [PATCH 2/3] LRU plate eviction in pregen - Over the plate cap, the least recently used idle plates are saved one at a time on the IO pool while submissions continue - Submissions wait only past the cap plus a small overshoot - Replaces trimming and unloading every idle plate, which evicted the active working set and read it back --- .../pregen/PregenMantleBackpressure.java | 84 +++++++++++++++---- .../pregen/PregenMantleBackpressureTest.java | 68 +++++++++++---- 2 files changed, 122 insertions(+), 30 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/world/pregen/PregenMantleBackpressure.java b/core/src/main/java/art/arcane/iris/world/pregen/PregenMantleBackpressure.java index 146294f66..293987213 100644 --- a/core/src/main/java/art/arcane/iris/world/pregen/PregenMantleBackpressure.java +++ b/core/src/main/java/art/arcane/iris/world/pregen/PregenMantleBackpressure.java @@ -18,16 +18,24 @@ package art.arcane.iris.world.pregen; +import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.math.M; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BooleanSupplier; import java.util.function.Supplier; +/** + * Keeps the resident tectonic plate count near its cap during pregeneration. Over the cap, the least recently + * used idle plates are saved and unloaded one at a time on the eviction executor while submissions continue; + * a submission only waits once residency passes the cap by the eviction overshoot. + */ public final class PregenMantleBackpressure { private static final long LOG_INTERVAL_MS = 5_000L; @@ -38,6 +46,9 @@ public final class PregenMantleBackpressure { private final Runnable onBudgetTimeout; private final Supplier diagnostics; private final BooleanSupplier cancelled; + private final Executor evictionExecutor; + private final AtomicBoolean evicting = new AtomicBoolean(); + private final AtomicBoolean evictionFailed = new AtomicBoolean(); private final ReentrantLock lock = new ReentrantLock(); private final Condition progressed = lock.newCondition(); @@ -46,6 +57,12 @@ public final class PregenMantleBackpressure { } public PregenMantleBackpressure(Supplier mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier diagnostics, BooleanSupplier cancelled) { + this(mantleSupplier, maxResidentTectonicPlates, waitMs, timeoutMs, onBudgetTimeout, diagnostics, cancelled, + command -> MultiBurst.ioBurst.execute(command)); + } + + public PregenMantleBackpressure(Supplier mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier diagnostics, BooleanSupplier cancelled, Executor evictionExecutor) { + this.evictionExecutor = evictionExecutor; this.mantleSupplier = mantleSupplier; this.maxResidentTectonicPlates = maxResidentTectonicPlates; this.waitMs = Math.max(1, waitMs); @@ -75,33 +92,38 @@ public final class PregenMantleBackpressure { return; } + evictionFailed.set(false); + requestEviction(mantle, cap); + int ceiling = cap + evictionOvershoot(cap); + if (mantle.getLoadedRegionCount() <= ceiling || evictionFailed.getAndSet(false)) { + return; + } + long waitStart = M.ms(); long lastLog = 0L; - while (mantle.getLoadedRegionCount() > cap) { + int resident = mantle.getLoadedRegionCount(); + while (resident > ceiling) { if (isCancelled()) { return; } - int freed; - int resident; + int before = resident; + if (!awaitProgress() || evictionFailed.getAndSet(false)) { + return; + } try { - mantle.trim(0L); - freed = mantle.unloadTectonicPlate(0); + requestEviction(mantle, cap); resident = mantle.getLoadedRegionCount(); - if (resident > cap && mantle.saveOldestIdleTectonicPlate()) { - freed++; - resident = mantle.getLoadedRegionCount(); - } } catch (Throwable e) { IrisLogging.reportError(e); - break; + return; } - if (resident <= cap) { - break; + if (resident <= ceiling) { + return; } long elapsed = M.ms() - waitStart; - if (elapsed >= timeoutMs && freed == 0) { + if (elapsed >= timeoutMs && resident >= before) { IrisLogging.warn("Pregen mantle backpressure exceeded " + timeoutMs + "ms with " + resident + " tectonic plates resident (residency target " + cap + "); no idle plate could be evicted. " + "Allowing the active dependency working set to exceed the residency target; " @@ -116,12 +138,42 @@ public final class PregenMantleBackpressure { // Pausing to stay under the plate cap is the design working, and it is reported every five // seconds for the whole duration of a large pregen. Only exceeding the budget is a warning. IrisLogging.info("Pregen mantle backpressure: " + resident + " tectonic plates resident (residency target " + cap - + "), freed " + freed + " last pass, waited " + elapsed + "ms."); + + "), waited " + elapsed + "ms."); } + } + } - if (!awaitProgress()) { - return; + static int evictionOvershoot(int cap) { + return Math.max(2, cap / 8); + } + + private void requestEviction(Mantle mantle, int cap) { + if (!evicting.compareAndSet(false, true)) { + return; + } + + try { + evictionExecutor.execute(() -> evictToCap(mantle, cap)); + } catch (Throwable e) { + evicting.set(false); + IrisLogging.reportError(e); + } + } + + private void evictToCap(Mantle mantle, int cap) { + try { + while (!mantle.isClosed() && !isCancelled() && mantle.getLoadedRegionCount() > cap + && mantle.saveOldestIdleTectonicPlate()) { + signalProgress(); } + } catch (Throwable e) { + if (!mantle.isClosed()) { + evictionFailed.set(true); + IrisLogging.reportError(e); + } + } finally { + evicting.set(false); + signalProgress(); } } diff --git a/core/src/test/java/art/arcane/iris/world/pregen/PregenMantleBackpressureTest.java b/core/src/test/java/art/arcane/iris/world/pregen/PregenMantleBackpressureTest.java index b013e55f8..2f6733e4f 100644 --- a/core/src/test/java/art/arcane/iris/world/pregen/PregenMantleBackpressureTest.java +++ b/core/src/test/java/art/arcane/iris/world/pregen/PregenMantleBackpressureTest.java @@ -16,6 +16,7 @@ import java.util.function.Supplier; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -63,16 +64,56 @@ public class PregenMantleBackpressureTest { } @Test - public void residencyOverTheConfiguredPlateCapIsTrimmedUntilItFits() { + public void residencyOverTheConfiguredPlateCapEvictsLeastRecentlyUsedPlatesUntilItFits() { Mantle mantle = mock(Mantle.class); - when(mantle.getLoadedRegionCount()).thenReturn(5, 5, 4); + AtomicInteger resident = new AtomicInteger(7); + when(mantle.getLoadedRegionCount()).thenAnswer(invocation -> resident.get()); + when(mantle.saveOldestIdleTectonicPlate()).thenAnswer(invocation -> { + resident.decrementAndGet(); + return true; + }); backpressure(() -> mantle, 4, 1_000L, () -> { }).enforceMantleBudget(); - verify(mantle, times(1)).trim(0L); - verify(mantle, times(1)).unloadTectonicPlate(0); + assertEquals(4, resident.get()); + verify(mantle, times(3)).saveOldestIdleTectonicPlate(); + verify(mantle, never()).trim(0L); + verify(mantle, never()).unloadTectonicPlate(0); + } + + @Test + public void residencyWithinTheEvictionOvershootDoesNotWaitForTheEvictor() { + Mantle mantle = mock(Mantle.class); + when(mantle.getLoadedRegionCount()).thenReturn(5); + List queued = new ArrayList<>(); + AtomicInteger timeouts = new AtomicInteger(); + + new PregenMantleBackpressure(() -> mantle, 4, 600_000, 0L, timeouts::incrementAndGet, () -> "test", () -> false, + queued::add).enforceMantleBudget(); + + assertEquals(1, queued.size()); + assertEquals(0, timeouts.get()); verify(mantle, never()).saveOldestIdleTectonicPlate(); + queued.getFirst().run(); + verify(mantle).saveOldestIdleTectonicPlate(); + } + + @Test + public void aSecondRequestWhileTheEvictorRunsDoesNotQueueAnotherEvictor() { + Mantle mantle = mock(Mantle.class); + when(mantle.getLoadedRegionCount()).thenReturn(5); + List queued = new ArrayList<>(); + PregenMantleBackpressure backpressure = new PregenMantleBackpressure(() -> mantle, 4, 5, 1_000L, () -> { + }, () -> "test", () -> false, queued::add); + + backpressure.enforceMantleBudget(); + backpressure.enforceMantleBudget(); + + assertEquals(1, queued.size()); + queued.getFirst().run(); + backpressure.enforceMantleBudget(); + assertEquals(2, queued.size()); } @Test @@ -94,15 +135,15 @@ public class PregenMantleBackpressureTest { } @Test - public void aTrimFailureEndsTheWaitInsteadOfSpinning() { + public void anEvictionFailureEndsTheWaitInsteadOfSpinning() { Mantle mantle = mock(Mantle.class); when(mantle.getLoadedRegionCount()).thenReturn(20); - doThrow(new IllegalStateException("mantle is gone")).when(mantle).trim(0L); + doThrow(new IllegalStateException("mantle is gone")).when(mantle).saveOldestIdleTectonicPlate(); AtomicInteger timeouts = new AtomicInteger(); backpressure(() -> mantle, 4, 1_000L, timeouts::incrementAndGet).enforceMantleBudget(); - verify(mantle, never()).unloadTectonicPlate(0); + verify(mantle, times(1)).saveOldestIdleTectonicPlate(); assertEquals(0, timeouts.get()); } @@ -115,7 +156,7 @@ public class PregenMantleBackpressureTest { backpressure(() -> mantle, 4, 0L, timeouts::incrementAndGet).enforceMantleBudget(); assertEquals(1, timeouts.get()); - verify(mantle).saveOldestIdleTectonicPlate(); + verify(mantle, atLeastOnce()).saveOldestIdleTectonicPlate(); } @Test @@ -124,12 +165,11 @@ public class PregenMantleBackpressureTest { when(mantle.getLoadedRegionCount()).thenReturn(20); AtomicInteger timeouts = new AtomicInteger(); PregenMantleBackpressure backpressure = new PregenMantleBackpressure( - () -> mantle, 4, 5, 0L, timeouts::incrementAndGet, () -> "cancelled", () -> true); + () -> mantle, 4, 5, 0L, timeouts::incrementAndGet, () -> "cancelled", () -> true, Runnable::run); backpressure.enforceMantleBudget(); - verify(mantle, never()).trim(0L); - verify(mantle, never()).unloadTectonicPlate(0); + verify(mantle, never()).saveOldestIdleTectonicPlate(); assertEquals(0, timeouts.get()); } @@ -141,7 +181,7 @@ public class PregenMantleBackpressureTest { PregenMantleBackpressure backpressure = new PregenMantleBackpressure( () -> mantle, 4, 5, 0L, timeouts::incrementAndGet, () -> "probing", () -> { throw new IllegalStateException("cancellation probe exploded"); - }); + }, Runnable::run); backpressure.enforceMantleBudget(); @@ -154,7 +194,7 @@ public class PregenMantleBackpressureTest { when(mantle.getLoadedRegionCount()).thenReturn(20, 20, 20, 3); PregenMantleBackpressure backpressure = new PregenMantleBackpressure( () -> mantle, 4, 600_000, 600_000L, () -> { - }, () -> "waiting"); + }, () -> "waiting", () -> false, Runnable::run); Thread waiter = new Thread(backpressure::enforceMantleBudget, "pregen-backpressure-waiter"); waiter.start(); @@ -320,6 +360,6 @@ public class PregenMantleBackpressureTest { Runnable onBudgetTimeout ) { return new PregenMantleBackpressure( - mantleSupplier, maxResidentTectonicPlates, 5, timeoutMs, onBudgetTimeout, () -> "test"); + mantleSupplier, maxResidentTectonicPlates, 5, timeoutMs, onBudgetTimeout, () -> "test", () -> false, Runnable::run); } } From 53b36fe197e7c798bb31e04615237a25d3e1b71a Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 13:51:05 -0400 Subject: [PATCH 3/3] Resident plate lookups - Loaded-plate probes use Mantle.getLoadedRegion instead of the region map --- .../iris/generation/hydrology/cave/HydrologyCaveStorage.java | 2 +- .../art/arcane/iris/generation/stage/IrisCarveModifier.java | 2 +- .../iris/world/history/GenerationHistoryRuntimeRouter.java | 2 +- .../iris/generation/stage/IrisCarveBoundaryFacesTest.java | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/cave/HydrologyCaveStorage.java b/core/src/main/java/art/arcane/iris/generation/hydrology/cave/HydrologyCaveStorage.java index 736df5051..5d9e6fc3a 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/cave/HydrologyCaveStorage.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/cave/HydrologyCaveStorage.java @@ -15,7 +15,7 @@ public final class HydrologyCaveStorage { } int chunkX = x >> 4; int chunkZ = z >> 4; - TectonicPlate plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5)); + TectonicPlate plate = mantle.getLoadedRegion(chunkX >> 5, chunkZ >> 5); if (plate == null || plate.isClosed()) { return null; } diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java index 64bf27b3e..7036f22f0 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java @@ -579,7 +579,7 @@ public class IrisCarveModifier extends EngineAssignedModifier } private MantleChunk existingMantleChunk(Mantle mantle, int chunkX, int chunkZ) { - TectonicPlate plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5)); + TectonicPlate plate = mantle.getLoadedRegion(chunkX >> 5, chunkZ >> 5); if (plate == null || plate.isClosed()) { return null; } diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationHistoryRuntimeRouter.java b/core/src/main/java/art/arcane/iris/world/history/GenerationHistoryRuntimeRouter.java index 919d219c7..9ef3809b1 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationHistoryRuntimeRouter.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationHistoryRuntimeRouter.java @@ -557,7 +557,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { Mantle mantle = access.mantle(); MantleChunk anchor = mantle.getChunk(regionX << 5, regionZ << 5).use(); try { - TectonicPlate plate = mantle.getLoadedRegions().get(Mantle.key(regionX, regionZ)); + TectonicPlate plate = mantle.getLoadedRegion(regionX, regionZ); HashMap bundles = new HashMap<>(); for (int x = 0; x < 32; x++) { for (int z = 0; z < 32; z++) { diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveBoundaryFacesTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveBoundaryFacesTest.java index 85c9eca15..80f4c5aa4 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveBoundaryFacesTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveBoundaryFacesTest.java @@ -21,7 +21,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.doReturn; @@ -72,7 +71,7 @@ public class IrisCarveBoundaryFacesTest { matter[0][0].slice(PreObjectMatterCell.class).set(0, 5, 0, PreObjectMatterCell.cavern(null)); Mantle mantle = mock(Mantle.class, RETURNS_DEEP_STUBS); TectonicPlate plate = mock(TectonicPlate.class); - when(mantle.getLoadedRegions().get(anyLong())).thenReturn(plate); + when(mantle.getLoadedRegion(anyInt(), anyInt())).thenReturn(plate); when(plate.get(31, 0)).thenReturn(chunks[1]); when(plate.get(1, 0)).thenReturn(chunks[2]); when(plate.get(0, 31)).thenReturn(chunks[3]);