mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Pack and drain object plans
- Store source plans as packed per-destination cell runs over a value table - Weigh shared payloads once and give tiles and markers finite weights - Release a plan once every touched destination consumed it, keeping its index - Rebuild only on repeated consumption; debug stats for builds and waits
This commit is contained in:
1 parent
241260a762
commit
ec07f55fd8
10 files changed
+802
-246
No files matched your search
+7
-3
@@ -139,9 +139,13 @@ public class MantleObjectComponentCollisionDeterminismTest {
|
||||
Map<Position, String> blocks = new HashMap<>();
|
||||
MantleWriter writer = writer(blocks);
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, destination, 0);
|
||||
MantleObjectComponent.replaySourceChunks(destination, 0, RADIUS, (sourceX, sourceZ) ->
|
||||
transaction.apply(cache.get(sourceX, sourceZ,
|
||||
() -> buildSourcePlan(writer, sourceX, sourceZ, scenario))));
|
||||
MantleObjectComponent.replaySourceChunks(destination, 0, RADIUS, (sourceX, sourceZ) -> {
|
||||
ObjectSourcePlan plan = cache.acquire(sourceX, sourceZ, destination, 0,
|
||||
() -> buildSourcePlan(writer, sourceX, sourceZ, scenario));
|
||||
if (plan != null) {
|
||||
transaction.apply(plan);
|
||||
}
|
||||
});
|
||||
transaction.commit();
|
||||
return blocks;
|
||||
}
|
||||
|
||||
+12
-4
@@ -56,21 +56,29 @@ public class MantleObjectComponentPlacementDeterminismTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryObjectSourcePlansRemainReusableAfterNeighborPublicationAndHotload() {
|
||||
public void consumedObjectSourcePlansRebuildIdenticallyAfterNeighborPublicationAndHotload() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.generate(List.of(1, 0));
|
||||
Map<Position, String> expected = fixture.snapshot();
|
||||
Map<Chunk, Integer> calls = Map.copyOf(fixture.sourceCalls);
|
||||
assertEquals(12, fixture.sourceCalls.size());
|
||||
for (Integer calls : fixture.sourceCalls.values()) {
|
||||
assertEquals(1, calls.intValue());
|
||||
}
|
||||
|
||||
fixture.generate(List.of(0, 1));
|
||||
|
||||
assertEquals(expected, fixture.snapshot());
|
||||
assertEquals(calls, fixture.sourceCalls);
|
||||
assertSourceCalls(fixture, 2, 1);
|
||||
fixture.component.hotload();
|
||||
fixture.generate(List.of(0, 1));
|
||||
assertEquals(expected, fixture.snapshot());
|
||||
assertSourceCalls(fixture, 3, 2);
|
||||
}
|
||||
|
||||
private static void assertSourceCalls(Fixture fixture, int writingSources, int emptySources) {
|
||||
for (Map.Entry<Chunk, Integer> entry : fixture.sourceCalls.entrySet()) {
|
||||
assertEquals(2, entry.getValue().intValue());
|
||||
boolean writes = entry.getKey().z() == 0 && (entry.getKey().x() == 0 || entry.getKey().x() == 1);
|
||||
assertEquals(entry.getKey().toString(), writes ? writingSources : emptySources, entry.getValue().intValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,12 +86,55 @@ public class ObjectContinuationTest {
|
||||
assertNull(decoded.getSlice(String.class).get(1, 5, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packedCrossingFragmentsKeepEachPlacementRunInSourceOrder() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
List<ObjectDestinationTransaction.Mutation> mutations = List.of(
|
||||
marker(1, "a"), marker(17, "a"), marker(33, "a"),
|
||||
marker(5, "b"),
|
||||
marker(18, "loose"),
|
||||
marker(40, "c"), marker(20, "c"), marker(2, "c"), marker(41, "d"));
|
||||
ObjectSourcePlan plan = new ObjectSourcePlan(mutations, List.of(
|
||||
new ObjectDestinationTransaction.PlacementRange(0, 3),
|
||||
new ObjectDestinationTransaction.PlacementRange(3, 4),
|
||||
new ObjectDestinationTransaction.PlacementRange(5, 9)));
|
||||
|
||||
plan.persistContinuations(fixture.writer, 0, 0, 0, 0);
|
||||
|
||||
ObjectContinuationBundle.ChunkPosition zero = new ObjectContinuationBundle.ChunkPosition(0, 0);
|
||||
ObjectContinuationBundle.ChunkPosition one = new ObjectContinuationBundle.ChunkPosition(1, 0);
|
||||
ObjectContinuationBundle.ChunkPosition two = new ObjectContinuationBundle.ChunkPosition(2, 0);
|
||||
assertEquals(2, fixture.bundles.size());
|
||||
assertFragment(fixture.bundles.get(1).fragments().get(0), 0, new ObjectContinuationBundle.Bounds(1, 1, 33, 1),
|
||||
List.of(zero, one, two), List.of(marker(17, "a")));
|
||||
assertFragment(fixture.bundles.get(1).fragments().get(1), 2, new ObjectContinuationBundle.Bounds(2, 1, 41, 1),
|
||||
List.of(two, one, zero), List.of(marker(20, "c")));
|
||||
assertFragment(fixture.bundles.get(2).fragments().get(0), 0, new ObjectContinuationBundle.Bounds(1, 1, 33, 1),
|
||||
List.of(zero, one, two), List.of(marker(33, "a")));
|
||||
assertFragment(fixture.bundles.get(2).fragments().get(1), 2, new ObjectContinuationBundle.Bounds(2, 1, 41, 1),
|
||||
List.of(two, one, zero), List.of(marker(40, "c"), marker(41, "d")));
|
||||
}
|
||||
|
||||
private static void assertFragment(ObjectContinuationBundle.Fragment fragment, int ordinal, ObjectContinuationBundle.Bounds bounds,
|
||||
List<ObjectContinuationBundle.ChunkPosition> touched,
|
||||
List<ObjectDestinationTransaction.Mutation> mutations) throws IOException {
|
||||
assertEquals(new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, 0, 0, ordinal), fragment.key());
|
||||
assertEquals(bounds, fragment.bounds());
|
||||
assertEquals(touched, fragment.touchedChunks());
|
||||
Matter decoded = fragment.decode();
|
||||
assertEquals(mutations.size(), decoded.getSlice(String.class).getEntryCount());
|
||||
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
|
||||
ObjectDestinationTransaction.SetMutation set = (ObjectDestinationTransaction.SetMutation) mutation;
|
||||
assertEquals(set.value(), decoded.getSlice(String.class).get(set.x() & 15, set.key().y(), set.z() & 15));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedDestinationsDoNotRetainContinuations() {
|
||||
Fixture fixture = new Fixture();
|
||||
when(fixture.chunk.isFlagged(MantleFlag.REAL)).thenReturn(true);
|
||||
List<ObjectDestinationTransaction.Mutation> mutations = List.of(marker(-1, "left"), marker(0, "right"));
|
||||
ObjectSourcePlan plan = new ObjectSourcePlan(mutations, List.of(mutations));
|
||||
ObjectSourcePlan plan = new ObjectSourcePlan(mutations, List.of(new ObjectDestinationTransaction.PlacementRange(0, 2)));
|
||||
|
||||
plan.persistContinuations(fixture.writer, -1, 0, -1, 0);
|
||||
|
||||
|
||||
+5
-7
@@ -78,7 +78,7 @@ public class ObjectDestinationTransactionTest {
|
||||
}
|
||||
ObjectSourcePlan plan = source.sourcePlanSince(0);
|
||||
assertEquals(16, source.mutationCheckpoint());
|
||||
assertEquals(Integer.MAX_VALUE, plan.estimatedRetainedBytes());
|
||||
assertTrue(plan.estimatedRetainedBytes() < Integer.MAX_VALUE);
|
||||
for (int[] position : positions) {
|
||||
int x = position[0];
|
||||
int z = position[1];
|
||||
@@ -87,9 +87,7 @@ public class ObjectDestinationTransactionTest {
|
||||
List<ObjectDestinationTransaction.Mutation> sourceMutations = plan.mutationsFor(x >> 4, z >> 4);
|
||||
List<ObjectDestinationTransaction.Mutation> replayed = destination.sourcePlanSince(0).mutationsFor(x >> 4, z >> 4);
|
||||
assertEquals(sourceMutations.size(), replayed.size());
|
||||
for (int index = 0; index < sourceMutations.size(); index++) {
|
||||
assertSame(sourceMutations.get(index), replayed.get(index));
|
||||
}
|
||||
assertEquals(sourceMutations, replayed);
|
||||
assertEquals(8, destination.mutationCheckpoint());
|
||||
assertSame(replacement, destination.get(x, 7, z));
|
||||
assertNull(destination.getDataIfPresent(x, 7, z, Identifier.class));
|
||||
@@ -143,8 +141,8 @@ public class ObjectDestinationTransactionTest {
|
||||
|
||||
List<ObjectDestinationTransaction.Mutation> replayed = destination.sourcePlanSince(0).mutationsFor(0, 0);
|
||||
assertEquals(2, replayed.size());
|
||||
assertSame(plan.mutationsFor(0, 0).get(5), replayed.get(0));
|
||||
assertSame(plan.mutationsFor(0, 0).get(6), replayed.get(1));
|
||||
assertEquals(plan.mutationsFor(0, 0).get(5), replayed.get(0));
|
||||
assertEquals(plan.mutationsFor(0, 0).get(6), replayed.get(1));
|
||||
assertNull(destination.getDataIfPresent(2, 4, 0, Identifier.class));
|
||||
assertNull(destination.getDataIfPresent(1, 4, 0, MatterCavern.class));
|
||||
assertEquals("tree", destination.getDataIfPresent(0, 4, 0, String.class));
|
||||
@@ -573,7 +571,7 @@ public class ObjectDestinationTransactionTest {
|
||||
ObjectDestinationTransaction destination = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
destination.apply(plan);
|
||||
assertNull(destination.getDataIfPresent(0, 4, 0, Identifier.class));
|
||||
assertSame(plan.mutationsFor(0, 0).getFirst(), destination.sourcePlanSince(0).mutationsFor(0, 0).getFirst());
|
||||
assertEquals(plan.mutationsFor(0, 0).getFirst(), destination.sourcePlanSince(0).mutationsFor(0, 0).getFirst());
|
||||
|
||||
assertThrows(IllegalStateException.class, destination::commit);
|
||||
verify(writer).clearData(0, 4, 0, Identifier.class);
|
||||
|
||||
+136
-103
@@ -4,7 +4,6 @@ import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -15,71 +14,116 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ObjectSourcePlanCacheTest {
|
||||
@Test
|
||||
public void budgetRetainsNeighboringSourceWorkingSetWithoutRebuilding() {
|
||||
public void untouchedDestinationsReuseTheSourceIndexWithoutRebuilding() {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
ObjectDestinationTransaction.DataKey key = new ObjectDestinationTransaction.DataKey(
|
||||
0, 4, 0, String.class);
|
||||
ObjectDestinationTransaction.Mutation mutation = new ObjectDestinationTransaction.SetMutation(key, "marker");
|
||||
ObjectSourcePlan expected = new ObjectSourcePlan(Collections.nCopies(2, mutation));
|
||||
assertTrue(expected.estimatedRetainedBytes() * 1024L < 1_048_576L);
|
||||
for (int sweep = 0; sweep < 3; sweep++) {
|
||||
for (int x = -16; x < 16; x++) {
|
||||
for (int z = -16; z < 16; z++) {
|
||||
assertSame(expected, cache.get(x, z, () -> {
|
||||
builds.incrementAndGet();
|
||||
return expected;
|
||||
}));
|
||||
ObjectSourcePlan plan = planTouching(new int[]{0, 0}, new int[]{1, 0});
|
||||
for (int x = -7; x <= 7; x++) {
|
||||
for (int z = -7; z <= 7; z++) {
|
||||
if ((x == 0 || x == 1) && z == 0) {
|
||||
continue;
|
||||
}
|
||||
assertNull(cache.acquire(0, 0, x, z, () -> {
|
||||
builds.incrementAndGet();
|
||||
return plan;
|
||||
}));
|
||||
}
|
||||
assertEquals(1024L, cache.estimatedSize());
|
||||
assertEquals(1024, builds.get());
|
||||
}
|
||||
assertEquals(1, builds.get());
|
||||
assertSame(plan, cache.acquire(0, 0, 1, 0, () -> {
|
||||
builds.incrementAndGet();
|
||||
return plan;
|
||||
}));
|
||||
assertEquals(1, builds.get());
|
||||
ObjectSourcePlanCache.Stats stats = cache.stats();
|
||||
assertEquals(1L, stats.builds());
|
||||
assertEquals(224L, stats.lookups());
|
||||
assertEquals(0L, stats.drained());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyDestinationConsumesARetainedPlanOnceAndRepeatsRebuild() {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
ObjectSourcePlan plan = planTouching(new int[]{0, 0}, new int[]{1, 0});
|
||||
ObjectSourcePlan rebuilt = planTouching(new int[]{0, 0}, new int[]{1, 0});
|
||||
assertSame(plan, cache.acquire(0, 0, 0, 0, () -> {
|
||||
builds.incrementAndGet();
|
||||
return plan;
|
||||
}));
|
||||
assertSame(plan, cache.acquire(0, 0, 1, 0, () -> {
|
||||
builds.incrementAndGet();
|
||||
return rebuilt;
|
||||
}));
|
||||
assertEquals(1, builds.get());
|
||||
assertEquals(1L, cache.stats().drained());
|
||||
assertTrue(cache.stats().retainedBytes() < plan.estimatedRetainedBytes());
|
||||
|
||||
assertNull(cache.acquire(0, 0, 2, 0, () -> {
|
||||
builds.incrementAndGet();
|
||||
return rebuilt;
|
||||
}));
|
||||
assertSame(rebuilt, cache.acquire(0, 0, 0, 0, () -> {
|
||||
builds.incrementAndGet();
|
||||
return rebuilt;
|
||||
}));
|
||||
assertEquals(2, builds.get());
|
||||
assertEquals(1L, cache.stats().replays());
|
||||
assertSame(rebuilt, cache.acquire(0, 0, 1, 0, () -> {
|
||||
builds.incrementAndGet();
|
||||
return plan;
|
||||
}));
|
||||
assertEquals(2, builds.get());
|
||||
assertEquals(2L, cache.stats().drained());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyPlansAreRetainedAsIndexesAndNeverRebuilt() {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
for (int sweep = 0; sweep < 3; sweep++) {
|
||||
for (int chunk = 0; chunk < 1000; chunk++) {
|
||||
assertNull(cache.acquire(chunk, 0, chunk, 0, () -> {
|
||||
builds.incrementAndGet();
|
||||
return new ObjectSourcePlan(List.of());
|
||||
}));
|
||||
}
|
||||
}
|
||||
assertEquals(1000, builds.get());
|
||||
assertEquals(1000L, cache.estimatedSize());
|
||||
cache.clear();
|
||||
assertEquals(0L, cache.estimatedSize());
|
||||
assertSame(expected, cache.get(-16, -16, () -> {
|
||||
builds.incrementAndGet();
|
||||
return expected;
|
||||
}));
|
||||
assertEquals(1025, builds.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrelatedCoordinatesBuildWhileAnotherPlanIsPending() throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(4096L);
|
||||
ObjectSourcePlan first = planAt(1);
|
||||
ObjectSourcePlan second = planAt(2);
|
||||
ObjectSourcePlan first = planTouching(new int[]{32113, 37});
|
||||
ObjectSourcePlan second = planTouching(new int[]{32667, 37});
|
||||
CountDownLatch firstEntered = new CountDownLatch(1);
|
||||
CountDownLatch releaseFirst = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
assertEquals(Long.hashCode(CacheKey.mix(CacheKey.key(32113, 37))),
|
||||
Long.hashCode(CacheKey.mix(CacheKey.key(32667, 37))));
|
||||
Future<ObjectSourcePlan> firstResult = executor.submit(() -> cache.get(32113, 37, () -> {
|
||||
Future<ObjectSourcePlan> firstResult = executor.submit(() -> cache.acquire(32113, 37, 32113, 37, () -> {
|
||||
firstEntered.countDown();
|
||||
try {
|
||||
assertTrue(releaseFirst.await(10L, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(failure);
|
||||
}
|
||||
await(releaseFirst);
|
||||
return first;
|
||||
}));
|
||||
assertTrue(firstEntered.await(5L, TimeUnit.SECONDS));
|
||||
Future<ObjectSourcePlan> secondResult = executor.submit(() -> cache.get(32667, 37, () -> second));
|
||||
Future<ObjectSourcePlan> secondResult = executor.submit(() -> cache.acquire(32667, 37, 32667, 37, () -> second));
|
||||
|
||||
assertSame(second, secondResult.get(3L, TimeUnit.SECONDS));
|
||||
releaseFirst.countDown();
|
||||
assertSame(first, firstResult.get(5L, TimeUnit.SECONDS));
|
||||
assertSame(first, cache.get(32113, 37, () -> second));
|
||||
assertSame(second, cache.get(32667, 37, () -> first));
|
||||
} finally {
|
||||
releaseFirst.countDown();
|
||||
executor.shutdownNow();
|
||||
@@ -87,9 +131,13 @@ public class ObjectSourcePlanCacheTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentRequestsBuildOneSourcePlan() throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(4096L);
|
||||
ObjectSourcePlan expected = new ObjectSourcePlan(List.of());
|
||||
public void concurrentDestinationsShareOneSourceBuild() throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);
|
||||
int[][] touched = new int[8][];
|
||||
for (int index = 0; index < touched.length; index++) {
|
||||
touched[index] = new int[]{index - 4, -7};
|
||||
}
|
||||
ObjectSourcePlan expected = planTouching(touched);
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
CountDownLatch ready = new CountDownLatch(8);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
@@ -97,10 +145,11 @@ public class ObjectSourcePlanCacheTest {
|
||||
List<Future<ObjectSourcePlan>> futures = new ArrayList<>();
|
||||
try {
|
||||
for (int index = 0; index < 8; index++) {
|
||||
int destinationX = index - 4;
|
||||
futures.add(executor.submit(() -> {
|
||||
ready.countDown();
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
return cache.get(4, -7, () -> {
|
||||
return cache.acquire(4, -7, destinationX, -7, () -> {
|
||||
builds.incrementAndGet();
|
||||
return expected;
|
||||
});
|
||||
@@ -116,16 +165,35 @@ public class ObjectSourcePlanCacheTest {
|
||||
}
|
||||
|
||||
assertEquals(1, builds.get());
|
||||
assertEquals(1L, cache.stats().drained());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oversizedPlansAreServedWithoutRetention() {
|
||||
ObjectSourcePlan large = planTouching(new int[]{0, 0}, new int[]{1, 0});
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(large.estimatedRetainedBytes());
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
|
||||
assertSame(large, cache.acquire(0, 0, 0, 0, () -> {
|
||||
builds.incrementAndGet();
|
||||
return large;
|
||||
}));
|
||||
assertEquals(0L, cache.estimatedSize());
|
||||
assertSame(large, cache.acquire(0, 0, 1, 0, () -> {
|
||||
builds.incrementAndGet();
|
||||
return large;
|
||||
}));
|
||||
assertEquals(2, builds.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheEvictsByRetainedBytesAndCanBeCleared() {
|
||||
ObjectSourcePlan first = planAt(0);
|
||||
ObjectSourcePlan second = planAt(1);
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(first.estimatedRetainedBytes());
|
||||
ObjectSourcePlan first = planTouching(new int[]{0, 0}, new int[]{9, 9});
|
||||
ObjectSourcePlan second = planTouching(new int[]{1, 0}, new int[]{9, 9});
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(first.estimatedRetainedBytes() + 128L);
|
||||
|
||||
cache.get(0, 0, () -> first);
|
||||
cache.get(1, 0, () -> second);
|
||||
cache.acquire(0, 0, 0, 0, () -> first);
|
||||
cache.acquire(1, 0, 1, 0, () -> second);
|
||||
|
||||
assertEquals(1L, cache.estimatedSize());
|
||||
cache.clear();
|
||||
@@ -140,12 +208,12 @@ public class ObjectSourcePlanCacheTest {
|
||||
@Test
|
||||
public void clearingPendingWorkKeepsNewGenerationIsolated() throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(4096L);
|
||||
ObjectSourcePlan old = planAt(1);
|
||||
ObjectSourcePlan replacement = planAt(2);
|
||||
ObjectSourcePlan old = planTouching(new int[]{7, 8});
|
||||
ObjectSourcePlan replacement = planTouching(new int[]{7, 8});
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
|
||||
Future<ObjectSourcePlan> first = executor.submit(() -> cache.get(7, 8, () -> {
|
||||
Future<ObjectSourcePlan> first = executor.submit(() -> cache.acquire(7, 8, 7, 8, () -> {
|
||||
entered.countDown();
|
||||
await(release);
|
||||
return old;
|
||||
@@ -153,25 +221,25 @@ public class ObjectSourcePlanCacheTest {
|
||||
try {
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
cache.clear();
|
||||
assertSame(replacement, cache.get(7, 8, () -> replacement));
|
||||
assertSame(replacement, cache.acquire(7, 8, 7, 8, () -> replacement));
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
assertSame(old, first.get(5L, TimeUnit.SECONDS));
|
||||
assertSame(replacement, cache.get(7, 8, () -> old));
|
||||
assertSame(old, cache.acquire(7, 8, 7, 8, () -> old));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void waitingForkJoinWorkerAllowsQueuedDependencyToRun() throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(4096L);
|
||||
ObjectSourcePlan expected = planAt(1);
|
||||
ObjectSourcePlan expected = planTouching(new int[]{7, 8}, new int[]{8, 8});
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch waiting = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
try (ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||
ForkJoinPool pool = new ForkJoinPool(1)) {
|
||||
Future<ObjectSourcePlan> first = owner.submit(() -> cache.get(7, 8, () -> {
|
||||
Future<ObjectSourcePlan> first = owner.submit(() -> cache.acquire(7, 8, 7, 8, () -> {
|
||||
entered.countDown();
|
||||
await(release);
|
||||
return expected;
|
||||
@@ -180,7 +248,7 @@ public class ObjectSourcePlanCacheTest {
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
Future<ObjectSourcePlan> second = pool.submit(() -> {
|
||||
waiting.countDown();
|
||||
return cache.get(7, 8, () -> { throw new AssertionError("Duplicate build"); });
|
||||
return cache.acquire(7, 8, 8, 8, () -> { throw new AssertionError("Duplicate build"); });
|
||||
});
|
||||
assertTrue(waiting.await(5L, TimeUnit.SECONDS));
|
||||
pool.submit(release::countDown).get(5L, TimeUnit.SECONDS);
|
||||
@@ -193,53 +261,23 @@ public class ObjectSourcePlanCacheTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedAndNullBuildsAreRetryableAndRecursionFailsImmediately() {
|
||||
public void failedAndMissingBuildsAreRetryableAndRecursionFailsImmediately() {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(4096L);
|
||||
IllegalArgumentException failure = new IllegalArgumentException("Build failed");
|
||||
assertSame(failure, assertThrows(IllegalArgumentException.class,
|
||||
() -> cache.get(7, 8, () -> { throw failure; })));
|
||||
assertNull(cache.get(7, 8, () -> null));
|
||||
assertThrows(IllegalStateException.class, () -> cache.get(7, 8,
|
||||
() -> cache.get(7, 8, () -> planAt(1))));
|
||||
ObjectSourcePlan expected = planAt(1);
|
||||
assertSame(expected, cache.get(7, 8, () -> expected));
|
||||
() -> cache.acquire(7, 8, 7, 8, () -> { throw failure; })));
|
||||
assertThrows(NullPointerException.class, () -> cache.acquire(7, 8, 7, 8, () -> null));
|
||||
assertThrows(IllegalStateException.class, () -> cache.acquire(7, 8, 7, 8,
|
||||
() -> cache.acquire(7, 8, 7, 8, () -> planTouching(new int[]{7, 8}))));
|
||||
ObjectSourcePlan expected = planTouching(new int[]{7, 8});
|
||||
assertSame(expected, cache.acquire(7, 8, 7, 8, () -> expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void byteBudgetScalesWithHeapAndHasAnUpperBound() {
|
||||
assertEquals(64L * 1024L * 1024L, ObjectSourcePlanCache.retainedByteBudget(4L * 1024L * 1024L * 1024L));
|
||||
assertEquals(16L * 1024L * 1024L, ObjectSourcePlanCache.retainedByteBudget(1024L * 1024L * 1024L));
|
||||
assertEquals(256L * 1024L * 1024L, ObjectSourcePlanCache.retainedByteBudget(Long.MAX_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyPlansConsumeTheByteBudget() {
|
||||
ObjectSourcePlan empty = new ObjectSourcePlan(List.of());
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(4L * empty.estimatedRetainedBytes());
|
||||
for (int chunk = 0; chunk < 1000; chunk++) {
|
||||
cache.get(chunk, 0, () -> empty);
|
||||
}
|
||||
assertTrue(cache.estimatedSize() <= 4L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oversizedAndUnmeasuredPlansAreReturnedWithoutRetention() {
|
||||
ObjectSourcePlan large = planAt(1);
|
||||
ObjectSourcePlan unmeasured = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(0, 4, 0, Object.class), new Object())));
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(large.estimatedRetainedBytes() - 1L);
|
||||
assertSame(large, cache.get(1, 0, () -> large));
|
||||
assertSame(unmeasured, cache.get(2, 0, () -> unmeasured));
|
||||
assertEquals(0L, cache.estimatedSize());
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
cache.get(1, 0, () -> {
|
||||
builds.incrementAndGet();
|
||||
return large;
|
||||
});
|
||||
assertEquals(1, builds.get());
|
||||
ObjectSourcePlanCache roomy = new ObjectSourcePlanCache(Long.MAX_VALUE);
|
||||
assertSame(unmeasured, roomy.get(2, 0, () -> unmeasured));
|
||||
assertEquals(0L, roomy.estimatedSize());
|
||||
assertEquals(128L * 1024L * 1024L, ObjectSourcePlanCache.retainedByteBudget(4L * 1024L * 1024L * 1024L));
|
||||
assertEquals(32L * 1024L * 1024L, ObjectSourcePlanCache.retainedByteBudget(1024L * 1024L * 1024L));
|
||||
assertEquals(512L * 1024L * 1024L, ObjectSourcePlanCache.retainedByteBudget(Long.MAX_VALUE));
|
||||
}
|
||||
|
||||
private static void await(CountDownLatch latch) {
|
||||
@@ -251,17 +289,12 @@ public class ObjectSourcePlanCacheTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static ObjectSourcePlan planAt(int x) {
|
||||
ObjectDestinationTransaction.DataKey key = new ObjectDestinationTransaction.DataKey(
|
||||
x,
|
||||
4,
|
||||
0,
|
||||
String.class
|
||||
);
|
||||
ObjectDestinationTransaction.Mutation mutation = new ObjectDestinationTransaction.SetMutation(
|
||||
key,
|
||||
"marker"
|
||||
);
|
||||
return new ObjectSourcePlan(List.of(mutation));
|
||||
private static ObjectSourcePlan planTouching(int[]... chunks) {
|
||||
List<ObjectDestinationTransaction.Mutation> mutations = new ArrayList<>();
|
||||
for (int[] chunk : chunks) {
|
||||
mutations.add(new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(chunk[0] << 4, 4, chunk[1] << 4, String.class), "marker"));
|
||||
}
|
||||
return new ObjectSourcePlan(mutations);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ObjectSourcePlanTest {
|
||||
@Test
|
||||
@@ -29,14 +37,85 @@ public class ObjectSourcePlanTest {
|
||||
|
||||
List<ObjectDestinationTransaction.Mutation> local = plan.mutationsFor(-1, -1);
|
||||
assertEquals(expected, local);
|
||||
assertSame(local, plan.mutationsFor(-1, -1));
|
||||
assertEquals(local, plan.mutationsFor(-1, -1));
|
||||
assertEquals(3, plan.mutationsFor(Integer.MIN_VALUE >> 4, Integer.MAX_VALUE >> 4).size());
|
||||
assertEquals(3, plan.mutationsFor(Integer.MAX_VALUE >> 4, Integer.MIN_VALUE >> 4).size());
|
||||
assertTrue(plan.mutationsFor(1, -1).isEmpty());
|
||||
assertTrue(plan.mutationsFor(-1, 1).isEmpty());
|
||||
assertEquals(5, plan.destinationCount());
|
||||
assertThrows(UnsupportedOperationException.class, () -> local.clear());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packedPlansReplayEveryDestinationInSourceOrder() {
|
||||
NativeBlockState[] states = new NativeBlockState[4];
|
||||
for (int index = 0; index < states.length; index++) {
|
||||
states[index] = mock(NativeBlockState.class);
|
||||
}
|
||||
NativeBlockState custom = mock(NativeBlockState.class);
|
||||
when(custom.isCustom()).thenReturn(true);
|
||||
MatterMarker marker = new MatterMarker("loot");
|
||||
Random random = new Random(1337L);
|
||||
for (int trial = 0; trial < 50; trial++) {
|
||||
int originX = random.nextInt(4096) - 2048;
|
||||
int originZ = random.nextInt(4096) - 2048;
|
||||
List<ObjectDestinationTransaction.Mutation> mutations = new ArrayList<>();
|
||||
List<ObjectDestinationTransaction.PlacementRange> placements = new ArrayList<>();
|
||||
int placementCount = 1 + random.nextInt(12);
|
||||
for (int placement = 0; placement < placementCount; placement++) {
|
||||
int start = mutations.size();
|
||||
String placementMarker = "trees/oak@" + random.nextInt(1000);
|
||||
int centerX = (originX << 4) + random.nextInt(16);
|
||||
int centerZ = (originZ << 4) + random.nextInt(16);
|
||||
int reach = 1 + random.nextInt(40);
|
||||
int blocks = 1 + random.nextInt(60);
|
||||
for (int block = 0; block < blocks; block++) {
|
||||
int x = centerX + random.nextInt(2 * reach + 1) - reach;
|
||||
int z = centerZ + random.nextInt(2 * reach + 1) - reach;
|
||||
int y = random.nextInt(768);
|
||||
mutations.add(switch (random.nextInt(6)) {
|
||||
case 0 -> new ObjectDestinationTransaction.CustomBlockMutation(key(x, y, z, NativeBlockState.class), custom);
|
||||
case 1 -> new ObjectDestinationTransaction.SetMutation(key(x, y, z, String.class), placementMarker);
|
||||
case 2 -> new ObjectDestinationTransaction.SetMutation(key(x, y, z, TreeBlockMaterial.class),
|
||||
new TreeBlockMaterial("minecraft:oak_log"));
|
||||
case 3 -> new ObjectDestinationTransaction.SetMutation(key(x, y, z, MatterMarker.class), marker);
|
||||
default -> {
|
||||
NativeBlockState state = states[random.nextInt(states.length)];
|
||||
yield new ObjectDestinationTransaction.SetMutation(key(x, y, z, NativeBlockState.class), state);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (random.nextInt(4) != 0) {
|
||||
placements.add(new ObjectDestinationTransaction.PlacementRange(start, mutations.size()));
|
||||
}
|
||||
}
|
||||
ObjectSourcePlan plan = new ObjectSourcePlan(mutations, placements);
|
||||
Map<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> expected = group(mutations);
|
||||
assertEquals(expected.size(), plan.destinationCount());
|
||||
for (Map.Entry<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> destination : expected.entrySet()) {
|
||||
List<ObjectDestinationTransaction.Mutation> replayed = plan.mutationsFor(destination.getKey().x(), destination.getKey().z());
|
||||
assertEquals(destination.getValue(), replayed);
|
||||
for (int index = 0; index < replayed.size(); index++) {
|
||||
Object original = value(destination.getValue().get(index));
|
||||
Object decoded = value(replayed.get(index));
|
||||
if (!(original instanceof TreeBlockMaterial) && !(original instanceof String)) {
|
||||
assertSame(original, decoded);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void heightsOutsideThePackedRangeAreRejected() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new ObjectSourcePlan(List.of(
|
||||
new ObjectDestinationTransaction.SetMutation(key(0, -1, 0, String.class), "marker"))));
|
||||
assertThrows(IllegalArgumentException.class, () -> new ObjectSourcePlan(List.of(
|
||||
new ObjectDestinationTransaction.SetMutation(key(0, 1 << 22, 0, String.class), "marker"))));
|
||||
assertThrows(IllegalArgumentException.class, () -> new ObjectSourcePlan(List.of(
|
||||
new ObjectDestinationTransaction.SetMutation(key(0, 4, 0, Object.class), "marker"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheWeightIncludesEachDestinationIndex() {
|
||||
ObjectDestinationTransaction.Mutation first = mutationAt(0, 0);
|
||||
@@ -50,6 +129,24 @@ public class ObjectSourcePlanTest {
|
||||
assertThrows(NullPointerException.class, () -> new ObjectSourcePlan(Arrays.asList(first, null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sharedPayloadsAreWeighedOncePerPlan() {
|
||||
List<ObjectDestinationTransaction.Mutation> one = List.of(mutationAt(0, 0));
|
||||
List<ObjectDestinationTransaction.Mutation> many = new ArrayList<>();
|
||||
for (int index = 0; index < 100; index++) {
|
||||
many.add(mutationAt(index & 15, 0));
|
||||
}
|
||||
List<ObjectDestinationTransaction.Mutation> trees = new ArrayList<>();
|
||||
for (int index = 0; index < 100; index++) {
|
||||
trees.add(new ObjectDestinationTransaction.SetMutation(key(index & 15, 4, 0, TreeBlockMaterial.class),
|
||||
new TreeBlockMaterial("minecraft:oak_log")));
|
||||
}
|
||||
int single = new ObjectSourcePlan(one).estimatedRetainedBytes();
|
||||
|
||||
assertEquals(99L * 8L, new ObjectSourcePlan(many).estimatedRetainedBytes() - single);
|
||||
assertTrue(new ObjectSourcePlan(trees).estimatedRetainedBytes() - single < 100L * 16L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retainedBytesIncludeVariableLengthMarkers() {
|
||||
ObjectDestinationTransaction.DataKey key = new ObjectDestinationTransaction.DataKey(0, 4, 0, String.class);
|
||||
@@ -58,6 +155,15 @@ public class ObjectSourcePlanTest {
|
||||
assertTrue(longMarker.estimatedRetainedBytes() - shortMarker.estimatedRetainedBytes() >= 8190);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownPayloadsAreWeighedInsteadOfDisablingRetention() {
|
||||
ObjectSourcePlan unknown = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(
|
||||
key(0, 4, 0, Object.class), new Object())));
|
||||
|
||||
assertTrue(unknown.estimatedRetainedBytes() > new ObjectSourcePlan(List.of(mutationAt(0, 0))).estimatedRetainedBytes());
|
||||
assertTrue(unknown.estimatedRetainedBytes() < 4096);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void continuationMetadataAddsRetainedBytesWithoutCountingPayloadsTwice() {
|
||||
List<ObjectDestinationTransaction.Mutation> shortMarkers = List.of(mutationAt(0, 0), mutationAt(16, 0));
|
||||
@@ -66,18 +172,54 @@ public class ObjectSourcePlanTest {
|
||||
new ObjectDestinationTransaction.DataKey(0, 4, 0, String.class), "a".repeat(4096)),
|
||||
new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(16, 4, 0, String.class), "b".repeat(4096)));
|
||||
ObjectDestinationTransaction.PlacementRange whole = new ObjectDestinationTransaction.PlacementRange(0, 2);
|
||||
ObjectSourcePlan shortPlain = new ObjectSourcePlan(shortMarkers);
|
||||
ObjectSourcePlan shortCrossing = new ObjectSourcePlan(shortMarkers, List.of(shortMarkers));
|
||||
ObjectSourcePlan shortCrossing = new ObjectSourcePlan(shortMarkers, List.of(whole));
|
||||
ObjectSourcePlan longPlain = new ObjectSourcePlan(longMarkers);
|
||||
ObjectSourcePlan longCrossing = new ObjectSourcePlan(longMarkers, List.of(longMarkers));
|
||||
ObjectSourcePlan repeatedCrossing = new ObjectSourcePlan(shortMarkers, List.of(shortMarkers, shortMarkers));
|
||||
ObjectSourcePlan longCrossing = new ObjectSourcePlan(longMarkers, List.of(whole));
|
||||
ObjectSourcePlan repeatedCrossing = new ObjectSourcePlan(shortMarkers, List.of(whole, whole));
|
||||
int metadataBytes = shortCrossing.estimatedRetainedBytes() - shortPlain.estimatedRetainedBytes();
|
||||
|
||||
assertTrue(metadataBytes > 0);
|
||||
assertEquals(metadataBytes, longCrossing.estimatedRetainedBytes() - longPlain.estimatedRetainedBytes());
|
||||
assertTrue(repeatedCrossing.estimatedRetainedBytes() > shortCrossing.estimatedRetainedBytes());
|
||||
assertSame(shortMarkers.getFirst(), shortCrossing.mutationsFor(0, 0).getFirst());
|
||||
assertSame(shortMarkers.getLast(), shortCrossing.mutationsFor(1, 0).getFirst());
|
||||
assertEquals(shortMarkers.getFirst(), shortCrossing.mutationsFor(0, 0).getFirst());
|
||||
assertEquals(shortMarkers.getLast(), shortCrossing.mutationsFor(1, 0).getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void destinationIndexKeepsOnlyTheTouchedChunks() {
|
||||
ObjectSourcePlan plan = new ObjectSourcePlan(List.of(mutationAt(-1, 3), mutationAt(40, 3), mutationAt(-2, 3)));
|
||||
ObjectSourcePlan index = plan.destinationIndex();
|
||||
|
||||
assertEquals(2, index.destinationCount());
|
||||
assertEquals(plan.destinationSlot(-1, 0), index.destinationSlot(-1, 0));
|
||||
assertEquals(plan.destinationSlot(2, 0), index.destinationSlot(2, 0));
|
||||
assertTrue(index.destinationSlot(0, 0) < 0);
|
||||
assertTrue(index.mutationsFor(-1, 0).isEmpty());
|
||||
assertTrue(index.estimatedRetainedBytes() < plan.estimatedRetainedBytes());
|
||||
}
|
||||
|
||||
private static Map<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> group(
|
||||
List<ObjectDestinationTransaction.Mutation> mutations
|
||||
) {
|
||||
Map<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> grouped = new LinkedHashMap<>();
|
||||
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
|
||||
grouped.computeIfAbsent(new ObjectContinuationBundle.ChunkPosition(mutation.x() >> 4, mutation.z() >> 4),
|
||||
ignored -> new ArrayList<>()).add(mutation);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
private static Object value(ObjectDestinationTransaction.Mutation mutation) {
|
||||
return switch (mutation) {
|
||||
case ObjectDestinationTransaction.SetMutation set -> set.value();
|
||||
case ObjectDestinationTransaction.CustomBlockMutation custom -> custom.state();
|
||||
};
|
||||
}
|
||||
|
||||
private static ObjectDestinationTransaction.DataKey key(int x, int y, int z, Class<?> type) {
|
||||
return new ObjectDestinationTransaction.DataKey(x, y, z, type);
|
||||
}
|
||||
|
||||
private static ObjectDestinationTransaction.Mutation mutationAt(int x, int z) {
|
||||
|
||||
Reference in new issue
Block a user