mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Tuning
This commit is contained in:
1 parent
8e3ef1b651
commit
197d2c2461
73 files changed
+4586
-424
No files matched your search
+118
@@ -0,0 +1,118 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyCacheBudgetTest {
|
||||
private static final long MEBIBYTE = 1024L * 1024L;
|
||||
|
||||
@Test
|
||||
public void retainedCacheAllowancesShareOneHeapScaledLimit() {
|
||||
for (long heap : new long[]{32L * MEBIBYTE, 1024L * MEBIBYTE, 2048L * MEBIBYTE, 4096L * MEBIBYTE, Long.MAX_VALUE}) {
|
||||
for (boolean regionalEnabled : new boolean[]{false, true}) {
|
||||
HydrologyCacheBudget budget = HydrologyCacheBudget.forHeap(heap, regionalEnabled);
|
||||
long total = budget.tileBytes() + budget.columnBytes() + budget.diagnosticBytes()
|
||||
+ budget.routingBytes() + budget.ownerBytes() + budget.edgeBytes() + budget.regionalDraftBytes()
|
||||
+ budget.regionalCoarseBytes() + budget.regionalTerrainBytes() + budget.regionalReachBytes();
|
||||
assertTrue(total <= Math.clamp(heap / 8L, 8L * MEBIBYTE, 256L * MEBIBYTE));
|
||||
assertTrue(total >= 8L * MEBIBYTE);
|
||||
assertEquals(budget.tileBytes() / 6L, budget.columnBytes());
|
||||
}
|
||||
}
|
||||
HydrologyCacheBudget twoGigabytes = HydrologyCacheBudget.forHeap(2048L * MEBIBYTE, true);
|
||||
assertEquals(96L * MEBIBYTE, twoGigabytes.tileBytes());
|
||||
assertEquals(32L * MEBIBYTE, twoGigabytes.ownerBytes());
|
||||
assertEquals(32L * MEBIBYTE, twoGigabytes.regionalTerrainBytes());
|
||||
HydrologyCacheBudget localOnly = HydrologyCacheBudget.forHeap(2048L * MEBIBYTE, false);
|
||||
assertEquals(twoGigabytes.tileBytes(), localOnly.tileBytes());
|
||||
assertEquals(96L * MEBIBYTE, localOnly.ownerBytes());
|
||||
assertEquals(24L * MEBIBYTE, localOnly.routingBytes());
|
||||
assertEquals(8L * MEBIBYTE, localOnly.edgeBytes());
|
||||
assertEquals(0L, localOnly.regionalDraftBytes() + localOnly.regionalCoarseBytes()
|
||||
+ localOnly.regionalTerrainBytes() + localOnly.regionalReachBytes());
|
||||
assertThrows(IllegalArgumentException.class, () -> HydrologyCacheBudget.forHeap(0L, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void disabledRegionalCachesLeaveTheirBudgetForLocalPlanning() throws Exception {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyPlannerSettings.defaults(),
|
||||
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
|
||||
HydrologyCacheBudget budget = HydrologyCacheBudget.runtime(false);
|
||||
assertEquals(budget.ownerBytes(), planner.resolvedOwners.policy().eviction().orElseThrow().getMaximum());
|
||||
assertEquals(budget.routingBytes(), planner.routingContexts.policy().eviction().orElseThrow().getMaximum());
|
||||
assertEquals(budget.edgeBytes(), planner.refinedEdgeCache.policy().eviction().orElseThrow().getMaximum());
|
||||
Field field = HydrologyRegionalPlanner.class.getDeclaredField("drafts");
|
||||
field.setAccessible(true);
|
||||
Cache<HydrologyTileKey, HydrologyRegionalNetwork> drafts =
|
||||
(Cache<HydrologyTileKey, HydrologyRegionalNetwork>) field.get(planner.regional);
|
||||
assertEquals(0L, drafts.policy().eviction().orElseThrow().getMaximum());
|
||||
drafts.put(new HydrologyTileKey(0, 0), HydrologyRegionalNetwork.EMPTY);
|
||||
drafts.cleanUp();
|
||||
assertEquals(0L, drafts.estimatedSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightsKeepEntryAndStationLimitsAndSaturateLargeValues() {
|
||||
assertEquals(64, HydrologyCacheWeights.bounded(1L, 1024L, 16));
|
||||
assertEquals(256, HydrologyCacheWeights.bounded(256L, 1024L, 16));
|
||||
assertEquals(512, HydrologyCacheWeights.bounded(1L, 1024L, 16, 50, 100));
|
||||
assertEquals(Integer.MAX_VALUE, HydrologyCacheWeights.bounded(Long.MAX_VALUE, 1024L, 16));
|
||||
Cache<Integer, Integer> cache = Caffeine.newBuilder().maximumWeight(1024L)
|
||||
.weigher((Integer key, Integer bytes) -> HydrologyCacheWeights.bounded(bytes, 1024L, 16)).build();
|
||||
for (int key = 0; key < 64; key++) {
|
||||
cache.put(key, 1);
|
||||
}
|
||||
cache.cleanUp();
|
||||
assertTrue(cache.estimatedSize() <= 16L);
|
||||
cache.put(64, 2048);
|
||||
cache.cleanUp();
|
||||
assertNull(cache.getIfPresent(64));
|
||||
assertTrue(cache.policy().eviction().orElseThrow().weightedSize().orElseThrow() <= 1024L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void largeRoutingContextsEvictByBytesBeforeTheEntryLimit() {
|
||||
HydrologyTerrainSample terrain = HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), (x, z) -> terrain);
|
||||
long budget = HydrologyCacheBudget.runtime().routingBytes();
|
||||
assertTrue(planner.routingContexts.policy().eviction().orElseThrow().isWeighted());
|
||||
assertEquals(budget, planner.routingContexts.policy().eviction().orElseThrow().getMaximum());
|
||||
HydrologyGridNode node = new HydrologyGridNode(0, 0, 0, 0, 0, 1L, terrain);
|
||||
int nodeCount = (int) (budget / 640L);
|
||||
SourceRoutingContext context = new SourceRoutingContext(
|
||||
new HydrologySampledGrid(0, 0, 0, 0, 16, 1, 16, Collections.nCopies(nodeCount, node)),
|
||||
null, null, List.of());
|
||||
for (int index = 0; index < 8; index++) {
|
||||
planner.routingContexts.put(new HydrologyTileKey(index, 0), context);
|
||||
}
|
||||
planner.routingContexts.cleanUp();
|
||||
assertTrue(planner.routingContexts.estimatedSize() < 8L);
|
||||
assertTrue(planner.routingContexts.policy().eviction().orElseThrow().weightedSize().orElseThrow() <= budget);
|
||||
assertNotNull(planner.routingContexts.asMap().values().iterator().next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regionalTreeWeightCountsSharedDownstreamLabelsOnce() {
|
||||
HydrologyRegionalGraph.Label downstream = new HydrologyRegionalGraph.Label(
|
||||
new HydrologyRegionalGraph.LabelState(0, 0, 63, 0D, 0D, 0), null);
|
||||
HydrologyRegionalGraph.Label first = new HydrologyRegionalGraph.Label(
|
||||
new HydrologyRegionalGraph.LabelState(1, 1, 63, 1D, 1D, 0), downstream);
|
||||
HydrologyRegionalGraph.Label second = new HydrologyRegionalGraph.Label(
|
||||
new HydrologyRegionalGraph.LabelState(3, 2, 63, 1D, 1D, 0), downstream);
|
||||
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(
|
||||
new HydrologyRegionalGraph.Label[]{first, second, null}, new int[4], List.of());
|
||||
assertEquals(3, tree.retainedLabelCount());
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -143,14 +143,15 @@ public class HydrologyDiagnosticCacheTest {
|
||||
}
|
||||
return fresh;
|
||||
});
|
||||
ExecutorService caller = Executors.newSingleThreadExecutor();
|
||||
ExecutorService caller = Executors.newFixedThreadPool(2);
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 4);
|
||||
try {
|
||||
Future<List<HydrologyDiagnosticCandidate>> first = caller.submit(() -> cache.diagnosticCandidates(key));
|
||||
assertTrue(started.await(5L, TimeUnit.SECONDS));
|
||||
cache.clear();
|
||||
assertEquals(fresh, cache.diagnosticCandidates(key));
|
||||
Future<List<HydrologyDiagnosticCandidate>> second = caller.submit(() -> cache.diagnosticCandidates(key));
|
||||
release.countDown();
|
||||
assertEquals(fresh, second.get(5L, TimeUnit.SECONDS));
|
||||
assertEquals(old, first.get(5L, TimeUnit.SECONDS));
|
||||
assertEquals(fresh, cache.diagnosticCandidates(key));
|
||||
assertEquals(2, attempts.get());
|
||||
|
||||
+82
-10
@@ -35,6 +35,8 @@ public class HydrologyFallbackParallelismTest {
|
||||
|
||||
@Test(timeout = 15000)
|
||||
public void publicationRejectionConsumesTheNextCompletedOutletWithoutRebuildingIt() throws Exception {
|
||||
int parallelism = HydrologyPlanningAdmission.effectiveParallelism(4);
|
||||
int batchSize = parallelism <= 2 ? 1 : Math.min(parallelism, HydrologyPlanningAdmission.maximumTrials());
|
||||
AtomicIntegerArray builds = new AtomicIntegerArray(4);
|
||||
Fixture fixture = fixture(outlet -> {
|
||||
builds.incrementAndGet(outlet);
|
||||
@@ -53,23 +55,89 @@ public class HydrologyFallbackParallelismTest {
|
||||
if (result.courses().isEmpty()) {
|
||||
return new CrossTilePublicationAdmission(result, List.of(), false);
|
||||
}
|
||||
for (int outlet = 0; outlet < 4; outlet++) {
|
||||
assertEquals("Every trial provider must finish before publication", 1, builds.get(outlet));
|
||||
}
|
||||
long course = result.courses().getFirst().id();
|
||||
int completed = Math.min(4, Math.ceilDiv((int) (course - 199L), batchSize) * batchSize);
|
||||
for (int outlet = 0; outlet < 4; outlet++) {
|
||||
assertEquals("Every admitted trial provider must finish before publication", outlet < completed ? 1 : 0, builds.get(outlet));
|
||||
}
|
||||
submitted.add(course);
|
||||
return course == 200L
|
||||
? new CrossTilePublicationAdmission(EMPTY, List.of(diagnostic(900L)), true)
|
||||
: new CrossTilePublicationAdmission(result, List.of(), false);
|
||||
}
|
||||
};
|
||||
HydrologyOwnerDraft draft = run(fixture, admission);
|
||||
HydrologyOwnerDraft draft = run(fixture, admission, 4);
|
||||
|
||||
assertEquals(List.of(200L, 201L), submitted);
|
||||
assertEquals(List.of(201L), draft.result().courses().stream().map(RiverCourse::id).toList());
|
||||
assertEquals(List.of(200L, 900L, 201L), draft.diagnostics().stream().map(HydrologyDiagnosticCandidate::id).toList());
|
||||
for (int outlet = 0; outlet < 4; outlet++) {
|
||||
assertEquals(1, builds.get(outlet));
|
||||
int completed = Math.min(4, Math.ceilDiv(2, batchSize) * batchSize);
|
||||
assertEquals(outlet < completed ? 1 : 0, builds.get(outlet));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 15000)
|
||||
public void limitedWorkersStopAfterTheFirstAcceptedOutlet() throws Exception {
|
||||
int[] poolSizes = Runtime.getRuntime().availableProcessors() <= 2 ? new int[]{1, 2, 16} : new int[]{1, 2};
|
||||
for (int parallelism : poolSizes) {
|
||||
AtomicIntegerArray builds = new AtomicIntegerArray(4);
|
||||
Fixture fixture = fixture(outlet -> builds.incrementAndGet(outlet));
|
||||
|
||||
HydrologyOwnerDraft draft = run(fixture, null, parallelism);
|
||||
|
||||
assertEquals(List.of(200L), draft.result().courses().stream().map(RiverCourse::id).toList());
|
||||
assertEquals(List.of(200L), draft.diagnostics().stream().map(HydrologyDiagnosticCandidate::id).toList());
|
||||
assertEquals(1, builds.get(0));
|
||||
for (int outlet = 1; outlet < 4; outlet++) {
|
||||
assertEquals("Unused outlets must not be compiled", 0, builds.get(outlet));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 15000)
|
||||
public void rejectedOutletsAdvanceToTheNextWorkerBoundedBatch() throws Exception {
|
||||
int batchSize = 1;
|
||||
AtomicIntegerArray builds = new AtomicIntegerArray(4);
|
||||
Fixture fixture = fixture(outlet -> {
|
||||
builds.incrementAndGet(outlet);
|
||||
if (outlet == 3) {
|
||||
throw new IllegalStateException("Unused fallback provider failed");
|
||||
}
|
||||
});
|
||||
ArrayList<Long> submitted = new ArrayList<>();
|
||||
CrossTileDraftAdmission admission = new CrossTileDraftAdmission() {
|
||||
@Override
|
||||
public void prepare() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CrossTilePublicationAdmission admit(HydrologyCaveCourseFilter.Result result) {
|
||||
if (result.courses().isEmpty()) {
|
||||
return new CrossTilePublicationAdmission(result, List.of(), false);
|
||||
}
|
||||
long course = result.courses().getFirst().id();
|
||||
submitted.add(course);
|
||||
for (int outlet = 0; outlet < 4; outlet++) {
|
||||
int completed = Math.min(4, Math.ceilDiv((int) (course - 199L), batchSize) * batchSize);
|
||||
int expected = outlet < completed ? 1 : 0;
|
||||
assertEquals("Only the demanded batch may be compiled", expected, builds.get(outlet));
|
||||
}
|
||||
return course < 202L
|
||||
? new CrossTilePublicationAdmission(EMPTY, List.of(diagnostic(course + 700L)), true)
|
||||
: new CrossTilePublicationAdmission(result, List.of(), false);
|
||||
}
|
||||
};
|
||||
|
||||
HydrologyOwnerDraft draft = run(fixture, admission, 2);
|
||||
|
||||
assertEquals(List.of(200L, 201L, 202L), submitted);
|
||||
assertEquals(List.of(202L), draft.result().courses().stream().map(RiverCourse::id).toList());
|
||||
assertEquals(List.of(200L, 900L, 201L, 901L, 202L),
|
||||
draft.diagnostics().stream().map(HydrologyDiagnosticCandidate::id).toList());
|
||||
for (int outlet = 0; outlet < 4; outlet++) {
|
||||
int completed = Math.min(4, Math.ceilDiv(3, batchSize) * batchSize);
|
||||
assertEquals("Rejected trials must not be rebuilt", outlet < completed ? 1 : 0, builds.get(outlet));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,8 +163,11 @@ public class HydrologyFallbackParallelismTest {
|
||||
try {
|
||||
Future<HydrologyOwnerDraft> pending = pool.submit(() -> fixture.resolver().compileOwnerDraft(KEY, null, false));
|
||||
assertTrue(failed.await(5, TimeUnit.SECONDS));
|
||||
assertTrue(blocked.await(5, TimeUnit.SECONDS));
|
||||
assertFalse(pending.isDone());
|
||||
if (HydrologyPlanningAdmission.maximumTrials() > 1
|
||||
&& HydrologyPlanningAdmission.effectiveParallelism(4) > 2) {
|
||||
assertTrue(blocked.await(5, TimeUnit.SECONDS));
|
||||
assertFalse(pending.isDone());
|
||||
}
|
||||
release.countDown();
|
||||
ExecutionException failure = assertThrows(ExecutionException.class, () -> pending.get(5, TimeUnit.SECONDS));
|
||||
Throwable cause = failure.getCause();
|
||||
@@ -104,7 +175,8 @@ public class HydrologyFallbackParallelismTest {
|
||||
cause = cause.getCause();
|
||||
}
|
||||
assertSame(expected, cause);
|
||||
assertTrue(completed.get());
|
||||
assertEquals(HydrologyPlanningAdmission.maximumTrials() > 1
|
||||
&& HydrologyPlanningAdmission.effectiveParallelism(4) > 2, completed.get());
|
||||
} finally {
|
||||
release.countDown();
|
||||
pool.shutdownNow();
|
||||
@@ -112,8 +184,8 @@ public class HydrologyFallbackParallelismTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static HydrologyOwnerDraft run(Fixture fixture, CrossTileDraftAdmission admission) throws Exception {
|
||||
ForkJoinPool pool = new ForkJoinPool(4);
|
||||
private static HydrologyOwnerDraft run(Fixture fixture, CrossTileDraftAdmission admission, int parallelism) throws Exception {
|
||||
ForkJoinPool pool = new ForkJoinPool(parallelism);
|
||||
try {
|
||||
return pool.submit(() -> fixture.resolver().compileOwnerDraft(KEY, admission, false)).get(10, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HydrologyLandHeightCacheTest {
|
||||
@Test
|
||||
public void evictionAndFailuresRetainExactScalarValues() {
|
||||
HydrologyLandHeightCache cache = new HydrologyLandHeightCache(1);
|
||||
HydrologyNaturalTerrainSampler sampler = mock(HydrologyNaturalTerrainSampler.class);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
when(sampler.sampleLandHeight(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
calls.incrementAndGet();
|
||||
int x = invocation.getArgument(0);
|
||||
return x == Integer.MIN_VALUE ? Double.NaN : -0D;
|
||||
});
|
||||
assertEquals(Double.NaN, cache.sample(Integer.MIN_VALUE, Integer.MAX_VALUE, sampler), 0D);
|
||||
assertEquals(Double.NaN, cache.sample(Integer.MIN_VALUE, Integer.MAX_VALUE, sampler), 0D);
|
||||
assertEquals(1, calls.get());
|
||||
assertEquals(Double.doubleToRawLongBits(-0D), Double.doubleToRawLongBits(cache.sample(0, 0, sampler)));
|
||||
assertEquals(Double.doubleToRawLongBits(-0D), Double.doubleToRawLongBits(cache.sample(0, 0, sampler)));
|
||||
assertEquals(2, calls.get());
|
||||
assertEquals(Double.NaN, cache.sample(Integer.MIN_VALUE, Integer.MAX_VALUE, sampler), 0D);
|
||||
assertEquals(3, calls.get());
|
||||
when(sampler.sampleLandHeight(7, 11)).thenThrow(new IllegalStateException("retry")).thenReturn(83D);
|
||||
assertThrows(IllegalStateException.class, () -> cache.sample(7, 11, sampler));
|
||||
assertEquals(83D, cache.sample(7, 11, sampler), 0D);
|
||||
assertEquals(83D, cache.sample(7, 11, sampler), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parallelAlternativesShareOneMissingSample() throws Exception {
|
||||
HydrologyLandHeightCache cache = new HydrologyLandHeightCache(64);
|
||||
HydrologyNaturalTerrainSampler sampler = mock(HydrologyNaturalTerrainSampler.class);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
CountDownLatch ready = new CountDownLatch(4);
|
||||
when(sampler.sampleLandHeight(-19, 33)).thenAnswer(invocation -> {
|
||||
calls.incrementAndGet();
|
||||
started.countDown();
|
||||
assertTrue(release.await(5, TimeUnit.SECONDS));
|
||||
return Double.NaN;
|
||||
});
|
||||
ExecutorService executor = Executors.newFixedThreadPool(4);
|
||||
try {
|
||||
List<Future<Double>> results = new ArrayList<>();
|
||||
for (int index = 0; index < 4; index++) {
|
||||
results.add(executor.submit(() -> {
|
||||
ready.countDown();
|
||||
return cache.sample(-19, 33, sampler);
|
||||
}));
|
||||
}
|
||||
assertTrue(ready.await(5, TimeUnit.SECONDS));
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
assertEquals(1, calls.get());
|
||||
release.countDown();
|
||||
for (Future<Double> result : results) {
|
||||
assertEquals(Double.NaN, result.get(5, TimeUnit.SECONDS), 0D);
|
||||
}
|
||||
assertEquals(1, calls.get());
|
||||
} finally {
|
||||
release.countDown();
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -78,6 +78,90 @@ public class HydrologyPlannerRoutingSamplerTest {
|
||||
assertThrows(IllegalStateException.class, () -> planner.plan(new HydrologyTileKey(0, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void landHeightMemoizationRetainsMissingSamplesOnlyWithinThePlanningScope() {
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
AtomicInteger failedCalls = new AtomicInteger();
|
||||
HydrologyNaturalTerrainSampler natural = new HydrologyNaturalTerrainSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
|
||||
throw new AssertionError("Bank heights do not need a routing grid");
|
||||
}
|
||||
|
||||
@Override
|
||||
public NaturalClassification classifyNatural(int x, int z) {
|
||||
throw new AssertionError("Bank height provider already classifies land");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample sampleBasis(int x, int z) {
|
||||
return switch (x) {
|
||||
case 10 -> HydrologyTerrainSample.openLand(87, 0D, "land");
|
||||
case 11 -> HydrologyTerrainSample.ocean(60, "ocean");
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public double sampleLandHeight(int x, int z) {
|
||||
heightCalls.incrementAndGet();
|
||||
if (x == 4 && failedCalls.incrementAndGet() == 1) {
|
||||
throw new IllegalStateException("Transient height failure");
|
||||
}
|
||||
return switch (x) {
|
||||
case 0 -> Double.NaN;
|
||||
case 1 -> 0D;
|
||||
case 2 -> Integer.MIN_VALUE;
|
||||
default -> 83D;
|
||||
};
|
||||
}
|
||||
};
|
||||
HydrologyTerrainSampler detail = (x, z) -> HydrologyTerrainSample.openLand(90, 0D, "land");
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlanner planner = new HydrologyPlanner(23L, settings, detail, natural,
|
||||
HydrologyGeometrySampler.deterministic(detail), -4096,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(detail, settings.seaLevel(), -4096, 4096));
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||
try {
|
||||
for (int pass = 0; pass < 3; pass++) {
|
||||
assertEquals(Double.NaN, planner.sampleLandHeight(0, -17), 0D);
|
||||
assertEquals(0D, planner.sampleLandHeight(1, -17), 0D);
|
||||
assertEquals(Integer.MIN_VALUE, planner.sampleLandHeight(2, -17), 0D);
|
||||
assertEquals(83D, planner.sampleLandHeight(3, -17), 0D);
|
||||
}
|
||||
assertEquals(4, heightCalls.get());
|
||||
planner.sampleBasis(10, -17);
|
||||
planner.sampleBasisWithoutSlope(11, -17);
|
||||
planner.sampleBasisWithoutSlope(12, -17);
|
||||
assertEquals(87D, planner.sampleLandHeight(10, -17), 0D);
|
||||
assertEquals(Double.NaN, planner.sampleLandHeight(11, -17), 0D);
|
||||
assertEquals(Double.NaN, planner.sampleLandHeight(12, -17), 0D);
|
||||
assertEquals(4, heightCalls.get());
|
||||
assertThrows(IllegalStateException.class, () -> planner.sampleLandHeight(4, -17));
|
||||
assertEquals(83D, planner.sampleLandHeight(4, -17), 0D);
|
||||
assertEquals(83D, planner.sampleLandHeight(4, -17), 0D);
|
||||
assertEquals(6, heightCalls.get());
|
||||
HydrologyLandHeightCache shared = planner.planningSamples.get().fallbackLandHeights();
|
||||
for (int trial = 0; trial < 3; trial++) {
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples(shared));
|
||||
assertEquals(Double.NaN, planner.sampleLandHeight(0, -17), 0D);
|
||||
assertEquals(0D, planner.sampleLandHeight(1, -17), 0D);
|
||||
assertEquals(Integer.MIN_VALUE, planner.sampleLandHeight(2, -17), 0D);
|
||||
assertEquals(83D, planner.sampleLandHeight(3, -17), 0D);
|
||||
assertEquals(83D, planner.sampleLandHeight(4, -17), 0D);
|
||||
}
|
||||
assertEquals(6, heightCalls.get());
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||
assertEquals(Double.NaN, planner.sampleLandHeight(0, -17), 0D);
|
||||
assertEquals(7, heightCalls.get());
|
||||
} finally {
|
||||
planner.planningSamples.remove();
|
||||
}
|
||||
assertEquals(Double.NaN, planner.sampleLandHeight(0, -17), 0D);
|
||||
assertEquals(Double.NaN, planner.sampleLandHeight(0, -17), 0D);
|
||||
assertEquals(9, heightCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oceanClassificationBypassesNaturalBasisSampling() {
|
||||
AtomicInteger basisCalls = new AtomicInteger();
|
||||
|
||||
@@ -95,6 +95,26 @@ public class HydrologyPlannerTest {
|
||||
return draft.invoke(resolution);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void independentOwnersReleaseCompilerWithoutChangingPublication() {
|
||||
HydrologyPlanner rootPlanner = new HydrologyPlanner(77L, EARLY_OWNER_SETTINGS, EARLY_OWNER_TERRAIN);
|
||||
CrossTileResolution root = rootPlanner.resolveCrossTileOwner(EARLY_OWNER_TILE);
|
||||
HydrologyFootprintCompiler rootCompiler = root.draft().footprintCompiler();
|
||||
assertNotNull(rootCompiler);
|
||||
assertFalse(root.draft().result().courses().isEmpty());
|
||||
|
||||
HydrologyPlanner dependencyPlanner = new HydrologyPlanner(77L, EARLY_OWNER_SETTINGS, EARLY_OWNER_TERRAIN);
|
||||
CrossTileResolvedOwner dependency = dependencyPlanner.resolveIndependentOwner(EARLY_OWNER_TILE);
|
||||
assertNull(dependency.draft().footprintCompiler());
|
||||
assertEquals(root.draft().withoutFootprintCompiler(), dependency.draft());
|
||||
assertEquals(root.observedRejections(), dependency.observedRejections());
|
||||
|
||||
int materializations = rootCompiler.fullMaterializationCount();
|
||||
assertTileContentsEqual(EARLY_OWNER_BASELINE, rootPlanner.crossTile.materializeAcceptedTile(root));
|
||||
assertTrue(rootCompiler.fullMaterializationCount() > materializations);
|
||||
assertTileContentsEqual(EARLY_OWNER_BASELINE, dependencyPlanner.plan(EARLY_OWNER_TILE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collidingRoutingContextKeysCompileIndependently() throws Exception {
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
@@ -251,8 +271,44 @@ public class HydrologyPlannerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void limitedWorkersSkipUnusedOwnersButResolveDemandedOwners() throws Exception {
|
||||
HydrologyTileKey ownerKey = new HydrologyTileKey(-1, -1);
|
||||
List<HydrologyTileKey> demanded = List.of(new HydrologyTileKey(-2, -2), new HydrologyTileKey(0, 0));
|
||||
int[] poolSizes = Runtime.getRuntime().availableProcessors() <= 2 ? new int[]{1, 2, 16} : new int[]{1, 2};
|
||||
for (int workers : poolSizes) {
|
||||
AtomicInteger grids = new AtomicInteger();
|
||||
HydrologyPlannerSettings settings = withPeriodTwo(standardSettings(4D, 2D, true, false, List.of()));
|
||||
HydrologyTerrainSample blocked = blockedTerrain();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> blocked;
|
||||
HydrologyPlanner planner = new HydrologyPlanner(77L, settings, terrain,
|
||||
new ContextRoutingSampler(blocked, request -> grids.incrementAndGet()),
|
||||
HydrologyGeometrySampler.deterministic(terrain), -4096,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(terrain, settings.seaLevel(), -4096, 4096));
|
||||
AutoCloseable admission = earlyAdmission(planner, ownerKey);
|
||||
try (ForkJoinPool pool = new ForkJoinPool(workers)) {
|
||||
pool.submit(() -> {
|
||||
((CrossTileDraftAdmission) admission).prepare();
|
||||
admission.close();
|
||||
return null;
|
||||
}).get(5, TimeUnit.SECONDS);
|
||||
assertTrue(earlyTasks(admission).isEmpty());
|
||||
assertTrue(cachedOwnerKeys(planner).isEmpty());
|
||||
assertEquals("Unused owner terrain must remain unsampled", 0, grids.get());
|
||||
|
||||
List<CrossTileResolvedOwner> resolved = pool.submit(() -> planner.resolveLowerRankOwners(
|
||||
demanded, new CrossTileResolutionContext(ownerKey, 64L, 4096), Map.of()
|
||||
)).get(30, TimeUnit.SECONDS);
|
||||
assertEquals(demanded, resolved.stream().map(owner -> owner.draft().key()).toList());
|
||||
assertEquals(Set.copyOf(demanded), cachedOwnerKeys(planner));
|
||||
assertEquals(demanded.size(), grids.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void earlyOwnersPrioritizeHigherRanksAndSkipRunningOwners() throws Exception {
|
||||
org.junit.Assume.assumeTrue(Runtime.getRuntime().availableProcessors() > 2);
|
||||
HydrologyPlannerSettings settings = withPeriodTwo(standardSettings(4D, 2D, true, false, List.of()));
|
||||
HydrologyPlanner planner = new HydrologyPlanner(77L, settings, (x, z) -> blockedTerrain());
|
||||
AutoCloseable admission = earlyAdmission(planner, new HydrologyTileKey(-1, -1));
|
||||
@@ -287,7 +343,7 @@ public class HydrologyPlannerTest {
|
||||
private final ArrayList<Runnable> submitted = new ArrayList<>();
|
||||
|
||||
private CapturingHydrologyPool() {
|
||||
super(1);
|
||||
super(3);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -337,7 +393,9 @@ public class HydrologyPlannerTest {
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
}
|
||||
assertTrue("Expected selected sources to prepare lower-rank owners", observedParallelOwner);
|
||||
if (HydrologyPlanningAdmission.effectiveParallelism(4) > 2) {
|
||||
assertTrue("Expected selected sources to prepare lower-rank owners", observedParallelOwner);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.testsupport.Await;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HydrologyPlanningAdmissionIntegrationTest {
|
||||
@Test(timeout = 15000)
|
||||
public void separateCachesShareTheProcessPlanningBudget() throws Exception {
|
||||
ArrayList<HydrologyPlanningAdmission.Permit> held = holdRoots(HydrologyPlanningAdmission.maximumRoots() - 1);
|
||||
ExecutorService workers = Executors.newFixedThreadPool(2);
|
||||
ExecutorService callers = Executors.newFixedThreadPool(2);
|
||||
CountDownLatch firstStarted = new CountDownLatch(1);
|
||||
CountDownLatch releaseFirst = new CountDownLatch(1);
|
||||
CountDownLatch secondStarted = new CountDownLatch(1);
|
||||
AtomicInteger active = new AtomicInteger();
|
||||
AtomicInteger peak = new AtomicInteger();
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyPlanner firstPlanner = mock(HydrologyPlanner.class);
|
||||
HydrologyPlanner secondPlanner = mock(HydrologyPlanner.class);
|
||||
HydrologyTileKey key = new HydrologyTileKey(0, 0);
|
||||
when(firstPlanner.plan(key)).thenAnswer(invocation -> {
|
||||
peak.accumulateAndGet(active.incrementAndGet(), Math::max);
|
||||
firstStarted.countDown();
|
||||
assertTrue(releaseFirst.await(5L, TimeUnit.SECONDS));
|
||||
active.decrementAndGet();
|
||||
return tile;
|
||||
});
|
||||
when(secondPlanner.plan(key)).thenAnswer(invocation -> {
|
||||
peak.accumulateAndGet(active.incrementAndGet(), Math::max);
|
||||
secondStarted.countDown();
|
||||
active.decrementAndGet();
|
||||
return tile;
|
||||
});
|
||||
HydrologyTileCache firstCache = new HydrologyTileCache(firstPlanner, 4, workers);
|
||||
HydrologyTileCache secondCache = new HydrologyTileCache(secondPlanner, 4, workers);
|
||||
try {
|
||||
Future<HydrologyTile> first = callers.submit(() -> firstCache.get(key));
|
||||
assertTrue(firstStarted.await(5L, TimeUnit.SECONDS));
|
||||
Future<HydrologyTile> second = callers.submit(() -> secondCache.get(key));
|
||||
awaitLoad(secondCache);
|
||||
assertFalse(secondStarted.await(150L, TimeUnit.MILLISECONDS));
|
||||
releaseFirst.countDown();
|
||||
assertSame(tile, first.get(5L, TimeUnit.SECONDS));
|
||||
assertSame(tile, second.get(5L, TimeUnit.SECONDS));
|
||||
assertEquals(1, peak.get());
|
||||
assertEquals(0, active.get());
|
||||
} finally {
|
||||
releaseFirst.countDown();
|
||||
releaseRoots(held);
|
||||
workers.shutdownNow();
|
||||
callers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
assertTrue(callers.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
firstCache.close();
|
||||
secondCache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 15000)
|
||||
public void closingAWaitingCacheCancelsAdmissionWithoutPublishingAnEmptyTile() throws Exception {
|
||||
ArrayList<HydrologyPlanningAdmission.Permit> held = holdRoots(HydrologyPlanningAdmission.maximumRoots());
|
||||
ExecutorService workers = Executors.newSingleThreadExecutor();
|
||||
ExecutorService callers = Executors.newFixedThreadPool(2);
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 4, workers);
|
||||
HydrologyTileKey key = new HydrologyTileKey(0, 0);
|
||||
try {
|
||||
Future<HydrologyTile> pending = callers.submit(() -> cache.get(key));
|
||||
awaitLoad(cache);
|
||||
callers.submit(cache::close).get(2L, TimeUnit.SECONDS);
|
||||
ExecutionException failure = assertThrows(ExecutionException.class,
|
||||
() -> pending.get(2L, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getCause() instanceof CancellationException);
|
||||
verify(planner, never()).plan(any(HydrologyTileKey.class));
|
||||
verify(planner, never()).emptyTile(any(HydrologyTileKey.class));
|
||||
assertEquals(0, cache.size());
|
||||
releaseRoots(held);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyPlanner nextPlanner = mock(HydrologyPlanner.class);
|
||||
when(nextPlanner.plan(key)).thenReturn(tile);
|
||||
try (HydrologyTileCache next = new HydrologyTileCache(nextPlanner, 4, workers)) {
|
||||
assertSame(tile, callers.submit(() -> next.get(key)).get(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
} finally {
|
||||
releaseRoots(held);
|
||||
workers.shutdownNow();
|
||||
callers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
assertTrue(callers.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static ArrayList<HydrologyPlanningAdmission.Permit> holdRoots(int count) {
|
||||
ArrayList<HydrologyPlanningAdmission.Permit> held = new ArrayList<>(count);
|
||||
for (int index = 0; index < count; index++) {
|
||||
held.add(HydrologyPlanningAdmission.acquireRoot(() -> false));
|
||||
}
|
||||
return held;
|
||||
}
|
||||
|
||||
private static void releaseRoots(ArrayList<HydrologyPlanningAdmission.Permit> held) {
|
||||
for (HydrologyPlanningAdmission.Permit permit : held) {
|
||||
permit.close();
|
||||
}
|
||||
held.clear();
|
||||
}
|
||||
|
||||
private static void awaitLoad(HydrologyTileCache cache) throws Exception {
|
||||
Field field = HydrologyTileCache.class.getDeclaredField("loading");
|
||||
field.setAccessible(true);
|
||||
Map<?, ?> loading = (Map<?, ?>) field.get(cache);
|
||||
Await.until("the cache load to start", Duration.ofSeconds(5L), () -> !loading.isEmpty());
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyPlanningAdmissionTest {
|
||||
@Test
|
||||
public void rootLimitAccountsForHeapAndProcessors() {
|
||||
long gibibyte = 1024L * 1024L * 1024L;
|
||||
assertEquals(1, HydrologyPlanningAdmission.rootLimit(gibibyte, 32));
|
||||
assertEquals(1, HydrologyPlanningAdmission.rootLimit(2L * gibibyte, 32));
|
||||
assertEquals(2, HydrologyPlanningAdmission.rootLimit(4L * gibibyte, 32));
|
||||
assertEquals(2, HydrologyPlanningAdmission.rootLimit(64L * gibibyte, 2));
|
||||
assertEquals(8, HydrologyPlanningAdmission.rootLimit(64L * gibibyte, 32));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlanningAdmission(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void speculativeParallelismUsesBothWorkersAndAvailableProcessors() {
|
||||
int[][] cases = {
|
||||
{16, 1, 1}, {16, 2, 2}, {16, 4, 4}, {16, 32, 16},
|
||||
{1, 16, 1}, {2, 16, 2}, {3, 16, 3}, {4, 16, 4},
|
||||
{-1, 16, 1}, {16, 0, 1}
|
||||
};
|
||||
for (int[] parameters : cases) {
|
||||
assertEquals("pool=" + parameters[0] + " processors=" + parameters[1], parameters[2],
|
||||
HydrologyPlanningAdmission.effectiveParallelism(parameters[0], parameters[1]));
|
||||
}
|
||||
assertEquals(Runtime.getRuntime().availableProcessors(),
|
||||
HydrologyPlanningAdmission.effectiveParallelism(Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void trialLimitSharesWorkingMemoryBetweenAdmittedRoots() {
|
||||
long gibibyte = 1024L * 1024L * 1024L;
|
||||
assertEquals(1, HydrologyPlanningAdmission.trialLimit(1280L * 1024L * 1024L, 1));
|
||||
assertEquals(2, HydrologyPlanningAdmission.trialLimit(2L * gibibyte, 1));
|
||||
assertEquals(2, HydrologyPlanningAdmission.trialLimit(4L * gibibyte, 2));
|
||||
assertEquals(1, HydrologyPlanningAdmission.trialLimit(gibibyte / 2L, 1));
|
||||
assertEquals(8, HydrologyPlanningAdmission.trialLimit(64L * gibibyte, 8));
|
||||
assertEquals(16, HydrologyPlanningAdmission.trialLimit(Long.MAX_VALUE, 8));
|
||||
assertThrows(IllegalArgumentException.class, () -> HydrologyPlanningAdmission.trialLimit(gibibyte, 0));
|
||||
}
|
||||
|
||||
@Test(timeout = 15000)
|
||||
public void independentCallersShareTheSameRootBudget() throws Exception {
|
||||
HydrologyPlanningAdmission admission = new HydrologyPlanningAdmission(2);
|
||||
AtomicInteger running = new AtomicInteger();
|
||||
AtomicInteger peak = new AtomicInteger();
|
||||
CountDownLatch firstBatch = new CountDownLatch(2);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
ExecutorService callers = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
ArrayList<Future<?>> results = new ArrayList<>();
|
||||
for (int index = 0; index < 8; index++) {
|
||||
results.add(callers.submit(() -> {
|
||||
try (HydrologyPlanningAdmission.Permit ignored = admission.acquire(() -> false)) {
|
||||
peak.accumulateAndGet(running.incrementAndGet(), Math::max);
|
||||
firstBatch.countDown();
|
||||
await(release);
|
||||
running.decrementAndGet();
|
||||
}
|
||||
}));
|
||||
}
|
||||
assertTrue(firstBatch.await(5L, TimeUnit.SECONDS));
|
||||
assertEquals(2, running.get());
|
||||
release.countDown();
|
||||
for (Future<?> result : results) {
|
||||
result.get(5L, TimeUnit.SECONDS);
|
||||
}
|
||||
assertEquals(2, peak.get());
|
||||
assertEquals(0, running.get());
|
||||
} finally {
|
||||
release.countDown();
|
||||
callers.shutdownNow();
|
||||
assertTrue(callers.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 15000)
|
||||
public void waitingRootsLeaveWorkersForNestedPlanning() throws Exception {
|
||||
HydrologyPlanningAdmission admission = new HydrologyPlanningAdmission(1);
|
||||
ForkJoinPool pool = new ForkJoinPool(2);
|
||||
CountDownLatch firstRoot = new CountDownLatch(1);
|
||||
CountDownLatch secondWaiting = new CountDownLatch(1);
|
||||
CountDownLatch childCompleted = new CountDownLatch(1);
|
||||
try {
|
||||
Future<List<Integer>> first = pool.submit(() -> {
|
||||
try (HydrologyPlanningAdmission.Permit ignored = admission.acquire(() -> false)) {
|
||||
firstRoot.countDown();
|
||||
await(secondWaiting);
|
||||
return HydrologyForkJoin.invokeAll(List.of(() -> {
|
||||
ForkJoinPool.managedBlock(new ForkJoinPool.ManagedBlocker() {
|
||||
@Override
|
||||
public boolean isReleasable() {
|
||||
return childCompleted.getCount() == 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean block() {
|
||||
await(childCompleted);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return 1;
|
||||
}, () -> {
|
||||
childCompleted.countDown();
|
||||
return 2;
|
||||
}), null);
|
||||
}
|
||||
});
|
||||
assertTrue(firstRoot.await(5L, TimeUnit.SECONDS));
|
||||
Future<Integer> second = pool.submit(() -> {
|
||||
secondWaiting.countDown();
|
||||
try (HydrologyPlanningAdmission.Permit ignored = admission.acquire(() -> false)) {
|
||||
return 3;
|
||||
}
|
||||
});
|
||||
assertEquals(List.of(1, 2), first.get(5L, TimeUnit.SECONDS));
|
||||
assertEquals(Integer.valueOf(3), second.get(5L, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
assertTrue(pool.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 15000)
|
||||
public void interruptedWaiterExitsWithoutLeakingOrReleasingAnotherRoot() throws Exception {
|
||||
HydrologyPlanningAdmission admission = new HydrologyPlanningAdmission(1);
|
||||
ExecutorService caller = Executors.newSingleThreadExecutor();
|
||||
AtomicReference<Thread> waitingThread = new AtomicReference<>();
|
||||
CountDownLatch waiting = new CountDownLatch(1);
|
||||
try (HydrologyPlanningAdmission.Permit held = admission.acquire(() -> false)) {
|
||||
Future<Boolean> interrupted = caller.submit(() -> {
|
||||
waitingThread.set(Thread.currentThread());
|
||||
waiting.countDown();
|
||||
assertThrows(CancellationException.class, () -> admission.acquire(() -> false));
|
||||
return Thread.currentThread().isInterrupted();
|
||||
});
|
||||
assertTrue(waiting.await(5L, TimeUnit.SECONDS));
|
||||
waitingThread.get().interrupt();
|
||||
assertTrue(interrupted.get(5L, TimeUnit.SECONDS));
|
||||
Future<?> next = caller.submit(() -> {
|
||||
try (HydrologyPlanningAdmission.Permit ignored = admission.acquire(() -> false)) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
assertThrows(TimeoutException.class, () -> next.get(150L, TimeUnit.MILLISECONDS));
|
||||
held.close();
|
||||
next.get(5L, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
caller.shutdownNow();
|
||||
assertTrue(caller.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 15000)
|
||||
public void closingWhileQueuedCancelsWithoutWaitingForTheActiveRoot() throws Exception {
|
||||
HydrologyPlanningAdmission admission = new HydrologyPlanningAdmission(1);
|
||||
AtomicBoolean closed = new AtomicBoolean();
|
||||
AtomicBoolean entered = new AtomicBoolean();
|
||||
CountDownLatch waiting = new CountDownLatch(1);
|
||||
ExecutorService caller = Executors.newSingleThreadExecutor();
|
||||
try (HydrologyPlanningAdmission.Permit ignored = admission.acquire(() -> false)) {
|
||||
Future<?> cancelled = caller.submit(() -> {
|
||||
waiting.countDown();
|
||||
assertThrows(CancellationException.class, () -> {
|
||||
try (HydrologyPlanningAdmission.Permit unused = admission.acquire(closed::get)) {
|
||||
entered.set(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
assertTrue(waiting.await(5L, TimeUnit.SECONDS));
|
||||
closed.set(true);
|
||||
cancelled.get(2L, TimeUnit.SECONDS);
|
||||
assertFalse(entered.get());
|
||||
} finally {
|
||||
caller.shutdownNow();
|
||||
assertTrue(caller.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
try (HydrologyPlanningAdmission.Permit ignored = admission.acquire(() -> false)) {
|
||||
assertThrows(CancellationException.class, () -> admission.acquire(() -> true));
|
||||
}
|
||||
}
|
||||
|
||||
private static void await(CountDownLatch latch) {
|
||||
try {
|
||||
assertTrue("Timed out awaiting planning", latch.await(5L, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException interruption) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError(interruption);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -15,7 +15,7 @@ import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyRegionalCacheTest {
|
||||
@Test
|
||||
public void sharedTerrainRetentionIsIndependentOfEachSearchBudget() {
|
||||
public void searchBudgetDoesNotLimitSubsequentSearchesOrTerrainResults() {
|
||||
AtomicInteger reads = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
reads.incrementAndGet();
|
||||
@@ -29,17 +29,17 @@ public class HydrologyRegionalCacheTest {
|
||||
}
|
||||
assertNull(first.sample(65536, 0));
|
||||
HydrologyRegionalTerrainRefiner.Samples second = refiner.new Samples();
|
||||
for (int x = 65536; x < 100000; x++) {
|
||||
for (int x = 65536; x < 70000; x++) {
|
||||
assertNotNull(second.sample(x, 0));
|
||||
}
|
||||
assertEquals(100000, reads.get());
|
||||
for (int x = 0; x < 100000; x++) {
|
||||
assertEquals(70000, reads.get());
|
||||
for (int x = 0; x < 70000; x++) {
|
||||
assertEquals(70, refiner.sample(x, 0).naturalHeight());
|
||||
}
|
||||
assertEquals(100000, reads.get());
|
||||
int readsBeforeClear = reads.get();
|
||||
refiner.clear();
|
||||
assertEquals(70, refiner.sample(0, 0).naturalHeight());
|
||||
assertEquals(100001, reads.get());
|
||||
assertEquals(readsBeforeClear + 1, reads.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+30
@@ -9,6 +9,8 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
@@ -31,6 +33,34 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HydrologyRegionalSourceParallelismTest {
|
||||
@Test(timeout = 15000)
|
||||
public void boundedExecutionPreservesTheWholePreselectedSourceWindow() throws Exception {
|
||||
int maximumTrials = HydrologyPlanningAdmission.maximumTrials();
|
||||
int requiredParallelism = Math.min(4, maximumTrials);
|
||||
CountDownLatch firstBatch = new CountDownLatch(requiredParallelism);
|
||||
AtomicInteger active = new AtomicInteger();
|
||||
AtomicInteger peak = new AtomicInteger();
|
||||
Set<Integer> visited = ConcurrentHashMap.newKeySet();
|
||||
Fixture fixture = fixture(4, source -> {
|
||||
peak.accumulateAndGet(active.incrementAndGet(), Math::max);
|
||||
visited.add(source);
|
||||
firstBatch.countDown();
|
||||
try {
|
||||
await(firstBatch);
|
||||
return source == 0 || source == 4 ? accepted(source) : rejected(source);
|
||||
} finally {
|
||||
active.decrementAndGet();
|
||||
}
|
||||
});
|
||||
HydrologyRegionalNetwork network = run(fixture);
|
||||
|
||||
assertEquals(List.of(100L, 104L), network.courses().stream().map(RiverCourse::id).toList());
|
||||
assertEquals(Set.of(0, 1, 2, 3, 4, 5, 6, 7), visited);
|
||||
assertTrue(peak.get() >= requiredParallelism);
|
||||
assertTrue(peak.get() <= maximumTrials);
|
||||
assertEquals(0, active.get());
|
||||
}
|
||||
|
||||
@Test(timeout = 15000)
|
||||
public void acceptsOutletsInSourceOrderWhenTheMiddleOfAWindowSucceeds() throws Exception {
|
||||
Fixture fixture = fixture(4, source -> source == 4 || source == 1 ? accepted(source) : rejected(source));
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
public class HydrologyRouteHeightProbeTest {
|
||||
@Test
|
||||
public void crossDropUsesScalarHeightsIncludingMissingAndExtremeValues() {
|
||||
HydrologyTerrainSample center = land(87);
|
||||
HydrologyTerrainSample[] sides = {null, HydrologyTerrainSample.ocean(20, "ocean"),
|
||||
land(20), land(100), land(Integer.MIN_VALUE), land(Integer.MAX_VALUE)};
|
||||
for (HydrologyTerrainSample side : sides) {
|
||||
Fixture fixture = fixture((x, z) -> side);
|
||||
int lowest = side == null || side.ocean() ? 87 : Math.min(87, side.naturalHeight());
|
||||
double expected = (double) (87 - lowest) * fixture.planner().settings.routing().valleyPreference()
|
||||
* HydrologyRouteGeometry.CROSS_DROP_WEIGHT;
|
||||
double actual = fixture.planner().routeGeometry.crossDropPenalty(
|
||||
new HydrologyPoint(21, 87, -37), new RouteDirection(3D, 4D), center);
|
||||
assertEquals(Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(actual));
|
||||
assertEquals(4, fixture.scalarCalls().get());
|
||||
assertEquals(0, fixture.basisCalls().get());
|
||||
}
|
||||
Fixture untouched = fixture((x, z) -> land(20));
|
||||
assertEquals(0D, untouched.planner().routeGeometry.crossDropPenalty(
|
||||
new HydrologyPoint(0, 87, 0), new RouteDirection(0D, 0D), center), 0D);
|
||||
assertEquals(0, untouched.scalarCalls().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pitChecksRetainMissingAndIntegerDifferenceSemantics() {
|
||||
int threshold = HydrologyPlannerSettings.defaults().hydraulics().waterfallMinimumDrop();
|
||||
HydrologyTerrainSample blocked = spy(land(threshold));
|
||||
doReturn(false).when(blocked).transitAllowed();
|
||||
HydrologyTerrainSample[] sides = {null, HydrologyTerrainSample.ocean(threshold, "ocean"),
|
||||
land(threshold - 1), land(threshold), land(Integer.MIN_VALUE), land(Integer.MAX_VALUE), blocked};
|
||||
for (int center : new int[]{0, Integer.MIN_VALUE, Integer.MAX_VALUE}) {
|
||||
for (HydrologyTerrainSample before : sides) {
|
||||
for (HydrologyTerrainSample after : sides) {
|
||||
Fixture fixture = fixture((x, z) -> x < 0 ? before : after);
|
||||
boolean expected = before != null && !before.ocean() && after != null && !after.ocean()
|
||||
&& before.naturalHeight() - center >= threshold
|
||||
&& after.naturalHeight() - center >= threshold;
|
||||
RouteCandidate candidate = new RouteCandidate(new HydrologyPoint(0, center, 0),
|
||||
0D, 0D, 0D, 0D, 0D, new RouteDirection(1D, 0D), true);
|
||||
assertEquals(expected, fixture.planner().routePaths.isTerrainPit(candidate));
|
||||
assertEquals(2, fixture.scalarCalls().get());
|
||||
assertEquals(0, fixture.basisCalls().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void crevassesUseScalarsWhileTransitionsStillCheckInteriorPolicy() {
|
||||
int threshold = HydrologyPlannerSettings.defaults().hydraulics().waterfallMinimumDrop();
|
||||
HydrologyTerrainSample blocked = spy(land(80));
|
||||
doReturn(false).when(blocked).transitAllowed();
|
||||
HydrologyTerrainSample[] terrain = {null, HydrologyTerrainSample.ocean(80, "ocean"),
|
||||
land(80), land(80 - threshold), land(81 - threshold), land(Integer.MIN_VALUE),
|
||||
land(Integer.MAX_VALUE), blocked};
|
||||
HydrologyPoint start = new HydrologyPoint(0, 80, 0);
|
||||
HydrologyPoint end = new HydrologyPoint(2, 80, 0);
|
||||
for (HydrologyTerrainSample endpoint : terrain) {
|
||||
for (HydrologyTerrainSample middle : terrain) {
|
||||
boolean endpointsMissing = endpoint == null || endpoint.ocean();
|
||||
boolean middleMissing = middle == null || middle.ocean();
|
||||
boolean crevasse = endpointsMissing || middleMissing
|
||||
|| endpoint.naturalHeight() - middle.naturalHeight() >= threshold;
|
||||
Fixture fixture = fixture((x, z) -> x == 1 ? middle : endpoint);
|
||||
assertEquals(crevasse, fixture.planner().routePaths.crossesTerrainCrevasse(start, end));
|
||||
assertEquals(endpointsMissing ? 2 : 3, fixture.scalarCalls().get());
|
||||
assertEquals(0, fixture.basisCalls().get());
|
||||
|
||||
fixture.scalarCalls().set(0);
|
||||
assertEquals(!crevasse && middle.transitAllowed(),
|
||||
fixture.planner().routePaths.traversableTerrainTransition(start, end));
|
||||
assertEquals(2, fixture.scalarCalls().get());
|
||||
assertEquals(endpointsMissing ? 0 : 1, fixture.basisCalls().get());
|
||||
}
|
||||
}
|
||||
Fixture adjacent = fixture((x, z) -> null);
|
||||
assertFalse(adjacent.planner().routePaths.crossesTerrainCrevasse(start, new HydrologyPoint(1, 80, 0)));
|
||||
assertEquals(0, adjacent.scalarCalls().get());
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample land(int height) {
|
||||
return HydrologyTerrainSample.openLand(height, 0D, "land");
|
||||
}
|
||||
|
||||
private static Fixture fixture(HydrologyTerrainSampler terrain) {
|
||||
AtomicInteger basisCalls = new AtomicInteger();
|
||||
AtomicInteger scalarCalls = new AtomicInteger();
|
||||
HydrologyNaturalTerrainSampler natural = new HydrologyNaturalTerrainSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
|
||||
throw new AssertionError("Height probes do not need a routing grid.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public NaturalClassification classifyNatural(int x, int z) {
|
||||
throw new AssertionError("Scalar probes already classify land.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample sampleBasis(int x, int z) {
|
||||
basisCalls.incrementAndGet();
|
||||
return terrain.sample(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double sampleLandHeight(int x, int z) {
|
||||
scalarCalls.incrementAndGet();
|
||||
HydrologyTerrainSample sampled = terrain.sample(x, z);
|
||||
return sampled == null || sampled.ocean() ? Double.NaN : sampled.naturalHeight();
|
||||
}
|
||||
};
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlanner planner = new HydrologyPlanner(91L, settings, terrain, natural,
|
||||
HydrologyGeometrySampler.deterministic(terrain), -4096,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(terrain, settings.seaLevel(), -4096, 4096));
|
||||
return new Fixture(planner, basisCalls, scalarCalls);
|
||||
}
|
||||
|
||||
private record Fixture(HydrologyPlanner planner, AtomicInteger basisCalls, AtomicInteger scalarCalls) {
|
||||
}
|
||||
}
|
||||
+24
-4
@@ -19,12 +19,12 @@ public class HydrologyRouteTurnCacheTest {
|
||||
@Test
|
||||
public void repeatedRefinementMatchesExhaustiveUncachedRoutes() throws Exception {
|
||||
Random random = new Random(712319L);
|
||||
Solver solver = new Solver();
|
||||
for (int scenario = 0; scenario < 40; scenario++) {
|
||||
Solver solver = new Solver();
|
||||
int count = 4 + random.nextInt(4);
|
||||
HydrologyPoint[][] points = new HydrologyPoint[count][];
|
||||
for (int layer = 0; layer < count; layer++) {
|
||||
points[layer] = new HydrologyPoint[layer == 0 || layer == count - 1 ? 1 : 3];
|
||||
points[layer] = new HydrologyPoint[layer == 0 || layer == count - 1 ? 1 : 1 + random.nextInt(4)];
|
||||
for (int candidate = 0; candidate < points[layer].length; candidate++) {
|
||||
points[layer][candidate] = new HydrologyPoint(layer * 12 + random.nextInt(5), 80,
|
||||
candidate * 9 + random.nextInt(7));
|
||||
@@ -41,6 +41,21 @@ public class HydrologyRouteTurnCacheTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void twoLayerRefinementResetsReusedCostsAndKeepsFiniteRoutes() throws Exception {
|
||||
Random random = new Random(173L);
|
||||
HydrologyPoint[][] points = {
|
||||
{new HydrologyPoint(0, 80, 0), new HydrologyPoint(0, 80, 4)},
|
||||
{new HydrologyPoint(12, 80, 0), new HydrologyPoint(12, 80, 4)}
|
||||
};
|
||||
Solver solver = new Solver();
|
||||
double[][][][] cache = new double[2][][][];
|
||||
for (int refinement = 0; refinement < 32; refinement++) {
|
||||
Fixture fixture = fixture(points, random);
|
||||
assertArrayEquals(exhaustive(fixture, 75D), solver.solve(fixture, 75D, cache));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void straightTiesAndDegenerateSegmentsKeepOriginalFirstChoice() throws Exception {
|
||||
HydrologyPoint[][] points = {
|
||||
@@ -199,6 +214,7 @@ public class HydrologyRouteTurnCacheTest {
|
||||
private final Object tangent;
|
||||
private final Method solve;
|
||||
private final Method indices;
|
||||
private HydrologyRouteGeometry.CurvatureWorkspace workspace;
|
||||
|
||||
private Solver() throws Exception {
|
||||
Class<?> direction = RouteDirection.class;
|
||||
@@ -210,7 +226,8 @@ public class HydrologyRouteTurnCacheTest {
|
||||
double.class, double.class, double.class, direction, boolean.class);
|
||||
candidateConstructor.setAccessible(true);
|
||||
solve = HydrologyRouteGeometry.class.getDeclaredMethod("selectCurvatureAwareTerrainRoute", List.class,
|
||||
double.class, double.class, double.class, double[][].class, double[][][].class, double[][][][].class);
|
||||
double.class, double.class, double.class, double[][].class, double[][][].class, double[][][][].class,
|
||||
HydrologyRouteGeometry.CurvatureWorkspace.class);
|
||||
solve.setAccessible(true);
|
||||
indices = solve.getReturnType().getDeclaredMethod("indices");
|
||||
indices.setAccessible(true);
|
||||
@@ -227,8 +244,11 @@ public class HydrologyRouteTurnCacheTest {
|
||||
}
|
||||
layers.add(candidates);
|
||||
}
|
||||
if (workspace == null) {
|
||||
workspace = new HydrologyRouteGeometry.CurvatureWorkspace(layers);
|
||||
}
|
||||
return (int[]) indices.invoke(solve.invoke(geometry, layers, 12D, maximumTurn, 12D,
|
||||
fixture.penalties(), fixture.transitions(), cache));
|
||||
fixture.penalties(), fixture.transitions(), cache, workspace));
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -87,6 +87,93 @@ public class HydrologySourceSpacingTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowerPriorityHaloSamplesDoNotBuildNeighbourOwnerRouting() {
|
||||
HydrologyPlanner planner = planner();
|
||||
HydrologySampledGrid grid = haloGrid(3D, 1D);
|
||||
HydrologyRoutingPlan routing = routing(grid, Map.of());
|
||||
HashMap<HydrologyTileKey, SourceRoutingContext> contexts = new HashMap<>();
|
||||
contexts.put(TILE, new SourceRoutingContext(grid, routing, routing, List.of()));
|
||||
|
||||
assertTrue(planner.sourcePlanner.globallyAdmittedSource(grid.nodeAt(5, 5),
|
||||
planner.settings.surface().sources(), HydrologySourcePlanner.SURFACE_SOURCE_SALT,
|
||||
true, contexts, new HashMap<>()));
|
||||
|
||||
assertEquals(Set.of(TILE), contexts.keySet());
|
||||
assertEquals(0L, planner.routingContexts.estimatedSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void higherPriorityHaloSourcesStillDemandTheirOwnerRouting() {
|
||||
HydrologyPlanner planner = planner();
|
||||
HydrologySampledGrid grid = haloGrid(1D, 3D);
|
||||
HydrologyRoutingPlan routing = routing(grid, Map.of());
|
||||
SourceRoutingContext context = new SourceRoutingContext(grid, routing, routing, List.of());
|
||||
HydrologyTileKey westernOwner = new HydrologyTileKey(-1, 0);
|
||||
planner.routingContexts.put(westernOwner, context);
|
||||
HashMap<HydrologyTileKey, SourceRoutingContext> contexts = new HashMap<>();
|
||||
contexts.put(TILE, context);
|
||||
|
||||
assertFalse(planner.sourcePlanner.globallyAdmittedSource(grid.nodeAt(5, 5),
|
||||
planner.settings.surface().sources(), HydrologySourcePlanner.SURFACE_SOURCE_SALT,
|
||||
true, contexts, new HashMap<>()));
|
||||
|
||||
assertEquals(Set.of(TILE, westernOwner), contexts.keySet());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestDependentSamplersAlwaysUseTheOwnerGridForSourcePriority() {
|
||||
HydrologyRoutingTerrainSampler routingSampler = new HydrologyRoutingTerrainSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
|
||||
throw new AssertionError("The owner grids are already cached");
|
||||
}
|
||||
|
||||
@Override
|
||||
public NaturalClassification classifyNatural(int x, int z) {
|
||||
throw new AssertionError("Source spacing uses sampled routing grids");
|
||||
}
|
||||
};
|
||||
HydrologyPlannerSettings settings = planner().settings;
|
||||
HydrologyTerrainSampler terrain = (x, z) -> land(HIGH, 0D);
|
||||
HydrologyPlanner planner = new HydrologyPlanner(1337L, settings, terrain, routingSampler,
|
||||
HydrologyGeometrySampler.deterministic(terrain), -4096,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(terrain, settings.seaLevel(), -4096, 4096));
|
||||
HydrologySampledGrid primaryGrid = haloGrid(1D, 0D);
|
||||
HydrologySampledGrid ownerGrid = haloGrid(1D, 3D);
|
||||
HydrologyRoutingPlan primaryRouting = routing(primaryGrid, Map.of());
|
||||
HydrologyRoutingPlan ownerRouting = routing(ownerGrid, Map.of());
|
||||
SourceRoutingContext primaryContext = new SourceRoutingContext(primaryGrid, primaryRouting, primaryRouting, List.of());
|
||||
SourceRoutingContext ownerContext = new SourceRoutingContext(ownerGrid, ownerRouting, ownerRouting, List.of());
|
||||
for (HydrologyTileKey key : List.of(new HydrologyTileKey(-1, -1), new HydrologyTileKey(-1, 0),
|
||||
new HydrologyTileKey(0, -1))) {
|
||||
planner.routingContexts.put(key, ownerContext);
|
||||
}
|
||||
HashMap<HydrologyTileKey, SourceRoutingContext> contexts = new HashMap<>();
|
||||
contexts.put(TILE, primaryContext);
|
||||
|
||||
assertFalse(routingSampler.supportsSharedGridSamples());
|
||||
assertFalse(planner.sourcePlanner.globallyAdmittedSource(primaryGrid.nodeAt(5, 5),
|
||||
settings.surface().sources(), HydrologySourcePlanner.SURFACE_SOURCE_SALT,
|
||||
true, contexts, new HashMap<>()));
|
||||
assertTrue(contexts.containsKey(new HydrologyTileKey(-1, 0)));
|
||||
}
|
||||
|
||||
private static HydrologySampledGrid haloGrid(double centerWeight, double westernWeight) {
|
||||
ArrayList<HydrologyGridNode> nodes = new ArrayList<>(WIDTH * WIDTH);
|
||||
for (int gridZ = 0; gridZ < WIDTH; gridZ++) {
|
||||
for (int gridX = 0; gridX < WIDTH; gridX++) {
|
||||
int index = gridZ * WIDTH + gridX;
|
||||
int x = (gridX - 5) * SPACING;
|
||||
int z = (gridZ - 5) * SPACING;
|
||||
double weight = z != 0 ? 0D : x == 0 ? centerWeight : x == -SPACING ? westernWeight : 0D;
|
||||
nodes.add(new HydrologyGridNode(index, gridX, gridZ, x, z, index + 1L, land(HIGH, weight)));
|
||||
}
|
||||
}
|
||||
return new HydrologySampledGrid(-5 * SPACING, -5 * SPACING, 0, 0,
|
||||
HydrologyPlannerSettings.defaults().routing().tileSize(), WIDTH, SPACING, List.copyOf(nodes));
|
||||
}
|
||||
|
||||
private static Field denseField() {
|
||||
HashMap<Long, Double> weights = new HashMap<>();
|
||||
for (int gridZ = 1; gridZ < WIDTH - 1; gridZ++) {
|
||||
|
||||
+60
@@ -4,6 +4,7 @@ import org.junit.Test;
|
||||
|
||||
import java.util.NavigableSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -31,6 +32,65 @@ public class HydrologySurfaceBankProbeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bankHeightQueriesPreserveExactPenaltiesWithoutBankPolicySamples() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> x + z == 7 ? null
|
||||
: x < -3 ? HydrologyTerrainSample.ocean(60, "ocean")
|
||||
: HydrologyTerrainSample.openLand(80 + Math.floorMod(x * 3 + z * 7, 31), 0D, "land");
|
||||
AtomicInteger basisCalls = new AtomicInteger();
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
HydrologyNaturalTerrainSampler natural = new HydrologyNaturalTerrainSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample sampleBasis(int x, int z) {
|
||||
basisCalls.incrementAndGet();
|
||||
return terrain.sample(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double sampleLandHeight(int x, int z) {
|
||||
heightCalls.incrementAndGet();
|
||||
HydrologyTerrainSample sampled = terrain.sample(x, z);
|
||||
return sampled == null || sampled.ocean() ? Double.NaN : sampled.naturalHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
|
||||
throw new AssertionError("Bank probes do not require a routing grid");
|
||||
}
|
||||
|
||||
@Override
|
||||
public NaturalClassification classifyNatural(int x, int z) {
|
||||
throw new AssertionError("Bank probes only require land heights");
|
||||
}
|
||||
};
|
||||
for (double angle : new double[]{0D, 0.37D, 0.7853981633974483D}) {
|
||||
HydrologyPlannerSettings settings = settings(3D);
|
||||
HydrologyPlanner reference = new HydrologyPlanner(91L, settings, terrain);
|
||||
HydrologyPlanner optimized = new HydrologyPlanner(91L, settings, terrain, natural,
|
||||
HydrologyGeometrySampler.deterministic(terrain), -4096,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(terrain, settings.seaLevel(), -4096, 4096));
|
||||
HydrologyPoint point = new HydrologyPoint(0, 80, 0);
|
||||
RouteCandidate candidate = new RouteCandidate(point, 0D, 0D, 0D, 0D, 0D,
|
||||
new RouteDirection(StrictMath.cos(angle), StrictMath.sin(angle)), true);
|
||||
basisCalls.set(0);
|
||||
heightCalls.set(0);
|
||||
double expected = reference.surfaceCourses.surfaceRouteCandidateBankPenalty(candidate);
|
||||
double actual = optimized.surfaceCourses.surfaceRouteCandidateBankPenalty(candidate);
|
||||
assertEquals(Double.doubleToLongBits(expected), Double.doubleToLongBits(actual));
|
||||
assertEquals(1, basisCalls.get());
|
||||
assertTrue(heightCalls.get() > 0);
|
||||
optimized.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||
try {
|
||||
assertEquals(expected, optimized.surfaceCourses.surfaceRouteCandidateBankPenalty(candidate), 0D);
|
||||
int firstPassHeightCalls = heightCalls.get();
|
||||
assertEquals(expected, optimized.surfaceCourses.surfaceRouteCandidateBankPenalty(candidate), 0D);
|
||||
assertEquals(firstPassHeightCalls, heightCalls.get());
|
||||
} finally {
|
||||
optimized.planningSamples.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** One block outside half the nominal maximum width - see HydrologySurfaceCoursePlanner. */
|
||||
private int innerRing() {
|
||||
return (int) StrictMath.ceil(
|
||||
|
||||
+44
@@ -7,8 +7,52 @@ import java.util.List;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class HydrologySurfaceRouteGeometryTest {
|
||||
@Test
|
||||
public void lazyTransitionGeometryIsReusedUntilResolvedHeightsChange() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(91L, HydrologyPlannerSettings.defaults(),
|
||||
(x, z) -> {
|
||||
throw new AssertionError("Geometry lookup must not sample terrain.");
|
||||
});
|
||||
RouteCandidate previous = candidate(0D, 0D);
|
||||
RouteCandidate current = spy(candidate(4D, 0D));
|
||||
HydrologyRouteGeometry.CurvatureWorkspace workspace = new HydrologyRouteGeometry.CurvatureWorkspace(
|
||||
List.of(List.of(previous), List.of(current)));
|
||||
double expected = planner.routePaths.routeTransitionGeometryCost(previous, current, 4D);
|
||||
clearInvocations(current);
|
||||
|
||||
for (int iteration = 0; iteration < 8; iteration++) {
|
||||
double actual = planner.routeGeometry.resolvedTransitionCost(
|
||||
previous, current, 4D, Double.NaN, workspace, 1, 0, 0);
|
||||
assertEquals(Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(actual));
|
||||
}
|
||||
verify(current, times(1)).offset();
|
||||
|
||||
RouteCandidate raised = spy(new RouteCandidate(new HydrologyPoint(4, 91, 0),
|
||||
current.continuousX(), current.continuousZ(), current.offset(), current.localScore(),
|
||||
current.terrainScore(), current.tangent(), true));
|
||||
double raisedExpected = planner.routePaths.routeTransitionGeometryCost(previous, raised, 4D);
|
||||
assertTrue(raisedExpected > expected);
|
||||
clearInvocations(raised);
|
||||
for (int iteration = 0; iteration < 8; iteration++) {
|
||||
double actual = planner.routeGeometry.resolvedTransitionCost(
|
||||
previous, raised, 4D, Double.NaN, workspace, 1, 0, 0);
|
||||
assertEquals(Double.doubleToRawLongBits(raisedExpected), Double.doubleToRawLongBits(actual));
|
||||
}
|
||||
verify(raised, times(1)).offset();
|
||||
assertEquals(Double.doubleToRawLongBits(-0D), Double.doubleToRawLongBits(
|
||||
planner.routeGeometry.resolvedTransitionCost(previous, raised, 4D, -0D,
|
||||
workspace, 1, 0, 0)));
|
||||
assertEquals(Double.POSITIVE_INFINITY,
|
||||
planner.routeGeometry.resolvedTransitionCost(previous, raised, 4D,
|
||||
Double.POSITIVE_INFINITY, workspace, 1, 0, 0), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceAnchorKeepsTheCoarseValleyWhenSmoothGroundIsMuchHigher() {
|
||||
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(
|
||||
|
||||
+204
-21
@@ -50,6 +50,181 @@ import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HydrologyTileCacheTest {
|
||||
@Test
|
||||
public void weightedTileCapacityStopsSpeculationWithoutBlockingDemand() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
RiverFootprint footprint = mock(RiverFootprint.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
when(tile.footprint()).thenReturn(footprint);
|
||||
when(footprint.size()).thenReturn((int) (HydrologyCacheBudget.runtime().tileBytes() * 3L / 5L / 64L));
|
||||
when(footprint.columns()).thenReturn(Map.of());
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, Runnable::run);
|
||||
int tileSize = emptySettings().routing().tileSize();
|
||||
cache.prefetchArea(0, 0, tileSize * 4 - 1, 0, 0, 0);
|
||||
cache.prefetchArea(tileSize, 0, tileSize * 4 - 1, 0, tileSize, 0);
|
||||
verify(planner, times(1)).plan(any(HydrologyTileKey.class));
|
||||
assertSame(tile, cache.get(new HydrologyTileKey(1, 0)));
|
||||
verify(planner, times(2)).plan(any(HydrologyTileKey.class));
|
||||
assertEquals(1, cache.size());
|
||||
cache.close();
|
||||
}
|
||||
|
||||
@Test(timeout = 10000L)
|
||||
public void nonblockingQueriesComposeOverBudgetTileSnapshotsOnOneWorker() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
RiverFootprint footprint = mock(RiverFootprint.class);
|
||||
HydrologyPlannerSettings settings = stalePrefetchSettings();
|
||||
when(planner.settings()).thenReturn(settings);
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
|
||||
when(tile.footprint()).thenReturn(footprint);
|
||||
when(footprint.size()).thenReturn((int) (HydrologyCacheBudget.runtime().tileBytes() * 3L / 5L / 64L));
|
||||
when(footprint.columns()).thenReturn(Map.of());
|
||||
ExecutorService worker = Executors.newSingleThreadExecutor();
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, worker, () -> true);
|
||||
cache.setNeighbourPrefetchEnabled(false);
|
||||
try {
|
||||
cache.get(new HydrologyTileKey(10, 10));
|
||||
assertEquals(1, cache.size());
|
||||
assertFalse(cache.columnSnapshot(0, 0).available());
|
||||
worker.submit(() -> {}).get(5L, TimeUnit.SECONDS);
|
||||
|
||||
assertEquals(1, cache.size());
|
||||
assertTrue(cache.isPlanned(0, 0));
|
||||
assertTrue(cache.columnSnapshot(0, 0).available());
|
||||
assertTrue(cache.columnAt(0, 0).isEmpty());
|
||||
verify(planner, times(5)).plan(any(HydrologyTileKey.class));
|
||||
} finally {
|
||||
worker.shutdownNow();
|
||||
assertTrue(worker.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonblockingQueryQueueIsBoundedAndDeduplicatesChunks() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
|
||||
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 2, queued::add, () -> true);
|
||||
cache.setNeighbourPrefetchEnabled(false);
|
||||
for (int query = 0; query < 100; query++) {
|
||||
assertFalse(cache.columnSnapshot(40, 40).available());
|
||||
}
|
||||
for (int query = 1; query < 100; query++) {
|
||||
assertFalse(cache.columnSnapshot(query * 64 + 40, 40).available());
|
||||
}
|
||||
assertEquals(1, queued.size());
|
||||
verify(planner, never()).plan(any(HydrologyTileKey.class));
|
||||
drainPrefetchTasks(queued);
|
||||
verify(planner, times(2)).plan(any(HydrologyTileKey.class));
|
||||
assertTrue(cache.columnSnapshot(40, 40).available());
|
||||
assertTrue(cache.columnSnapshot(104, 40).available());
|
||||
cache.close();
|
||||
}
|
||||
|
||||
@Test(timeout = 10000L)
|
||||
public void owningWorkerClaimsCompositionQueuedBehindIt() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
ForkJoinPool worker = new ForkJoinPool(1, ForkJoinPool.defaultForkJoinWorkerThreadFactory,
|
||||
null, false, 1, 1, 1, pool -> true, 60L, TimeUnit.SECONDS);
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, worker);
|
||||
cache.setNeighbourPrefetchEnabled(false);
|
||||
try {
|
||||
Future<Optional<HydrologyColumnSample>> result = worker.submit(() -> {
|
||||
started.countDown();
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
return cache.columnAt(40, 40);
|
||||
});
|
||||
assertTrue(started.await(5L, TimeUnit.SECONDS));
|
||||
assertFalse(cache.isPlanned(40, 40));
|
||||
release.countDown();
|
||||
assertTrue(result.get(5L, TimeUnit.SECONDS).isEmpty());
|
||||
worker.submit(() -> {}).get(5L, TimeUnit.SECONDS);
|
||||
assertTrue(cache.isPlanned(40, 40));
|
||||
verify(planner, times(1)).plan(any(HydrologyTileKey.class));
|
||||
} finally {
|
||||
release.countDown();
|
||||
worker.shutdownNow();
|
||||
assertTrue(worker.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 10000L)
|
||||
public void displacedCompositionTaskDoesNotCancelItsClaimedFuture() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
CountDownLatch planning = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenAnswer(invocation -> {
|
||||
planning.countDown();
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
return tile;
|
||||
});
|
||||
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
|
||||
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
|
||||
ExecutorService callers = Executors.newFixedThreadPool(2);
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add);
|
||||
cache.setNeighbourPrefetchEnabled(false);
|
||||
try {
|
||||
assertFalse(cache.isPlanned(40, 40));
|
||||
Runnable displaced = queued.remove();
|
||||
Future<Optional<HydrologyColumnSample>> owner = callers.submit(() -> cache.columnAt(40, 40));
|
||||
assertTrue(planning.await(5L, TimeUnit.SECONDS));
|
||||
Future<Optional<HydrologyColumnSample>> waiter = callers.submit(() -> cache.columnAt(40, 40));
|
||||
assertThrows(TimeoutException.class, () -> waiter.get(100L, TimeUnit.MILLISECONDS));
|
||||
displaced.run();
|
||||
assertThrows(TimeoutException.class, () -> waiter.get(100L, TimeUnit.MILLISECONDS));
|
||||
release.countDown();
|
||||
assertTrue(owner.get(5L, TimeUnit.SECONDS).isEmpty());
|
||||
assertTrue(waiter.get(5L, TimeUnit.SECONDS).isEmpty());
|
||||
verify(planner, times(1)).plan(any(HydrologyTileKey.class));
|
||||
} finally {
|
||||
release.countDown();
|
||||
callers.shutdownNow();
|
||||
assertTrue(callers.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearingAndClosingCancelQueuedNonblockingComposition() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
|
||||
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 2, queued::add, () -> true);
|
||||
cache.setNeighbourPrefetchEnabled(false);
|
||||
assertFalse(cache.columnSnapshot(40, 40).available());
|
||||
cache.clear();
|
||||
assertFalse(cache.columnSnapshot(104, 40).available());
|
||||
drainPrefetchTasks(queued);
|
||||
verify(planner, never()).plan(new HydrologyTileKey(0, 0));
|
||||
verify(planner, times(1)).plan(new HydrologyTileKey(1, 0));
|
||||
assertTrue(cache.columnSnapshot(104, 40).available());
|
||||
assertFalse(cache.columnSnapshot(168, 40).available());
|
||||
cache.close();
|
||||
drainPrefetchTasks(queued);
|
||||
verify(planner, never()).plan(new HydrologyTileKey(2, 0));
|
||||
assertEquals(0, cache.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interruptedWaiterExitsWithoutCancellingTheSharedPlan() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
@@ -957,17 +1132,18 @@ public class HydrologyTileCacheTest {
|
||||
assertTrue(order.subList(0, 4).containsAll(List.of(new HydrologyTileKey(0, 0), new HydrologyTileKey(1, 0), new HydrologyTileKey(0, 1), new HydrologyTileKey(1, 1))));
|
||||
}
|
||||
@Test
|
||||
public void tileBatchesPlanMissingTilesConcurrentlyOnThePrefetchExecutor() throws Exception {
|
||||
public void tileBatchesUseTheAvailableRootBudgetOnThePrefetchExecutor() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
AtomicInteger inFlight = new AtomicInteger();
|
||||
AtomicInteger peak = new AtomicInteger();
|
||||
CountDownLatch allStarted = new CountDownLatch(4);
|
||||
int admitted = Math.min(4, HydrologyPlanningAdmission.maximumRoots());
|
||||
CountDownLatch allStarted = new CountDownLatch(admitted);
|
||||
doAnswer(invocation -> {
|
||||
peak.accumulateAndGet(inFlight.incrementAndGet(), Math::max);
|
||||
allStarted.countDown();
|
||||
assertTrue("every batch member should plan at the same time", allStarted.await(5, TimeUnit.SECONDS));
|
||||
assertTrue("the admitted batch should plan concurrently", allStarted.await(5, TimeUnit.SECONDS));
|
||||
inFlight.decrementAndGet();
|
||||
return tile;
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
@@ -984,7 +1160,7 @@ public class HydrologyTileCacheTest {
|
||||
List<HydrologyTile> tiles = cache.tiles(keys);
|
||||
|
||||
assertEquals(4, tiles.size());
|
||||
assertEquals(4, peak.get());
|
||||
assertEquals(admitted, peak.get());
|
||||
verify(planner, times(4)).plan(any(HydrologyTileKey.class));
|
||||
assertEquals(4, cache.size());
|
||||
} finally {
|
||||
@@ -1007,7 +1183,7 @@ public class HydrologyTileCacheTest {
|
||||
private void assertConcurrentTilePlanning(List<HydrologyTileKey> keys) throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
CountDownLatch started = new CountDownLatch(keys.size());
|
||||
CountDownLatch started = new CountDownLatch(Math.min(keys.size(), HydrologyPlanningAdmission.maximumRoots()));
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
doAnswer(invocation -> {
|
||||
started.countDown();
|
||||
@@ -1019,7 +1195,7 @@ public class HydrologyTileCacheTest {
|
||||
try {
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, executor);
|
||||
Future<List<HydrologyTile>> result = caller.submit(() -> cache.tiles(keys));
|
||||
assertTrue("colliding keys must start before either plan completes", started.await(5, TimeUnit.SECONDS));
|
||||
assertTrue("colliding keys must use the available root budget", started.await(5, TimeUnit.SECONDS));
|
||||
release.countDown();
|
||||
assertEquals(keys.size(), result.get(5, TimeUnit.SECONDS).size());
|
||||
for (HydrologyTileKey key : keys) {
|
||||
@@ -1138,8 +1314,9 @@ public class HydrologyTileCacheTest {
|
||||
Future<HydrologyTile> oldResult = callers.submit(() -> cache.get(key));
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
cache.clear();
|
||||
assertSame(newTile, callers.submit(() -> cache.get(key)).get(5, TimeUnit.SECONDS));
|
||||
Future<HydrologyTile> newResult = callers.submit(() -> cache.get(key));
|
||||
release.countDown();
|
||||
assertSame(newTile, newResult.get(5, TimeUnit.SECONDS));
|
||||
assertSame(oldTile, oldResult.get(5, TimeUnit.SECONDS));
|
||||
assertSame(newTile, cache.get(key));
|
||||
assertEquals(2, calls.get());
|
||||
@@ -1502,17 +1679,27 @@ public class HydrologyTileCacheTest {
|
||||
List<HydrologyTileKey> keys = List.of(
|
||||
new HydrologyTileKey(0, 0), new HydrologyTileKey(1, 0), new HydrologyTileKey(2, 0));
|
||||
Future<List<HydrologyTile>> result = caller.submit(() -> cache.tiles(keys));
|
||||
Runnable firstDemand = queued.poll(5, TimeUnit.SECONDS);
|
||||
Runnable secondDemand = queued.poll(5, TimeUnit.SECONDS);
|
||||
assertNotNull(firstDemand);
|
||||
assertNotNull(secondDemand);
|
||||
|
||||
Field demandField = HydrologyTileCache.class.getDeclaredField("demandBatches");
|
||||
Field queueField = HydrologyTileCache.class.getDeclaredField("prefetchQueue");
|
||||
demandField.setAccessible(true);
|
||||
queueField.setAccessible(true);
|
||||
Object demandLock = queueField.get(cache);
|
||||
Await.until("the demand batch to register", Duration.ofSeconds(5L), () -> {
|
||||
synchronized (demandLock) {
|
||||
try {
|
||||
return demandField.getInt(cache) > 0;
|
||||
} catch (IllegalAccessException failure) {
|
||||
throw new IllegalStateException(failure);
|
||||
}
|
||||
}
|
||||
});
|
||||
initialPrefetch.run();
|
||||
assertTrue(queued.isEmpty());
|
||||
assertSame(tile, cache.get(new HydrologyTileKey(0, 0)));
|
||||
assertTrue(queued.isEmpty());
|
||||
Runnable firstDemand = queued.poll(5, TimeUnit.SECONDS);
|
||||
assertNotNull(firstDemand);
|
||||
firstDemand.run();
|
||||
assertTrue(queued.isEmpty());
|
||||
Runnable secondDemand = queued.poll(5, TimeUnit.SECONDS);
|
||||
assertNotNull(secondDemand);
|
||||
secondDemand.run();
|
||||
assertEquals(3, result.get(5, TimeUnit.SECONDS).size());
|
||||
Runnable resumedPrefetch = queued.poll(5, TimeUnit.SECONDS);
|
||||
@@ -1788,12 +1975,8 @@ public class HydrologyTileCacheTest {
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
List<Runnable> queued = new ArrayList<>();
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add, () -> true);
|
||||
assertFalse(cache.columnSnapshot(40, 40).available());
|
||||
for (Runnable task : List.copyOf(queued)) {
|
||||
task.run();
|
||||
}
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, null, () -> true);
|
||||
cache.get(new HydrologyTileKey(0, 0));
|
||||
AtomicBoolean evicted = new AtomicBoolean();
|
||||
when(tile.columnAt(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
if (evicted.compareAndSet(false, true)) {
|
||||
|
||||
+65
@@ -1,6 +1,14 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxel;
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxelPrecondition;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveMode;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveRejection;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveSource;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
@@ -17,6 +25,9 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
@@ -50,6 +61,60 @@ public class StudioHydrologyTileStoreTest {
|
||||
assertThrows(UnsupportedOperationException.class, () -> restored.regionalCourseIds().clear());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripsNonemptyCompactCavePlan() throws Exception {
|
||||
CavePosition wet = new CavePosition(0, 20, 0);
|
||||
CavePosition dry = new CavePosition(0, 21, 0);
|
||||
CavePosition guard = new CavePosition(Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
|
||||
CavePosition boundary = new CavePosition(Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||
LinkedHashMap<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
actions.put(guard, HydrologyCaveAction.SEAL_GUARD);
|
||||
actions.put(dry, HydrologyCaveAction.DRY_AIR);
|
||||
actions.put(wet, HydrologyCaveAction.WET_SOURCE);
|
||||
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
preconditions.put(wet, new CaveVoxelPrecondition(CaveVoxel.SOLID, false));
|
||||
preconditions.put(boundary, new CaveVoxelPrecondition(CaveVoxel.CAVE_AIR, true));
|
||||
preconditions.put(guard, new CaveVoxelPrecondition(CaveVoxel.LAVA, false));
|
||||
preconditions.put(dry, new CaveVoxelPrecondition(CaveVoxel.CAVE_AIR, false));
|
||||
HydrologyCavePlan plan = new HydrologyCavePlan(
|
||||
new HydrologyCaveSource(4L, wet, dry, 20, HydrologyCaveMode.CLOSED_COMPONENT),
|
||||
HydrologyCaveRejection.NONE, actions, preconditions, OptionalLong.empty());
|
||||
HydraulicSegment segment = new HydraulicSegment(3L, 4L, HydrologyFeatureType.UNDERGROUND_POOL,
|
||||
20, 20, 1, 1, false, false, List.of(new HydrologyPoint(0, 20, 0)),
|
||||
new HydraulicChannelProfile(new double[]{1D}, new double[]{1D}));
|
||||
RiverCourse course = new RiverCourse(4L, RiverCourseType.SEA_CAVE, OptionalLong.empty(),
|
||||
OptionalLong.empty(), "default", 1, List.of(), List.of(segment));
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(5L, HydrologyFeatureType.UNDERGROUND_POOL,
|
||||
4L, 3L, 0, 20, 0, 0, 0, true);
|
||||
HydrologyColumnLayer layer = new HydrologyColumnLayer(feature, 19, 20, 21,
|
||||
true, false, false, true, false, false, true, true, false,
|
||||
"default", "plains", "plains", "plains", "plains", "plains");
|
||||
HydrologyColumnSample column = new HydrologyColumnSample(0, 0, 80, 63, false, "plains", List.of(layer));
|
||||
HydrologyTile original = new HydrologyTile(new HydrologyTileKey(0, 0), 91L, 17L, 64,
|
||||
List.of(), List.of(), List.of(), List.of(course), Set.of(), List.of(plan), List.of(),
|
||||
new RiverFootprint(Map.of(RiverFootprint.pack(0, 0), column)));
|
||||
StudioHydrologyTileStore store = store();
|
||||
store.save(original);
|
||||
HydrologyTile restored = store.load(original.key()).orElseThrow();
|
||||
assertEquals(original, restored);
|
||||
HydrologyCavePlan restoredPlan = restored.cavePlans().getFirst();
|
||||
assertEquals(actions, restoredPlan.actions());
|
||||
assertEquals(preconditions, restoredPlan.baselinePreconditions());
|
||||
Comparator<CavePosition> persistedOrder = Comparator.comparingInt(CavePosition::x)
|
||||
.thenComparingInt(CavePosition::z).thenComparingInt(CavePosition::y);
|
||||
ArrayList<CavePosition> expectedActions = new ArrayList<>(actions.keySet());
|
||||
expectedActions.sort(persistedOrder);
|
||||
ArrayList<CavePosition> expectedPreconditions = new ArrayList<>(preconditions.keySet());
|
||||
expectedPreconditions.sort(persistedOrder);
|
||||
assertEquals(expectedActions, new ArrayList<>(restoredPlan.actions().keySet()));
|
||||
assertEquals(expectedPreconditions, new ArrayList<>(restoredPlan.baselinePreconditions().keySet()));
|
||||
assertThrows(UnsupportedOperationException.class, () -> restoredPlan.actions().clear());
|
||||
assertThrows(UnsupportedOperationException.class, () -> restoredPlan.baselinePreconditions().remove(boundary));
|
||||
ArrayList<CavePosition> published = new ArrayList<>();
|
||||
restoredPlan.forEachActionIn(0, 0, 16, 16, (position, action) -> published.add(position));
|
||||
assertEquals(List.of(wet, dry), published);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entryRequiresRegionalOwnershipMetadata() throws Exception {
|
||||
HydrologyTile original = tile();
|
||||
|
||||
+133
@@ -7,9 +7,14 @@ import java.util.OptionalLong;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -84,6 +89,134 @@ public class HydrologyCavePlanTest {
|
||||
assertEquals(List.of(inside, preconditionOnly), visitedPreconditions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compactMapsPreserveAllCoordinateBitsAndIndependentOrders() {
|
||||
Random random = new Random(718622L);
|
||||
ArrayList<CavePosition> positions = new ArrayList<>();
|
||||
for (int x : new int[]{Integer.MIN_VALUE, -1, 0, Integer.MAX_VALUE}) {
|
||||
for (int y : new int[]{Integer.MIN_VALUE, -1, 0, Integer.MAX_VALUE}) {
|
||||
for (int z : new int[]{Integer.MIN_VALUE, -1, 0, Integer.MAX_VALUE}) {
|
||||
positions.add(new CavePosition(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < 4096; index++) {
|
||||
positions.add(new CavePosition(random.nextInt(), random.nextInt(), random.nextInt()));
|
||||
}
|
||||
LinkedHashMap<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
HydrologyCaveAction[] actionTypes = HydrologyCaveAction.values();
|
||||
CaveVoxel[] voxels = CaveVoxel.values();
|
||||
for (int index = 0; index < positions.size(); index++) {
|
||||
if (index % 3 != 0) {
|
||||
actions.put(positions.get(index), actionTypes[index % actionTypes.length]);
|
||||
}
|
||||
}
|
||||
Collections.shuffle(positions, random);
|
||||
for (int index = 0; index < positions.size(); index++) {
|
||||
preconditions.put(positions.get(index), new CaveVoxelPrecondition(voxels[index % voxels.length], index % 2 == 0));
|
||||
}
|
||||
HydrologyCavePlan plan = new HydrologyCavePlan(source(), HydrologyCaveRejection.NONE,
|
||||
actions, preconditions, OptionalLong.empty());
|
||||
assertEquals(actions, plan.actions());
|
||||
assertEquals(plan.actions(), actions);
|
||||
assertEquals(actions.hashCode(), plan.actions().hashCode());
|
||||
assertEquals(preconditions, plan.baselinePreconditions());
|
||||
assertEquals(plan.baselinePreconditions(), preconditions);
|
||||
assertEquals(preconditions.hashCode(), plan.baselinePreconditions().hashCode());
|
||||
assertEquals(new ArrayList<>(actions.entrySet()), new ArrayList<>(plan.actions().entrySet()));
|
||||
assertEquals(new ArrayList<>(preconditions.entrySet()), new ArrayList<>(plan.baselinePreconditions().entrySet()));
|
||||
assertEquals(new ArrayList<>(actions.values()), new ArrayList<>(plan.actions().values()));
|
||||
assertEquals(new ArrayList<>(preconditions.values()), new ArrayList<>(plan.baselinePreconditions().values()));
|
||||
assertTrue(plan.allPreconditions((position, baseline) -> baseline.equals(preconditions.get(position))));
|
||||
LinkedHashMap<CavePosition, HydrologyCaveAction> visitedActions = new LinkedHashMap<>();
|
||||
LinkedHashMap<CavePosition, CaveVoxelPrecondition> visitedPreconditions = new LinkedHashMap<>();
|
||||
plan.forEachAction(visitedActions::put);
|
||||
plan.forEachPrecondition(visitedPreconditions::put);
|
||||
assertEquals(new ArrayList<>(actions.entrySet()), new ArrayList<>(visitedActions.entrySet()));
|
||||
assertEquals(new ArrayList<>(preconditions.entrySet()), new ArrayList<>(visitedPreconditions.entrySet()));
|
||||
assertNull(plan.actions().get("absent"));
|
||||
assertNull(plan.baselinePreconditions().get(new CavePosition(42, 43, 44)));
|
||||
actions.clear();
|
||||
preconditions.clear();
|
||||
assertEquals(visitedActions, plan.actions());
|
||||
assertEquals(visitedPreconditions, plan.baselinePreconditions());
|
||||
assertThrows(UnsupportedOperationException.class, () -> plan.actions().entrySet().iterator().next().setValue(HydrologyCaveAction.DRY_AIR));
|
||||
assertThrows(UnsupportedOperationException.class, () -> plan.baselinePreconditions().values().clear());
|
||||
Iterator<CavePosition> iterator = plan.actions().keySet().iterator();
|
||||
iterator.next();
|
||||
assertThrows(UnsupportedOperationException.class, iterator::remove);
|
||||
while (iterator.hasNext()) {
|
||||
iterator.next();
|
||||
}
|
||||
assertThrows(NoSuchElementException.class, iterator::next);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spatialQueriesPreserveExtremeCoordinatesAndChunkOrder() {
|
||||
List<CavePosition> positions = List.of(
|
||||
new CavePosition(Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE),
|
||||
new CavePosition(Integer.MAX_VALUE - 1, Integer.MIN_VALUE, Integer.MAX_VALUE - 1),
|
||||
new CavePosition(16, 1, 16),
|
||||
new CavePosition(-1, 2, 0),
|
||||
new CavePosition(0, 3, -1),
|
||||
new CavePosition(1, 4, 1),
|
||||
new CavePosition(1, 5, 1)
|
||||
);
|
||||
LinkedHashMap<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
CaveVoxelPrecondition baseline = new CaveVoxelPrecondition(CaveVoxel.SOLID, false);
|
||||
for (CavePosition position : positions) {
|
||||
actions.put(position, HydrologyCaveAction.WET_SOURCE);
|
||||
}
|
||||
for (CavePosition position : positions.reversed()) {
|
||||
preconditions.put(position, baseline);
|
||||
}
|
||||
HydrologyCavePlan plan = new HydrologyCavePlan(source(), HydrologyCaveRejection.NONE,
|
||||
actions, preconditions, OptionalLong.empty());
|
||||
ArrayList<CavePosition> visited = new ArrayList<>();
|
||||
plan.forEachActionIn(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE + 1, Integer.MIN_VALUE + 1,
|
||||
(position, action) -> visited.add(position));
|
||||
assertEquals(List.of(positions.getFirst()), visited);
|
||||
visited.clear();
|
||||
plan.forEachActionIn(Integer.MAX_VALUE - 1, Integer.MAX_VALUE - 1, Integer.MAX_VALUE, Integer.MAX_VALUE,
|
||||
(position, action) -> visited.add(position));
|
||||
assertEquals(List.of(positions.get(1)), visited);
|
||||
visited.clear();
|
||||
plan.forEachActionIn(-16, -16, 32, 32, (position, action) -> visited.add(position));
|
||||
List<CavePosition> expectedOrder = List.of(positions.get(4), positions.get(3), positions.get(5), positions.get(6), positions.get(2));
|
||||
assertEquals(expectedOrder, visited);
|
||||
visited.clear();
|
||||
assertTrue(plan.allPreconditionsIn(-16, -16, 32, 32, (position, precondition) -> {
|
||||
visited.add(position);
|
||||
return true;
|
||||
}));
|
||||
assertEquals(expectedOrder, visited);
|
||||
assertFalse(plan.allPreconditionsIn(-16, -16, 32, 32, (position, precondition) -> false));
|
||||
assertTrue(plan.intersectsActions(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE + 1, Integer.MIN_VALUE + 1));
|
||||
assertFalse(plan.intersectsActions(2, 2, 16, 16));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retainedSizeIncludesPrimitiveLookupAndSpatialStorage() {
|
||||
LinkedHashMap<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
CaveVoxelPrecondition baseline = new CaveVoxelPrecondition(CaveVoxel.SOLID, false);
|
||||
for (int y = 0; y < 1024; y++) {
|
||||
CavePosition position = new CavePosition(0, y, 0);
|
||||
actions.put(position, HydrologyCaveAction.WET_SOURCE);
|
||||
preconditions.put(position, baseline);
|
||||
}
|
||||
HydrologyCavePlan plan = new HydrologyCavePlan(source(), HydrologyCaveRejection.NONE,
|
||||
actions, preconditions, OptionalLong.empty());
|
||||
long estimated = plan.estimatedRetainedBytes();
|
||||
assertTrue(estimated > 13L * preconditions.size());
|
||||
assertTrue(estimated < 48L * preconditions.size());
|
||||
assertEquals(estimated, plan.estimatedRetainedBytes());
|
||||
assertEquals(actions, plan.actions());
|
||||
assertEquals(preconditions, plan.baselinePreconditions());
|
||||
}
|
||||
|
||||
private HydrologyCaveSource source() {
|
||||
return new HydrologyCaveSource(
|
||||
1L,
|
||||
|
||||
+209
@@ -3,8 +3,18 @@ package art.arcane.iris.generation.hydrology.runtime;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyRoutingTerrainSampler;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.studio.generation.BiomeBuffetLayout;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -20,8 +30,101 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class IrisHydrologyRoutingTerrainSamplerTest {
|
||||
@Test
|
||||
public void bankHeightsAtSeaLevelSkipOceanIntentSampling() {
|
||||
AtomicInteger classifierCalls = new AtomicInteger();
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> {
|
||||
throw new AssertionError("Scalar bank sampling must not load a terrain basis.");
|
||||
},
|
||||
(int x, int z) -> x == 0 ? 62.5D : 62.49D,
|
||||
(int x, int z) -> {
|
||||
classifierCalls.incrementAndGet();
|
||||
return true;
|
||||
},
|
||||
63
|
||||
),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16)
|
||||
);
|
||||
|
||||
assertEquals(63D, sampler.sampleLandHeight(0, 0), 0D);
|
||||
assertEquals(0, classifierCalls.get());
|
||||
assertTrue(Double.isNaN(sampler.sampleLandHeight(1, 0)));
|
||||
assertEquals(1, classifierCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buffetBankHeightsMatchFullBasisBeforeAndAfterCacheWarmup() throws Exception {
|
||||
IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
|
||||
IrisBiome biome = new IrisBiome().setInferredType(InferredType.LAND);
|
||||
IrisRegion region = new IrisRegion();
|
||||
BiomeBuffetLayout buffet = mock(BiomeBuffetLayout.class);
|
||||
doReturn(new BiomeBuffetLayout.Cell(biome, region)).when(buffet).terrain(anyDouble(), anyDouble());
|
||||
ProceduralStream<InferredType> bridge = ProceduralStream.of(
|
||||
(x, z) -> InferredType.SEA,
|
||||
Interpolated.of(value -> 0D, value -> InferredType.SEA)
|
||||
);
|
||||
ProceduralStream<IrisRegion> regions = ProceduralStream.of(
|
||||
(x, z) -> region,
|
||||
Interpolated.of(value -> 0D, value -> region)
|
||||
);
|
||||
setComplexField(complex, "biomeBuffet", buffet);
|
||||
setComplexField(complex, "bridgeStream", bridge);
|
||||
setComplexField(complex, "regionStream", regions);
|
||||
Method natural = IrisComplex.class.getDeclaredMethod(
|
||||
"sampleHydrologyNatural", Engine.class, int.class, int.class, double.class
|
||||
);
|
||||
Method ocean = IrisComplex.class.getDeclaredMethod("sampleNaturalOcean", int.class, int.class);
|
||||
natural.setAccessible(true);
|
||||
ocean.setAccessible(true);
|
||||
|
||||
for (boolean warmBasis : new boolean[]{false, true}) {
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double height) -> {
|
||||
try {
|
||||
IrisHydrologyNaturalSample sample = (IrisHydrologyNaturalSample)
|
||||
natural.invoke(complex, null, x, z, height);
|
||||
return sample.ocean() ? oceanBasis(x, z, height) : basis(x, z, height);
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
throw new AssertionError(exception);
|
||||
}
|
||||
},
|
||||
(int x, int z) -> 60.25D,
|
||||
(int x, int z) -> {
|
||||
try {
|
||||
return (boolean) ocean.invoke(complex, x, z);
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
throw new AssertionError(exception);
|
||||
}
|
||||
},
|
||||
63
|
||||
),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(64)
|
||||
);
|
||||
if (warmBasis) {
|
||||
sampler.sampleBasisWithoutSlope(7, 11);
|
||||
}
|
||||
assertEquals(60D, sampler.sampleLandHeight(7, 11), 0D);
|
||||
HydrologyTerrainSample full = sampler.sampleBasisWithoutSlope(7, 11);
|
||||
assertEquals(false, full.ocean());
|
||||
assertEquals(full.naturalHeight(), sampler.sampleLandHeight(7, 11), 0D);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setComplexField(IrisComplex complex, String name, Object value) throws Exception {
|
||||
Field field = IrisComplex.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(complex, value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void guardedGridSamplesOneAlignedBasisAtEachCoordinate() {
|
||||
int[] spacings = new int[]{64, 128, 256};
|
||||
@@ -137,6 +240,82 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
|
||||
assertEquals(sloped, slopeFree.withSlope(sloped.slope()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void landHeightProbesMatchFullTerrainWithoutBuildingPolicySamples() {
|
||||
AtomicInteger basisCalls = new AtomicInteger();
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
int seaLevel = 63;
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> {
|
||||
basisCalls.incrementAndGet();
|
||||
boolean ocean = IrisHydrologyRoutingTerrainSampler.physicalOcean(x % 2 == 0, naturalHeight, seaLevel);
|
||||
return ocean ? oceanBasis(x, z, naturalHeight) : basis(x, z, naturalHeight);
|
||||
},
|
||||
(int x, int z) -> {
|
||||
heightCalls.incrementAndGet();
|
||||
return seaLevel + x * 0.5D;
|
||||
},
|
||||
(int x, int z) -> x % 2 == 0,
|
||||
seaLevel
|
||||
), IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(128));
|
||||
for (int x = -20; x <= 20; x++) {
|
||||
double expected = x % 2 == 0 && StrictMath.round(seaLevel + x * 0.5D) < seaLevel
|
||||
? Double.NaN : (int) StrictMath.round(seaLevel + x * 0.5D);
|
||||
assertEquals(expected, sampler.sampleLandHeight(x, -17), 0D);
|
||||
assertEquals(expected, sampler.sampleLandHeight(x, -17), 0D);
|
||||
}
|
||||
assertEquals(0, basisCalls.get());
|
||||
assertEquals(41, heightCalls.get());
|
||||
for (int x = -20; x <= 20; x++) {
|
||||
HydrologyTerrainSample terrain = sampler.sampleBasisWithoutSlope(x, -17);
|
||||
assertEquals(terrain.ocean() ? Double.NaN : terrain.naturalHeight(), sampler.sampleLandHeight(x, -17), 0D);
|
||||
}
|
||||
assertEquals(41, basisCalls.get());
|
||||
assertEquals(41, heightCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void landHeightProbesPassNonFiniteSamplesThroughTheExistingBasisRecovery() {
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
AtomicInteger basisCalls = new AtomicInteger();
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> {
|
||||
assertTrue(Double.isNaN(naturalHeight));
|
||||
basisCalls.incrementAndGet();
|
||||
return basis(x, z, 87D);
|
||||
},
|
||||
(int x, int z) -> {
|
||||
heightCalls.incrementAndGet();
|
||||
return Double.NaN;
|
||||
},
|
||||
(int x, int z) -> true,
|
||||
63
|
||||
), IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16));
|
||||
|
||||
assertEquals(87D, sampler.sampleLandHeight(-5, 9), 0D);
|
||||
assertEquals(87D, sampler.sampleLandHeight(-5, 9), 0D);
|
||||
assertEquals(1, heightCalls.get());
|
||||
assertEquals(1, basisCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedLandHeightProbesDoNotMemoizeANonFiniteHeight() {
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
(int x, int z, double naturalHeight) -> basis(x, z, naturalHeight),
|
||||
(int x, int z) -> heightCalls.incrementAndGet() == 1 ? Double.NaN : 87D,
|
||||
(int x, int z) -> false,
|
||||
63
|
||||
), IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> sampler.sampleLandHeight(-5, 9));
|
||||
assertEquals(87D, sampler.sampleLandHeight(-5, 9), 0D);
|
||||
assertEquals(2, heightCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adjacentGridsShareBasesAndProduceIdenticalOverlap() {
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
@@ -167,6 +346,36 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sharedGridSamplesMatchAtPaddedEdgesAndNegativeOwnershipSeams() {
|
||||
IrisHydrologyRoutingTerrainSampler sampler = sampler(4096);
|
||||
assertTrue(sampler.supportsSharedGridSamples());
|
||||
int spacing = 64;
|
||||
int width = 5;
|
||||
int primaryMinimum = -128;
|
||||
HydrologyTerrainSample[] primary = sampler.sampleGrid(new HydrologyRoutingTerrainSampler.GridRequest(
|
||||
primaryMinimum, primaryMinimum, width, spacing));
|
||||
for (int shiftX : new int[]{-128, 128}) {
|
||||
for (int shiftZ : new int[]{-128, 128}) {
|
||||
int minimumX = primaryMinimum + shiftX;
|
||||
int minimumZ = primaryMinimum + shiftZ;
|
||||
HydrologyTerrainSample[] adjacent = sampler.sampleGrid(new HydrologyRoutingTerrainSampler.GridRequest(
|
||||
minimumX, minimumZ, width, spacing));
|
||||
int firstX = Math.max(primaryMinimum, minimumX);
|
||||
int firstZ = Math.max(primaryMinimum, minimumZ);
|
||||
int lastX = Math.min(primaryMinimum + (width - 1) * spacing, minimumX + (width - 1) * spacing);
|
||||
int lastZ = Math.min(primaryMinimum + (width - 1) * spacing, minimumZ + (width - 1) * spacing);
|
||||
for (int z = firstZ; z <= lastZ; z += spacing) {
|
||||
for (int x = firstX; x <= lastX; x += spacing) {
|
||||
int primaryIndex = (z - primaryMinimum) / spacing * width + (x - primaryMinimum) / spacing;
|
||||
int adjacentIndex = (z - minimumZ) / spacing * width + (x - minimumX) / spacing;
|
||||
assertSampleRawBits(primary[primaryIndex], adjacent[adjacentIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serialGridKeepsRowMajorProviderCallOrder() {
|
||||
List<String> calls = new ArrayList<>();
|
||||
|
||||
+52
@@ -20,6 +20,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -303,6 +304,57 @@ public class SurfaceFootprintCompilerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preparedWindowsRetainColumnsAdmissionAndExcavationAcrossFallsAndMouths() {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 190
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand((x < 100 ? 100 : 90) + Math.abs(z) / 3, 0D, "land");
|
||||
SurfaceCourseResult built = new SurfaceCourseBuilder(HydrologyPlannerSettings.defaults().surface(), sampler,
|
||||
CONSTANT_GEOMETRY, SEA_LEVEL).build(7L, 1L, "water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(180, 0, 0)),
|
||||
SurfaceTerminal.TRIBUTARY, 40, 64);
|
||||
assertNull(built.rejection());
|
||||
RiverCourse falling = course(built.segments());
|
||||
RiverCourse mouth = course(List.of(segment(1L, HydrologyFeatureType.MOUTH, SEA_LEVEL, SEA_LEVEL,
|
||||
points(180, 200, SEA_LEVEL))));
|
||||
RiverCourse curved = course(List.of(segment(1L, HydrologyFeatureType.SURFACE_POOL, 79, 79,
|
||||
List.of(new HydrologyPoint(-100, 79, -50), new HydrologyPoint(0, 79, 30),
|
||||
new HydrologyPoint(100, 79, -50)))));
|
||||
for (RiverCourse course : List.of(falling, mouth, curved)) {
|
||||
SurfaceFootprintCompiler compiler = compiler(sampler);
|
||||
SurfaceFootprintCompiler.PreparedCourse prepared = compiler.prepare(course);
|
||||
for (int x = -128; x < 256; x += 32) {
|
||||
SurfaceBounds bounds = new SurfaceBounds(x, -80, x + 31, 80);
|
||||
assertEquals(compiler.compile(course, bounds), compiler.compile(prepared, bounds));
|
||||
}
|
||||
assertEquals(compiler.compile(course), compiler.compile(prepared, null));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subsequentPreparedWindowsDoNotResampleDistantCourseGeometry() {
|
||||
AtomicInteger distantSamples = new AtomicInteger();
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> {
|
||||
if (x >= 200) {
|
||||
distantSamples.incrementAndGet();
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(90 + Math.abs(z) / 2, 0D, "land");
|
||||
};
|
||||
RiverCourse course = course(List.of(segment(1L, HydrologyFeatureType.SURFACE_POOL, 79, 79,
|
||||
points(0, 400, 79))));
|
||||
SurfaceFootprintCompiler compiler = compiler(sampler);
|
||||
SurfaceFootprintCompiler.PreparedCourse prepared = compiler.prepare(course);
|
||||
assertEquals(0, distantSamples.get());
|
||||
compiler.compile(prepared, new SurfaceBounds(0, -16, 31, 16));
|
||||
assertTrue(distantSamples.get() > 0);
|
||||
distantSamples.set(0);
|
||||
SurfaceBounds bounds = new SurfaceBounds(32, -16, 63, 16);
|
||||
SurfaceFootprint reused = compiler.compile(prepared, bounds);
|
||||
assertEquals(0, distantSamples.get());
|
||||
assertEquals(compiler.compile(course, bounds), reused);
|
||||
assertTrue(distantSamples.get() > 0);
|
||||
}
|
||||
|
||||
private static SurfaceLayerColumn column(SurfaceFootprint footprint, int x, int z) {
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
if (column.x() == x && column.z() == z) {
|
||||
|
||||
+103
-5
@@ -22,6 +22,8 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -33,6 +35,8 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class EngineMantleCoverageTest {
|
||||
@@ -122,7 +126,7 @@ public class EngineMantleCoverageTest {
|
||||
Chunk neighbor = new Chunk(-2, 0);
|
||||
fixture.real().remove(neighbor);
|
||||
AtomicInteger reads = new AtomicInteger();
|
||||
when(fixture.mantle().hasFlag(-2, 0, MantleFlag.REAL)).thenAnswer(invocation -> {
|
||||
when(fixture.mantle().hasLoadedFlag(-2, 0, MantleFlag.REAL)).thenAnswer(invocation -> {
|
||||
if (reads.incrementAndGet() > 1) {
|
||||
fixture.real().add(neighbor);
|
||||
return true;
|
||||
@@ -181,6 +185,95 @@ public class EngineMantleCoverageTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupDefersUnloadedCoverageWithoutReadingFlags() {
|
||||
CoverageFixture fixture = fixture(1);
|
||||
fill(fixture, 2);
|
||||
when(fixture.mantle().isChunkLoaded(anyInt(), anyInt())).thenReturn(false);
|
||||
List<Chunk> actual = new ArrayList<>();
|
||||
|
||||
fixture.engine().cleanupChunksCoveredBy(0, 0, true,
|
||||
(x, z) -> actual.add(new Chunk(x, z)));
|
||||
|
||||
assertTrue(actual.isEmpty());
|
||||
verify(fixture.mantle(), never()).hasLoadedFlag(anyInt(), anyInt(), any());
|
||||
verify(fixture.mantle(), never()).getChunk(anyInt(), anyInt());
|
||||
|
||||
when(fixture.mantle().isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
fixture.engine().cleanupChunksCoveredBy(0, 0, true,
|
||||
(x, z) -> actual.add(new Chunk(x, z)));
|
||||
assertEquals(9, actual.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupSkipsTargetEvictedAfterCoverageWithoutLoadingIt() {
|
||||
CoverageFixture fixture = fixture(0);
|
||||
fill(fixture, 0);
|
||||
doReturn(false).when(fixture.mantle()).withLoadedChunk(eq(0), eq(0), any());
|
||||
List<Chunk> actual = new ArrayList<>();
|
||||
|
||||
fixture.engine().cleanupChunksCoveredBy(0, 0, true,
|
||||
(x, z) -> actual.add(new Chunk(x, z)));
|
||||
|
||||
assertTrue(actual.isEmpty());
|
||||
assertTrue(fixture.cleaned().isEmpty());
|
||||
verify(fixture.mantle(), never()).getChunk(anyInt(), anyInt());
|
||||
verify(fixture.mantle(), never()).hasFlag(anyInt(), anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupSkipsHaloEvictedAfterResidencyCheck() {
|
||||
CoverageFixture fixture = fixture(1);
|
||||
fill(fixture, 2);
|
||||
when(fixture.mantle().hasLoadedFlag(anyInt(), anyInt(), eq(MantleFlag.REAL))).thenReturn(false);
|
||||
List<Chunk> actual = new ArrayList<>();
|
||||
|
||||
fixture.engine().cleanupChunksCoveredBy(0, 0, true,
|
||||
(x, z) -> actual.add(new Chunk(x, z)));
|
||||
|
||||
assertTrue(actual.isEmpty());
|
||||
verify(fixture.mantle(), never()).withLoadedChunk(anyInt(), anyInt(), any());
|
||||
verify(fixture.mantle(), never()).getChunk(anyInt(), anyInt());
|
||||
verify(fixture.mantle(), never()).hasFlag(anyInt(), anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void busyTargetCanBeCleanedOnRetryAndCallbackRunsOutsideGuard() {
|
||||
CoverageFixture fixture = fixture(0);
|
||||
fill(fixture, 0);
|
||||
AtomicBoolean busy = new AtomicBoolean(true);
|
||||
AtomicBoolean guarded = new AtomicBoolean();
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
when(chunk.use()).thenReturn(chunk);
|
||||
doAnswer(invocation -> {
|
||||
if (busy.getAndSet(false)) {
|
||||
return false;
|
||||
}
|
||||
guarded.set(true);
|
||||
try {
|
||||
Predicate<MantleChunk<Matter>> action = invocation.getArgument(2);
|
||||
boolean cleaned = action.test(chunk);
|
||||
if (cleaned) {
|
||||
fixture.cleaned().add(new Chunk(0, 0));
|
||||
}
|
||||
return cleaned;
|
||||
} finally {
|
||||
guarded.set(false);
|
||||
}
|
||||
}).when(fixture.mantle()).withLoadedChunk(eq(0), eq(0), any());
|
||||
List<Chunk> actual = new ArrayList<>();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
fixture.engine().cleanupChunksCoveredBy(0, 0, true, (x, z) -> {
|
||||
assertTrue("Callback must run after releasing region guard", !guarded.get());
|
||||
actual.add(new Chunk(x, z));
|
||||
});
|
||||
}
|
||||
assertEquals(List.of(new Chunk(0, 0)), actual);
|
||||
verify(fixture.mantle(), never()).getChunk(anyInt(), anyInt());
|
||||
verify(fixture.mantle(), never()).hasFlag(anyInt(), anyInt(), any());
|
||||
}
|
||||
|
||||
private static List<Chunk> fullScan(CoverageFixture fixture, int x, int z, int radius) {
|
||||
List<Chunk> result = new ArrayList<>();
|
||||
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
|
||||
@@ -236,7 +329,8 @@ public class EngineMantleCoverageTest {
|
||||
doReturn(mantle).when(engine).getMantle();
|
||||
doReturn(radius).when(engine).getRadius();
|
||||
doReturn(0).when(engine).getRealRadius();
|
||||
when(mantle.hasFlag(anyInt(), anyInt(), any())).thenAnswer(invocation -> {
|
||||
when(mantle.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
|
||||
when(mantle.hasLoadedFlag(anyInt(), anyInt(), any())).thenAnswer(invocation -> {
|
||||
Chunk position = new Chunk(invocation.getArgument(0), invocation.getArgument(1));
|
||||
MantleFlag flag = invocation.getArgument(2);
|
||||
if (flag == MantleFlag.REAL) {
|
||||
@@ -245,9 +339,13 @@ public class EngineMantleCoverageTest {
|
||||
}
|
||||
return cleaned.contains(position);
|
||||
});
|
||||
when(mantle.getChunk(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
cleaned.add(new Chunk(invocation.getArgument(0), invocation.getArgument(1)));
|
||||
return chunk;
|
||||
when(mantle.withLoadedChunk(anyInt(), anyInt(), any())).thenAnswer(invocation -> {
|
||||
Predicate<MantleChunk<Matter>> action = invocation.getArgument(2);
|
||||
boolean result = action.test(chunk);
|
||||
if (result) {
|
||||
cleaned.add(new Chunk(invocation.getArgument(0), invocation.getArgument(1)));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
when(chunk.use()).thenReturn(chunk);
|
||||
doAnswer(invocation -> null).when(chunk).raiseFlagUnchecked(eq(MantleFlag.CLEANED), any());
|
||||
|
||||
+5
-6
@@ -120,20 +120,19 @@ public class IrisComplexHydrologyNoiseCacheTest {
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000L)
|
||||
public void concurrentFirstDemandsShareOneReservation() throws Exception {
|
||||
public void concurrentCacheExpansionsShareOneReservation() throws Exception {
|
||||
try (Fixture fixture = new Fixture(1_024);
|
||||
ExecutorService callers = Executors.newFixedThreadPool(2)) {
|
||||
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
|
||||
HydrologyTileCache cache = fixture.planningCache(budget);
|
||||
CountDownLatch entered = new CountDownLatch(2);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
cache.setTerrainPreparation(() -> {
|
||||
Runnable prepare = () -> {
|
||||
entered.countDown();
|
||||
await(release);
|
||||
fixture.complex.expandHydrologyNoiseCaches(budget);
|
||||
});
|
||||
Future<?> first = callers.submit(() -> cache.get(new HydrologyTileKey(0, 0)));
|
||||
Future<?> second = callers.submit(() -> cache.get(new HydrologyTileKey(1, 0)));
|
||||
};
|
||||
Future<?> first = callers.submit(prepare);
|
||||
Future<?> second = callers.submit(prepare);
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
assertEquals(0L, budget.reservedBytes());
|
||||
release.countDown();
|
||||
|
||||
+7
-4
@@ -278,13 +278,15 @@ public class IrisEngineEntryHydrologyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exactOriginChunkDemandsAllFourTilesConcurrently() throws Exception {
|
||||
public void exactOriginChunkDemandsAllFourTilesWithinTheRootBudget() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyPlannerSettings settings = mock(HydrologyPlannerSettings.class);
|
||||
when(settings.routing()).thenReturn(HydrologyPlannerSettings.defaults().routing());
|
||||
when(settings.publicationRadius()).thenReturn(64);
|
||||
when(planner.settings()).thenReturn(settings);
|
||||
CountDownLatch started = new CountDownLatch(4);
|
||||
int admitted = (int) Math.max(1L, Math.min(4L, Math.min(Runtime.getRuntime().availableProcessors(),
|
||||
Runtime.getRuntime().maxMemory() / (2L * 1024L * 1024L * 1024L))));
|
||||
CountDownLatch started = new CountDownLatch(admitted);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
Set<HydrologyTileKey> keys = ConcurrentHashMap.newKeySet();
|
||||
when(planner.plan(any())).thenAnswer(invocation -> {
|
||||
@@ -302,12 +304,13 @@ public class IrisEngineEntryHydrologyTest {
|
||||
Future<?> preparation = caller.submit(() -> cache.prepareChunkColumns(0, 0));
|
||||
try {
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
assertEquals(Set.of(new HydrologyTileKey(-1, -1), new HydrologyTileKey(0, -1),
|
||||
new HydrologyTileKey(-1, 0), new HydrologyTileKey(0, 0)), keys);
|
||||
assertEquals(admitted, keys.size());
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
preparation.get(5, TimeUnit.SECONDS);
|
||||
assertEquals(Set.of(new HydrologyTileKey(-1, -1), new HydrologyTileKey(0, -1),
|
||||
new HydrologyTileKey(-1, 0), new HydrologyTileKey(0, 0)), keys);
|
||||
cache.prepareChunkColumns(0, 0);
|
||||
assertEquals(4, keys.size());
|
||||
cache.close();
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
||||
import art.arcane.volmlib.util.matter.IrisMatter;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterBiomeInject;
|
||||
import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class IrisEngineMantleReadTest {
|
||||
@Test
|
||||
public void missingReadsAndRemovalsLeaveSectionEmpty() {
|
||||
MantleDataAdapter<Matter> adapter = IrisEngineMantle.createRuntimeDataAdapter(mock(IrisData.class));
|
||||
Matter section = new IrisMatter(16, 16, 16);
|
||||
|
||||
for (int y = 0; y < 16; y++) {
|
||||
assertNull(adapter.get(section, 0, y, 0, MatterBiomeInject.class));
|
||||
adapter.remove(section, 0, y, 0, MatterBiomeInject.class);
|
||||
}
|
||||
|
||||
assertTrue(section.getSliceMap().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void existingSlicesKeepValuesAndCoordinateRemoval() {
|
||||
MantleDataAdapter<Matter> adapter = IrisEngineMantle.createRuntimeDataAdapter(mock(IrisData.class));
|
||||
Matter section = new IrisMatter(16, 16, 16);
|
||||
BiomeInjectMatter slice = new BiomeInjectMatter(16, 16, 16);
|
||||
MatterBiomeInject value = BiomeInjectMatter.get("minecraft:plains");
|
||||
section.putSlice(MatterBiomeInject.class, slice);
|
||||
slice.set(0, 0, 0, value);
|
||||
slice.set(4, 4, 4, value);
|
||||
|
||||
assertSame(value, adapter.get(section, 4, 4, 4, MatterBiomeInject.class));
|
||||
adapter.remove(section, 0, 0, 0, MatterBiomeInject.class);
|
||||
|
||||
assertNull(adapter.get(section, 0, 0, 0, MatterBiomeInject.class));
|
||||
assertSame(value, adapter.get(section, 4, 4, 4, MatterBiomeInject.class));
|
||||
assertEquals(1, section.getSliceMap().size());
|
||||
}
|
||||
}
|
||||
@@ -241,6 +241,11 @@ public class ContentKeyValidatorTest {
|
||||
|
||||
private record FakeRegistries(List<String> blocks, List<String> items, List<String> entities,
|
||||
Map<String, List<NativeBlockProperty>> properties) implements PlatformRegistries {
|
||||
@Override
|
||||
public NativeBlockState decodeBlockState(String key) {
|
||||
return blockOrNull(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState block(String key) {
|
||||
return null;
|
||||
|
||||
@@ -209,6 +209,11 @@ public class SchemaBuilderIdentityTest {
|
||||
private static final List<NativeBlockProperty> LOG_PROPERTIES = List.of(
|
||||
new NativeBlockProperty("axis", "string", "y", List.of("x", "y", "z"), null));
|
||||
|
||||
@Override
|
||||
public NativeBlockState decodeBlockState(String key) {
|
||||
return blockOrNull(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState block(String key) {
|
||||
return null;
|
||||
|
||||
@@ -459,6 +459,11 @@ public class SchemaBuilderParityTest {
|
||||
}
|
||||
|
||||
private static final class FakeRegistries implements PlatformRegistries {
|
||||
@Override
|
||||
public NativeBlockState decodeBlockState(String key) {
|
||||
return blockOrNull(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState block(String key) {
|
||||
return null;
|
||||
|
||||
@@ -80,6 +80,11 @@ final class CompatFixtures {
|
||||
final Set<String> potionEffects = new HashSet<>(List.of("minecraft:speed", "minecraft:strength"));
|
||||
int blockLookups = 0;
|
||||
|
||||
@Override
|
||||
public NativeBlockState decodeBlockState(String key) {
|
||||
return blockOrNull(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState block(String key) {
|
||||
NativeBlockState state = blockOrNull(key, false);
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package art.arcane.iris.platform.bukkit;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.platform.registry.RegistryUtil;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||
import art.arcane.iris.world.storage.matter.PreObjectMatterCell;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.matter.IrisMatter;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.type.Leaves;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class BukkitPersistedBlockStateTest {
|
||||
@BeforeClass
|
||||
public static void installServer() {
|
||||
BukkitTestServer.install();
|
||||
try (MockedStatic<RegistryUtil> registry = mockStatic(RegistryUtil.class)) {
|
||||
registry.when(() -> RegistryUtil.find(Material.class, "grass", "short_grass"))
|
||||
.thenReturn(Material.SHORT_GRASS);
|
||||
BukkitBlockResolution.getAir();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodingPreservesExplicitPropertiesDespiteGenerationLeafPolicy() {
|
||||
BukkitRegistries registries = new BukkitRegistries();
|
||||
IrisSettings configuration = new IrisSettings();
|
||||
configuration.getGenerator().setPreventLeafDecay(true);
|
||||
String key = "minecraft:oak_leaves[distance=2,persistent=false,waterlogged=true]";
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class, CALLS_REAL_METHODS);
|
||||
MockedStatic<IrisSettings> settings = mockStatic(IrisSettings.class)) {
|
||||
bukkit.when(() -> Bukkit.createBlockData(anyString()))
|
||||
.thenAnswer(invocation -> parsed(invocation.getArgument(0)));
|
||||
settings.when(IrisSettings::get).thenReturn(configuration);
|
||||
|
||||
assertEquals(key.replace("persistent=false", "persistent=true"), registries.blockOrNull(key).key());
|
||||
NativeBlockState decoded = registries.decodeBlockState(key);
|
||||
assertEquals(key, decoded.key());
|
||||
assertSame(decoded, registries.decodeBlockState(key));
|
||||
assertEquals(key, ((BlockData) decoded.nativeHandle()).getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bothMatterSlicesRoundTripFullNativeProperties() throws IOException {
|
||||
BukkitRegistries registries = new BukkitRegistries();
|
||||
IrisPlatform previous = IrisPlatforms.getOrNull();
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.unbind();
|
||||
IrisPlatforms.bind(platform);
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class, CALLS_REAL_METHODS)) {
|
||||
bukkit.when(() -> Bukkit.createBlockData(anyString()))
|
||||
.thenAnswer(invocation -> parsed(invocation.getArgument(0)));
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
Matter matter = new IrisMatter(16, 16, 16);
|
||||
String[] keys = {
|
||||
"minecraft:oak_leaves[distance=3,persistent=false,waterlogged=false]",
|
||||
"minecraft:oak_leaves[distance=6,persistent=true,waterlogged=true]",
|
||||
"minecraft:oak_stairs[facing=east,half=top,shape=inner_left,waterlogged=true]"
|
||||
};
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
NativeBlockState block = BukkitBlockState.of(parsed(keys[i]));
|
||||
matter.<NativeBlockState>slice(NativeBlockState.class).set(i, 2, 3, block);
|
||||
matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class)
|
||||
.set(i, 2, 3, PreObjectMatterCell.block(block));
|
||||
}
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
matter.write(bytes);
|
||||
Matter restored = Matter.read(new ByteArrayInputStream(bytes.toByteArray()));
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
assertEquals(keys[i], restored.<NativeBlockState>getSlice(NativeBlockState.class).get(i, 2, 3).key());
|
||||
assertEquals(keys[i], restored.<PreObjectMatterCell>getSlice(PreObjectMatterCell.class)
|
||||
.get(i, 2, 3).block().key());
|
||||
}
|
||||
} finally {
|
||||
IrisPlatforms.unbind();
|
||||
if (previous != null) {
|
||||
IrisPlatforms.bind(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodingRetainsCustomProviderIdentityAndRegisteredStates() {
|
||||
BukkitRegistries registries = new BukkitRegistries();
|
||||
String key = "test:persisted_leaves[distance=2,persistent=false]";
|
||||
BlockData carrier = parsed("minecraft:oak_leaves[distance=2,persistent=false,waterlogged=false]");
|
||||
IrisCustomData custom = IrisCustomData.of(carrier, Identifier.fromString(key));
|
||||
ExternalDataSVC provider = mock(ExternalDataSVC.class);
|
||||
when(provider.getBlockData(Identifier.fromString(key))).thenReturn(Optional.of(custom));
|
||||
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||
services.when(() -> IrisServices.get(ExternalDataSVC.class)).thenReturn(provider);
|
||||
NativeBlockState decoded = registries.decodeBlockState(key);
|
||||
assertEquals(key, decoded.key());
|
||||
assertEquals(key, decoded.deferredPlacementKey());
|
||||
assertSame(custom, decoded.nativeHandle());
|
||||
}
|
||||
BukkitBlockResolution.registerCustomBlockData("test", "registered_persisted_leaves", carrier);
|
||||
assertSame(carrier, registries.decodeBlockState("test:registered_persisted_leaves").nativeHandle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidPersistedPropertiesAreNotSilentlyStripped() {
|
||||
String key = "minecraft:oak_leaves[distance=invalid,persistent=false]";
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class, CALLS_REAL_METHODS)) {
|
||||
bukkit.when(() -> Bukkit.createBlockData(key)).thenThrow(new IllegalArgumentException("invalid distance"));
|
||||
assertThrows(IllegalArgumentException.class, () -> new BukkitRegistries().decodeBlockState(key));
|
||||
}
|
||||
}
|
||||
|
||||
private static BlockData parsed(String key) {
|
||||
if (!key.startsWith("minecraft:oak_leaves[")) {
|
||||
return BukkitTestServer.blockData(key);
|
||||
}
|
||||
Leaves leaves = mock(Leaves.class);
|
||||
AtomicBoolean persistent = new AtomicBoolean(key.contains("persistent=true"));
|
||||
doAnswer(invocation -> key.replaceFirst("persistent=(true|false)", "persistent=" + persistent.get()))
|
||||
.when(leaves).getAsString();
|
||||
doAnswer(invocation -> key.replaceFirst("persistent=(true|false)", "persistent=" + persistent.get()))
|
||||
.when(leaves).getAsString(anyBoolean());
|
||||
doAnswer(invocation -> {
|
||||
persistent.set(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(leaves).setPersistent(anyBoolean());
|
||||
doReturn(Material.OAK_LEAVES).when(leaves).getMaterial();
|
||||
return leaves;
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,11 @@ public final class CompatTestPlatform {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState decodeBlockState(String key) {
|
||||
return blockOrNull(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState block(String key) {
|
||||
NativeBlockState state = blockOrNull(key, false);
|
||||
|
||||
@@ -25,6 +25,11 @@ import javax.management.MBeanServer;
|
||||
import javax.management.ObjectName;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@@ -40,15 +45,147 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleHeapPressureTest {
|
||||
private static final MantleHeapPressure.PanicGcPolicy POLICY =
|
||||
new MantleHeapPressure.PanicGcPolicy(10_000L, 60_000L, 60_000L, 240_000L);
|
||||
new MantleHeapPressure.PanicGcPolicy(250L, 5_000L, 10_000L, 60_000L, 60_000L, 240_000L);
|
||||
|
||||
@Test
|
||||
public void engagedAdmissionStartsReclaimAfterUsageDropsBeforeFirstRequest() {
|
||||
ReclaimerFixture fixture = new ReclaimerFixture(0);
|
||||
MantleHeapPressure.HeapPressureGate gate = new MantleHeapPressure.HeapPressureGate(
|
||||
0.92D, 0.82D, 60_000L, fixture.clock::get, fixture.reclaimer::resetEpisode);
|
||||
|
||||
assertEquals(true, gate.update(0.95D));
|
||||
fixture.clock.set(100L);
|
||||
double fraction = 0.85D;
|
||||
fixture.reclaimer.request(fraction, gate.update(fraction));
|
||||
fixture.clock.set(349L);
|
||||
fixture.reclaimer.request(fraction, gate.update(fraction));
|
||||
assertEquals(0, fixture.explicitCalls.get());
|
||||
fixture.clock.set(350L);
|
||||
fixture.reclaimer.request(fraction, gate.update(fraction));
|
||||
assertEquals(1, fixture.explicitCalls.get());
|
||||
assertEquals(0, fixture.diagnosticCalls.get());
|
||||
|
||||
fixture.clock.set(10_349L);
|
||||
fixture.reclaimer.request(fraction, gate.update(fraction));
|
||||
assertEquals(0, fixture.diagnosticCalls.get());
|
||||
fixture.clock.set(10_350L);
|
||||
fixture.reclaimer.request(fraction, gate.update(fraction));
|
||||
assertEquals(1, fixture.diagnosticCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ungatedUsageBelowHighWaterDoesNotStartReclaim() {
|
||||
ReclaimerFixture fixture = new ReclaimerFixture(0);
|
||||
MantleHeapPressure.HeapPressureGate gate = new MantleHeapPressure.HeapPressureGate(
|
||||
0.92D, 0.82D, 60_000L, fixture.clock::get, fixture.reclaimer::resetEpisode);
|
||||
double fraction = 0.85D;
|
||||
|
||||
fixture.reclaimer.request(fraction, gate.update(fraction));
|
||||
fixture.clock.set(120_000L);
|
||||
fixture.reclaimer.request(fraction, gate.update(fraction));
|
||||
|
||||
assertEquals(0, fixture.explicitCalls.get());
|
||||
assertEquals(0, fixture.diagnosticCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recoveredSpikeCancelsPendingExplicitGc() {
|
||||
ReclaimerFixture fixture = new ReclaimerFixture(0);
|
||||
MantleHeapPressure.HeapPressureGate gate = new MantleHeapPressure.HeapPressureGate(
|
||||
0.92D, 0.82D, 60_000L, fixture.clock::get, fixture.reclaimer::resetEpisode);
|
||||
|
||||
assertEquals(true, gate.update(0.95D));
|
||||
fixture.reclaimer.request(0.95D, false);
|
||||
fixture.clock.set(100L);
|
||||
assertEquals(false, gate.update(0.81D));
|
||||
fixture.clock.set(250L);
|
||||
fixture.reclaimer.request(0.90D, false);
|
||||
assertEquals(0, fixture.explicitCalls.get());
|
||||
assertEquals(0, fixture.diagnosticCalls.get());
|
||||
|
||||
fixture.reclaimer.request(0.95D, false);
|
||||
fixture.clock.set(300L);
|
||||
fixture.reclaimer.request(0.82D, false);
|
||||
fixture.clock.set(500L);
|
||||
fixture.reclaimer.request(0.90D, false);
|
||||
assertEquals(0, fixture.explicitCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitGcWaitsForGraceWhileHysteresisPressureStillReclaims() {
|
||||
ReclaimerFixture fixture = new ReclaimerFixture(0);
|
||||
|
||||
fixture.reclaimer.request(0.95D, false);
|
||||
fixture.clock.set(249L);
|
||||
fixture.reclaimer.request(0.95D, false);
|
||||
assertEquals(0, fixture.explicitCalls.get());
|
||||
fixture.clock.set(250L);
|
||||
fixture.reclaimer.request(0.85D, false);
|
||||
fixture.reclaimer.request(0.85D, false);
|
||||
assertEquals(1, fixture.explicitCalls.get());
|
||||
assertEquals(0, fixture.diagnosticCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recoveredEpisodesShareExplicitCooldownAndEscalateFromActualAttempt() {
|
||||
ReclaimerFixture fixture = new ReclaimerFixture(0);
|
||||
fixture.beginExplicit(0.95D);
|
||||
fixture.reclaimer.resetEpisode();
|
||||
fixture.clock.set(1_000L);
|
||||
fixture.reclaimer.request(0.95D, false);
|
||||
fixture.clock.set(1_250L);
|
||||
fixture.reclaimer.request(0.95D, false);
|
||||
fixture.clock.set(5_249L);
|
||||
fixture.reclaimer.request(0.95D, false);
|
||||
assertEquals(1, fixture.explicitCalls.get());
|
||||
fixture.clock.set(5_250L);
|
||||
fixture.reclaimer.request(0.95D, false);
|
||||
assertEquals(2, fixture.explicitCalls.get());
|
||||
fixture.clock.set(15_249L);
|
||||
fixture.reclaimer.request(0.95D, false);
|
||||
assertEquals(0, fixture.diagnosticCalls.get());
|
||||
fixture.clock.set(15_250L);
|
||||
fixture.reclaimer.request(0.95D, false);
|
||||
assertEquals(1, fixture.diagnosticCalls.get());
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000L)
|
||||
public void concurrentRequestersShareOneExplicitAttempt() throws Exception {
|
||||
ReclaimerFixture fixture = new ReclaimerFixture(0);
|
||||
fixture.reclaimer.request(0.95D, false);
|
||||
fixture.clock.set(250L);
|
||||
CountDownLatch ready = new CountDownLatch(2);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
try (ExecutorService executor = Executors.newFixedThreadPool(2)) {
|
||||
List<Future<?>> requests = new ArrayList<>();
|
||||
for (int index = 0; index < 2; index++) {
|
||||
requests.add(executor.submit(() -> {
|
||||
ready.countDown();
|
||||
start.await();
|
||||
fixture.reclaimer.request(0.95D, false);
|
||||
return null;
|
||||
}));
|
||||
}
|
||||
try {
|
||||
assertEquals(true, ready.await(5L, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
start.countDown();
|
||||
}
|
||||
for (Future<?> request : requests) {
|
||||
request.get(5L, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
assertEquals(1, fixture.explicitCalls.get());
|
||||
assertEquals(0, fixture.diagnosticCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalPressureNeverRequestsEitherGcPath() {
|
||||
ReclaimerFixture fixture = new ReclaimerFixture(0);
|
||||
|
||||
fixture.reclaimer.request(0.91D);
|
||||
fixture.reclaimer.request(0.91D, false);
|
||||
fixture.clock.addAndGet(120_000L);
|
||||
fixture.reclaimer.request(0.91D);
|
||||
fixture.reclaimer.request(0.91D, false);
|
||||
|
||||
assertEquals(0, fixture.explicitCalls.get());
|
||||
assertEquals(0, fixture.diagnosticCalls.get());
|
||||
@@ -58,13 +195,13 @@ public class MantleHeapPressureTest {
|
||||
public void sustainedPanicInvokesDiagnosticOnlyAfterNormalReclaimGrace() {
|
||||
ReclaimerFixture fixture = new ReclaimerFixture(0);
|
||||
|
||||
fixture.reclaimer.request(0.97D);
|
||||
fixture.clock.set(9_999L);
|
||||
fixture.reclaimer.request(0.97D);
|
||||
fixture.clock.set(10_000L);
|
||||
fixture.reclaimer.request(0.97D);
|
||||
fixture.clock.set(69_999L);
|
||||
fixture.reclaimer.request(0.99D);
|
||||
fixture.beginExplicit(0.97D);
|
||||
fixture.clock.set(10_249L);
|
||||
fixture.reclaimer.request(0.97D, false);
|
||||
fixture.clock.set(10_250L);
|
||||
fixture.reclaimer.request(0.97D, false);
|
||||
fixture.clock.set(70_249L);
|
||||
fixture.reclaimer.request(0.99D, false);
|
||||
|
||||
assertEquals(1, fixture.explicitCalls.get());
|
||||
assertEquals(1, fixture.diagnosticCalls.get());
|
||||
@@ -75,16 +212,16 @@ public class MantleHeapPressureTest {
|
||||
public void recoveredEpisodeResetsButDiagnosticCooldownStillApplies() {
|
||||
ReclaimerFixture fixture = new ReclaimerFixture(0);
|
||||
|
||||
fixture.reclaimer.request(0.97D);
|
||||
fixture.clock.set(10_000L);
|
||||
fixture.reclaimer.request(0.97D);
|
||||
fixture.beginExplicit(0.97D);
|
||||
fixture.clock.set(10_250L);
|
||||
fixture.reclaimer.request(0.97D, false);
|
||||
fixture.reclaimer.resetEpisode();
|
||||
fixture.clock.set(20_000L);
|
||||
fixture.reclaimer.request(0.97D);
|
||||
fixture.clock.set(30_000L);
|
||||
fixture.reclaimer.request(0.97D);
|
||||
fixture.clock.set(70_000L);
|
||||
fixture.reclaimer.request(0.97D);
|
||||
fixture.beginExplicit(0.97D);
|
||||
fixture.clock.set(30_250L);
|
||||
fixture.reclaimer.request(0.97D, false);
|
||||
fixture.clock.set(70_250L);
|
||||
fixture.reclaimer.request(0.97D, false);
|
||||
|
||||
assertEquals(2, fixture.explicitCalls.get());
|
||||
assertEquals(2, fixture.diagnosticCalls.get());
|
||||
@@ -94,11 +231,11 @@ public class MantleHeapPressureTest {
|
||||
public void highWaterEpisodeDiagnosesAfterNormalReclaimLeavesHeapBelowHighWater() {
|
||||
ReclaimerFixture fixture = new ReclaimerFixture(0);
|
||||
|
||||
fixture.reclaimer.request(0.93D);
|
||||
fixture.clock.set(9_999L);
|
||||
fixture.reclaimer.request(0.91D);
|
||||
fixture.clock.set(10_000L);
|
||||
fixture.reclaimer.request(0.91D);
|
||||
fixture.beginExplicit(0.93D);
|
||||
fixture.clock.set(10_249L);
|
||||
fixture.reclaimer.request(0.91D, false);
|
||||
fixture.clock.set(10_250L);
|
||||
fixture.reclaimer.request(0.91D, false);
|
||||
|
||||
assertEquals(1, fixture.explicitCalls.get());
|
||||
assertEquals(1, fixture.diagnosticCalls.get());
|
||||
@@ -109,13 +246,13 @@ public class MantleHeapPressureTest {
|
||||
public void successfulDiagnosticRetriesAfterCooldownWhenPressureRemainsHigh() {
|
||||
ReclaimerFixture fixture = new ReclaimerFixture(0);
|
||||
|
||||
fixture.reclaimer.request(0.97D);
|
||||
fixture.clock.set(10_000L);
|
||||
fixture.reclaimer.request(0.91D);
|
||||
fixture.clock.set(69_999L);
|
||||
fixture.reclaimer.request(0.91D);
|
||||
fixture.clock.set(70_000L);
|
||||
fixture.reclaimer.request(0.91D);
|
||||
fixture.beginExplicit(0.97D);
|
||||
fixture.clock.set(10_250L);
|
||||
fixture.reclaimer.request(0.91D, false);
|
||||
fixture.clock.set(70_249L);
|
||||
fixture.reclaimer.request(0.91D, false);
|
||||
fixture.clock.set(70_250L);
|
||||
fixture.reclaimer.request(0.91D, false);
|
||||
|
||||
assertEquals(1, fixture.explicitCalls.get());
|
||||
assertEquals(2, fixture.diagnosticCalls.get());
|
||||
@@ -126,13 +263,13 @@ public class MantleHeapPressureTest {
|
||||
public void failedDiagnosticRetriesAfterBackoffWithinSameEpisode() {
|
||||
ReclaimerFixture fixture = new ReclaimerFixture(1);
|
||||
|
||||
fixture.reclaimer.request(0.97D);
|
||||
fixture.clock.set(10_000L);
|
||||
fixture.reclaimer.request(0.91D);
|
||||
fixture.clock.set(69_999L);
|
||||
fixture.reclaimer.request(0.91D);
|
||||
fixture.clock.set(70_000L);
|
||||
fixture.reclaimer.request(0.91D);
|
||||
fixture.beginExplicit(0.97D);
|
||||
fixture.clock.set(10_250L);
|
||||
fixture.reclaimer.request(0.91D, false);
|
||||
fixture.clock.set(70_249L);
|
||||
fixture.reclaimer.request(0.91D, false);
|
||||
fixture.clock.set(70_250L);
|
||||
fixture.reclaimer.request(0.91D, false);
|
||||
|
||||
assertEquals(1, fixture.explicitCalls.get());
|
||||
assertEquals(2, fixture.diagnosticCalls.get());
|
||||
@@ -151,6 +288,7 @@ public class MantleHeapPressureTest {
|
||||
releases::incrementAndGet);
|
||||
|
||||
assertEquals(true, gate.update(0.93D));
|
||||
clock.set(30_000L);
|
||||
assertEquals(true, gate.update(0.89D));
|
||||
clock.set(60_999L);
|
||||
assertEquals(true, gate.update(0.89D));
|
||||
@@ -161,7 +299,7 @@ public class MantleHeapPressureTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renewedHighPressureRestartsHysteresisAndCanReengageAfterRelease() {
|
||||
public void renewedHighPressureKeepsEpisodeDeadlineAndCanReengageAfterRelease() {
|
||||
AtomicLong clock = new AtomicLong(5_000L);
|
||||
AtomicInteger releases = new AtomicInteger();
|
||||
MantleHeapPressure.HeapPressureGate gate = new MantleHeapPressure.HeapPressureGate(
|
||||
@@ -173,15 +311,64 @@ public class MantleHeapPressureTest {
|
||||
|
||||
assertEquals(true, gate.update(0.93D));
|
||||
assertEquals(true, gate.update(0.88D));
|
||||
clock.set(35_000L);
|
||||
assertEquals(true, gate.update(0.94D));
|
||||
assertEquals(true, gate.update(0.88D));
|
||||
clock.set(64_999L);
|
||||
assertEquals(true, gate.update(0.93D));
|
||||
assertEquals(true, gate.update(0.88D));
|
||||
clock.set(124_998L);
|
||||
assertEquals(true, gate.update(0.88D));
|
||||
clock.set(124_999L);
|
||||
clock.set(65_000L);
|
||||
assertEquals(true, gate.update(0.92D));
|
||||
assertEquals(0, releases.get());
|
||||
assertEquals(false, gate.update(0.88D));
|
||||
assertEquals(1, releases.get());
|
||||
|
||||
clock.set(70_000L);
|
||||
assertEquals(true, gate.update(0.92D));
|
||||
clock.set(129_999L);
|
||||
assertEquals(true, gate.update(0.88D));
|
||||
clock.set(130_000L);
|
||||
assertEquals(false, gate.update(0.88D));
|
||||
assertEquals(2, releases.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prolongedHighPressureWaitsForSubHighSampleAfterEpisodeDeadline() {
|
||||
AtomicLong clock = new AtomicLong(5_000L);
|
||||
AtomicInteger releases = new AtomicInteger();
|
||||
MantleHeapPressure.HeapPressureGate gate = new MantleHeapPressure.HeapPressureGate(
|
||||
0.92D,
|
||||
0.82D,
|
||||
60_000L,
|
||||
clock::get,
|
||||
releases::incrementAndGet);
|
||||
|
||||
assertEquals(true, gate.update(0.95D));
|
||||
clock.set(65_000L);
|
||||
assertEquals(true, gate.update(0.92D));
|
||||
clock.set(125_000L);
|
||||
assertEquals(true, gate.update(0.97D));
|
||||
assertEquals(0, releases.get());
|
||||
assertEquals(false, gate.update(0.91D));
|
||||
assertEquals(1, releases.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroHysteresisStillBlocksHighWaterSamples() {
|
||||
AtomicLong clock = new AtomicLong();
|
||||
AtomicInteger releases = new AtomicInteger();
|
||||
MantleHeapPressure.HeapPressureGate gate = new MantleHeapPressure.HeapPressureGate(
|
||||
0.92D,
|
||||
0.82D,
|
||||
0L,
|
||||
clock::get,
|
||||
releases::incrementAndGet);
|
||||
|
||||
assertEquals(true, gate.update(0.92D));
|
||||
assertEquals(true, gate.update(0.95D));
|
||||
assertEquals(0, releases.get());
|
||||
assertEquals(false, gate.update(0.919D));
|
||||
assertEquals(1, releases.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -236,6 +423,12 @@ public class MantleHeapPressureTest {
|
||||
private final List<Throwable> failures = new ArrayList<>();
|
||||
private final MantleHeapPressure.PanicGcReclaimer reclaimer;
|
||||
|
||||
private void beginExplicit(double fraction) {
|
||||
reclaimer.request(fraction, false);
|
||||
clock.addAndGet(POLICY.explicitGraceMs());
|
||||
reclaimer.request(fraction, false);
|
||||
}
|
||||
|
||||
private ReclaimerFixture(int failuresBeforeSuccess) {
|
||||
MantleHeapPressure.PanicGcActions actions = new MantleHeapPressure.PanicGcActions(
|
||||
clock::get,
|
||||
|
||||
@@ -5,6 +5,9 @@ import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -51,7 +54,7 @@ public class PregenMantleBackpressureTest {
|
||||
backpressure(() -> mantle, 4, 1_000L, () -> {
|
||||
}).enforceMantleBudget();
|
||||
|
||||
verify(mantle, never()).trim(0L, 0);
|
||||
verify(mantle, never()).trim(0L);
|
||||
verify(mantle, never()).unloadTectonicPlate(0);
|
||||
}
|
||||
|
||||
@@ -63,7 +66,7 @@ public class PregenMantleBackpressureTest {
|
||||
backpressure(() -> mantle, 4, 1_000L, () -> {
|
||||
}).enforceMantleBudget();
|
||||
|
||||
verify(mantle, times(1)).trim(0L, 0);
|
||||
verify(mantle, times(1)).trim(0L);
|
||||
verify(mantle, times(1)).unloadTectonicPlate(0);
|
||||
}
|
||||
|
||||
@@ -71,7 +74,7 @@ public class PregenMantleBackpressureTest {
|
||||
public void aTrimFailureEndsTheWaitInsteadOfSpinning() {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
when(mantle.getLoadedRegionCount()).thenReturn(20);
|
||||
doThrow(new IllegalStateException("mantle is gone")).when(mantle).trim(0L, 0);
|
||||
doThrow(new IllegalStateException("mantle is gone")).when(mantle).trim(0L);
|
||||
AtomicInteger timeouts = new AtomicInteger();
|
||||
|
||||
backpressure(() -> mantle, 4, 1_000L, timeouts::incrementAndGet).enforceMantleBudget();
|
||||
@@ -101,7 +104,7 @@ public class PregenMantleBackpressureTest {
|
||||
|
||||
backpressure.enforceMantleBudget();
|
||||
|
||||
verify(mantle, never()).trim(0L, 0);
|
||||
verify(mantle, never()).trim(0L);
|
||||
verify(mantle, never()).unloadTectonicPlate(0);
|
||||
assertEquals(0, timeouts.get());
|
||||
}
|
||||
@@ -140,6 +143,95 @@ public class PregenMantleBackpressureTest {
|
||||
assertFalse(waiter.isAlive());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void heapPressurePersistsIdlePlatesBeforeGcEvenBelowOrWithoutACountCap() {
|
||||
for (int cap : new int[]{0, 9, 16, 96}) {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
when(mantle.getLoadedRegionCount()).thenReturn(9);
|
||||
List<String> actions = new ArrayList<>();
|
||||
AtomicBoolean pressured = new AtomicBoolean(true);
|
||||
AtomicInteger timeouts = new AtomicInteger();
|
||||
when(mantle.saveOldestIdleTectonicPlate()).thenAnswer(invocation -> {
|
||||
actions.add("persist-and-unload");
|
||||
return true;
|
||||
});
|
||||
|
||||
backpressure(() -> mantle, cap, 1_000L, timeouts::incrementAndGet)
|
||||
.awaitHeapHeadroom(pressured::get, () -> {
|
||||
actions.add("gc");
|
||||
pressured.set(false);
|
||||
});
|
||||
|
||||
assertEquals(List.of("persist-and-unload", "gc"), actions);
|
||||
assertEquals(0, timeouts.get());
|
||||
verify(mantle, never()).trim(0L, 0);
|
||||
verify(mantle, never()).trim(0L);
|
||||
verify(mantle, never()).unloadTectonicPlate(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void heapHeadroomDoesNotEvictRecentlyUsedPlatesWithoutPressure() {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
AtomicInteger collections = new AtomicInteger();
|
||||
|
||||
backpressure(() -> mantle, 16, 1_000L, () -> {
|
||||
}).awaitHeapHeadroom(() -> false, collections::incrementAndGet);
|
||||
|
||||
verify(mantle, never()).trim(0L);
|
||||
verify(mantle, never()).unloadTectonicPlate(0);
|
||||
verify(mantle, never()).saveOldestIdleTectonicPlate();
|
||||
assertEquals(0, collections.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pinnedPlatesStillRespectThePressureWaitDeadline() {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
when(mantle.getLoadedRegionCount()).thenReturn(9);
|
||||
when(mantle.saveOldestIdleTectonicPlate()).thenReturn(false);
|
||||
AtomicInteger collections = new AtomicInteger();
|
||||
AtomicInteger timeouts = new AtomicInteger();
|
||||
|
||||
backpressure(() -> mantle, 16, 0L, timeouts::incrementAndGet)
|
||||
.awaitHeapHeadroom(() -> true, collections::incrementAndGet);
|
||||
|
||||
verify(mantle, times(1)).saveOldestIdleTectonicPlate();
|
||||
verify(mantle, never()).unloadTectonicPlate(0);
|
||||
assertEquals(1, collections.get());
|
||||
assertEquals(1, timeouts.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedPressureEvictionStillAttemptsGcAndRespectsTheDeadline() {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
doThrow(new IllegalStateException("plate write failed")).when(mantle).saveOldestIdleTectonicPlate();
|
||||
AtomicInteger collections = new AtomicInteger();
|
||||
AtomicInteger timeouts = new AtomicInteger();
|
||||
|
||||
backpressure(() -> mantle, 16, 0L, timeouts::incrementAndGet)
|
||||
.awaitHeapHeadroom(() -> true, collections::incrementAndGet);
|
||||
|
||||
assertEquals(1, collections.get());
|
||||
assertEquals(1, timeouts.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelledHeapPressureDoesNotEvictOrCollect() {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
AtomicInteger collections = new AtomicInteger();
|
||||
AtomicInteger timeouts = new AtomicInteger();
|
||||
PregenMantleBackpressure backpressure = new PregenMantleBackpressure(
|
||||
() -> mantle, 16, 5, 0L, timeouts::incrementAndGet, () -> "cancelled", () -> true);
|
||||
|
||||
backpressure.awaitHeapHeadroom(() -> true, collections::incrementAndGet);
|
||||
|
||||
verify(mantle, never()).trim(0L);
|
||||
verify(mantle, never()).unloadTectonicPlate(0);
|
||||
verify(mantle, never()).saveOldestIdleTectonicPlate();
|
||||
assertEquals(0, collections.get());
|
||||
assertEquals(0, timeouts.get());
|
||||
}
|
||||
|
||||
private static PregenMantleBackpressure backpressure(
|
||||
Supplier<Mantle> mantleSupplier,
|
||||
int maxResidentTectonicPlates,
|
||||
|
||||
@@ -36,7 +36,10 @@ public class PregenWaitPolicyContractTest {
|
||||
|
||||
assertTrue(backpressure.contains("progressed.await(waitMs, TimeUnit.MILLISECONDS)"));
|
||||
assertTrue(method(backpressure, "public void enforceMantleBudget()").contains("awaitProgress()"));
|
||||
assertTrue(method(backpressure, "public void awaitHeapHeadroom()").contains("awaitProgress()"));
|
||||
assertTrue(method(backpressure, "public void awaitHeapHeadroom()")
|
||||
.contains("awaitHeapHeadroom(MantleHeapPressure::overHighWater, MantleHeapPressure::requestPanicReclaim)"));
|
||||
assertTrue(method(backpressure, "void awaitHeapHeadroom(BooleanSupplier heapPressure, Runnable panicReclaim)")
|
||||
.contains("awaitProgress()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in new issue
Block a user