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:
Brian Neumann-Fopiano
2026-09-28 11:12:37 -04:00
parent 241260a762
commit ec07f55fd8
10 changed files with 802 additions and 246 deletions
@@ -79,9 +79,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
private static final byte LIQUID_FORCED_AIR = 3;
private static final Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
private static final long SOURCE_PLAN_STATS_INTERVAL_MS = 30_000L;
private final Object collisionRuleLock;
private final ObjectSourcePlanCache sourcePlans;
private final AtomicLong lastSourcePlanStatsLog = new AtomicLong();
private volatile int collisionRuleState;
public MantleObjectComponent(EngineMantle engineMantle) {
@@ -143,13 +145,33 @@ public class MantleObjectComponent extends IrisMantleComponent {
z,
getRadius(),
(sourceX, sourceZ) -> {
ObjectSourcePlan plan = sourcePlans.get(sourceX, sourceZ,
ObjectSourcePlan plan = sourcePlans.acquire(sourceX, sourceZ, x, z,
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
transaction.apply(plan);
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
if (plan != null) {
transaction.apply(plan);
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
}
}
);
transaction.commit();
logSourcePlanStats();
}
private void logSourcePlanStats() {
long now = System.currentTimeMillis();
long last = lastSourcePlanStatsLog.get();
if (now - last < SOURCE_PLAN_STATS_INTERVAL_MS || !lastSourcePlanStatsLog.compareAndSet(last, now)) {
return;
}
ObjectSourcePlanCache.Stats stats = sourcePlans.stats();
IrisLogging.debug("Object source plans: lookups=" + stats.lookups()
+ " builds=" + stats.builds()
+ " waits=" + stats.waits()
+ " replays=" + stats.replays()
+ " drained=" + stats.drained()
+ " retained=" + stats.retained()
+ " retainedMiB=" + (stats.retainedBytes() >> 20)
+ " budgetMiB=" + (stats.budgetBytes() >> 20));
}
private ObjectSourcePlan buildSourcePlan(
@@ -54,10 +54,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
if (checkpoint < 0 || checkpoint > mutations.size()) {
throw new IllegalArgumentException("Mutation checkpoint is outside the transaction");
}
ArrayList<List<Mutation>> accepted = new ArrayList<>();
ArrayList<PlacementRange> accepted = new ArrayList<>();
for (PlacementRange placement : placements) {
if (placement.start() >= checkpoint) {
accepted.add(List.copyOf(mutations.subList(placement.start(), placement.end())));
accepted.add(new PlacementRange(placement.start() - checkpoint, placement.end() - checkpoint));
}
}
return new ObjectSourcePlan(mutations.subList(checkpoint, mutations.size()), accepted);
@@ -432,7 +432,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
}
}
private record PlacementRange(int start, int end) {
record PlacementRange(int start, int end) {
}
record DataKey(int x, int y, int z, Class<?> type) {
@@ -4,93 +4,242 @@ import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import art.arcane.volmlib.util.matter.MatterMarker;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
final class ObjectSourcePlan {
private static final int PLAN_BYTES = 256;
private static final int DESTINATION_INDEX_BYTES = 96;
private static final int MUTATION_BYTES = 96;
private static final int PLACEMENT_BYTES = 256;
private static final int PLACEMENT_DESTINATION_BYTES = 128;
private static final int REFERENCE_BYTES = 8;
private final Long2ObjectOpenHashMap<List<ObjectDestinationTransaction.Mutation>> destinations;
static final ObjectSourcePlan EMPTY = new ObjectSourcePlan(List.of());
private static final int PLAN_BYTES = 160;
private static final int DESTINATION_BYTES = 12;
private static final int MUTATION_BYTES = 8;
private static final int VALUE_BYTES = 8;
private static final int PLACEMENT_BYTES = 128;
private static final int PLACEMENT_DESTINATION_BYTES = 40;
private static final int UNKNOWN_PAYLOAD_BYTES = 256;
private static final int CUSTOM_BIT = 1 << 8;
private static final int Y_SHIFT = 9;
private static final int MAXIMUM_Y = (1 << (31 - Y_SHIFT)) - 1;
private final long[] destinations;
private final int[] offsets;
private final int[] cells;
private final int[] valueIndexes;
private final Object[] values;
private final Placement[] placements;
private final int estimatedRetainedBytes;
private final List<Placement> placements;
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) {
this(mutations, List.of());
}
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<List<ObjectDestinationTransaction.Mutation>> placements) {
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<ObjectDestinationTransaction.PlacementRange> accepted) {
int count = mutations.size();
long[] mutationDestinations = new long[count];
Long2IntOpenHashMap destinationSlots = new Long2IntOpenHashMap();
destinationSlots.defaultReturnValue(-1);
LongArrayList firstSeen = new LongArrayList();
for (int index = 0; index < count; index++) {
ObjectDestinationTransaction.Mutation mutation = Objects.requireNonNull(mutations.get(index), "mutation");
long destination = destinationKey(mutation.x() >> 4, mutation.z() >> 4);
mutationDestinations[index] = destination;
if (destinationSlots.putIfAbsent(destination, firstSeen.size()) == -1) {
firstSeen.add(destination);
}
}
destinations = firstSeen.toLongArray();
Arrays.sort(destinations);
for (int slot = 0; slot < destinations.length; slot++) {
destinationSlots.put(destinations[slot], slot);
}
offsets = new int[destinations.length + 1];
for (int index = 0; index < count; index++) {
offsets[destinationSlots.get(mutationDestinations[index]) + 1]++;
}
for (int slot = 0; slot < destinations.length; slot++) {
offsets[slot + 1] += offsets[slot];
}
int[] cursor = Arrays.copyOf(offsets, destinations.length);
int[] positions = new int[count];
cells = new int[count];
valueIndexes = new int[count];
ValueTable table = new ValueTable();
for (int index = 0; index < count; index++) {
ObjectDestinationTransaction.Mutation mutation = mutations.get(index);
int position = cursor[destinationSlots.get(mutationDestinations[index])]++;
positions[index] = position;
ObjectDestinationTransaction.DataKey key = mutation.key();
if (key.y() < 0 || key.y() > MAXIMUM_Y) {
throw new IllegalArgumentException("Object mutation height is outside the plan range: " + key.y());
}
Object value;
int custom;
switch (mutation) {
case ObjectDestinationTransaction.SetMutation set -> {
value = Objects.requireNonNull(set.value(), "mutation value");
if (typeOf(value) != key.type()) {
throw new IllegalArgumentException("Object mutation type " + key.type().getName()
+ " does not match its value " + value.getClass().getName());
}
custom = 0;
}
case ObjectDestinationTransaction.CustomBlockMutation block -> {
value = Objects.requireNonNull(block.state(), "custom block state");
custom = CUSTOM_BIT;
}
}
cells[position] = (key.y() << Y_SHIFT) | custom | ((key.z() & 15) << 4) | (key.x() & 15);
valueIndexes[position] = table.indexOf(value);
}
values = table.values.toArray();
ArrayList<Placement> crossing = new ArrayList<>();
for (int ordinal = 0; ordinal < placements.size(); ordinal++) {
List<ObjectDestinationTransaction.Mutation> placement = placements.get(ordinal);
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> fragments = group(placement);
if (fragments.size() > 1) {
crossing.add(new Placement(ordinal, bounds(placement), List.copyOf(fragments.keySet()), fragments));
for (int ordinal = 0; ordinal < accepted.size(); ordinal++) {
ObjectDestinationTransaction.PlacementRange range = accepted.get(ordinal);
Placement placement = crossingPlacement(ordinal, range.start(), range.end(), mutations, mutationDestinations, destinationSlots, positions);
if (placement != null) {
crossing.add(placement);
}
}
this.placements = List.copyOf(crossing);
Long2ObjectOpenHashMap<ArrayList<ObjectDestinationTransaction.Mutation>> grouped = new Long2ObjectOpenHashMap<>(2);
long weight = PLAN_BYTES;
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
long key = destinationKey(mutation.x() >> 4, mutation.z() >> 4);
ArrayList<ObjectDestinationTransaction.Mutation> local = grouped.get(key);
if (local == null) {
local = new ArrayList<>();
grouped.put(key, local);
weight += DESTINATION_INDEX_BYTES;
}
local.add(mutation);
Object payload = switch (mutation) {
case ObjectDestinationTransaction.SetMutation set -> set.value();
case ObjectDestinationTransaction.CustomBlockMutation custom -> custom.state();
};
weight = Math.min(Integer.MAX_VALUE, weight + MUTATION_BYTES + payloadBytes(payload));
}
destinations = new Long2ObjectOpenHashMap<>(grouped.size());
for (Long2ObjectMap.Entry<ArrayList<ObjectDestinationTransaction.Mutation>> entry : grouped.long2ObjectEntrySet()) {
destinations.put(entry.getLongKey(), List.copyOf(entry.getValue()));
}
for (Placement placement : this.placements) {
placements = crossing.toArray(new Placement[0]);
long weight = PLAN_BYTES
+ (long) destinations.length * DESTINATION_BYTES
+ (long) count * MUTATION_BYTES
+ table.payloadBytes
+ (long) values.length * VALUE_BYTES;
for (Placement placement : placements) {
weight += PLACEMENT_BYTES + (long) placement.touched().size() * PLACEMENT_DESTINATION_BYTES;
for (List<ObjectDestinationTransaction.Mutation> fragment : placement.fragments().values()) {
long retainedCapacity = Math.max(10L, fragment.size() + (fragment.size() + 1L) / 2L);
weight += retainedCapacity * REFERENCE_BYTES;
}
estimatedRetainedBytes = (int) Math.min(Integer.MAX_VALUE, weight);
}
private ObjectSourcePlan(long[] destinations) {
this.destinations = destinations;
this.offsets = new int[destinations.length + 1];
this.cells = new int[0];
this.valueIndexes = new int[0];
this.values = new Object[0];
this.placements = new Placement[0];
this.estimatedRetainedBytes = PLAN_BYTES + destinations.length * DESTINATION_BYTES;
}
ObjectSourcePlan destinationIndex() {
return new ObjectSourcePlan(destinations);
}
private static Placement crossingPlacement(
int ordinal,
int start,
int end,
List<ObjectDestinationTransaction.Mutation> mutations,
long[] mutationDestinations,
Long2IntOpenHashMap destinationSlots,
int[] positions
) {
if (start < 0 || end > mutations.size() || start >= end) {
throw new IllegalArgumentException("Object placement range is outside the plan");
}
long first = mutationDestinations[start];
int index = start + 1;
while (index < end && mutationDestinations[index] == first) {
index++;
}
if (index == end) {
return null;
}
LongArrayList touched = new LongArrayList(4);
Long2IntOpenHashMap touchedIndexes = new Long2IntOpenHashMap(4);
touchedIndexes.defaultReturnValue(-1);
ArrayList<int[]> runs = new ArrayList<>(4);
int minimumX = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (int mutationIndex = start; mutationIndex < end; mutationIndex++) {
ObjectDestinationTransaction.Mutation mutation = mutations.get(mutationIndex);
minimumX = Math.min(minimumX, mutation.x());
minimumZ = Math.min(minimumZ, mutation.z());
maximumX = Math.max(maximumX, mutation.x());
maximumZ = Math.max(maximumZ, mutation.z());
long destination = mutationDestinations[mutationIndex];
int touchedIndex = touchedIndexes.get(destination);
if (touchedIndex < 0) {
touchedIndexes.put(destination, touched.size());
touched.add(destination);
runs.add(new int[]{destinationSlots.get(destination), positions[mutationIndex], 1});
} else {
runs.get(touchedIndex)[2]++;
}
}
this.estimatedRetainedBytes = weight >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) weight;
int size = touched.size();
ArrayList<ObjectContinuationBundle.ChunkPosition> chunks = new ArrayList<>(size);
int[] slots = new int[size];
int[] runStarts = new int[size];
int[] runLengths = new int[size];
for (int touchedIndex = 0; touchedIndex < size; touchedIndex++) {
long destination = touched.getLong(touchedIndex);
chunks.add(new ObjectContinuationBundle.ChunkPosition(destinationX(destination), destinationZ(destination)));
int[] run = runs.get(touchedIndex);
slots[touchedIndex] = run[0];
runStarts[touchedIndex] = run[1];
runLengths[touchedIndex] = run[2];
}
return new Placement(ordinal, new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ),
List.copyOf(chunks), slots, runStarts, runLengths);
}
int destinationSlot(int chunkX, int chunkZ) {
return Arrays.binarySearch(destinations, destinationKey(chunkX, chunkZ));
}
int destinationCount() {
return destinations.length;
}
boolean isEmpty() {
return destinations.length == 0;
}
List<ObjectDestinationTransaction.Mutation> mutationsFor(int chunkX, int chunkZ) {
List<ObjectDestinationTransaction.Mutation> mutations = destinations.get(destinationKey(chunkX, chunkZ));
return mutations == null ? List.of() : mutations;
int slot = destinationSlot(chunkX, chunkZ);
return slot < 0 ? List.of() : decode(chunkX, chunkZ, offsets[slot], offsets[slot + 1] - offsets[slot]);
}
void persistContinuations(MantleWriter writer, int sourceX, int sourceZ, int destinationX, int destinationZ) {
ObjectContinuationBundle.ChunkPosition destination = new ObjectContinuationBundle.ChunkPosition(destinationX, destinationZ);
if (placements.length == 0) {
return;
}
int destinationSlot = destinationSlot(destinationX, destinationZ);
if (destinationSlot < 0) {
return;
}
for (Placement placement : placements) {
if (!placement.fragments().containsKey(destination)) {
if (!placement.touches(destinationSlot)) {
continue;
}
ObjectContinuationBundle.PlacementKey key = new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, sourceX, sourceZ, placement.ordinal());
for (Map.Entry<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> entry : placement.fragments().entrySet()) {
ObjectContinuationBundle.ChunkPosition target = entry.getKey();
if (target.equals(destination)) {
for (int touchedIndex = 0; touchedIndex < placement.slots().length; touchedIndex++) {
if (placement.slots()[touchedIndex] == destinationSlot) {
continue;
}
ObjectContinuationBundle.ChunkPosition target = placement.touched().get(touchedIndex);
int runStart = placement.runStarts()[touchedIndex];
int runLength = placement.runLengths()[touchedIndex];
writer.withChunkFence(target.x(), target.z(), () -> {
MantleChunk<Matter> chunk = writer.acquireChunk(target.x(), target.z());
if (chunk.isFlagged(MantleFlag.REAL)) {
@@ -107,13 +256,32 @@ final class ObjectSourcePlan {
}
}
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(key, placement.bounds(), placement.touched(),
encode(entry.getValue(), writer.getMantle().getWorldHeight()));
encode(decode(target.x(), target.z(), runStart, runLength), writer.getMantle().getWorldHeight()));
writer.setData(x, 0, z, bundle == null ? new ObjectContinuationBundle(List.of(fragment)) : bundle.with(fragment));
});
}
}
}
private List<ObjectDestinationTransaction.Mutation> decode(int chunkX, int chunkZ, int start, int length) {
ObjectDestinationTransaction.Mutation[] decoded = new ObjectDestinationTransaction.Mutation[length];
int baseX = chunkX << 4;
int baseZ = chunkZ << 4;
for (int index = 0; index < length; index++) {
int cell = cells[start + index];
int x = baseX | (cell & 15);
int z = baseZ | ((cell >>> 4) & 15);
int y = cell >>> Y_SHIFT;
Object value = values[valueIndexes[start + index]];
decoded[index] = (cell & CUSTOM_BIT) != 0
? new ObjectDestinationTransaction.CustomBlockMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value)
: new ObjectDestinationTransaction.SetMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value);
}
return List.of(decoded);
}
static byte[] encode(List<ObjectDestinationTransaction.Mutation> mutations, int height) {
IrisMatterSupport.ensureRegistered();
Matter payload = new IrisMatter(16, height, 16);
@@ -134,54 +302,41 @@ final class ObjectSourcePlan {
return ObjectContinuationBundle.encode(payload);
}
private static LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> group(
List<ObjectDestinationTransaction.Mutation> mutations
) {
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> grouped = new LinkedHashMap<>();
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
ObjectContinuationBundle.ChunkPosition destination = new ObjectContinuationBundle.ChunkPosition(mutation.x() >> 4, mutation.z() >> 4);
grouped.computeIfAbsent(destination, ignored -> new ArrayList<>()).add(mutation);
}
return grouped;
}
private static ObjectContinuationBundle.Bounds bounds(List<ObjectDestinationTransaction.Mutation> mutations) {
int minimumX = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
minimumX = Math.min(minimumX, mutation.x());
minimumZ = Math.min(minimumZ, mutation.z());
maximumX = Math.max(maximumX, mutation.x());
maximumZ = Math.max(maximumZ, mutation.z());
}
return new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ);
}
int estimatedRetainedBytes() {
return estimatedRetainedBytes;
}
static Class<?> typeOf(Object value) {
return value instanceof NativeBlockState ? NativeBlockState.class : value.getClass();
}
static long destinationKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
}
private static int destinationX(long destination) {
return (int) (destination >> 32);
}
private static int destinationZ(long destination) {
return (int) destination;
}
private static long payloadBytes(Object value) {
return switch (value) {
case null -> 0L;
case String text -> stringBytes(text);
case TreeBlockMaterial material -> 24L + stringBytes(material.materialKey());
case Identifier identifier -> 32L + stringBytes(identifier.namespace()) + stringBytes(identifier.key());
case NativeBlockState ignored -> 64L;
case MatterCavern cavern -> 32L + stringBytes(cavern.getCustomBiome());
case HydrologyCaveCell cell -> 64L + stringBytes(cell.fluidProfileKey()) + stringBytes(cell.floodedBiomeKey());
case Byte ignored -> 24L;
case Short ignored -> 24L;
case Integer ignored -> 24L;
case Long ignored -> 24L;
case Float ignored -> 24L;
case Double ignored -> 24L;
case Boolean ignored -> 24L;
case Character ignored -> 24L;
case TreeBlockMaterial material -> 16L + stringBytes(material.materialKey());
case Identifier identifier -> 24L + stringBytes(identifier.namespace()) + stringBytes(identifier.key());
case NativeBlockState ignored -> 0L;
case MatterCavern cavern -> 24L + stringBytes(cavern.getCustomBiome());
case HydrologyCaveCell cell -> 48L + stringBytes(cell.fluidProfileKey()) + stringBytes(cell.floodedBiomeKey());
case MatterMarker marker -> 16L + stringBytes(marker.getTag());
case TileWrapper ignored -> 4L * UNKNOWN_PAYLOAD_BYTES;
case Enum<?> ignored -> 0L;
default -> Integer.MAX_VALUE;
case Number ignored -> 16L;
case Boolean ignored -> 16L;
case Character ignored -> 16L;
default -> UNKNOWN_PAYLOAD_BYTES;
};
}
@@ -189,12 +344,45 @@ final class ObjectSourcePlan {
return value == null ? 0L : 48L + 2L * value.length();
}
private record Placement(int ordinal, ObjectContinuationBundle.Bounds bounds,
List<ObjectContinuationBundle.ChunkPosition> touched,
Map<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> fragments) {
private static final class ValueTable {
private final ArrayList<Object> values = new ArrayList<>();
private final Object2IntOpenHashMap<Object> equalValues = new Object2IntOpenHashMap<>();
private final Reference2IntOpenHashMap<Object> identityValues = new Reference2IntOpenHashMap<>();
private long payloadBytes;
private ValueTable() {
equalValues.defaultReturnValue(-1);
identityValues.defaultReturnValue(-1);
}
private int indexOf(Object value) {
boolean byValue = value instanceof String || value instanceof TreeBlockMaterial;
int index = byValue ? equalValues.getInt(value) : identityValues.getInt(value);
if (index >= 0) {
return index;
}
index = values.size();
values.add(value);
payloadBytes += payloadBytes(value);
if (byValue) {
equalValues.put(value, index);
} else {
identityValues.put(value, index);
}
return index;
}
}
private static long destinationKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
private record Placement(int ordinal, ObjectContinuationBundle.Bounds bounds,
List<ObjectContinuationBundle.ChunkPosition> touched,
int[] slots, int[] runStarts, int[] runLengths) {
private boolean touches(int slot) {
for (int touchedSlot : slots) {
if (touchedSlot == slot) {
return true;
}
}
return false;
}
}
}
@@ -8,14 +8,21 @@ import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Supplier;
import static art.arcane.iris.generation.cache.Cache.key;
final class ObjectSourcePlanCache {
private static final long MAXIMUM_RETAINED_BYTES = 256L * 1024L * 1024L;
private static final long MAXIMUM_RETAINED_BYTES = 512L * 1024L * 1024L;
private static final int ENTRY_BYTES = 64;
private final long maximumRetainedBytes;
private final LongAdder lookups = new LongAdder();
private final LongAdder builds = new LongAdder();
private final LongAdder waits = new LongAdder();
private final LongAdder replays = new LongAdder();
private final LongAdder drained = new LongAdder();
private volatile State state;
ObjectSourcePlanCache() {
@@ -31,33 +38,71 @@ final class ObjectSourcePlanCache {
}
static long retainedByteBudget(long maximumHeapBytes) {
return Math.max(1L, Math.min(MAXIMUM_RETAINED_BYTES, maximumHeapBytes / 64L));
return Math.max(1L, Math.min(MAXIMUM_RETAINED_BYTES, maximumHeapBytes / 32L));
}
ObjectSourcePlan get(int sourceChunkX, int sourceChunkZ, Supplier<ObjectSourcePlan> builder) {
/**
* Returns the plan whose mutations the destination still has to replay, or null when the source writes nothing
* there. Each destination consumes a retained plan once; a plan whose destinations were all consumed keeps only
* its destination index, so a repeated consumption rebuilds it instead of trusting released state.
*/
ObjectSourcePlan acquire(int sourceChunkX, int sourceChunkZ, int destinationChunkX, int destinationChunkZ,
Supplier<ObjectSourcePlan> builder) {
Objects.requireNonNull(builder, "Source plan builder");
lookups.increment();
State current = state;
long source = CacheKey.mix(key(sourceChunkX, sourceChunkZ));
ObjectSourcePlan cached = current.plans.getIfPresent(source);
if (cached != null) {
return cached;
Entry entry = current.plans.getIfPresent(source);
if (entry == null) {
entry = load(current, source, builder);
}
int slot = entry.slot(destinationChunkX, destinationChunkZ);
if (slot < 0) {
return null;
}
Claim claim = entry.claim(slot);
if (claim == null) {
replays.increment();
return replay(current, source, entry, destinationChunkX, destinationChunkZ, builder);
}
if (claim.last()) {
drained.increment();
current.plans.asMap().replace(source, entry, entry.released());
}
return claim.plan();
}
private ObjectSourcePlan replay(State current, long source, Entry consumed, int destinationChunkX, int destinationChunkZ,
Supplier<ObjectSourcePlan> builder) {
Entry rebuilt = new Entry(build(builder));
int slot = rebuilt.slot(destinationChunkX, destinationChunkZ);
Claim claim = slot < 0 ? null : rebuilt.claim(slot);
if (claim == null) {
return null;
}
if (!claim.last() && rebuilt.weight() <= maximumRetainedBytes) {
current.plans.asMap().replace(source, consumed, rebuilt);
}
return claim.plan();
}
private Entry load(State current, long source, Supplier<ObjectSourcePlan> builder) {
Pending created = new Pending(Thread.currentThread());
Pending pending = current.pending.putIfAbsent(source, created);
if (pending != null) {
waits.increment();
return pending.await();
}
try {
ObjectSourcePlan plan = current.plans.getIfPresent(source);
if (plan == null) {
plan = builder.get();
if (plan != null && plan.estimatedRetainedBytes() < Integer.MAX_VALUE
&& plan.estimatedRetainedBytes() <= maximumRetainedBytes) {
current.plans.put(source, plan);
Entry entry = current.plans.getIfPresent(source);
if (entry == null) {
entry = new Entry(build(builder));
if (entry.weight() <= maximumRetainedBytes) {
current.plans.put(source, entry);
}
}
created.result.complete(plan);
return plan;
created.result.complete(entry);
return entry;
} catch (RuntimeException | Error failure) {
created.failure = failure;
created.result.completeExceptionally(failure);
@@ -67,6 +112,11 @@ final class ObjectSourcePlanCache {
}
}
private ObjectSourcePlan build(Supplier<ObjectSourcePlan> builder) {
builds.increment();
return Objects.requireNonNull(builder.get(), "Source plan builder returned no plan");
}
void clear() {
state = new State(maximumRetainedBytes);
}
@@ -77,28 +127,96 @@ final class ObjectSourcePlanCache {
return current.plans.estimatedSize();
}
Stats stats() {
State current = state;
current.plans.cleanUp();
long retainedBytes = current.plans.policy().eviction()
.map(eviction -> eviction.weightedSize().orElse(0L))
.orElse(0L);
return new Stats(lookups.sum(), builds.sum(), waits.sum(), replays.sum(), drained.sum(),
current.plans.estimatedSize(), retainedBytes, maximumRetainedBytes);
}
record Stats(long lookups, long builds, long waits, long replays, long drained, long retained,
long retainedBytes, long budgetBytes) {
}
private static final class State {
private final Cache<Long, ObjectSourcePlan> plans;
private final Cache<Long, Entry> plans;
private final ConcurrentHashMap<Long, Pending> pending = new ConcurrentHashMap<>();
private State(long maximumRetainedBytes) {
plans = Caffeine.newBuilder()
.maximumWeight(maximumRetainedBytes)
.weigher((Long key, ObjectSourcePlan plan) -> plan.estimatedRetainedBytes())
.weigher((Long key, Entry entry) -> entry.weight())
.build();
}
}
private record Claim(ObjectSourcePlan plan, boolean last) {
}
private static final class Entry {
private final ObjectSourcePlan index;
private final boolean[] claimed;
private final int weight;
private ObjectSourcePlan plan;
private int remaining;
private Entry(ObjectSourcePlan plan) {
this.index = plan.isEmpty() ? ObjectSourcePlan.EMPTY : plan;
this.plan = plan.isEmpty() ? null : plan;
this.claimed = new boolean[plan.destinationCount()];
this.remaining = claimed.length;
this.weight = saturatedWeight(ENTRY_BYTES + (long) claimed.length + index.estimatedRetainedBytes());
}
private Entry(ObjectSourcePlan index, boolean[] claimed) {
this.index = index;
this.claimed = claimed;
this.weight = saturatedWeight(ENTRY_BYTES + (long) claimed.length + index.estimatedRetainedBytes());
}
private int slot(int destinationChunkX, int destinationChunkZ) {
return index.destinationSlot(destinationChunkX, destinationChunkZ);
}
private synchronized Claim claim(int slot) {
ObjectSourcePlan retained = plan;
if (retained == null || claimed[slot]) {
return null;
}
claimed[slot] = true;
if (--remaining == 0) {
plan = null;
return new Claim(retained, true);
}
return new Claim(retained, false);
}
private Entry released() {
return new Entry(index.destinationIndex(), claimed);
}
private int weight() {
return weight;
}
private static int saturatedWeight(long weight) {
return (int) Math.min(Integer.MAX_VALUE, weight);
}
}
private static final class Pending {
private final Thread owner;
private final CompletableFuture<ObjectSourcePlan> result = new CompletableFuture<>();
private final CompletableFuture<Entry> result = new CompletableFuture<>();
private Throwable failure;
private Pending(Thread owner) {
this.owner = owner;
}
private ObjectSourcePlan await() {
private Entry await() {
if (owner == Thread.currentThread()) {
throw new IllegalStateException("Recursive source plan construction");
}
@@ -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;
}
@@ -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);
@@ -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);
@@ -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) {