mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 08:07:42 +00:00
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:
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());
|
||||
}
|
||||
}
|
||||
+17
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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))));
|
||||
}
|
||||
}
|
||||
+83
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user