mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Merged parallel waits
- Caller-run mantle components when the window is full, barrier help, generation scope propagated to component tasks
This commit is contained in:
commit
23637beb0c
3 files changed
+346
-48
No files matched your search
@@ -3,6 +3,7 @@ package art.arcane.iris.generation.mantle;
|
|||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
|
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
import art.arcane.iris.generation.context.ChunkContext;
|
import art.arcane.iris.generation.context.ChunkContext;
|
||||||
@@ -106,6 +107,7 @@ public interface MatterGenerator {
|
|||||||
// write into released chunks.
|
// write into released chunks.
|
||||||
List<MatterComponentTask> outstandingTasks = null;
|
List<MatterComponentTask> outstandingTasks = null;
|
||||||
List<DeferredComponent> deferredComponents = null;
|
List<DeferredComponent> deferredComponents = null;
|
||||||
|
IrisEngine.GenerationRuntimeBinding generationRuntime = scopedGenerationRuntime();
|
||||||
SubmissionWindow submissions = multicore && !DISPATCHER.ownsCurrentThread()
|
SubmissionWindow submissions = multicore && !DISPATCHER.ownsCurrentThread()
|
||||||
? new SubmissionWindow()
|
? new SubmissionWindow()
|
||||||
: null;
|
: null;
|
||||||
@@ -231,7 +233,8 @@ public interface MatterGenerator {
|
|||||||
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
|
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
|
||||||
MantleComponent component = eligibleComponents[componentIndex];
|
MantleComponent component = eligibleComponents[componentIndex];
|
||||||
outstandingTasks.add(runComponentAsync(
|
outstandingTasks.add(runComponentAsync(
|
||||||
chunk, component, writer, finalPassX, finalPassZ, context, submissions));
|
chunk, component, writer, finalPassX, finalPassZ, context, submissions,
|
||||||
|
generationRuntime));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
|
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
|
||||||
@@ -248,6 +251,7 @@ public interface MatterGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (asyncComponents) {
|
if (asyncComponents) {
|
||||||
|
runQueuedComponentTasks(outstandingTasks, generationRuntime);
|
||||||
awaitComponentTasks(outstandingTasks);
|
awaitComponentTasks(outstandingTasks);
|
||||||
} else if (deferredComponents != null) {
|
} else if (deferredComponents != null) {
|
||||||
runDeferredComponents(deferredComponents, writer, context);
|
runDeferredComponents(deferredComponents, writer, context);
|
||||||
@@ -382,6 +386,19 @@ public interface MatterGenerator {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IrisEngine.GenerationRuntimeBinding scopedGenerationRuntime() {
|
||||||
|
return getEngine() instanceof IrisEngine irisEngine && irisEngine.hasGenerationRuntimeScope()
|
||||||
|
? irisEngine.captureGenerationRuntimeBinding()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisEngine.GenerationRuntimeScope openGenerationRuntime(IrisEngine.GenerationRuntimeBinding binding) {
|
||||||
|
if (binding == null || !(getEngine() instanceof IrisEngine irisEngine)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return irisEngine.openGenerationRuntimeScope(binding);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean shouldGenerateComponent(MantleComponent component) {
|
private boolean shouldGenerateComponent(MantleComponent component) {
|
||||||
if (!component.isEnabled()) {
|
if (!component.isEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -398,7 +415,8 @@ public interface MatterGenerator {
|
|||||||
int chunkX,
|
int chunkX,
|
||||||
int chunkZ,
|
int chunkZ,
|
||||||
ChunkContext context,
|
ChunkContext context,
|
||||||
SubmissionWindow submissions
|
SubmissionWindow submissions,
|
||||||
|
IrisEngine.GenerationRuntimeBinding generationRuntime
|
||||||
) {
|
) {
|
||||||
MantleFlag flag = component.getFlag();
|
MantleFlag flag = component.getFlag();
|
||||||
MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag);
|
MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag);
|
||||||
@@ -428,24 +446,25 @@ public interface MatterGenerator {
|
|||||||
if (running != null) {
|
if (running != null) {
|
||||||
return running;
|
return running;
|
||||||
}
|
}
|
||||||
submissions.acquire();
|
boolean submit = submissions.reserve();
|
||||||
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
|
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
|
if (submit) {
|
||||||
submissions.release();
|
submissions.release();
|
||||||
|
}
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
if (!submit) {
|
||||||
|
completeComponentTask(task, chunk, component, writer, chunkX, chunkZ, context, null);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
IrisContext callerContext = IrisContext.get();
|
IrisContext callerContext = IrisContext.get();
|
||||||
submissions.track(task);
|
submissions.track(task);
|
||||||
task.setSubmission(DISPATCHER.submit(() -> completeComponentTask(
|
Runnable work = () -> runComponentTask(
|
||||||
task,
|
task, chunk, component, writer, chunkX, chunkZ, context, callerContext, generationRuntime);
|
||||||
chunk,
|
task.queue(work, generationRuntime);
|
||||||
component,
|
task.setSubmission(DISPATCHER.submit(work));
|
||||||
writer,
|
|
||||||
chunkX,
|
|
||||||
chunkZ,
|
|
||||||
context,
|
|
||||||
callerContext)));
|
|
||||||
return task;
|
return task;
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
task.cancel(throwable);
|
task.cancel(throwable);
|
||||||
@@ -489,6 +508,20 @@ public interface MatterGenerator {
|
|||||||
deferredComponents.clear();
|
deferredComponents.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void runQueuedComponentTasks(List<MatterComponentTask> tasks,
|
||||||
|
IrisEngine.GenerationRuntimeBinding helperRuntime) {
|
||||||
|
boolean interrupted = Thread.interrupted();
|
||||||
|
try {
|
||||||
|
for (int index = tasks.size() - 1; index >= 0; index--) {
|
||||||
|
tasks.get(index).runIfQueued(helperRuntime);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (interrupted) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pass barrier. Waits for every launched task, even if one of them failed, so that no task is
|
* Pass barrier. Waits for every launched task, even if one of them failed, so that no task is
|
||||||
* still writing through the writer once this returns. The first failure is rethrown.
|
* still writing through the writer once this returns. The first failure is rethrown.
|
||||||
@@ -592,19 +625,42 @@ public interface MatterGenerator {
|
|||||||
int chunkZ,
|
int chunkZ,
|
||||||
ChunkContext context,
|
ChunkContext context,
|
||||||
IrisContext callerContext
|
IrisContext callerContext
|
||||||
|
) {
|
||||||
|
Throwable failure = runComponentTask(task, chunk, component, writer, chunkX, chunkZ, context, callerContext, null);
|
||||||
|
if (failure instanceof RuntimeException runtime) {
|
||||||
|
throw runtime;
|
||||||
|
}
|
||||||
|
if (failure instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (failure != null) {
|
||||||
|
throw new CompletionException(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Throwable runComponentTask(
|
||||||
|
MatterComponentTask task,
|
||||||
|
MantleChunk<Matter> chunk,
|
||||||
|
MantleComponent component,
|
||||||
|
MantleWriter writer,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ,
|
||||||
|
ChunkContext context,
|
||||||
|
IrisContext callerContext,
|
||||||
|
IrisEngine.GenerationRuntimeBinding generationRuntime
|
||||||
) {
|
) {
|
||||||
if (!task.start()) {
|
if (!task.start()) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
Throwable failure = null;
|
Throwable failure = null;
|
||||||
try {
|
try {
|
||||||
runComponentWithContext(chunk, component, writer, chunkX, chunkZ, context, callerContext);
|
runComponentWithContext(chunk, component, writer, chunkX, chunkZ, context, callerContext, generationRuntime);
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
failure = throwable;
|
failure = throwable;
|
||||||
throw throwable;
|
|
||||||
} finally {
|
} finally {
|
||||||
task.finish(failure);
|
task.finish(failure);
|
||||||
}
|
}
|
||||||
|
return failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runComponentWithContext(
|
private void runComponentWithContext(
|
||||||
@@ -614,14 +670,16 @@ public interface MatterGenerator {
|
|||||||
int chunkX,
|
int chunkX,
|
||||||
int chunkZ,
|
int chunkZ,
|
||||||
ChunkContext context,
|
ChunkContext context,
|
||||||
IrisContext callerContext
|
IrisContext callerContext,
|
||||||
|
IrisEngine.GenerationRuntimeBinding generationRuntime
|
||||||
) {
|
) {
|
||||||
if (callerContext == null) {
|
if (callerContext == null && generationRuntime == null) {
|
||||||
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
|
runComponentInline(chunk, component, writer, chunkX, chunkZ, context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try (IrisContext.Scope ignored = IrisContext.open(
|
try (IrisEngine.GenerationRuntimeScope runtimeScope = openGenerationRuntime(generationRuntime);
|
||||||
|
IrisContext.Scope contextScope = callerContext == null ? null : IrisContext.open(
|
||||||
callerContext.getEngine(),
|
callerContext.getEngine(),
|
||||||
callerContext.getGenerationSessionId(),
|
callerContext.getGenerationSessionId(),
|
||||||
callerContext.getChunkContext())) {
|
callerContext.getChunkContext())) {
|
||||||
@@ -647,6 +705,8 @@ public interface MatterGenerator {
|
|||||||
private final MatterTaskKey key;
|
private final MatterTaskKey key;
|
||||||
private final CompletableFuture<Void> future = new CompletableFuture<>();
|
private final CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
private volatile Future<?> submission;
|
private volatile Future<?> submission;
|
||||||
|
private volatile Runnable queuedWork;
|
||||||
|
private volatile IrisEngine.GenerationRuntimeBinding queuedRuntime;
|
||||||
private volatile Throwable failure;
|
private volatile Throwable failure;
|
||||||
private boolean started;
|
private boolean started;
|
||||||
private boolean slowWarningIssued;
|
private boolean slowWarningIssued;
|
||||||
@@ -667,6 +727,22 @@ public interface MatterGenerator {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized boolean isStarted() {
|
||||||
|
return started;
|
||||||
|
}
|
||||||
|
|
||||||
|
void queue(Runnable work, IrisEngine.GenerationRuntimeBinding runtime) {
|
||||||
|
queuedRuntime = runtime;
|
||||||
|
queuedWork = work;
|
||||||
|
}
|
||||||
|
|
||||||
|
void runIfQueued(IrisEngine.GenerationRuntimeBinding helperRuntime) {
|
||||||
|
Runnable work = queuedWork;
|
||||||
|
if (work != null && !future.isDone() && (helperRuntime == null || helperRuntime == queuedRuntime)) {
|
||||||
|
work.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
synchronized void setSubmission(Future<?> submission) {
|
synchronized void setSubmission(Future<?> submission) {
|
||||||
this.submission = submission;
|
this.submission = submission;
|
||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
@@ -720,7 +796,7 @@ public interface MatterGenerator {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Future<?> submitted = submission;
|
Future<?> submitted = submission;
|
||||||
boolean dropped = submitted != null && submitted.isDone();
|
boolean dropped = submitted != null && submitted.isDone() && !isStarted();
|
||||||
long waited = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
|
long waited = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
|
||||||
if (dropped) {
|
if (dropped) {
|
||||||
IllegalStateException dispatchFailure = new IllegalStateException(
|
IllegalStateException dispatchFailure = new IllegalStateException(
|
||||||
@@ -775,13 +851,20 @@ public interface MatterGenerator {
|
|||||||
private final Semaphore slots = new Semaphore(COMPONENT_SUBMISSION_LIMIT);
|
private final Semaphore slots = new Semaphore(COMPONENT_SUBMISSION_LIMIT);
|
||||||
private volatile Throwable failure;
|
private volatile Throwable failure;
|
||||||
|
|
||||||
void acquire() {
|
boolean reserve() {
|
||||||
slots.acquireUninterruptibly();
|
|
||||||
Throwable observed = failure;
|
Throwable observed = failure;
|
||||||
|
if (observed != null) {
|
||||||
|
throw new CompletionException(observed);
|
||||||
|
}
|
||||||
|
if (!slots.tryAcquire()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
observed = failure;
|
||||||
if (observed != null) {
|
if (observed != null) {
|
||||||
slots.release();
|
slots.release();
|
||||||
throw new CompletionException(observed);
|
throw new CompletionException(observed);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void release() {
|
void release() {
|
||||||
|
|||||||
+222
-19
@@ -2,6 +2,7 @@ package art.arcane.iris.generation.mantle;
|
|||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
import art.arcane.iris.testsupport.Await;
|
import art.arcane.iris.testsupport.Await;
|
||||||
import art.arcane.iris.testsupport.PlatformBinding;
|
import art.arcane.iris.testsupport.PlatformBinding;
|
||||||
@@ -76,11 +77,13 @@ public class MatterGeneratorConcurrencyTest {
|
|||||||
@Test
|
@Test
|
||||||
public void externalWindowsBoundManagedBlockedComponentSubmissions() throws Exception {
|
public void externalWindowsBoundManagedBlockedComponentSubmissions() throws Exception {
|
||||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||||
int maximum = 2 * MatterGenerator.COMPONENT_SUBMISSION_LIMIT;
|
int pooled = 2 * MatterGenerator.COMPONENT_SUBMISSION_LIMIT;
|
||||||
|
int maximum = pooled + 2;
|
||||||
CountDownLatch admitted = new CountDownLatch(maximum);
|
CountDownLatch admitted = new CountDownLatch(maximum);
|
||||||
CountDownLatch excess = new CountDownLatch(1);
|
CountDownLatch excess = new CountDownLatch(1);
|
||||||
CountDownLatch release = new CountDownLatch(1);
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
AtomicInteger started = new AtomicInteger();
|
AtomicInteger started = new AtomicInteger();
|
||||||
|
AtomicInteger startedOnDispatcher = new AtomicInteger();
|
||||||
AtomicInteger completed = new AtomicInteger();
|
AtomicInteger completed = new AtomicInteger();
|
||||||
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 64) {
|
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 64) {
|
||||||
@Override
|
@Override
|
||||||
@@ -88,6 +91,9 @@ public class MatterGeneratorConcurrencyTest {
|
|||||||
if (started.incrementAndGet() > maximum) {
|
if (started.incrementAndGet() > maximum) {
|
||||||
excess.countDown();
|
excess.countDown();
|
||||||
}
|
}
|
||||||
|
if (MatterGenerator.DISPATCHER.ownsCurrentThread()) {
|
||||||
|
startedOnDispatcher.incrementAndGet();
|
||||||
|
}
|
||||||
admitted.countDown();
|
admitted.countDown();
|
||||||
awaitManaged(release);
|
awaitManaged(release);
|
||||||
completed.incrementAndGet();
|
completed.incrementAndGet();
|
||||||
@@ -102,6 +108,7 @@ public class MatterGeneratorConcurrencyTest {
|
|||||||
await(admitted);
|
await(admitted);
|
||||||
assertFalse(excess.await(300L, TimeUnit.MILLISECONDS));
|
assertFalse(excess.await(300L, TimeUnit.MILLISECONDS));
|
||||||
assertEquals(maximum, started.get());
|
assertEquals(maximum, started.get());
|
||||||
|
assertEquals(pooled, startedOnDispatcher.get());
|
||||||
assertEquals(maximum, activeClaims(fixture, 0, 4) + activeClaims(fixture, 32, 4));
|
assertEquals(maximum, activeClaims(fixture, 0, 4) + activeClaims(fixture, 32, 4));
|
||||||
verify(fixture.mantle.getChunk(-4, -4), never()).release();
|
verify(fixture.mantle.getChunk(-4, -4), never()).release();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -358,14 +365,23 @@ public class MatterGeneratorConcurrencyTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void multicoreComponentRunsOnTheDispatcher() {
|
public void multicoreComponentsSpreadAcrossTheDispatcherWhileTheCallerHelps() {
|
||||||
GeneratorFixture fixture = new GeneratorFixture();
|
GeneratorFixture fixture = new GeneratorFixture();
|
||||||
Thread caller = Thread.currentThread();
|
Thread caller = Thread.currentThread();
|
||||||
Set<Thread> threads = ConcurrentHashMap.newKeySet();
|
AtomicInteger dispatcherRuns = new AtomicInteger();
|
||||||
|
AtomicInteger completed = new AtomicInteger();
|
||||||
|
CountDownLatch dispatched = new CountDownLatch(1);
|
||||||
RecordingComponent ordinary = new RecordingComponent(ReservedFlag.OBJECT, 0, 16) {
|
RecordingComponent ordinary = new RecordingComponent(ReservedFlag.OBJECT, 0, 16) {
|
||||||
@Override
|
@Override
|
||||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||||
threads.add(Thread.currentThread());
|
if (Thread.currentThread() == caller) {
|
||||||
|
await(dispatched);
|
||||||
|
} else {
|
||||||
|
assertTrue(MatterGenerator.DISPATCHER.ownsCurrentThread());
|
||||||
|
dispatcherRuns.incrementAndGet();
|
||||||
|
dispatched.countDown();
|
||||||
|
}
|
||||||
|
completed.incrementAndGet();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
TestMatterGenerator generator = fixture.generator(List.of(
|
TestMatterGenerator generator = fixture.generator(List.of(
|
||||||
@@ -374,41 +390,157 @@ public class MatterGeneratorConcurrencyTest {
|
|||||||
|
|
||||||
generator.generateMatter(0, 0, true, fixture.context);
|
generator.generateMatter(0, 0, true, fixture.context);
|
||||||
|
|
||||||
assertFalse(threads.isEmpty());
|
assertEquals(9, completed.get());
|
||||||
assertFalse(threads.contains(caller));
|
assertTrue(dispatcherRuns.get() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void asyncComponentReceivesCallerContextAndCallerScopeIsRestored() {
|
public void componentsReceiveTheCallerContextOnEveryThreadAndCallerScopeIsRestored() {
|
||||||
GeneratorFixture fixture = new GeneratorFixture();
|
GeneratorFixture fixture = new GeneratorFixture();
|
||||||
AtomicReference<IrisContext> observed = new AtomicReference<>();
|
Thread callerThread = Thread.currentThread();
|
||||||
AtomicReference<Thread> observedThread = new AtomicReference<>();
|
Set<IrisContext> observed = ConcurrentHashMap.newKeySet();
|
||||||
RecordingComponent concurrent = new RecordingComponent(ReservedFlag.CARVED, 0, 0) {
|
AtomicInteger dispatcherRuns = new AtomicInteger();
|
||||||
|
CountDownLatch dispatched = new CountDownLatch(1);
|
||||||
|
RecordingComponent concurrent = new RecordingComponent(ReservedFlag.CARVED, 0, 16) {
|
||||||
@Override
|
@Override
|
||||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||||
observed.set(IrisContext.require());
|
observed.add(IrisContext.require());
|
||||||
observedThread.set(Thread.currentThread());
|
if (Thread.currentThread() == callerThread) {
|
||||||
|
await(dispatched);
|
||||||
|
} else {
|
||||||
|
assertTrue(MatterGenerator.DISPATCHER.ownsCurrentThread());
|
||||||
|
dispatcherRuns.incrementAndGet();
|
||||||
|
dispatched.countDown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
TestMatterGenerator generator = fixture.generator(List.of(
|
TestMatterGenerator generator = fixture.generator(List.of(
|
||||||
new MantlePass(List.of(concurrent), 0, 0)
|
new MantlePass(List.of(concurrent), 1, 0)
|
||||||
));
|
));
|
||||||
|
|
||||||
assertNull(IrisContext.get());
|
assertNull(IrisContext.get());
|
||||||
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 91L, fixture.context)) {
|
try (IrisContext.Scope ignored = IrisContext.open(fixture.engine, 91L, fixture.context)) {
|
||||||
IrisContext caller = IrisContext.require();
|
IrisContext caller = IrisContext.require();
|
||||||
Thread callerThread = Thread.currentThread();
|
|
||||||
generator.generateMatter(0, 0, true, fixture.context);
|
generator.generateMatter(0, 0, true, fixture.context);
|
||||||
|
|
||||||
assertNotSame(callerThread, observedThread.get());
|
assertTrue(dispatcherRuns.get() > 0);
|
||||||
assertSame(fixture.engine, observed.get().getEngine());
|
for (IrisContext context : observed) {
|
||||||
assertSame(fixture.context, observed.get().getChunkContext());
|
assertSame(fixture.engine, context.getEngine());
|
||||||
assertEquals(91L, observed.get().getGenerationSessionId());
|
assertSame(fixture.context, context.getChunkContext());
|
||||||
|
assertEquals(91L, context.getGenerationSessionId());
|
||||||
|
}
|
||||||
assertSame(caller, IrisContext.require());
|
assertSame(caller, IrisContext.require());
|
||||||
}
|
}
|
||||||
assertNull(IrisContext.get());
|
assertNull(IrisContext.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void externalBarrierRunsItsQueuedComponentsWhileTheDispatcherIsBusy() throws Exception {
|
||||||
|
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||||
|
CountDownLatch releaseDispatcher = new CountDownLatch(1);
|
||||||
|
List<Future<?>> blockers = occupyDispatcher(releaseDispatcher);
|
||||||
|
AtomicReference<Thread> generationThread = new AtomicReference<>();
|
||||||
|
Set<Thread> threads = ConcurrentHashMap.newKeySet();
|
||||||
|
AtomicInteger completed = new AtomicInteger();
|
||||||
|
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 16) {
|
||||||
|
@Override
|
||||||
|
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||||
|
threads.add(Thread.currentThread());
|
||||||
|
completed.incrementAndGet();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 1, 0)));
|
||||||
|
try {
|
||||||
|
CompletableFuture<Void> generation = CompletableFuture.runAsync(() -> {
|
||||||
|
generationThread.set(Thread.currentThread());
|
||||||
|
generator.generateMatter(0, 0, true, fixture.context);
|
||||||
|
});
|
||||||
|
generation.get(5L, TimeUnit.SECONDS);
|
||||||
|
assertEquals(9, completed.get());
|
||||||
|
assertEquals(Set.of(generationThread.get()), threads);
|
||||||
|
assertEquals(0, activeClaims(fixture, 0, 1));
|
||||||
|
} finally {
|
||||||
|
releaseDispatcher.countDown();
|
||||||
|
for (Future<?> blocker : blockers) {
|
||||||
|
blocker.get(10L, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void dispatcherComponentsRunInsideTheCallersGenerationRuntime() {
|
||||||
|
GeneratorFixture fixture = new GeneratorFixture(true, true);
|
||||||
|
Thread caller = Thread.currentThread();
|
||||||
|
fixture.bindRuntime(caller);
|
||||||
|
IrisEngine engine = (IrisEngine) fixture.engine;
|
||||||
|
AtomicInteger dispatcherRuns = new AtomicInteger();
|
||||||
|
AtomicInteger unscopedRuns = new AtomicInteger();
|
||||||
|
AtomicInteger completed = new AtomicInteger();
|
||||||
|
CountDownLatch dispatched = new CountDownLatch(1);
|
||||||
|
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 64) {
|
||||||
|
@Override
|
||||||
|
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||||
|
if (!engine.hasGenerationRuntimeScope() || engine.captureGenerationRuntimeBinding() != fixture.runtime) {
|
||||||
|
unscopedRuns.incrementAndGet();
|
||||||
|
}
|
||||||
|
if (Thread.currentThread() == caller) {
|
||||||
|
await(dispatched);
|
||||||
|
} else {
|
||||||
|
dispatcherRuns.incrementAndGet();
|
||||||
|
dispatched.countDown();
|
||||||
|
}
|
||||||
|
completed.incrementAndGet();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 4, 0)));
|
||||||
|
|
||||||
|
generator.generateMatter(0, 0, true, fixture.context);
|
||||||
|
|
||||||
|
assertEquals(81, completed.get());
|
||||||
|
assertEquals(0, unscopedRuns.get());
|
||||||
|
assertTrue(dispatcherRuns.get() > 0);
|
||||||
|
assertEquals(fixture.openedRuntimes.get(), fixture.closedRuntimes.get());
|
||||||
|
assertTrue(fixture.openedRuntimes.get() >= dispatcherRuns.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void helpersOnlyRunQueuedComponentsBoundToTheirOwnRuntime() {
|
||||||
|
GeneratorFixture fixture = new GeneratorFixture();
|
||||||
|
IrisEngine.GenerationRuntimeBinding own = mock(IrisEngine.GenerationRuntimeBinding.class);
|
||||||
|
IrisEngine.GenerationRuntimeBinding foreign = mock(IrisEngine.GenerationRuntimeBinding.class);
|
||||||
|
AtomicInteger runs = new AtomicInteger();
|
||||||
|
MatterGenerator.MatterComponentTask bound = new MatterGenerator.MatterComponentTask(
|
||||||
|
new MatterGenerator.MatterTaskKey(fixture.mantle, 0, 0, ReservedFlag.OBJECT));
|
||||||
|
bound.queue(runs::incrementAndGet, own);
|
||||||
|
MatterGenerator.MatterComponentTask unbound = new MatterGenerator.MatterComponentTask(
|
||||||
|
new MatterGenerator.MatterTaskKey(fixture.mantle, 1, 0, ReservedFlag.OBJECT));
|
||||||
|
unbound.queue(runs::incrementAndGet, null);
|
||||||
|
|
||||||
|
bound.runIfQueued(foreign);
|
||||||
|
unbound.runIfQueued(foreign);
|
||||||
|
assertEquals(0, runs.get());
|
||||||
|
bound.runIfQueued(own);
|
||||||
|
unbound.runIfQueued(null);
|
||||||
|
bound.runIfQueued(null);
|
||||||
|
assertEquals(3, runs.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Future<?>> occupyDispatcher(CountDownLatch release) {
|
||||||
|
int blockers = Math.max(1, MultiBurst.burst.parallelism()) * 4;
|
||||||
|
List<Future<?>> futures = new ArrayList<>(blockers);
|
||||||
|
for (int index = 0; index < blockers; index++) {
|
||||||
|
futures.add(MultiBurst.burst.submit(() -> {
|
||||||
|
try {
|
||||||
|
assertTrue(release.await(30L, TimeUnit.SECONDS));
|
||||||
|
} catch (InterruptedException interruption) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return futures;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void terrainPhasePreparesContentReachWithoutPublishingPlanned() {
|
public void terrainPhasePreparesContentReachWithoutPublishingPlanned() {
|
||||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||||
@@ -701,6 +833,42 @@ public class MatterGeneratorConcurrencyTest {
|
|||||||
assertFalse(MatterGenerator.IN_FLIGHT_COMPONENTS.containsKey(key));
|
assertFalse(MatterGenerator.IN_FLIGHT_COMPONENTS.containsKey(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void componentStartedByAHelperIsNotDroppedWhenItsSubmissionFinishesFirst() throws Exception {
|
||||||
|
GeneratorFixture fixture = new GeneratorFixture();
|
||||||
|
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(fixture.mantle, 3, 0, ReservedFlag.OBJECT);
|
||||||
|
MatterGenerator.MatterComponentTask task = new MatterGenerator.MatterComponentTask(key);
|
||||||
|
CountDownLatch running = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
CountDownLatch polls = new CountDownLatch(2);
|
||||||
|
task.queue(() -> {
|
||||||
|
if (task.start()) {
|
||||||
|
running.countDown();
|
||||||
|
await(release);
|
||||||
|
task.finish(null);
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
Future<?> submission = mock(Future.class);
|
||||||
|
when(submission.isDone()).thenAnswer(invocation -> {
|
||||||
|
polls.countDown();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
task.setSubmission(submission);
|
||||||
|
CompletableFuture<Void> helper = CompletableFuture.runAsync(() -> task.runIfQueued(null));
|
||||||
|
await(running);
|
||||||
|
CompletableFuture<Void> waiter = CompletableFuture.runAsync(
|
||||||
|
() -> task.await(MatterGenerator.COMPONENT_TASK_TIMEOUT_MS));
|
||||||
|
try {
|
||||||
|
await(polls);
|
||||||
|
assertFalse(waiter.isDone());
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
}
|
||||||
|
helper.get(5L, TimeUnit.SECONDS);
|
||||||
|
waiter.get(5L, TimeUnit.SECONDS);
|
||||||
|
verify(submission, never()).cancel(anyBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void interruptedWaiterDrainsRunningTaskAndRestoresInterrupt() throws Exception {
|
public void interruptedWaiterDrainsRunningTaskAndRestoresInterrupt() throws Exception {
|
||||||
GeneratorFixture fixture = new GeneratorFixture();
|
GeneratorFixture fixture = new GeneratorFixture();
|
||||||
@@ -814,22 +982,49 @@ public class MatterGeneratorConcurrencyTest {
|
|||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
private final Mantle<Matter> mantle;
|
private final Mantle<Matter> mantle;
|
||||||
private final ChunkContext context;
|
private final ChunkContext context;
|
||||||
|
private final IrisEngine.GenerationRuntimeBinding runtime = mock(IrisEngine.GenerationRuntimeBinding.class);
|
||||||
|
private final ThreadLocal<Integer> runtimeDepth = ThreadLocal.withInitial(() -> 0);
|
||||||
|
private final AtomicInteger openedRuntimes = new AtomicInteger();
|
||||||
|
private final AtomicInteger closedRuntimes = new AtomicInteger();
|
||||||
|
private volatile Thread boundThread;
|
||||||
|
|
||||||
private GeneratorFixture() {
|
private GeneratorFixture() {
|
||||||
this(false);
|
this(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GeneratorFixture(boolean lockingChunks) {
|
||||||
|
this(lockingChunks, false);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param lockingChunks true to give every chunk a real per-flag lock and flag state, so a
|
* @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
|
* generation that reaches a chunk another generation is working on
|
||||||
* blocks exactly like production
|
* blocks exactly like production
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private GeneratorFixture(boolean lockingChunks) {
|
private GeneratorFixture(boolean lockingChunks, boolean runtimeBoundCaller) {
|
||||||
IrisDimension dimension = mock(IrisDimension.class);
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
when(dimension.isUseMantle()).thenReturn(true);
|
when(dimension.isUseMantle()).thenReturn(true);
|
||||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||||
|
if (runtimeBoundCaller) {
|
||||||
|
IrisEngine irisEngine = mock(IrisEngine.class);
|
||||||
|
when(irisEngine.hasGenerationRuntimeScope()).thenAnswer(invocation -> runtimeBound());
|
||||||
|
when(irisEngine.captureGenerationRuntimeBinding()).thenAnswer(invocation -> runtimeBound() ? runtime : null);
|
||||||
|
when(irisEngine.openGenerationRuntimeScope(runtime)).thenAnswer(invocation -> {
|
||||||
|
openedRuntimes.incrementAndGet();
|
||||||
|
runtimeDepth.set(runtimeDepth.get() + 1);
|
||||||
|
IrisEngine.GenerationRuntimeScope scope = mock(IrisEngine.GenerationRuntimeScope.class);
|
||||||
|
doAnswer(close -> {
|
||||||
|
runtimeDepth.set(runtimeDepth.get() - 1);
|
||||||
|
closedRuntimes.incrementAndGet();
|
||||||
|
return null;
|
||||||
|
}).when(scope).close();
|
||||||
|
return scope;
|
||||||
|
});
|
||||||
|
engine = irisEngine;
|
||||||
|
} else {
|
||||||
engine = mock(Engine.class);
|
engine = mock(Engine.class);
|
||||||
|
}
|
||||||
when(engine.getDimension()).thenReturn(dimension);
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
when(engine.getMantle()).thenReturn(engineMantle);
|
when(engine.getMantle()).thenReturn(engineMantle);
|
||||||
mantle = mock(Mantle.class);
|
mantle = mock(Mantle.class);
|
||||||
@@ -846,6 +1041,14 @@ public class MatterGeneratorConcurrencyTest {
|
|||||||
when(context.getGenerationSessionId()).thenReturn(91L);
|
when(context.getGenerationSessionId()).thenReturn(91L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void bindRuntime(Thread thread) {
|
||||||
|
boundThread = thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean runtimeBound() {
|
||||||
|
return Thread.currentThread() == boundThread || runtimeDepth.get() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private TestMatterGenerator generator(List<MantlePass> passes) {
|
private TestMatterGenerator generator(List<MantlePass> passes) {
|
||||||
return new TestMatterGenerator(engine, mantle, passes);
|
return new TestMatterGenerator(engine, mantle, passes);
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-8
@@ -42,6 +42,7 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.FutureTask;
|
import java.util.concurrent.FutureTask;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
@@ -431,25 +432,36 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
|||||||
};
|
};
|
||||||
MultiBurst workerPool = new MultiBurst("Generation Runtime Scope Test", Thread.NORM_PRIORITY, () -> 2);
|
MultiBurst workerPool = new MultiBurst("Generation Runtime Scope Test", Thread.NORM_PRIORITY, () -> 2);
|
||||||
when(detached.target.getBurster()).thenReturn(workerPool);
|
when(detached.target.getBurster()).thenReturn(workerPool);
|
||||||
AtomicReference<EngineTarget> observedTarget = new AtomicReference<>();
|
Set<EngineTarget> observedTargets = new CopyOnWriteArraySet<>();
|
||||||
AtomicReference<Thread> observedThread = new AtomicReference<>();
|
Set<Thread> workerThreads = new CopyOnWriteArraySet<>();
|
||||||
|
CountDownLatch workerRan = new CountDownLatch(1);
|
||||||
|
Thread caller = Thread.currentThread();
|
||||||
EngineStage stage = (x, z, blocks, biomes, multicore, context) -> {
|
EngineStage stage = (x, z, blocks, biomes, multicore, context) -> {
|
||||||
observedTarget.set(engine.getTarget());
|
observedTargets.add(engine.getTarget());
|
||||||
observedThread.set(Thread.currentThread());
|
if (Thread.currentThread() == caller) {
|
||||||
|
try {
|
||||||
|
assertTrue(workerRan.await(5L, TimeUnit.SECONDS));
|
||||||
|
} catch (InterruptedException interruption) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new AssertionError(interruption);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workerThreads.add(Thread.currentThread());
|
||||||
|
workerRan.countDown();
|
||||||
};
|
};
|
||||||
ChunkContext context = mock(ChunkContext.class);
|
ChunkContext context = mock(ChunkContext.class);
|
||||||
when(context.getGenerationSessionId()).thenReturn(17L);
|
when(context.getGenerationSessionId()).thenReturn(17L);
|
||||||
Thread caller = Thread.currentThread();
|
|
||||||
|
|
||||||
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(binding)) {
|
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(binding)) {
|
||||||
mode.burst(stage).generate(0, 0, null, null, true, context);
|
mode.burst(stage, stage).generate(0, 0, null, null, true, context);
|
||||||
} finally {
|
} finally {
|
||||||
mode.close();
|
mode.close();
|
||||||
workerPool.close();
|
workerPool.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
assertSame(detached.target, observedTarget.get());
|
assertEquals(Set.of(detached.target), observedTargets);
|
||||||
assertNotSame(caller, observedThread.get());
|
assertTrue(!workerThreads.isEmpty() && !workerThreads.contains(caller));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in new issue
Block a user