Pregeneration throughput

A pregeneration now generates in multicore mode: the matter stage runs
on the calling thread so its mantle window fans out across the burst
pool while biome and terrain run alongside, components are claimed
through the in-flight map so overlapping windows join instead of
blocking, and each region's chunks are ordered in stride-4 lattices so
consecutive chunks never share a window. Hydrology tiles for the whole
area are planned ahead nearest the centre first on a dedicated planning
pool, and the ring around every needed tile stays planned as the front
moves. Height bounds at grid corners are shared between threads and
from planning into generation, the sole engine of a pack is answered
from a snapshot instead of the registry lock, composed chunk keys are
mixed, cached tiles are answered in place, the burst pool grows to two
workers per core for the run, and large chunk-worker pools admit up to
256 requests.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-02 13:56:58 -04:00
1 parent 88bd8feeeb
commit df34f328e7
23 files changed
+918 -32

No files matched your search

@@ -89,4 +89,21 @@ public class IrisDataEngineRegistryTest {
public void dereference() {
}
}
@Test
public void soleEngineResolvesWithoutAContextAndDropsOnceClosed() throws Exception {
data = IrisData.openRuntime(temporaryFolder.newFolder("pack"));
AtomicBoolean closed = new AtomicBoolean(false);
Engine engine = engine(closed);
data.registerEngine(engine);
assertSame(engine, data.getEngine());
assertSame(engine, data.getEngine());
closed.set(true);
assertNull(data.getEngine());
assertEquals(0, data.getEngines().size());
}
}
@@ -8,6 +8,8 @@ import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import art.arcane.iris.util.common.parallel.MultiBurst;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@@ -112,4 +114,19 @@ public class PregenPerformanceProfileTest {
System.setProperty("iris.cache.fast", fastCache);
}
}
@Test
public void pregenerationWantsTwoBurstWorkersPerCore() {
assertEquals(32, PregenPerformanceProfile.pregenBurstParallelism(16));
assertEquals(4, PregenPerformanceProfile.pregenBurstParallelism(1));
}
@Test
public void raisingBurstParallelismOnlyGrowsThePool() {
int before = MultiBurst.burst.parallelism();
assertFalse(MultiBurst.burst.raiseParallelism(before));
assertTrue(MultiBurst.burst.raiseParallelism(before + 1));
assertEquals(before + 1, MultiBurst.burst.parallelism());
assertFalse(MultiBurst.burst.raiseParallelism(before));
}
}
@@ -0,0 +1,50 @@
package art.arcane.iris.core.pregenerator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.Position2;
import org.junit.Test;
import java.util.HashSet;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class PregenTaskLatticeOrderTest {
@Test
public void regionOrderVisitsEveryChunkOnceInStrideSpacedLattices() {
KList<Position2> order = new KList<>();
PregenTask.iterateRegion(0, 0, (x, z) -> order.add(new Position2(x, z)));
assertEquals(1024, order.size());
Set<Long> unique = new HashSet<>();
for (Position2 position : order) {
assertTrue(unique.add(((long) position.getX() << 32) ^ (position.getZ() & 0xffffffffL)));
}
int stride = PregenTask.CHUNK_LATTICE_STRIDE;
int perLattice = 1024 / (stride * stride);
for (int lattice = 0; lattice < stride * stride; lattice++) {
Position2 first = order.get(lattice * perLattice);
for (int index = lattice * perLattice; index < (lattice + 1) * perLattice; index++) {
Position2 position = order.get(index);
assertEquals(Math.floorMod(first.getX(), stride), Math.floorMod(position.getX(), stride));
assertEquals(Math.floorMod(first.getZ(), stride), Math.floorMod(position.getZ(), stride));
}
}
}
@Test
public void chunksGeneratedBackToBackNeverShareAMantleWindow() {
KList<Position2> order = new KList<>();
PregenTask.iterateRegion(3, -2, (x, z) -> order.add(new Position2(x, z)));
int perLattice = 1024 / (PregenTask.CHUNK_LATTICE_STRIDE * PregenTask.CHUNK_LATTICE_STRIDE);
for (int index = 1; index < perLattice; index++) {
Position2 previous = order.get(index - 1);
Position2 current = order.get(index);
int distance = Math.max(Math.abs(previous.getX() - current.getX()), Math.abs(previous.getZ() - current.getZ()));
assertTrue("consecutive chunks " + previous + " and " + current, distance >= PregenTask.CHUNK_LATTICE_STRIDE);
}
}
}
@@ -21,8 +21,8 @@ public class AsyncPregenMethodConcurrencyCapTest {
assertEquals(16, AsyncPregenMethod.computePaperLikeRecommendedCap(1));
assertEquals(32, AsyncPregenMethod.computePaperLikeRecommendedCap(4));
assertEquals(96, AsyncPregenMethod.computePaperLikeRecommendedCap(12));
assertEquals(128, AsyncPregenMethod.computePaperLikeRecommendedCap(80));
assertEquals(128, AsyncPregenMethod.computePaperLikeRecommendedCap(128));
assertEquals(256, AsyncPregenMethod.computePaperLikeRecommendedCap(80));
assertEquals(256, AsyncPregenMethod.computePaperLikeRecommendedCap(128));
}
@Test
@@ -0,0 +1,89 @@
package art.arcane.iris.engine;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class SharedCornerBoundsTest {
@Test
public void storedCornersComeBackAndUnknownCornersMiss() {
SharedCornerBounds cache = new SharedCornerBounds(1 << 10);
long key = SharedCornerBounds.key(12, -40, 1);
assertFalse(cache.contains(key));
cache.put(key, 0x1234_5678_9ABC_DEF0L);
assertTrue(cache.contains(key));
assertEquals(0x1234_5678_9ABC_DEF0L, cache.get(key));
assertFalse(cache.contains(SharedCornerBounds.key(12, -40, 0)));
}
@Test
public void keysSeparateCoordinatesAndInterpolators() {
assertNotEquals(SharedCornerBounds.key(1, 2, 0), SharedCornerBounds.key(2, 1, 0));
assertNotEquals(SharedCornerBounds.key(1, 2, 0), SharedCornerBounds.key(1, 2, 1));
assertNotEquals(SharedCornerBounds.key(-1, 2, 0), SharedCornerBounds.key(1, 2, 0));
assertNotEquals(0L, SharedCornerBounds.key(0, 0, 0));
}
@Test
public void aCollidingCornerReplacesTheOldOne() {
SharedCornerBounds cache = new SharedCornerBounds(2);
long first = SharedCornerBounds.key(0, 0, 0);
long colliding = first;
for (int gx = 1; gx < 100_000; gx++) {
long candidate = SharedCornerBounds.key(gx, 0, 0);
if (cache.slot(candidate) == cache.slot(first)) {
colliding = candidate;
break;
}
}
assertNotEquals(first, colliding);
cache.put(first, 1L);
cache.put(colliding, 2L);
assertFalse(cache.contains(first));
assertEquals(2L, cache.get(colliding));
}
@Test
public void concurrentReadersNeverSeeAnotherCornersValue() throws InterruptedException {
SharedCornerBounds cache = new SharedCornerBounds(1 << 8);
AtomicBoolean torn = new AtomicBoolean();
List<Thread> threads = new ArrayList<>();
for (int worker = 0; worker < 8; worker++) {
int seed = worker;
threads.add(new Thread(() -> {
long state = 0x9E3779B97F4A7C15L * (seed + 1);
for (int step = 0; step < 400_000; step++) {
state ^= state << 13;
state ^= state >>> 7;
state ^= state << 17;
int gx = (int) (state & 1023);
int gz = (int) ((state >>> 10) & 1023);
long key = SharedCornerBounds.key(gx, gz, (int) (state & 1));
if ((state & 2) == 0) {
cache.put(key, key * 31L);
} else if (cache.contains(key) && cache.get(key) != key * 31L) {
torn.set(true);
}
}
}));
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
assertFalse(torn.get());
}
}
@@ -0,0 +1,120 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class EngineModeBurstAroundTest {
private static IrisSettings previousSettings;
@BeforeClass
public static void bindPlatform() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class);
IrisPlatform platform = mock(IrisPlatform.class);
when(registries.block(anyString())).thenReturn(mock(PlatformBlockState.class));
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@AfterClass
public static void unbindPlatform() {
IrisPlatforms.unbind();
IrisSettings.settings = previousSettings;
}
private static EngineMode mode() {
Engine engine = mock(Engine.class);
when(engine.burst()).thenReturn(MultiBurst.burst);
EngineMode mode = mock(EngineMode.class, CALLS_REAL_METHODS);
when(mode.getEngine()).thenReturn(engine);
return mode;
}
@Test
public void multicoreRunsTheInlineStageHereAndTheOthersOnThePoolBeforeReturning() {
EngineMode mode = mode();
ChunkContext context = mock(ChunkContext.class);
Thread caller = Thread.currentThread();
Set<Thread> parallelThreads = ConcurrentHashMap.newKeySet();
List<String> completed = new CopyOnWriteArrayList<>();
EngineStage inline = (x, z, blocks, biomes, multicore, ctx) -> {
assertTrue(multicore);
assertEquals(caller, Thread.currentThread());
completed.add("inline");
};
EngineStage first = (x, z, blocks, biomes, multicore, ctx) -> {
parallelThreads.add(Thread.currentThread());
completed.add("first");
};
EngineStage second = (x, z, blocks, biomes, multicore, ctx) -> {
parallelThreads.add(Thread.currentThread());
completed.add("second");
};
mode.burstAround(inline, first, second).generate(0, 0, null, null, true, context);
assertEquals(3, completed.size());
assertTrue(completed.containsAll(List.of("inline", "first", "second")));
assertFalse(parallelThreads.contains(caller));
}
@Test
public void withoutMulticoreEveryStageRunsOnTheCallerParallelOnesFirst() {
EngineMode mode = mode();
ChunkContext context = mock(ChunkContext.class);
Thread caller = Thread.currentThread();
List<String> order = new CopyOnWriteArrayList<>();
EngineStage inline = (x, z, blocks, biomes, multicore, ctx) -> {
assertEquals(caller, Thread.currentThread());
order.add("inline");
};
EngineStage first = (x, z, blocks, biomes, multicore, ctx) -> {
assertEquals(caller, Thread.currentThread());
order.add("first");
};
mode.burstAround(inline, first).generate(0, 0, null, null, false, context);
assertEquals(List.of("first", "inline"), order);
}
@Test
public void aParallelFailureSurfacesAfterTheInlineStageFinished() {
EngineMode mode = mode();
ChunkContext context = mock(ChunkContext.class);
List<String> completed = new CopyOnWriteArrayList<>();
EngineStage inline = (x, z, blocks, biomes, multicore, ctx) -> completed.add("inline");
EngineStage failing = (x, z, blocks, biomes, multicore, ctx) -> {
throw new IllegalStateException("boom");
};
IllegalStateException failure = org.junit.Assert.assertThrows(IllegalStateException.class,
() -> mode.burstAround(inline, failing).generate(0, 0, null, null, true, context));
assertEquals("boom", failure.getMessage());
assertEquals(List.of("inline"), completed);
}
}
@@ -0,0 +1,20 @@
package art.arcane.iris.engine.framework;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class EngineMulticoreDecisionTest {
@Test
public void liveWorldsGenerateInlineUnlessForced() {
assertFalse(Engine.generateMulticore(false, false));
assertTrue(Engine.generateMulticore(true, false));
}
@Test
public void pregenerationSpreadsStagesAndMantleWorkAcrossCores() {
assertTrue(Engine.generateMulticore(false, true));
assertTrue(Engine.generateMulticore(true, true));
}
}
@@ -372,4 +372,51 @@ public class HydrologyTileCacheTest {
assertTrue(tile.outlets().isEmpty());
assertTrue(tile.columnAt(0, 0).isEmpty());
}
@Test
public void chunkPreparationPrefetchesTheRingOfTilesAroundTheOnesItNeeded() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = emptySettings();
when(planner.settings()).thenReturn(settings);
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
HydrologyTileCache eager = new HydrologyTileCache(planner, 64, Runnable::run);
HydrologyTileCache lazy = new HydrologyTileCache(planner, 64);
eager.prepareChunkColumns(32, 48);
lazy.prepareChunkColumns(32, 48);
int tileSize = settings.routing().tileSize();
int radius = settings.publicationRadius();
int width = Math.floorDiv(32 + 15 + radius, tileSize) - Math.floorDiv(32 - radius, tileSize) + 1;
int height = Math.floorDiv(48 + 15 + radius, tileSize) - Math.floorDiv(48 - radius, tileSize) + 1;
assertEquals(width * height, lazy.size());
assertEquals((width + 2) * (height + 2), eager.size());
}
@Test
public void areaPrefetchPlansEveryTouchingTileNearestTheCentreFirst() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = emptySettings();
when(planner.settings()).thenReturn(settings);
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
List<HydrologyTileKey> order = new java.util.ArrayList<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 256, Runnable::run);
org.mockito.Mockito.doAnswer(invocation -> {
order.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
int tileSize = settings.routing().tileSize();
cache.prefetchArea(0, 0, tileSize * 2 - 1, tileSize * 2 - 1, tileSize, tileSize);
int radius = settings.publicationRadius();
int extra = radius > 0 ? 1 : 0;
int span = 2 + 2 * extra;
assertEquals(span * span, order.size());
assertEquals(new HydrologyTileKey(1, 1), order.getFirst());
assertTrue(order.subList(0, 4).containsAll(List.of(new HydrologyTileKey(0, 0), new HydrologyTileKey(1, 0), new HydrologyTileKey(0, 1), new HydrologyTileKey(1, 1))));
}
}
@@ -17,6 +17,7 @@ import art.arcane.volmlib.util.matter.Matter;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import art.arcane.iris.util.common.parallel.MultiBurst;
import java.util.List;
import java.util.Set;
@@ -210,8 +211,17 @@ public class MatterGeneratorConcurrencyTest {
private final Mantle<Matter> mantle;
private final ChunkContext context;
@SuppressWarnings("unchecked")
private GeneratorFixture() {
this(false);
}
/**
* @param lockingChunks true to give every chunk a real per-flag lock and flag state, so a
* generation that reaches a chunk another generation is working on
* blocks exactly like production
*/
@SuppressWarnings("unchecked")
private GeneratorFixture(boolean lockingChunks) {
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.isUseMantle()).thenReturn(true);
EngineMantle engineMantle = mock(EngineMantle.class);
@@ -224,7 +234,7 @@ public class MatterGeneratorConcurrencyTest {
int chunkX = invocation.getArgument(0);
int chunkZ = invocation.getArgument(1);
long key = (((long) chunkX) << 32) ^ (chunkZ & 0xffffffffL);
return chunks.computeIfAbsent(key, ignored -> chunk());
return chunks.computeIfAbsent(key, ignored -> lockingChunks ? lockingChunk() : chunk());
});
context = mock(ChunkContext.class);
when(context.getGenerationSessionId()).thenReturn(91L);
@@ -234,6 +244,30 @@ public class MatterGeneratorConcurrencyTest {
return new TestMatterGenerator(engine, mantle, passes);
}
@SuppressWarnings("unchecked")
private static MantleChunk<Matter> lockingChunk() {
MantleChunk<Matter> chunk = mock(MantleChunk.class);
java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock();
Set<Object> flagged = ConcurrentHashMap.newKeySet();
when(chunk.use()).thenReturn(chunk);
when(chunk.isFlagged(any())).thenAnswer(invocation -> flagged.contains(invocation.getArgument(0)));
doAnswer(invocation -> {
Object flag = invocation.getArgument(0);
Runnable task = invocation.getArgument(1);
lock.lock();
try {
if (!flagged.contains(flag)) {
task.run();
flagged.add(flag);
}
} finally {
lock.unlock();
}
return null;
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
return chunk;
}
@SuppressWarnings("unchecked")
private static MantleChunk<Matter> chunk() {
MantleChunk<Matter> chunk = mock(MantleChunk.class);
@@ -284,4 +318,51 @@ public class MatterGeneratorConcurrencyTest {
return passes;
}
}
@Test
public void dispatcherThreadFinishesItsOwnChunksBeforeJoiningAComponentAnotherGenerationOwns() throws Exception {
GeneratorFixture fixture = new GeneratorFixture(true);
CountDownLatch sharedStarted = new CountDownLatch(1);
CountDownLatch releaseShared = new CountDownLatch(1);
Set<Long> secondCompleted = ConcurrentHashMap.newKeySet();
AtomicReference<Thread> firstOwner = new AtomicReference<>();
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 16) {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
if (x == 1 && z == 0 && firstOwner.compareAndSet(null, Thread.currentThread())) {
sharedStarted.countDown();
await(releaseShared);
return;
}
if (firstOwner.get() != null && Thread.currentThread() != firstOwner.get()) {
secondCompleted.add((((long) x) << 32) ^ (z & 0xffffffffL));
}
}
};
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 1, 0)));
java.util.concurrent.CompletableFuture<Void> first = MultiBurst.burst.completeValueAsync(() -> {
generator.generateMatter(0, 0, true, fixture.context);
return null;
});
await(sharedStarted);
java.util.concurrent.CompletableFuture<Void> second = MultiBurst.burst.completeValueAsync(() -> {
generator.generateMatter(2, 0, true, fixture.context);
return null;
});
long deadline = System.currentTimeMillis() + 10_000L;
while (secondCompleted.size() < 7 && System.currentTimeMillis() < deadline) {
Thread.sleep(10L);
}
// The second generation's window is x 1..3, z -1..1. The first generation finished (1,-1)
// before parking on (1,0), so the second one owns the remaining seven chunks and must
// complete all of them while (1,0) is still held instead of blocking on it first.
assertEquals(7, secondCompleted.size());
assertFalse(second.isDone());
releaseShared.countDown();
first.get(10L, TimeUnit.SECONDS);
second.get(10L, TimeUnit.SECONDS);
}
}