mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +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:
1 parent
e030ac3be5
commit
1be563bd28
14 files changed
+1032
-297
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user