mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Unblock pregen throughput limiters
- Replace the pregen cache global monitor with atomic per-plate and per-region state and a lock-free resident-plate memo - Serialize plate eviction and its disk write under the residency lock so a reloaded plate can never lose a concurrent bit - Retry a chunk or region cache write whose plate was retired mid-write - Delete SynchronizedCache and the isThreadSafe wrapper it existed for - Replace the AsyncPregenMethod semaphore and permit monitor with PregenAdmissionGate, one condition that signals on release, adaptive-limit change and cancellation - Attribute the adaptive and permit waits separately from the single gate wait - Wait on a progress signal instead of sleeping 25ms in PregenMantleBackpressure, signalled by chunk completion - Wait on a state signal instead of sleeping 50ms and 150ms in IrisPregenerator pause, heap pressure and tectonic reclaim loops - Restore the burst pool parallelism when the last pregen job finishes - Add PregenCacheConcurrencyTest, PregenAdmissionGateTest and PregenWaitPolicyContractTest
This commit is contained in:
@@ -35,6 +35,9 @@ import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.Looper;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -43,6 +46,8 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
public class IrisPregenerator {
|
||||
private static final double INVALID = 9223372036854775807d;
|
||||
private static final AtomicLong JOB_SEQUENCE = new AtomicLong(0L);
|
||||
private static final long GATE_WAIT_MS = 50L;
|
||||
private static final long RECLAIM_WAIT_MS = 150L;
|
||||
private final long jobId;
|
||||
private final PregenTask task;
|
||||
private final PregeneratorMethod generator;
|
||||
@@ -50,6 +55,8 @@ public class IrisPregenerator {
|
||||
private final Looper ticker;
|
||||
private final AtomicBoolean paused;
|
||||
private final AtomicBoolean shutdown;
|
||||
private final ReentrantLock stateLock = new ReentrantLock();
|
||||
private final Condition stateChanged = stateLock.newCondition();
|
||||
private final PregenRateTracker rateTracker;
|
||||
private final KList<Integer> chunksPerSecondHistory;
|
||||
private final AtomicLong generated;
|
||||
@@ -193,6 +200,7 @@ public class IrisPregenerator {
|
||||
|
||||
public void close() {
|
||||
shutdown.set(true);
|
||||
signalState();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -295,6 +303,7 @@ public class IrisPregenerator {
|
||||
reclaimTectonicPlates(mantle);
|
||||
}
|
||||
});
|
||||
shutdownStep("parallelism", PregenPerformanceProfile::restore);
|
||||
shutdownStep("protocol", () -> {
|
||||
IrisProtocolServer protocolServer = IrisServices.getOrNull(IrisProtocolServer.class);
|
||||
if (protocolServer != null) {
|
||||
@@ -337,7 +346,7 @@ public class IrisPregenerator {
|
||||
}
|
||||
|
||||
previousLoaded = loaded;
|
||||
J.sleep(150);
|
||||
awaitStateChange(RECLAIM_WAIT_MS);
|
||||
}
|
||||
|
||||
int loadedAfter = mantle.getLoadedRegionCount();
|
||||
@@ -348,7 +357,7 @@ public class IrisPregenerator {
|
||||
|
||||
private void visitRegion(int x, int z, boolean regions) {
|
||||
while (paused.get() && !shutdown.get()) {
|
||||
J.sleep(50);
|
||||
awaitStateChange(GATE_WAIT_MS);
|
||||
}
|
||||
|
||||
if (shutdown.get()) {
|
||||
@@ -376,7 +385,7 @@ public class IrisPregenerator {
|
||||
if (!paused.get()) {
|
||||
reclaimHeapPressure();
|
||||
}
|
||||
J.sleep(50);
|
||||
awaitStateChange(GATE_WAIT_MS);
|
||||
}
|
||||
|
||||
if (shutdown.get()) {
|
||||
@@ -439,10 +448,32 @@ public class IrisPregenerator {
|
||||
|
||||
public void pause() {
|
||||
paused.set(true);
|
||||
signalState();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
paused.set(false);
|
||||
signalState();
|
||||
}
|
||||
|
||||
private void signalState() {
|
||||
stateLock.lock();
|
||||
try {
|
||||
stateChanged.signalAll();
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitStateChange(long millis) {
|
||||
stateLock.lock();
|
||||
try {
|
||||
stateChanged.await(millis, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
stateLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private PregenListener listenify(PregenListener listener) {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.pregenerator;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
public final class PregenAdmissionGate {
|
||||
public record Wait(long adaptiveMs, long permitMs) {
|
||||
}
|
||||
|
||||
private final int permits;
|
||||
private final long boundMs;
|
||||
private final LongSupplier clock;
|
||||
private final ReentrantLock lock = new ReentrantLock(true);
|
||||
private final Condition changed = lock.newCondition();
|
||||
private int free;
|
||||
private volatile int freeSnapshot;
|
||||
|
||||
public PregenAdmissionGate(int permits, long boundMs, LongSupplier clock) {
|
||||
if (permits < 1) {
|
||||
throw new IllegalArgumentException("A pregen admission gate needs at least one permit");
|
||||
}
|
||||
if (boundMs < 1L) {
|
||||
throw new IllegalArgumentException("A pregen admission gate needs a positive wait bound");
|
||||
}
|
||||
this.permits = permits;
|
||||
this.boundMs = boundMs;
|
||||
this.clock = clock;
|
||||
this.free = permits;
|
||||
this.freeSnapshot = permits;
|
||||
}
|
||||
|
||||
public int permits() {
|
||||
return permits;
|
||||
}
|
||||
|
||||
public int availablePermits() {
|
||||
return freeSnapshot;
|
||||
}
|
||||
|
||||
public Wait admit(BooleanSupplier overAdaptiveLimit, BooleanSupplier cancelled) throws InterruptedException {
|
||||
long adaptiveMs = 0L;
|
||||
long permitMs = 0L;
|
||||
lock.lock();
|
||||
try {
|
||||
while (true) {
|
||||
if (cancelled.getAsBoolean()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean blockedByLimit = overAdaptiveLimit.getAsBoolean();
|
||||
if (!blockedByLimit && free > 0) {
|
||||
free--;
|
||||
freeSnapshot = free;
|
||||
return new Wait(adaptiveMs, permitMs);
|
||||
}
|
||||
|
||||
long start = clock.getAsLong();
|
||||
changed.await(boundMs, TimeUnit.MILLISECONDS);
|
||||
long waited = Math.max(0L, clock.getAsLong() - start);
|
||||
if (blockedByLimit) {
|
||||
adaptiveMs += waited;
|
||||
} else {
|
||||
permitMs += waited;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (free < permits) {
|
||||
free++;
|
||||
freeSnapshot = free;
|
||||
}
|
||||
changed.signalAll();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void wake() {
|
||||
lock.lock();
|
||||
try {
|
||||
changed.signalAll();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean awaitDrain(long warningInterval, TimeUnit timeUnit, Runnable onWait) {
|
||||
boolean interrupted = false;
|
||||
lock.lock();
|
||||
try {
|
||||
while (free < permits) {
|
||||
boolean signalled = false;
|
||||
try {
|
||||
signalled = changed.await(warningInterval, timeUnit);
|
||||
} catch (InterruptedException e) {
|
||||
interrupted = true;
|
||||
continue;
|
||||
}
|
||||
if (!signalled && free < permits) {
|
||||
onWait.run();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
return interrupted;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,9 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -33,6 +36,8 @@ public final class PregenMantleBackpressure {
|
||||
private final Runnable onBudgetTimeout;
|
||||
private final Supplier<String> diagnostics;
|
||||
private final BooleanSupplier cancelled;
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final Condition progressed = lock.newCondition();
|
||||
|
||||
public PregenMantleBackpressure(Supplier<Mantle> mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier<String> diagnostics) {
|
||||
this(mantleSupplier, maxResidentTectonicPlates, waitMs, timeoutMs, onBudgetTimeout, diagnostics, () -> false);
|
||||
@@ -108,10 +113,7 @@ public final class PregenMantleBackpressure {
|
||||
+ "), freed " + freed + " last pass, waited " + elapsed + "ms.");
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(waitMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
if (!awaitProgress()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -154,15 +156,34 @@ public final class PregenMantleBackpressure {
|
||||
+ (mantle != null ? " (" + mantle.getLoadedRegionCount() + " plates resident)" : "") + ".");
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(waitMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
if (!awaitProgress()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void signalProgress() {
|
||||
lock.lock();
|
||||
try {
|
||||
progressed.signalAll();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean awaitProgress() {
|
||||
lock.lock();
|
||||
try {
|
||||
progressed.await(waitMs, TimeUnit.MILLISECONDS);
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCancelled() {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
return true;
|
||||
|
||||
@@ -28,8 +28,11 @@ import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class PregenPerformanceProfile {
|
||||
private static final AtomicInteger PARALLELISM_HOLDERS = new AtomicInteger();
|
||||
|
||||
private PregenPerformanceProfile() {
|
||||
}
|
||||
|
||||
@@ -45,12 +48,33 @@ public final class PregenPerformanceProfile {
|
||||
}
|
||||
|
||||
if (MultiBurst.burst.raiseParallelism(pregenBurstParallelism(Runtime.getRuntime().availableProcessors()))) {
|
||||
PARALLELISM_HOLDERS.incrementAndGet();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases one hold on the pregeneration burst parallelism. The pool returns to the parallelism it had
|
||||
* before the first pregeneration raised it once the last concurrent job releases its hold.
|
||||
*/
|
||||
public static boolean restore() {
|
||||
while (true) {
|
||||
int holders = PARALLELISM_HOLDERS.get();
|
||||
if (holders <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (PARALLELISM_HOLDERS.compareAndSet(holders, holders - 1)) {
|
||||
return holders == 1 && MultiBurst.burst.restoreParallelism();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int parallelismHolders() {
|
||||
return PARALLELISM_HOLDERS.get();
|
||||
}
|
||||
|
||||
public static void apply(Engine engine) {
|
||||
apply();
|
||||
if (engine != null) {
|
||||
|
||||
+1
-11
@@ -6,10 +6,6 @@ import art.arcane.volmlib.util.documentation.RegionCoordinates;
|
||||
import java.io.File;
|
||||
|
||||
public interface PregenCache {
|
||||
default boolean isThreadSafe() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
boolean isChunkCached(int x, int z);
|
||||
|
||||
@@ -32,16 +28,10 @@ public interface PregenCache {
|
||||
}
|
||||
|
||||
default PregenCache sync() {
|
||||
if (isThreadSafe()) return this;
|
||||
return new SynchronizedCache(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
PregenCache EMPTY = new PregenCache() {
|
||||
@Override
|
||||
public boolean isThreadSafe() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChunkCached(int x, int z) {
|
||||
return false;
|
||||
|
||||
+217
-149
@@ -26,6 +26,9 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLongArray;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
public class PregenCacheImpl implements PregenCache {
|
||||
// Daemon threads: this static pool is never shut down, and non-daemon threads here block JVM exit
|
||||
@@ -36,12 +39,15 @@ public class PregenCacheImpl implements PregenCache {
|
||||
return thread;
|
||||
});
|
||||
private static final AtomicBoolean DISPATCHER_REGISTERED = new AtomicBoolean();
|
||||
private static final short SIZE = 1024;
|
||||
private static final int SIZE = 1024;
|
||||
private static final int RETIRED_WRITE_RETRIES = 4;
|
||||
|
||||
private final File directory;
|
||||
private final int maxSize;
|
||||
private final AtomicBoolean missingStorageReported = new AtomicBoolean();
|
||||
private final Object residencyLock = new Object();
|
||||
private final Long2ObjectLinkedOpenHashMap<Plate> cache = new Long2ObjectLinkedOpenHashMap<>();
|
||||
private volatile Plate recent;
|
||||
|
||||
public PregenCacheImpl(File directory, int maxSize) {
|
||||
this.directory = directory;
|
||||
@@ -78,35 +84,119 @@ public class PregenCacheImpl implements PregenCache {
|
||||
@Override
|
||||
@ChunkCoordinates
|
||||
public void cacheChunk(int x, int z) {
|
||||
getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 10), PowerOfTwoCoordinates.floorDivPow2(z, 10)).cache(
|
||||
PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS),
|
||||
PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(z, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS),
|
||||
region -> region.cache(
|
||||
PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
|
||||
PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS)
|
||||
)
|
||||
);
|
||||
int plateX = PowerOfTwoCoordinates.floorDivPow2(x, 10);
|
||||
int plateZ = PowerOfTwoCoordinates.floorDivPow2(z, 10);
|
||||
int regionX = PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS);
|
||||
int regionZ = PowerOfTwoCoordinates.localMaskPow2(PowerOfTwoCoordinates.floorDivPow2(z, 5), PowerOfTwoCoordinates.REGION_CHUNK_BITS);
|
||||
int localX = PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS);
|
||||
int localZ = PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS);
|
||||
|
||||
for (int attempt = 0; attempt < RETIRED_WRITE_RETRIES; attempt++) {
|
||||
Plate plate = getPlate(plateX, plateZ);
|
||||
plate.cache(regionX, regionZ, region -> region.cache(localX, localZ));
|
||||
if (!plate.retired) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RegionCoordinates
|
||||
public void cacheRegion(int x, int z) {
|
||||
getPlate(PowerOfTwoCoordinates.floorDivPow2(x, 5), PowerOfTwoCoordinates.floorDivPow2(z, 5)).cache(
|
||||
PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
|
||||
PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS),
|
||||
Region::cache
|
||||
);
|
||||
int plateX = PowerOfTwoCoordinates.floorDivPow2(x, 5);
|
||||
int plateZ = PowerOfTwoCoordinates.floorDivPow2(z, 5);
|
||||
int regionX = PowerOfTwoCoordinates.localMaskPow2(x, PowerOfTwoCoordinates.REGION_CHUNK_BITS);
|
||||
int regionZ = PowerOfTwoCoordinates.localMaskPow2(z, PowerOfTwoCoordinates.REGION_CHUNK_BITS);
|
||||
|
||||
for (int attempt = 0; attempt < RETIRED_WRITE_RETRIES; attempt++) {
|
||||
Plate plate = getPlate(plateX, plateZ);
|
||||
plate.cache(regionX, regionZ, Region::cache);
|
||||
if (!plate.retired) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write() {
|
||||
if (cache.isEmpty()) {
|
||||
List<Plate> resident;
|
||||
synchronized (residencyLock) {
|
||||
if (cache.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
resident = new ArrayList<>(cache.values());
|
||||
}
|
||||
|
||||
writePlates(resident);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trim(long unloadDuration) {
|
||||
synchronized (residencyLock) {
|
||||
if (cache.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long threshold = System.currentTimeMillis() - unloadDuration;
|
||||
List<Plate> resident = new ArrayList<>(cache.size());
|
||||
Iterator<Plate> iterator = cache.values().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Plate plate = iterator.next();
|
||||
if (plate.lastAccess < threshold) {
|
||||
iterator.remove();
|
||||
plate.retired = true;
|
||||
}
|
||||
resident.add(plate);
|
||||
}
|
||||
|
||||
writePlates(resident);
|
||||
}
|
||||
}
|
||||
|
||||
private Plate getPlate(int x, int z) {
|
||||
long key = key(x, z);
|
||||
Plate memo = recent;
|
||||
if (memo != null && memo.key == key && !memo.retired) {
|
||||
return memo;
|
||||
}
|
||||
|
||||
return residePlate(key, x, z);
|
||||
}
|
||||
|
||||
private Plate residePlate(long key, int x, int z) {
|
||||
Plate resolved;
|
||||
synchronized (residencyLock) {
|
||||
resolved = cache.getAndMoveToFirst(key);
|
||||
if (resolved == null) {
|
||||
resolved = readPlate(key, x, z);
|
||||
cache.putAndMoveToFirst(key, resolved);
|
||||
List<Plate> evicted = null;
|
||||
while (cache.size() > maxSize) {
|
||||
Plate removed = cache.removeLast();
|
||||
removed.retired = true;
|
||||
if (evicted == null) {
|
||||
evicted = new ArrayList<>(2);
|
||||
}
|
||||
evicted.add(removed);
|
||||
}
|
||||
if (evicted != null) {
|
||||
writePlates(evicted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recent = resolved;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void writePlates(List<Plate> plates) {
|
||||
if (plates.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>(cache.size());
|
||||
for (Plate plate : cache.values()) {
|
||||
if (!plate.dirty) {
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>(plates.size());
|
||||
for (Plate plate : plates) {
|
||||
if (!plate.dirty.get()) {
|
||||
continue;
|
||||
}
|
||||
futures.add(CompletableFuture.runAsync(() -> writePlate(plate), DISPATCHER));
|
||||
@@ -117,84 +207,41 @@ public class PregenCacheImpl implements PregenCache {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trim(long unloadDuration) {
|
||||
if (cache.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long threshold = System.currentTimeMillis() - unloadDuration;
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>(cache.size());
|
||||
Iterator<Plate> iterator = cache.values().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Plate plate = iterator.next();
|
||||
if (plate.lastAccess < threshold) {
|
||||
iterator.remove();
|
||||
}
|
||||
futures.add(CompletableFuture.runAsync(() -> writePlate(plate), DISPATCHER));
|
||||
}
|
||||
|
||||
for (CompletableFuture<Void> future : futures) {
|
||||
future.join();
|
||||
}
|
||||
}
|
||||
|
||||
private Plate getPlate(int x, int z) {
|
||||
long key = key(x, z);
|
||||
Plate plate = cache.getAndMoveToFirst(key);
|
||||
if (plate != null) {
|
||||
return plate;
|
||||
}
|
||||
|
||||
Plate loaded = readPlate(x, z);
|
||||
cache.putAndMoveToFirst(key, loaded);
|
||||
|
||||
if (cache.size() > maxSize) {
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>(cache.size() - maxSize);
|
||||
while (cache.size() > maxSize) {
|
||||
Plate evicted = cache.removeLast();
|
||||
futures.add(CompletableFuture.runAsync(() -> writePlate(evicted), DISPATCHER));
|
||||
}
|
||||
for (CompletableFuture<Void> future : futures) {
|
||||
future.join();
|
||||
}
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
|
||||
private Plate readPlate(int x, int z) {
|
||||
private Plate readPlate(long key, int x, int z) {
|
||||
File file = fileForPlate(x, z);
|
||||
if (!file.exists()) {
|
||||
return new Plate(x, z);
|
||||
return new Plate(key, x, z);
|
||||
}
|
||||
|
||||
try (DataInputStream input = new DataInputStream(new LZ4BlockInputStream(new FileInputStream(file)))) {
|
||||
return readPlate(x, z, input);
|
||||
return readPlate(key, x, z, input);
|
||||
} catch (IOException e) {
|
||||
IrisLogging.error("Failed to read pregen cache " + file);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
|
||||
return new Plate(x, z);
|
||||
return new Plate(key, x, z);
|
||||
}
|
||||
|
||||
private void writePlate(Plate plate) {
|
||||
if (!plate.dirty) {
|
||||
return;
|
||||
}
|
||||
if (!prepareCacheDirectory()) {
|
||||
return;
|
||||
}
|
||||
synchronized (plate) {
|
||||
if (!plate.dirty.compareAndSet(true, false)) {
|
||||
return;
|
||||
}
|
||||
if (!prepareCacheDirectory()) {
|
||||
plate.dirty.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
File file = null;
|
||||
try {
|
||||
file = fileForPlate(plate.x, plate.z);
|
||||
IO.write(file, output -> new DataOutputStream(new LZ4BlockOutputStream(output)), plate::write);
|
||||
plate.dirty = false;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error("Failed to write pregen cache " + (file != null ? file : "c." + plate.x + "." + plate.z));
|
||||
IrisLogging.reportError(e);
|
||||
File file = null;
|
||||
try {
|
||||
file = fileForPlate(plate.x, plate.z);
|
||||
IO.write(file, output -> new DataOutputStream(new LZ4BlockOutputStream(output)), plate::write);
|
||||
} catch (Throwable e) {
|
||||
plate.dirty.set(true);
|
||||
IrisLogging.error("Failed to write pregen cache " + (file != null ? file : "c." + plate.x + "." + plate.z));
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,36 +290,48 @@ public class PregenCacheImpl implements PregenCache {
|
||||
}
|
||||
|
||||
private static class Plate {
|
||||
private final long key;
|
||||
private final int x;
|
||||
private final int z;
|
||||
private short count;
|
||||
private Region[] regions;
|
||||
private boolean dirty;
|
||||
private long lastAccess;
|
||||
private final AtomicInteger count;
|
||||
private final AtomicReferenceArray<Region> regions;
|
||||
private final AtomicBoolean dirty = new AtomicBoolean();
|
||||
private volatile long lastAccess;
|
||||
private volatile boolean retired;
|
||||
|
||||
private Plate(int x, int z) {
|
||||
this(x, z, (short) 0, new Region[1024]);
|
||||
private Plate(long key, int x, int z) {
|
||||
this(key, x, z, 0, new AtomicReferenceArray<>(SIZE));
|
||||
}
|
||||
|
||||
private Plate(int x, int z, short count, Region[] regions) {
|
||||
private Plate(long key, int x, int z, int count, AtomicReferenceArray<Region> regions) {
|
||||
this.key = key;
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.count = count;
|
||||
this.count = new AtomicInteger(count);
|
||||
this.regions = regions;
|
||||
this.lastAccess = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private int completed() {
|
||||
return Math.min(count.get(), SIZE);
|
||||
}
|
||||
|
||||
private boolean cache(int x, int z, RegionCacheOp op) {
|
||||
lastAccess = System.currentTimeMillis();
|
||||
if (count == SIZE) {
|
||||
if (completed() == SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AtomicReferenceArray<Region> current = regions;
|
||||
if (current == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int index = PowerOfTwoCoordinates.packLocal32(x, z);
|
||||
Region region = regions[index];
|
||||
Region region = current.get(index);
|
||||
if (region == null) {
|
||||
region = new Region();
|
||||
regions[index] = region;
|
||||
Region created = new Region();
|
||||
region = current.compareAndSet(index, null, created) ? created : current.get(index);
|
||||
}
|
||||
|
||||
CacheResult result = op.apply(region);
|
||||
@@ -280,25 +339,26 @@ public class PregenCacheImpl implements PregenCache {
|
||||
return false;
|
||||
}
|
||||
|
||||
dirty = true;
|
||||
dirty.set(true);
|
||||
if (result != CacheResult.COMPLETED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
count++;
|
||||
if (count == SIZE) {
|
||||
regions = null;
|
||||
}
|
||||
return true;
|
||||
return count.incrementAndGet() >= SIZE;
|
||||
}
|
||||
|
||||
private boolean isCached(int x, int z, RegionPredicate predicate) {
|
||||
lastAccess = System.currentTimeMillis();
|
||||
if (count == SIZE) {
|
||||
if (completed() == SIZE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Region region = regions[PowerOfTwoCoordinates.packLocal32(x, z)];
|
||||
AtomicReferenceArray<Region> current = regions;
|
||||
if (current == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Region region = current.get(PowerOfTwoCoordinates.packLocal32(x, z));
|
||||
if (region == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -306,12 +366,15 @@ public class PregenCacheImpl implements PregenCache {
|
||||
}
|
||||
|
||||
private void write(DataOutput output) throws IOException {
|
||||
Varint.writeSignedVarInt(count, output);
|
||||
if (regions == null) {
|
||||
int total = completed();
|
||||
Varint.writeSignedVarInt(total, output);
|
||||
AtomicReferenceArray<Region> current = regions;
|
||||
if (total == SIZE || current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Region region : regions) {
|
||||
for (int index = 0; index < SIZE; index++) {
|
||||
Region region = current.get(index);
|
||||
output.writeBoolean(region == null);
|
||||
if (region != null) {
|
||||
region.write(output);
|
||||
@@ -321,33 +384,32 @@ public class PregenCacheImpl implements PregenCache {
|
||||
}
|
||||
|
||||
private static class Region {
|
||||
private short count;
|
||||
private long[] words;
|
||||
private final AtomicInteger count;
|
||||
private final AtomicLongArray words;
|
||||
|
||||
private Region() {
|
||||
this((short) 0, new long[64]);
|
||||
this(0, new AtomicLongArray(64));
|
||||
}
|
||||
|
||||
private Region(short count, long[] words) {
|
||||
this.count = count;
|
||||
private Region(int count, AtomicLongArray words) {
|
||||
this.count = new AtomicInteger(count);
|
||||
this.words = words;
|
||||
}
|
||||
|
||||
private int completed() {
|
||||
return Math.min(count.get(), SIZE);
|
||||
}
|
||||
|
||||
private CacheResult cache() {
|
||||
if (count == SIZE) {
|
||||
return CacheResult.NOOP;
|
||||
}
|
||||
count = SIZE;
|
||||
words = null;
|
||||
return CacheResult.COMPLETED;
|
||||
return count.getAndSet(SIZE) >= SIZE ? CacheResult.NOOP : CacheResult.COMPLETED;
|
||||
}
|
||||
|
||||
private CacheResult cache(int x, int z) {
|
||||
if (count == SIZE) {
|
||||
if (completed() == SIZE) {
|
||||
return CacheResult.NOOP;
|
||||
}
|
||||
|
||||
long[] value = words;
|
||||
AtomicLongArray value = words;
|
||||
if (value == null) {
|
||||
return CacheResult.NOOP;
|
||||
}
|
||||
@@ -355,73 +417,79 @@ public class PregenCacheImpl implements PregenCache {
|
||||
int index = PowerOfTwoCoordinates.packLocal32(x, z);
|
||||
int wordIndex = index >> 6;
|
||||
long bit = 1L << (index & 63);
|
||||
boolean current = (value[wordIndex] & bit) != 0L;
|
||||
if (current) {
|
||||
return CacheResult.NOOP;
|
||||
while (true) {
|
||||
long current = value.get(wordIndex);
|
||||
if ((current & bit) != 0L) {
|
||||
return CacheResult.NOOP;
|
||||
}
|
||||
if (value.compareAndSet(wordIndex, current, current | bit)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
count++;
|
||||
if (count == SIZE) {
|
||||
words = null;
|
||||
return CacheResult.COMPLETED;
|
||||
}
|
||||
|
||||
value[wordIndex] = value[wordIndex] | bit;
|
||||
return CacheResult.SET;
|
||||
return count.incrementAndGet() >= SIZE ? CacheResult.COMPLETED : CacheResult.SET;
|
||||
}
|
||||
|
||||
private boolean isCached() {
|
||||
return count == SIZE;
|
||||
return completed() == SIZE;
|
||||
}
|
||||
|
||||
private boolean isCached(int x, int z) {
|
||||
int index = PowerOfTwoCoordinates.packLocal32(x, z);
|
||||
if (count == SIZE) {
|
||||
if (completed() == SIZE) {
|
||||
return true;
|
||||
}
|
||||
return (words[index >> 6] & (1L << (index & 63))) != 0L;
|
||||
|
||||
AtomicLongArray value = words;
|
||||
if (value == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int index = PowerOfTwoCoordinates.packLocal32(x, z);
|
||||
return (value.get(index >> 6) & (1L << (index & 63))) != 0L;
|
||||
}
|
||||
|
||||
private void write(DataOutput output) throws IOException {
|
||||
Varint.writeSignedVarInt(count, output);
|
||||
if (words == null) {
|
||||
int total = completed();
|
||||
Varint.writeSignedVarInt(total, output);
|
||||
AtomicLongArray value = words;
|
||||
if (total == SIZE || value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (long word : words) {
|
||||
Varint.writeUnsignedVarLong(word, output);
|
||||
for (int index = 0; index < value.length(); index++) {
|
||||
Varint.writeUnsignedVarLong(value.get(index), output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Plate readPlate(int x, int z, DataInput input) throws IOException {
|
||||
private static Plate readPlate(long key, int x, int z, DataInput input) throws IOException {
|
||||
int count = Varint.readSignedVarInt(input);
|
||||
if (count == 1024) {
|
||||
return new Plate(x, z, SIZE, null);
|
||||
if (count == SIZE) {
|
||||
return new Plate(key, x, z, SIZE, null);
|
||||
}
|
||||
|
||||
Region[] regions = new Region[1024];
|
||||
for (int i = 0; i < regions.length; i++) {
|
||||
AtomicReferenceArray<Region> regions = new AtomicReferenceArray<>(SIZE);
|
||||
for (int index = 0; index < SIZE; index++) {
|
||||
boolean isNull = input.readBoolean();
|
||||
if (!isNull) {
|
||||
regions[i] = readRegion(input);
|
||||
regions.set(index, readRegion(input));
|
||||
}
|
||||
}
|
||||
|
||||
return new Plate(x, z, (short) count, regions);
|
||||
return new Plate(key, x, z, count, regions);
|
||||
}
|
||||
|
||||
private static Region readRegion(DataInput input) throws IOException {
|
||||
int count = Varint.readSignedVarInt(input);
|
||||
if (count == 1024) {
|
||||
if (count == SIZE) {
|
||||
return new Region(SIZE, null);
|
||||
}
|
||||
|
||||
long[] words = new long[64];
|
||||
for (int i = 0; i < words.length; i++) {
|
||||
words[i] = Varint.readUnsignedVarLong(input);
|
||||
AtomicLongArray words = new AtomicLongArray(64);
|
||||
for (int index = 0; index < words.length(); index++) {
|
||||
words.set(index, Varint.readUnsignedVarLong(input));
|
||||
}
|
||||
|
||||
return new Region((short) count, words);
|
||||
return new Region(count, words);
|
||||
}
|
||||
}
|
||||
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
package art.arcane.iris.core.pregenerator.cache;
|
||||
|
||||
record SynchronizedCache(PregenCache cache) implements PregenCache {
|
||||
@Override
|
||||
public boolean isThreadSafe() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChunkCached(int x, int z) {
|
||||
synchronized (cache) {
|
||||
return cache.isChunkCached(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegionCached(int x, int z) {
|
||||
synchronized (cache) {
|
||||
return cache.isRegionCached(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cacheChunk(int x, int z) {
|
||||
synchronized (cache) {
|
||||
cache.cacheChunk(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cacheRegion(int x, int z) {
|
||||
synchronized (cache) {
|
||||
cache.cacheRegion(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write() {
|
||||
synchronized (cache) {
|
||||
cache.write();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trim(long unloadDuration) {
|
||||
synchronized (cache) {
|
||||
cache.trim(unloadDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-68
@@ -22,6 +22,7 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.core.IrisPaperLikeBackendMode;
|
||||
import art.arcane.iris.core.IrisRuntimeSchedulerMode;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.pregenerator.PregenAdmissionGate;
|
||||
import art.arcane.iris.core.pregenerator.PregenDiagnostics;
|
||||
import art.arcane.iris.core.pregenerator.PregenListener;
|
||||
import art.arcane.iris.core.pregenerator.PregenMantleBackpressure;
|
||||
@@ -51,7 +52,6 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -68,6 +68,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
|
||||
private static final long CLOSE_DRAIN_WARNING_SECONDS = 60L;
|
||||
private static final long FLUSH_TIMEOUT_SECONDS = 120L;
|
||||
private static final long ADMISSION_WAIT_BOUND_MS = 500L;
|
||||
private final World world;
|
||||
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
|
||||
private final IrisPaperLikeBackendMode paperLikeBackendMode;
|
||||
@@ -82,7 +83,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
private final String chunkAccessMode;
|
||||
private final ChunkRequestExecutor executor;
|
||||
private final Executor slowRequestExecutor;
|
||||
private final Semaphore semaphore;
|
||||
private final PregenAdmissionGate admission;
|
||||
private final int threads;
|
||||
private final int slowRequestWarningSeconds;
|
||||
private final int slowRequestWarnIntervalMs;
|
||||
@@ -111,7 +112,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
|
||||
private final AtomicBoolean closing = new AtomicBoolean();
|
||||
private final AtomicBoolean holdsWorkerBoost = new AtomicBoolean();
|
||||
private final Object permitMonitor = new Object();
|
||||
private volatile Engine metricsEngine;
|
||||
private volatile Mantle cachedMantle;
|
||||
private final PregenMantleBackpressure backpressure;
|
||||
@@ -161,7 +161,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
this.runtimeCpuThreads = detectedCpuThreads;
|
||||
this.effectiveWorkerThreads = workerThreadsForCap;
|
||||
this.recommendedRuntimeConcurrencyCap = configuredThreads;
|
||||
this.semaphore = new Semaphore(this.threads, true);
|
||||
this.admission = new PregenAdmissionGate(this.threads, ADMISSION_WAIT_BOUND_MS, M::ms);
|
||||
this.slowRequestWarningSeconds = pregen.getChunkLoadTimeoutSeconds();
|
||||
this.slowRequestExecutor = CompletableFuture.delayedExecutor(this.slowRequestWarningSeconds, TimeUnit.SECONDS);
|
||||
this.slowRequestWarnIntervalMs = pregen.getTimeoutWarnIntervalMs();
|
||||
@@ -527,11 +527,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
} finally {
|
||||
try {
|
||||
markFinished(success);
|
||||
} finally {
|
||||
semaphore.release();
|
||||
}
|
||||
markFinished(success);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,7 +583,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
int next = Math.max(adaptiveMinInFlightLimit, current - 1);
|
||||
if (adaptiveInFlightLimit.compareAndSet(current, next)) {
|
||||
logAdaptiveLimit("decrease", next);
|
||||
notifyPermitWaiters();
|
||||
admission.wake();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -603,7 +599,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
int next = nextAdaptiveInFlightLimit(current, threads);
|
||||
if (adaptiveInFlightLimit.compareAndSet(current, next)) {
|
||||
logAdaptiveLimit("increase", next);
|
||||
notifyPermitWaiters();
|
||||
admission.wake();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -689,7 +685,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
private String metricsSnapshot() {
|
||||
long stalledFor = Math.max(0L, M.ms() - lastProgressAt.get());
|
||||
return "world=" + world.getName()
|
||||
+ " permits=" + semaphore.availablePermits() + "/" + threads
|
||||
+ " permits=" + admission.availablePermits() + "/" + threads
|
||||
+ " adaptiveLimit=" + adaptiveInFlightLimit.get()
|
||||
+ " inFlight=" + inFlight.get()
|
||||
+ " submitted=" + submitted.get()
|
||||
@@ -716,13 +712,8 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
if (after < 0) {
|
||||
inFlight.compareAndSet(after, 0);
|
||||
}
|
||||
notifyPermitWaiters();
|
||||
}
|
||||
|
||||
private void notifyPermitWaiters() {
|
||||
synchronized (permitMonitor) {
|
||||
permitMonitor.notifyAll();
|
||||
}
|
||||
admission.release();
|
||||
backpressure.signalProgress();
|
||||
}
|
||||
|
||||
private void recordAdaptiveWait(long waitedMs) {
|
||||
@@ -818,14 +809,12 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
@Override
|
||||
public void close() {
|
||||
closing.set(true);
|
||||
notifyPermitWaiters();
|
||||
admission.wake();
|
||||
|
||||
// A stop request interrupts the pregen worker; shield the drain and flush so chunks still hit disk.
|
||||
boolean interrupted = Thread.interrupted();
|
||||
try {
|
||||
interrupted |= awaitDrain(
|
||||
semaphore,
|
||||
threads,
|
||||
interrupted |= admission.awaitDrain(
|
||||
CLOSE_DRAIN_WARNING_SECONDS,
|
||||
TimeUnit.SECONDS,
|
||||
() -> IrisLogging.warn("Async pregen is still draining outstanding chunks. " + metricsSnapshot())
|
||||
@@ -843,26 +832,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
}
|
||||
|
||||
static boolean awaitDrain(
|
||||
Semaphore semaphore,
|
||||
int permits,
|
||||
long warningInterval,
|
||||
TimeUnit timeUnit,
|
||||
Runnable onWait
|
||||
) {
|
||||
boolean interrupted = false;
|
||||
while (true) {
|
||||
try {
|
||||
if (semaphore.tryAcquire(permits, warningInterval, timeUnit)) {
|
||||
return interrupted;
|
||||
}
|
||||
onWait.run();
|
||||
} catch (InterruptedException e) {
|
||||
interrupted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCancelled() {
|
||||
return closing.get() || Thread.currentThread().isInterrupted();
|
||||
}
|
||||
@@ -890,37 +859,26 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
return;
|
||||
}
|
||||
|
||||
PregenAdmissionGate.Wait waited;
|
||||
try {
|
||||
long waitStart = M.ms();
|
||||
synchronized (permitMonitor) {
|
||||
while (inFlight.get() >= adaptiveInFlightLimit.get()) {
|
||||
if (isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
permitMonitor.wait(500L);
|
||||
}
|
||||
}
|
||||
long adaptiveWait = Math.max(0L, M.ms() - waitStart);
|
||||
if (adaptiveWait > 0L) {
|
||||
recordAdaptiveWait(adaptiveWait);
|
||||
}
|
||||
|
||||
long permitWaitStart = M.ms();
|
||||
while (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) {
|
||||
if (isCancelled()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
long permitWait = Math.max(0L, M.ms() - permitWaitStart);
|
||||
if (permitWait > 0L) {
|
||||
recordPermitWait(permitWait);
|
||||
}
|
||||
waited = admission.admit(() -> inFlight.get() >= adaptiveInFlightLimit.get(), this::isCancelled);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
|
||||
if (waited == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (waited.adaptiveMs() > 0L) {
|
||||
recordAdaptiveWait(waited.adaptiveMs());
|
||||
}
|
||||
|
||||
if (waited.permitMs() > 0L) {
|
||||
recordPermitWait(waited.permitMs());
|
||||
}
|
||||
|
||||
regionPending.computeIfAbsent(rkey(x >> 5, z >> 5), k -> new AtomicInteger(1)).incrementAndGet();
|
||||
markSubmitted();
|
||||
executor.generate(x, z, listener);
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.IntSupplier;
|
||||
|
||||
public class MultiBurst extends MultiBurstSupport {
|
||||
@@ -21,6 +22,7 @@ public class MultiBurst extends MultiBurstSupport {
|
||||
* plans never pin the workers that chunk generation's short stage and mantle tasks need.
|
||||
*/
|
||||
public static final MultiBurst hydrology = new MultiBurst("Iris Hydrology", () -> Math.max(2, IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())));
|
||||
private final AtomicInteger parallelismBaseline = new AtomicInteger();
|
||||
|
||||
public MultiBurst() {
|
||||
this("Iris");
|
||||
@@ -41,16 +43,38 @@ public class MultiBurst extends MultiBurstSupport {
|
||||
/**
|
||||
* Raises the pool's parallelism to at least {@code target} workers. Pregeneration fans every
|
||||
* chunk's stages and mantle windows into this pool while some tasks block on unmanaged waits,
|
||||
* so a pregeneration box wants more workers than cores; the pool never shrinks back.
|
||||
* so a pregeneration box wants more workers than cores. {@link #restoreParallelism()} puts it back.
|
||||
*/
|
||||
public boolean raiseParallelism(int target) {
|
||||
if (!(service() instanceof ForkJoinPool pool) || target <= pool.getParallelism()) {
|
||||
return false;
|
||||
}
|
||||
parallelismBaseline.compareAndSet(0, pool.getParallelism());
|
||||
pool.setParallelism(target);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pool to the parallelism it had before the first {@link #raiseParallelism(int)} that took
|
||||
* effect. A pool that was never raised, or that is not a {@link ForkJoinPool}, is left alone.
|
||||
*/
|
||||
public boolean restoreParallelism() {
|
||||
int baseline = parallelismBaseline.getAndSet(0);
|
||||
if (baseline <= 0 || !(service() instanceof ForkJoinPool pool) || pool.getParallelism() <= baseline) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
pool.setParallelism(baseline);
|
||||
} catch (RuntimeException e) {
|
||||
IrisLogging.reportError("Iris could not return the burst pool to its pre-pregeneration parallelism of "
|
||||
+ baseline + "; it stays at " + pool.getParallelism() + ".", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int parallelism() {
|
||||
return service() instanceof ForkJoinPool pool ? pool.getParallelism() : -1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package art.arcane.iris.core.pregenerator;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PregenAdmissionGateTest {
|
||||
private static final long GENEROUS_BOUND_MS = 60_000L;
|
||||
|
||||
@Test
|
||||
public void admissionConsumesOnePermitAndReturnsItOnRelease() throws Exception {
|
||||
PregenAdmissionGate gate = new PregenAdmissionGate(2, GENEROUS_BOUND_MS, System::currentTimeMillis);
|
||||
|
||||
assertEquals(2, gate.availablePermits());
|
||||
assertNotNull(gate.admit(() -> false, () -> false));
|
||||
assertEquals(1, gate.availablePermits());
|
||||
gate.release();
|
||||
assertEquals(2, gate.availablePermits());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void releaseNeverPushesThePermitCountAboveTheConfiguredCeiling() throws Exception {
|
||||
PregenAdmissionGate gate = new PregenAdmissionGate(1, GENEROUS_BOUND_MS, System::currentTimeMillis);
|
||||
|
||||
gate.release();
|
||||
gate.release();
|
||||
|
||||
assertEquals(1, gate.availablePermits());
|
||||
assertNotNull(gate.admit(() -> false, () -> false));
|
||||
assertEquals(0, gate.availablePermits());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aBlockedAdmissionWakesOnTheReleaseSignalRatherThanTheTimeout() throws Exception {
|
||||
PregenAdmissionGate gate = new PregenAdmissionGate(1, GENEROUS_BOUND_MS, System::currentTimeMillis);
|
||||
assertNotNull(gate.admit(() -> false, () -> false));
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<PregenAdmissionGate.Wait> blocked = executor.submit(() -> {
|
||||
started.countDown();
|
||||
return gate.admit(() -> false, () -> false);
|
||||
});
|
||||
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
while (gate.availablePermits() != 0) {
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
gate.release();
|
||||
|
||||
assertNotNull(blocked.get(5, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aBlockedAdmissionWakesOnTheAdaptiveLimitSignalWithoutWaitingOutTheBound() throws Exception {
|
||||
PregenAdmissionGate gate = new PregenAdmissionGate(4, GENEROUS_BOUND_MS, System::currentTimeMillis);
|
||||
AtomicBoolean overLimit = new AtomicBoolean(true);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<PregenAdmissionGate.Wait> blocked = executor.submit(() -> {
|
||||
started.countDown();
|
||||
return gate.admit(overLimit::get, () -> false);
|
||||
});
|
||||
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
Thread.yield();
|
||||
overLimit.set(false);
|
||||
gate.wake();
|
||||
|
||||
assertNotNull(blocked.get(5, TimeUnit.SECONDS));
|
||||
assertEquals(3, gate.availablePermits());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aCancelledAdmissionReturnsNothingAndConsumesNoPermit() throws Exception {
|
||||
PregenAdmissionGate gate = new PregenAdmissionGate(1, GENEROUS_BOUND_MS, System::currentTimeMillis);
|
||||
|
||||
assertNull(gate.admit(() -> true, () -> true));
|
||||
assertEquals(1, gate.availablePermits());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aCancellationRaisedWhileBlockedEndsTheWait() throws Exception {
|
||||
PregenAdmissionGate gate = new PregenAdmissionGate(1, GENEROUS_BOUND_MS, System::currentTimeMillis);
|
||||
assertNotNull(gate.admit(() -> false, () -> false));
|
||||
AtomicBoolean cancelled = new AtomicBoolean();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<PregenAdmissionGate.Wait> blocked = executor.submit(() -> {
|
||||
started.countDown();
|
||||
return gate.admit(() -> false, cancelled::get);
|
||||
});
|
||||
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
Thread.yield();
|
||||
cancelled.set(true);
|
||||
gate.wake();
|
||||
|
||||
assertNull(blocked.get(5, TimeUnit.SECONDS));
|
||||
assertEquals(0, gate.availablePermits());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theAdaptiveAndPermitWaitsAreAttributedSeparately() throws Exception {
|
||||
AtomicLong clock = new AtomicLong();
|
||||
AtomicBoolean overLimit = new AtomicBoolean(true);
|
||||
PregenAdmissionGate gate = new PregenAdmissionGate(1, 5L, () -> clock.addAndGet(10L));
|
||||
assertNotNull(gate.admit(() -> false, () -> false));
|
||||
|
||||
AtomicInteger passes = new AtomicInteger();
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<PregenAdmissionGate.Wait> blocked = executor.submit(() -> gate.admit(() -> {
|
||||
if (passes.incrementAndGet() > 2) {
|
||||
overLimit.set(false);
|
||||
}
|
||||
return overLimit.get();
|
||||
}, () -> false));
|
||||
|
||||
while (passes.get() <= 3) {
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
gate.release();
|
||||
|
||||
PregenAdmissionGate.Wait wait = blocked.get(5, TimeUnit.SECONDS);
|
||||
assertNotNull(wait);
|
||||
assertTrue("adaptive wait was not attributed: " + wait, wait.adaptiveMs() > 0L);
|
||||
assertTrue("permit wait was not attributed: " + wait, wait.permitMs() > 0L);
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void drainReturnsOnlyAfterEveryPermitIsBack() throws Exception {
|
||||
PregenAdmissionGate gate = new PregenAdmissionGate(2, GENEROUS_BOUND_MS, System::currentTimeMillis);
|
||||
assertNotNull(gate.admit(() -> false, () -> false));
|
||||
assertNotNull(gate.admit(() -> false, () -> false));
|
||||
AtomicInteger warnings = new AtomicInteger();
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<Boolean> drain = executor.submit(() -> gate.awaitDrain(5L, TimeUnit.MILLISECONDS, warnings::incrementAndGet));
|
||||
|
||||
while (warnings.get() < 2) {
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
gate.release();
|
||||
gate.release();
|
||||
|
||||
assertEquals(Boolean.FALSE, drain.get(5, TimeUnit.SECONDS));
|
||||
assertEquals(2, gate.availablePermits());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void aGateWithoutPermitsIsRejected() {
|
||||
new PregenAdmissionGate(0, GENEROUS_BOUND_MS, System::currentTimeMillis);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void aGateWithoutAWaitBoundIsRejected() {
|
||||
new PregenAdmissionGate(1, 0L, System::currentTimeMillis);
|
||||
}
|
||||
}
|
||||
+37
@@ -107,4 +107,41 @@ public class PregenPerformanceProfileTest {
|
||||
assertEquals(before + 1, MultiBurst.burst.parallelism());
|
||||
assertFalse(MultiBurst.burst.raiseParallelism(before));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restoringBurstParallelismReturnsThePoolToItsPrePregenerationSize() {
|
||||
int before = MultiBurst.ioBurst.parallelism();
|
||||
assertTrue(MultiBurst.ioBurst.raiseParallelism(before + 3));
|
||||
assertEquals(before + 3, MultiBurst.ioBurst.parallelism());
|
||||
|
||||
assertTrue(MultiBurst.ioBurst.restoreParallelism());
|
||||
assertEquals(before, MultiBurst.ioBurst.parallelism());
|
||||
assertFalse(MultiBurst.ioBurst.restoreParallelism());
|
||||
assertEquals(before, MultiBurst.ioBurst.parallelism());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aSecondRaiseKeepsTheOriginalBaselineSoOneRestoreIsEnough() {
|
||||
int before = MultiBurst.ioBurst.parallelism();
|
||||
assertTrue(MultiBurst.ioBurst.raiseParallelism(before + 2));
|
||||
assertTrue(MultiBurst.ioBurst.raiseParallelism(before + 5));
|
||||
|
||||
assertTrue(MultiBurst.ioBurst.restoreParallelism());
|
||||
assertEquals(before, MultiBurst.ioBurst.parallelism());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theBurstParallelismHoldIsReleasedByAMatchingRestore() {
|
||||
while (PregenPerformanceProfile.parallelismHolders() > 0) {
|
||||
PregenPerformanceProfile.restore();
|
||||
}
|
||||
assertFalse(PregenPerformanceProfile.restore());
|
||||
assertEquals(0, PregenPerformanceProfile.parallelismHolders());
|
||||
|
||||
PregenPerformanceProfile.apply();
|
||||
int holders = PregenPerformanceProfile.parallelismHolders();
|
||||
PregenPerformanceProfile.restore();
|
||||
|
||||
assertEquals(Math.max(0, holders - 1), PregenPerformanceProfile.parallelismHolders());
|
||||
}
|
||||
}
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package art.arcane.iris.core.pregenerator;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PregenWaitPolicyContractTest {
|
||||
@Test
|
||||
public void noPregenSourceSleepPollsWhileWaitingForCapacity() throws IOException {
|
||||
List<String> offenders = new ArrayList<>();
|
||||
Path pregenRoot = sourceRoot().resolve("art/arcane/iris/core/pregenerator");
|
||||
try (Stream<Path> sources = Files.walk(pregenRoot)) {
|
||||
for (Path source : sources.filter(path -> path.toString().endsWith(".java")).toList()) {
|
||||
String text = Files.readString(source);
|
||||
if (text.contains("Thread.sleep(") || text.contains("J.sleep(")) {
|
||||
offenders.add(source.getFileName().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("sleep polling on the pregen path: " + offenders, List.of(), offenders);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mantleBackpressureWaitsOnAProgressSignalWithABoundedTimeout() throws IOException {
|
||||
String backpressure = read("art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java");
|
||||
|
||||
assertTrue(backpressure.contains("progressed.await(waitMs, TimeUnit.MILLISECONDS)"));
|
||||
assertTrue(method(backpressure, "public void enforceMantleBudget()").contains("awaitProgress()"));
|
||||
assertTrue(method(backpressure, "public void awaitHeapHeadroom()").contains("awaitProgress()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkCompletionSignalsBothTheAdmissionGateAndTheBackpressureWait() throws IOException {
|
||||
String finished = method(read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java"),
|
||||
"private void markFinished(boolean success)");
|
||||
|
||||
assertTrue(finished, finished.contains("admission.release()"));
|
||||
assertTrue(finished, finished.contains("backpressure.signalProgress()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pauseResumeAndStopSignalThePregenGateInsteadOfLettingItPoll() throws IOException {
|
||||
String pregenerator = read("art/arcane/iris/core/pregenerator/IrisPregenerator.java");
|
||||
|
||||
assertTrue(method(pregenerator, "public void pause()").contains("signalState()"));
|
||||
assertTrue(method(pregenerator, "public void resume()").contains("signalState()"));
|
||||
assertTrue(method(pregenerator, "public void close()").contains("signalState()"));
|
||||
assertTrue(method(pregenerator, "private void visitRegion(").contains("awaitStateChange(GATE_WAIT_MS)"));
|
||||
assertTrue(method(pregenerator, "private void reclaimTectonicPlates(").contains("awaitStateChange(RECLAIM_WAIT_MS)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aFinishedPregenReleasesTheBurstParallelismItRaised() throws IOException {
|
||||
String shutdown = method(read("art/arcane/iris/core/pregenerator/IrisPregenerator.java"), "private void shutdown()");
|
||||
|
||||
assertTrue(shutdown, shutdown.contains("shutdownStep(\"parallelism\", PregenPerformanceProfile::restore)"));
|
||||
}
|
||||
|
||||
private static Path sourceRoot() throws IOException {
|
||||
Path current;
|
||||
try {
|
||||
current = Path.of(PregenWaitPolicyContractTest.class.getProtectionDomain()
|
||||
.getCodeSource()
|
||||
.getLocation()
|
||||
.toURI());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IOException("Unable to resolve the core source root", e);
|
||||
}
|
||||
while (current != null && !Files.isDirectory(current.resolve("src/main/java"))) {
|
||||
current = current.getParent();
|
||||
}
|
||||
if (current == null) {
|
||||
throw new IOException("Unable to locate the core source root");
|
||||
}
|
||||
return current.resolve("src/main/java");
|
||||
}
|
||||
|
||||
private static String read(String relativePath) throws IOException {
|
||||
return Files.readString(sourceRoot().resolve(relativePath));
|
||||
}
|
||||
|
||||
private static String method(String source, String signature) {
|
||||
int start = source.indexOf(signature);
|
||||
assertTrue("Missing source contract signature: " + signature, start >= 0);
|
||||
int openBrace = source.indexOf('{', start);
|
||||
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
|
||||
int depth = 0;
|
||||
for (int index = openBrace; index < source.length(); index++) {
|
||||
char current = source.charAt(index);
|
||||
if (current == '{') {
|
||||
depth++;
|
||||
} else if (current == '}') {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
return source.substring(start, index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package art.arcane.iris.core.pregenerator.cache;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CyclicBarrier;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PregenCacheConcurrencyTest {
|
||||
private static final int THREADS = 8;
|
||||
|
||||
private File directory;
|
||||
private ExecutorService pool;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
directory = Files.createTempDirectory("iris-pregen-cache-concurrency").toFile();
|
||||
pool = Executors.newFixedThreadPool(THREADS);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
pool.shutdownNow();
|
||||
pool.awaitTermination(30, TimeUnit.SECONDS);
|
||||
deleteRecursively(directory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyConcurrentlyCachedChunkIsReadBackAsCached() throws Exception {
|
||||
PregenCache cache = PregenCache.create(directory);
|
||||
int chunksPerThread = 512;
|
||||
runAll(thread -> () -> {
|
||||
for (int index = 0; index < chunksPerThread; index++) {
|
||||
int x = (thread * chunksPerThread) + index;
|
||||
cache.cacheChunk(x, thread);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
for (int thread = 0; thread < THREADS; thread++) {
|
||||
for (int index = 0; index < chunksPerThread; index++) {
|
||||
int x = (thread * chunksPerThread) + index;
|
||||
assertTrue("chunk " + x + "," + thread, cache.isChunkCached(x, thread));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentWritersToOneRegionNeverLoseABit() throws Exception {
|
||||
PregenCache cache = PregenCache.create(directory);
|
||||
runAll(thread -> () -> {
|
||||
for (int index = thread; index < 1024; index += THREADS) {
|
||||
cache.cacheChunk(index & 31, index >> 5);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
for (int index = 0; index < 1024; index++) {
|
||||
assertTrue("chunk " + index, cache.isChunkCached(index & 31, index >> 5));
|
||||
}
|
||||
assertTrue(cache.isRegionCached(0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aRegionIsOnlyReportedCachedOnceEveryChunkInItLanded() throws Exception {
|
||||
PregenCache cache = PregenCache.create(directory);
|
||||
AtomicInteger seenComplete = new AtomicInteger();
|
||||
runAll(thread -> () -> {
|
||||
for (int index = thread; index < 1023; index += THREADS) {
|
||||
cache.cacheChunk(index & 31, index >> 5);
|
||||
if (cache.isRegionCached(0, 0)) {
|
||||
seenComplete.incrementAndGet();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
assertEquals(0, seenComplete.get());
|
||||
assertFalse(cache.isRegionCached(0, 0));
|
||||
cache.cacheChunk(1023 & 31, 1023 >> 5);
|
||||
assertTrue(cache.isRegionCached(0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentReadsAndWritesNeverInventACachedChunk() throws Exception {
|
||||
PregenCache cache = PregenCache.create(directory);
|
||||
AtomicInteger falsePositives = new AtomicInteger();
|
||||
runAll(thread -> () -> {
|
||||
if (thread % 2 == 0) {
|
||||
for (int index = 0; index < 2048; index++) {
|
||||
cache.cacheChunk(index, 0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int index = 0; index < 2048; index++) {
|
||||
if (cache.isChunkCached(index, 7000)) {
|
||||
falsePositives.incrementAndGet();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
assertEquals(0, falsePositives.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aConcurrentWriteAndFlushRoundTripsThroughDiskWithoutLosingChunks() throws Exception {
|
||||
PregenCache cache = PregenCache.create(directory);
|
||||
runAll(thread -> () -> {
|
||||
for (int index = 0; index < 256; index++) {
|
||||
cache.cacheChunk((thread * 256) + index, 4);
|
||||
if (index % 64 == 0) {
|
||||
cache.write();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
cache.write();
|
||||
|
||||
PregenCache reopened = PregenCache.create(directory);
|
||||
for (int thread = 0; thread < THREADS; thread++) {
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = (thread * 256) + index;
|
||||
assertTrue("chunk " + x + " lost across a flush", reopened.isChunkCached(x, 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plateEvictionUnderConcurrencyStillPersistsEveryChunk() throws Exception {
|
||||
PregenCacheImpl cache = new PregenCacheImpl(directory, 1);
|
||||
int platesPerThread = 6;
|
||||
runAll(thread -> () -> {
|
||||
for (int plate = 0; plate < platesPerThread; plate++) {
|
||||
cache.cacheChunk(plate * 1024, thread);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
cache.write();
|
||||
|
||||
PregenCache reopened = PregenCache.create(directory);
|
||||
for (int thread = 0; thread < THREADS; thread++) {
|
||||
for (int plate = 0; plate < platesPerThread; plate++) {
|
||||
assertTrue("chunk " + (plate * 1024) + "," + thread + " lost to eviction",
|
||||
reopened.isChunkCached(plate * 1024, thread));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void runAll(ThreadTask task) throws Exception {
|
||||
CyclicBarrier barrier = new CyclicBarrier(THREADS);
|
||||
List<Future<Void>> futures = new ArrayList<>(THREADS);
|
||||
for (int thread = 0; thread < THREADS; thread++) {
|
||||
Callable<Void> body = task.forThread(thread);
|
||||
futures.add(pool.submit(() -> {
|
||||
barrier.await(30, TimeUnit.SECONDS);
|
||||
return body.call();
|
||||
}));
|
||||
}
|
||||
|
||||
for (Future<Void> future : futures) {
|
||||
future.get(60, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteRecursively(File file) {
|
||||
File[] children = file.listFiles();
|
||||
if (children != null) {
|
||||
for (File child : children) {
|
||||
deleteRecursively(child);
|
||||
}
|
||||
}
|
||||
file.delete();
|
||||
}
|
||||
|
||||
private interface ThreadTask {
|
||||
Callable<Void> forThread(int thread);
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -1,17 +1,18 @@
|
||||
package art.arcane.iris.core.pregenerator.methods;
|
||||
|
||||
import art.arcane.iris.core.pregenerator.PregenAdmissionGate;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -101,13 +102,13 @@ public class AsyncPregenMethodConcurrencyCapTest {
|
||||
|
||||
@Test
|
||||
public void closeDrainWaitsPastWarningIntervalsUntilEveryPermitReturns() throws Exception {
|
||||
Semaphore semaphore = new Semaphore(0);
|
||||
PregenAdmissionGate gate = new PregenAdmissionGate(2, 5L, System::currentTimeMillis);
|
||||
assertNotNull(gate.admit(() -> false, () -> false));
|
||||
assertNotNull(gate.admit(() -> false, () -> false));
|
||||
AtomicInteger warnings = new AtomicInteger();
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<Boolean> drain = executor.submit(() -> AsyncPregenMethod.awaitDrain(
|
||||
semaphore,
|
||||
2,
|
||||
Future<Boolean> drain = executor.submit(() -> gate.awaitDrain(
|
||||
10L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
warnings::incrementAndGet
|
||||
@@ -116,11 +117,12 @@ public class AsyncPregenMethodConcurrencyCapTest {
|
||||
while (warnings.get() == 0) {
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
semaphore.release(2);
|
||||
gate.release();
|
||||
gate.release();
|
||||
|
||||
assertFalse(drain.get(1L, TimeUnit.SECONDS));
|
||||
assertTrue(warnings.get() > 0);
|
||||
assertEquals(0, semaphore.availablePermits());
|
||||
assertEquals(2, gate.availablePermits());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user