This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-21 10:04:22 -04:00
1 parent 8e3ef1b651
commit 197d2c2461
73 files changed
+4586 -424

No files matched your search

@@ -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());
}
}
@@ -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());
@@ -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 {
@@ -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));
}
}
}
@@ -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
@@ -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());
}
}
@@ -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);
}
}
}
@@ -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
@@ -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));
@@ -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) {
}
}
@@ -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,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++) {
@@ -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(
@@ -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(
@@ -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)) {
@@ -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();
@@ -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,
@@ -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<>();
@@ -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) {
@@ -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());
@@ -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();
@@ -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);
@@ -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