stabilize pregeneration

This commit is contained in:
Brian Neumann-Fopiano committed 2026-08-15 15:57:36 -04:00
1 parent 4535bf3eed
commit d196ffe853
32 files changed
+2013 -169

No files matched your search

@@ -6,7 +6,6 @@ import art.arcane.iris.core.pack.PackDirectoryResolver;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
@@ -247,27 +246,29 @@ public final class WorldReplacementFilesystem {
Path root = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize();
requireDirectory(root, "pack root");
MessageDigest digest = sha256();
List<Path> files;
List<FingerprintEntry> entries;
try (Stream<Path> stream = Files.walk(root)) {
files = stream
entries = stream
.filter(path -> !path.equals(root))
.sorted(Comparator.comparing(path -> root.relativize(path).toString()))
.map(path -> fingerprintEntry(root, path))
.sorted(Comparator.comparing(FingerprintEntry::relativeString))
.toList();
}
for (Path file : files) {
BasicFileAttributes attributes = requireSafeEntry(file);
Path relative = root.relativize(file);
String separator = root.getFileSystem().getSeparator();
byte[] buffer = new byte[8192];
for (FingerprintEntry entry : entries) {
BasicFileAttributes attributes = requireSafeEntry(entry.path());
Path relative = entry.relative();
if (isGeneratedPackMetadata(relative, attributes)) {
continue;
}
update(digest, relative.toString().replace(file.getFileSystem().getSeparator(), "/"));
update(digest, entry.relativeString().replace(separator, "/"));
digest.update((byte) (attributes.isDirectory() ? 1 : 0));
if (!attributes.isRegularFile()) {
continue;
}
update(digest, Long.toString(attributes.size()));
try (InputStream input = Files.newInputStream(file)) {
byte[] buffer = new byte[8192];
try (InputStream input = Files.newInputStream(entry.path())) {
int read;
while ((read = input.read(buffer)) >= 0) {
digest.update(buffer, 0, read);
@@ -277,6 +278,11 @@ public final class WorldReplacementFilesystem {
return HexFormat.of().formatHex(digest.digest());
}
private static FingerprintEntry fingerprintEntry(Path root, Path path) {
Path relative = root.relativize(path);
return new FingerprintEntry(path, relative, relative.toString());
}
private static State inspect(ReplacementPaths paths) throws IOException {
return new State(
directoryPresent(paths.target(), "replacement target"),
@@ -405,7 +411,10 @@ public final class WorldReplacementFilesystem {
private static void update(MessageDigest digest, String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
digest.update(ByteBuffer.allocate(Integer.BYTES).putInt(bytes.length).array());
digest.update((byte) (bytes.length >>> 24));
digest.update((byte) (bytes.length >>> 16));
digest.update((byte) (bytes.length >>> 8));
digest.update((byte) bytes.length);
digest.update(bytes);
}
@@ -474,4 +483,7 @@ public final class WorldReplacementFilesystem {
private record State(boolean targetPresent, boolean stagePresent, boolean backupPresent) {
}
private record FingerprintEntry(Path path, Path relative, String relativeString) {
}
}
@@ -463,7 +463,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
public synchronized void hotloaded() {
StructureGraphCatalog.invalidate(this);
IrisObjectScale.invalidate();
IrisObjectScale.invalidate(this);
closed = false;
possibleSnippets = new KMap<>();
builder = new GsonBuilder()
@@ -529,7 +529,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
public void dump() {
StructureGraphCatalog.invalidate(this);
IrisObjectScale.invalidate();
IrisObjectScale.invalidate(this);
for (ResourceLoader<?> i : loaders.values()) {
i.clearCache();
}
@@ -763,19 +763,13 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
}
public void loadPrefetch(Engine engine) {
BurstExecutor b = MultiBurst.ioBurst.burst(loaders.size());
for (ResourceLoader<?> i : loaders.values()) {
b.queue(() -> {
try {
i.loadFirstAccess(engine);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
for (ResourceLoader<?> loader : loaders.values()) {
try {
loader.loadFirstAccess(engine);
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
b.complete();
IrisLogging.debug("Loaded Prefetch Cache to reduce generation disk use.");
}
}
@@ -52,12 +52,16 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
@@ -79,6 +83,11 @@ import java.util.zip.GZIPOutputStream;
public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
public static final AtomicDouble tlt = new AtomicDouble(0);
private static final int CACHE_SIZE = 100000;
private static final int PREFETCH_MAGIC = 0x49504632;
private static final int PREFETCH_FORMAT_VERSION = 1;
private static final int PREFETCH_ENTRY_LIMIT = 1_024;
private static final int PREFETCH_KEY_LENGTH_LIMIT = 1_024;
private static final long PREFETCH_FILE_SIZE_LIMIT = 4L * 1_024L * 1_024L;
private static volatile ExecutorService schemaBuildExecutor;
private static final Set<String> schemaBuildQueue = ConcurrentHashMap.newKeySet();
protected final AtomicCache<KList<File>> folderCache;
@@ -93,6 +102,7 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
protected IrisData manager;
protected AtomicInteger loads;
protected ChronoLatch sec;
private volatile boolean firstAccessOverflowed;
private final Options options;
public ResourceLoader(
@@ -106,6 +116,7 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
this.options = Objects.requireNonNull(options, "options");
this.manager = manager;
firstAccess = new KSet<>();
firstAccessOverflowed = false;
folderCache = new AtomicCache<>();
sec = new ChronoLatch(5000);
loads = new AtomicInteger();
@@ -499,13 +510,28 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
KSet<String> set = firstAccess;
// contains-first: CHM.add takes the bin monitor even when the key is present, and
// every generation thread hammers the same few keys through this line.
if (set != null && !set.contains(name)) set.add(name);
if (set != null && !set.contains(name)) {
if (set.size() < prefetchEntryLimit()) {
set.add(name);
} else {
firstAccessOverflowed = true;
}
}
return loadCache.get(name);
}
private File prefetchFile(Engine engine) {
String id = "DIM" + Math.abs(engine.getSeedManager().getSeed() + engine.getDimension().getVersion() + engine.getDimension().getLoadKey().hashCode());
return IrisPlatforms.get().dataFile("prefetch/" + id + "/" + Math.abs(getFolderName().hashCode()) + ".ipfch");
String dimensionIdentity = digestIdentity(
root.toPath().toAbsolutePath().normalize().toString(),
Long.toString(engine.getSeedManager().getSeed()),
Integer.toString(engine.getDimension().getVersion()),
Objects.requireNonNullElse(engine.getDimension().getLoadKey(), ""));
String loaderIdentity = digestIdentity(getFolderName());
return IrisPlatforms.get().dataFile(
"prefetch",
"v2",
dimensionIdentity,
loaderIdentity + ".ipfch");
}
public void loadFirstAccess(Engine engine) throws IOException {
@@ -515,33 +541,43 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
return;
}
KList<String> s = new KList<>();
if (file.length() > PREFETCH_FILE_SIZE_LIMIT) {
discardPrefetch(file, "file exceeds " + PREFETCH_FILE_SIZE_LIMIT + " bytes");
return;
}
KList<String> entries = new KList<>();
try (FileInputStream fin = new FileInputStream(file);
GZIPInputStream gzi = new GZIPInputStream(fin);
DataInputStream din = new DataInputStream(gzi)) {
int m = din.readInt();
int magic = din.readInt();
int version = din.readInt();
int count = din.readInt();
if (m < 0) {
throw new IOException("Bad prefetch count " + m);
if (magic != PREFETCH_MAGIC || version != PREFETCH_FORMAT_VERSION) {
throw new IOException("unsupported prefetch format");
}
for (int i = 0; i < m; i++) {
s.add(din.readUTF());
if (count < 0 || count > prefetchEntryLimit()) {
throw new IOException("bad prefetch count " + count);
}
for (int index = 0; index < count; index++) {
String key = din.readUTF();
if (key.isBlank() || key.length() > PREFETCH_KEY_LENGTH_LIMIT) {
throw new IOException("invalid prefetch key length " + key.length());
}
entries.add(key);
}
} catch (IOException e) {
IrisLogging.warn("Discarding corrupt prefetch " + file.getPath() + ": " + e.getMessage());
if (!file.delete()) {
IrisLogging.warn("Couldn't delete corrupt prefetch " + file.getPath());
}
discardPrefetch(file, e.getMessage());
return;
}
IrisLogging.info("Loading " + s.size() + " prefetch " + getFolderName());
IrisLogging.info("Loading " + entries.size() + " prefetch " + getFolderName());
firstAccess = null;
loadAllParallel(s);
for (String entry : entries) {
load(entry);
}
}
public void saveFirstAccess(Engine engine) throws IOException {
@@ -549,6 +585,18 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
if (set == null) return;
KList<String> snapshot = new KList<>(set);
File file = prefetchFile(engine);
if (firstAccessOverflowed || snapshot.size() > prefetchEntryLimit()) {
Files.deleteIfExists(file.toPath());
firstAccess = null;
return;
}
for (String key : snapshot) {
if (key == null || key.isBlank() || key.length() > PREFETCH_KEY_LENGTH_LIMIT) {
Files.deleteIfExists(file.toPath());
firstAccess = null;
return;
}
}
File parent = file.getParentFile();
if (parent == null) {
@@ -565,10 +613,12 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
try (FileOutputStream fos = new FileOutputStream(temp);
GZIPOutputStream gzo = new CustomOutputStream(fos, 9);
DataOutputStream dos = new DataOutputStream(gzo)) {
dos.writeInt(PREFETCH_MAGIC);
dos.writeInt(PREFETCH_FORMAT_VERSION);
dos.writeInt(snapshot.size());
for (String i : snapshot) {
dos.writeUTF(i);
for (String key : snapshot) {
dos.writeUTF(key);
}
}
@@ -584,6 +634,34 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
firstAccess = null;
}
private int prefetchEntryLimit() {
return (int) Math.min(loadCache.getMaxSize(), PREFETCH_ENTRY_LIMIT);
}
private void discardPrefetch(File file, String reason) {
IrisLogging.warn("Discarding corrupt prefetch " + file.getPath() + ": " + reason);
if (!file.delete()) {
IrisLogging.warn("Couldn't delete corrupt prefetch " + file.getPath());
}
}
private static String digestIdentity(String... components) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
for (String component : components) {
byte[] value = Objects.requireNonNullElse(component, "").getBytes(StandardCharsets.UTF_8);
digest.update((byte) (value.length >>> 24));
digest.update((byte) (value.length >>> 16));
digest.update((byte) (value.length >>> 8));
digest.update((byte) value.length);
digest.update(value);
}
return HexFormat.of().formatHex(digest.digest());
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable", exception);
}
}
public KList<File> getFolders() {
return folderCache.aquire(() -> {
KList<File> fc = new KList<>();
@@ -20,11 +20,19 @@ package art.arcane.iris.core.pregenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public final class PregenPerformanceProfile {
private static final long HOTLOAD_ACQUISITION_TIMEOUT_SECONDS = 30L;
private static final AtomicBoolean JVM_HINT_LOGGED = new AtomicBoolean(false);
private PregenPerformanceProfile() {
@@ -55,10 +63,43 @@ public final class PregenPerformanceProfile {
}
public static void apply(Engine engine) {
boolean changed = apply();
if (changed && engine != null) {
apply();
if (requiresNoiseCacheRefresh(engine)) {
engine.hotloadComplex();
IrisLogging.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast"));
logApplied();
}
}
public static void applyToGenerator(PlatformChunkGenerator generator) {
apply();
Engine engine = generator == null ? null : generator.getEngine();
if (requiresNoiseCacheRefresh(engine)) {
generator.hotloadComplexAsync(HOTLOAD_ACQUISITION_TIMEOUT_SECONDS, TimeUnit.SECONDS).join();
logApplied();
}
}
private static boolean requiresNoiseCacheRefresh(Engine engine) {
if (engine == null) {
return false;
}
IrisComplex complex = engine.getComplex();
if (complex == null) {
return true;
}
ProceduralStream<Double> heightStream = complex.getHeightStream();
if (!(heightStream instanceof MeteredCache cache)) {
return true;
}
ProceduralStream<IrisBiome> caveBiomeStream = complex.getCaveBiomeStream();
if (!(caveBiomeStream instanceof CachedStream2D<?> cachedStream) || !cachedStream.usesFastCache()) {
return true;
}
long configuredMaxSize = (long) IrisSettings.get().getPerformance().getNoiseCacheSize() * 256L;
return cache.getMaxSize() != configuredMaxSize;
}
private static void logApplied() {
IrisLogging.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast"));
}
}
@@ -48,6 +48,7 @@ import java.util.Queue;
import java.util.concurrent.CompletableFuture;
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;
@@ -62,7 +63,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
// land minutes after its replacement started) from shrinking the pool under the live job.
private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
private static final AtomicInteger BOOST_HOLDERS = new AtomicInteger();
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
private static final int ADAPTIVE_SLOW_REQUEST_STEP = 3;
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
private static final long CLOSE_DRAIN_TIMEOUT_SECONDS = 60L;
private static final long FLUSH_TIMEOUT_SECONDS = 120L;
@@ -78,11 +79,12 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private final Method directChunkAtAsyncUrgentMethod;
private final Method directChunkAtAsyncMethod;
private final String chunkAccessMode;
private final Executor executor;
private final ChunkRequestExecutor executor;
private final Executor slowRequestExecutor;
private final Semaphore semaphore;
private final int threads;
private final int timeoutSeconds;
private final int timeoutWarnIntervalMs;
private final int slowRequestWarningSeconds;
private final int slowRequestWarnIntervalMs;
private final boolean urgent;
private final ConcurrentHashMap<Long, AtomicInteger> regionPending;
private final ConcurrentHashMap<Long, Queue<Chunk>> regionChunks;
@@ -95,10 +97,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private volatile int boundsMaxRegionZ;
private final AtomicInteger adaptiveInFlightLimit;
private final int adaptiveMinInFlightLimit;
private final AtomicInteger timeoutStreak = new AtomicInteger();
private final AtomicLong lastTimeoutLogAt = new AtomicLong(0L);
private final AtomicInteger slowRequestStreak = new AtomicInteger();
private final AtomicLong lastSlowRequestLogAt = new AtomicLong(0L);
private final AtomicLong lastFailedReleaseLogAt = new AtomicLong(0L);
private final AtomicInteger suppressedTimeoutLogs = new AtomicInteger();
private final AtomicInteger suppressedSlowRequestLogs = new AtomicInteger();
private final AtomicLong lastAdaptiveLogAt = new AtomicLong(0L);
private final AtomicInteger inFlight = new AtomicInteger();
private final AtomicLong submitted = new AtomicLong();
@@ -158,8 +160,9 @@ public class AsyncPregenMethod implements PregeneratorMethod {
this.effectiveWorkerThreads = workerThreadsForCap;
this.recommendedRuntimeConcurrencyCap = configuredThreads;
this.semaphore = new Semaphore(this.threads, true);
this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds();
this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs();
this.slowRequestWarningSeconds = pregen.getChunkLoadTimeoutSeconds();
this.slowRequestExecutor = CompletableFuture.delayedExecutor(this.slowRequestWarningSeconds, TimeUnit.SECONDS);
this.slowRequestWarnIntervalMs = pregen.getTimeoutWarnIntervalMs();
this.urgent = false;
this.regionPending = new ConcurrentHashMap<>();
this.regionChunks = new ConcurrentHashMap<>();
@@ -266,8 +269,8 @@ public class AsyncPregenMethod implements PregeneratorMethod {
long now = M.ms();
long last = lastFailedReleaseLogAt.get();
if (now - last >= timeoutWarnIntervalMs && lastFailedReleaseLogAt.compareAndSet(last, now)) {
IrisLogging.warn("Released region slot for failed or timed out chunk at " + x + "," + z + ". " + metricsSnapshot());
if (now - last >= slowRequestWarnIntervalMs && lastFailedReleaseLogAt.compareAndSet(last, now)) {
IrisLogging.warn("Released region slot for failed chunk at " + x + "," + z + ". " + metricsSnapshot());
}
}
@@ -457,16 +460,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private Chunk onChunkFutureFailure(int x, int z, Throwable throwable) {
try {
Throwable root = throwable;
while (root.getCause() != null) {
root = root.getCause();
}
if (root instanceof TimeoutException) {
onTimeout(x, z);
} else {
IrisLogging.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot());
}
IrisLogging.warn("Failed async pregen chunk load at " + x + "," + z + ". " + metricsSnapshot());
IrisLogging.reportError(throwable);
} catch (Throwable e) {
@@ -505,32 +499,36 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
}
private void onTimeout(int x, int z) {
int streak = timeoutStreak.incrementAndGet();
if (streak % ADAPTIVE_TIMEOUT_STEP == 0) {
private void onSlowRequest(int x, int z) {
if (closing.get()) {
return;
}
int streak = slowRequestStreak.incrementAndGet();
if (streak % ADAPTIVE_SLOW_REQUEST_STEP == 0) {
lowerAdaptiveInFlightLimit();
}
long now = M.ms();
long last = lastTimeoutLogAt.get();
if (now - last < timeoutWarnIntervalMs || !lastTimeoutLogAt.compareAndSet(last, now)) {
suppressedTimeoutLogs.incrementAndGet();
long last = lastSlowRequestLogAt.get();
if (now - last < slowRequestWarnIntervalMs || !lastSlowRequestLogAt.compareAndSet(last, now)) {
suppressedSlowRequestLogs.incrementAndGet();
return;
}
int suppressed = suppressedTimeoutLogs.getAndSet(0);
int suppressed = suppressedSlowRequestLogs.getAndSet(0);
String suppressedText = suppressed <= 0 ? "" : " suppressed=" + suppressed;
IrisLogging.warn("Timed out async pregen chunk load at " + x + "," + z
+ " after " + timeoutSeconds + "s."
IrisLogging.warn("Async pregen chunk load at " + x + "," + z
+ " is still pending after " + slowRequestWarningSeconds + "s."
+ " adaptiveLimit=" + adaptiveInFlightLimit.get()
+ suppressedText + " " + metricsSnapshot());
}
private void onSuccess() {
int streak = timeoutStreak.get();
int streak = slowRequestStreak.get();
if (streak > 0) {
int newStreak = Math.max(0, streak - 2);
timeoutStreak.compareAndSet(streak, newStreak);
slowRequestStreak.compareAndSet(streak, newStreak);
if (newStreak > 0) {
return;
}
@@ -606,14 +604,23 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
static int resolvePaperLikeConcurrencyWorkerThreads(int detectedWorkerPoolThreads, int detectedCpuThreads, int configuredWorldGenThreads) {
int provisionedWorkerThreads = Math.max(1, configuredWorldGenThreads);
if (detectedWorkerPoolThreads > 0) {
return Math.max(detectedWorkerPoolThreads, provisionedWorkerThreads);
return detectedWorkerPoolThreads;
}
int provisionedWorkerThreads = Math.max(1, configuredWorldGenThreads);
return Math.max(provisionedWorkerThreads, detectedCpuThreads);
}
static <T> CompletableFuture<T> observeSlowRequest(CompletableFuture<T> future, Executor executor, Runnable onSlowRequest) {
executor.execute(() -> {
if (!future.isDone()) {
onSlowRequest.run();
}
});
return future;
}
static int computeFoliaRecommendedCap(int workerThreads) {
int normalizedWorkers = Math.max(1, workerThreads);
int recommendedCap = normalizedWorkers * 8;
@@ -742,7 +749,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
+ ", effectiveWorkerThreads=" + effectiveWorkerThreads
+ ", recommendedCap=" + recommendedRuntimeConcurrencyCap
+ ", urgent=" + urgent
+ ", timeout=" + timeoutSeconds + "s");
+ ", slowWarning=" + slowRequestWarningSeconds + "s");
if (workerPoolThreads > 0 && holdsWorkerBoost.compareAndSet(false, true)) {
acquireWorkerThreadBoost();
}
@@ -892,6 +899,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return CompletableFuture.failedFuture(new IllegalStateException("Failed to request async chunk " + x + "," + z + " in world " + world.getName(), failure));
}
private CompletableFuture<Chunk> requestMonitoredChunkAsync(int x, int z) {
return observeSlowRequest(requestChunkAsync(x, z), slowRequestExecutor, () -> onSlowRequest(x, z));
}
@SuppressWarnings("unchecked")
private CompletableFuture<Chunk> invokeChunkFuture(Method method, int x, int z, boolean generate, boolean urgentRequest) throws Throwable {
Object result;
@@ -1016,17 +1027,16 @@ public class AsyncPregenMethod implements PregeneratorMethod {
});
}
private interface Executor {
private interface ChunkRequestExecutor {
void generate(int x, int z, PregenListener listener);
default void shutdown() {}
}
private class FoliaRegionExecutor implements Executor {
private class FoliaRegionExecutor implements ChunkRequestExecutor {
@Override
public void generate(int x, int z, PregenListener listener) {
try {
requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
requestMonitoredChunkAsync(x, z)
.whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable));
return;
} catch (Throwable ignored) {
@@ -1034,8 +1044,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
Runnable regionTask = () -> {
try {
requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
requestMonitoredChunkAsync(x, z)
.whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable));
} catch (Throwable e) {
completeChunk(x, z, listener, null, e);
@@ -1049,16 +1058,14 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
}
private class ServiceExecutor implements Executor {
private class ServiceExecutor implements ChunkRequestExecutor {
private final ExecutorService service = new MultiBurst("Iris Async Pregen");
public void generate(int x, int z, PregenListener listener) {
try {
service.submit(() -> {
try {
Chunk i = requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
.get();
Chunk i = requestMonitoredChunkAsync(x, z).get();
completeChunk(x, z, listener, i, null);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
@@ -1078,12 +1085,11 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
}
private class TicketExecutor implements Executor {
private class TicketExecutor implements ChunkRequestExecutor {
@Override
public void generate(int x, int z, PregenListener listener) {
try {
requestChunkAsync(x, z)
.orTimeout(timeoutSeconds, TimeUnit.SECONDS)
requestMonitoredChunkAsync(x, z)
.whenComplete((chunk, throwable) -> completeChunk(x, z, listener, chunk, throwable));
} catch (Throwable e) {
completeChunk(x, z, listener, null, e);
@@ -31,8 +31,10 @@ public final class EngineMaintenance {
}
public static boolean shouldRun(Engine engine) {
if (engine.isStudio()
&& !IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
if (!shouldRunStudioMaintenance(
engine.isStudio(),
IrisSettings.get().getPerformance().isTrimMantleInStudio(),
engine.isStudio() && MantleHeapPressure.overHighWater())) {
return false;
}
if (GoldenHashEngine.isActive()) {
@@ -43,6 +45,14 @@ public final class EngineMaintenance {
|| !WorldMaintenance.isWorldMaintenanceActive(engine.getWorld().identity());
}
static boolean shouldRunStudioMaintenance(
boolean studio,
boolean trimMantleInStudio,
boolean heapPressure
) {
return !studio || trimMantleInStudio || heapPressure;
}
public static int workerParallelism() {
return IrisSettings.get().getPerformance().getEngineSVC().getParallelism();
}
@@ -51,6 +51,7 @@ public class IrisPackBenchmarking {
.name("PackBenchmarking")
.start(() -> {
IrisLogging.info("Setting up benchmark environment ");
IrisToolbelt.applyPregenPerformanceProfile();
IO.delete(IrisWorldStorage.dimensionRoot("benchmark"));
createBenchmark();
while (!IrisToolbelt.isIrisWorld(benchmarkWorld())) {
@@ -338,9 +338,31 @@ public class IrisToolbelt {
}
public static void applyPregenPerformanceProfile(Engine engine) {
PlatformChunkGenerator generator = resolvePlatformGenerator(engine);
if (generator != null) {
PregenPerformanceProfile.applyToGenerator(generator);
return;
}
PregenPerformanceProfile.apply(engine);
}
private static PlatformChunkGenerator resolvePlatformGenerator(Engine engine) {
if (engine == null) {
return null;
}
World world = BukkitWorldBinding.world(engine.getWorld());
if (world == null) {
return null;
}
if (!(world.getGenerator() instanceof PlatformChunkGenerator generator)) {
throw new IllegalStateException("Live Iris engine is not bound to its platform chunk generator.");
}
if (generator.getEngine() != engine) {
throw new IllegalStateException("Live Iris engine does not match its platform chunk generator runtime.");
}
return generator;
}
public static boolean supportsStrictSerialPregeneration() {
return PaperLib.isPaper();
}
@@ -0,0 +1,52 @@
package art.arcane.iris.engine.data.cache;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.function.Function;
public final class LazyBoundedCache<K, V> {
private final int maximumSize;
private transient LinkedHashMap<K, V> entries;
public LazyBoundedCache(int maximumSize) {
if (maximumSize <= 0) {
throw new IllegalArgumentException("Maximum cache size must be positive");
}
this.maximumSize = maximumSize;
}
public synchronized V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
K requiredKey = Objects.requireNonNull(key, "Cache key");
Function<? super K, ? extends V> requiredMapping = Objects.requireNonNull(mappingFunction, "Cache mapping function");
if (entries != null) {
V cached = entries.get(requiredKey);
if (cached != null) {
return cached;
}
}
V computed = requiredMapping.apply(requiredKey);
if (computed == null) {
return null;
}
if (entries == null) {
entries = new LinkedHashMap<>(maximumSize, 1F, true);
}
entries.put(requiredKey, computed);
if (entries.size() > maximumSize) {
Iterator<K> iterator = entries.keySet().iterator();
iterator.next();
iterator.remove();
}
return computed;
}
synchronized boolean isInitialized() {
return entries != null;
}
synchronized int size() {
return entries == null ? 0 : entries.size();
}
}
@@ -23,6 +23,7 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.cache.LazyBoundedCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.DependsOn;
@@ -39,7 +40,6 @@ import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.context.IrisContext;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -81,9 +81,8 @@ public class IrisBiome extends IrisRegistrant implements IRare {
private final transient AtomicCache<Color> cacheColorDepositLoad = new AtomicCache<>();
private final transient AtomicCache<CNG> childrenCell = new AtomicCache<>();
@Getter(AccessLevel.NONE)
private final transient ConcurrentLinkedHashMap<Long, CNG> biomeGenerators = new ConcurrentLinkedHashMap.Builder<Long, CNG>()
.maximumWeightedCapacity(BIOME_GENERATOR_CACHE_SIZE)
.build();
private final transient LazyBoundedCache<Long, CNG> biomeGenerators =
new LazyBoundedCache<>(BIOME_GENERATOR_CACHE_SIZE);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile SeededBiomeGenerator recentBiomeGenerator;
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.cache.LazyBoundedCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.Required;
@@ -27,7 +28,6 @@ import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.stream.ProceduralStream;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -73,10 +73,8 @@ public class IrisExpressionLoad {
Collections.synchronizedMap(new IdentityHashMap<>());
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient final ConcurrentLinkedHashMap<StandaloneStyleKey, CNG> standaloneStyleCache =
new ConcurrentLinkedHashMap.Builder<StandaloneStyleKey, CNG>()
.maximumWeightedCapacity(STYLE_CACHE_SIZE)
.build();
private transient final LazyBoundedCache<StandaloneStyleKey, CNG> standaloneStyleCache =
new LazyBoundedCache<>(STYLE_CACHE_SIZE);
public double getValue(RNG rng, IrisData data, double x, double z) {
if (engineValue != null) {
@@ -155,10 +153,7 @@ public class IrisExpressionLoad {
private static final class EngineCache {
private final AtomicCache<ProceduralStream<Double>> stream = new AtomicCache<>();
private final AtomicCache<Double> value = new AtomicCache<>();
private final ConcurrentLinkedHashMap<Long, CNG> styles =
new ConcurrentLinkedHashMap.Builder<Long, CNG>()
.maximumWeightedCapacity(STYLE_CACHE_SIZE)
.build();
private final LazyBoundedCache<Long, CNG> styles = new LazyBoundedCache<>(STYLE_CACHE_SIZE);
}
private static final class StandaloneStyleKey {
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.LazyBoundedCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
@@ -30,7 +31,6 @@ import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.noise.ExpressionNoise;
import art.arcane.iris.util.project.noise.ImageNoise;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -54,10 +54,8 @@ public class IrisGeneratorStyle {
private static final ConcurrentHashMap<String, String> ACTIVE_CACHE_KEYS = new ConcurrentHashMap<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ConcurrentLinkedHashMap<GeneratorCacheKey, CNG> generatorCache =
new ConcurrentLinkedHashMap.Builder<GeneratorCacheKey, CNG>()
.maximumWeightedCapacity(GENERATOR_CACHE_SIZE)
.build();
private final transient LazyBoundedCache<GeneratorCacheKey, CNG> generatorCache =
new LazyBoundedCache<>(GENERATOR_CACHE_SIZE);
@Desc("The base noise style. Used when neither expression nor imageMap is set; a failed expression also falls back to this style.")
private NoiseStyle style = NoiseStyle.FLAT;
@@ -22,6 +22,7 @@ import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.cache.LazyBoundedCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.annotations.ArrayType;
@@ -38,7 +39,6 @@ import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import com.google.gson.annotations.SerializedName;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -69,10 +69,8 @@ public class IrisObjectPlacement {
private static final int SURFACE_WARP_CACHE_SIZE = 8;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ConcurrentLinkedHashMap<SurfaceWarpCacheKey, CNG> surfaceWarpCache =
new ConcurrentLinkedHashMap.Builder<SurfaceWarpCacheKey, CNG>()
.maximumWeightedCapacity(SURFACE_WARP_CACHE_SIZE)
.build();
private final transient LazyBoundedCache<SurfaceWarpCacheKey, CNG> surfaceWarpCache =
new LazyBoundedCache<>(SURFACE_WARP_CACHE_SIZE);
@RegistryListResource(IrisObject.class)
@Required
@ArrayType(min = 1, type = String.class)
@@ -18,18 +18,24 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.volmlib.util.math.RNG;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
@Snippet("object-scale")
@Accessors(chain = true)
@NoArgsConstructor
@@ -37,23 +43,16 @@ import lombok.experimental.Accessors;
@Desc("Scale objects")
@Data
public class IrisObjectScale {
private static final int CACHE_INITIAL_CAPACITY = 256;
private static final int CACHE_MAX_WEIGHT = 8192;
private static final ConcurrentLinkedHashMap<CacheKey, KList<IrisObject>> cache
= new ConcurrentLinkedHashMap.Builder<CacheKey, KList<IrisObject>>()
.initialCapacity(CACHE_INITIAL_CAPACITY)
.maximumWeightedCapacity(CACHE_MAX_WEIGHT)
.concurrencyLevel(32)
.build();
private static final long CACHE_MIN_ESTIMATED_BYTES = 16L * 1024L * 1024L;
private static final long CACHE_MAX_ESTIMATED_BYTES = 64L * 1024L * 1024L;
private static final long CACHE_ENTRY_ESTIMATED_BYTES = 512L;
private static final long CACHE_VARIANT_ESTIMATED_BYTES = 256L;
private static final long CACHE_ESTIMATED_BYTES_PER_VOXEL = 128L;
private static final int CACHE_LOAD_LOCK_COUNT = 256;
private static final ScaleCache CACHE = new ScaleCache(resolveCacheMaximumEstimatedBytes());
/**
* Coarse flush wired into IrisData dump/hotload: keys hold strong IrisObject references
* (and through them the owning IrisData), so without this every hotload or world unload
* pinned the previous pack graph for the process lifetime. Entries are pure derived data,
* so a flushed still-live entry just recomputes.
*/
public static void invalidate() {
cache.clear();
public static void invalidate(IrisData owner) {
CACHE.invalidate(owner);
}
@MinNumber(0.01)
@@ -104,37 +103,315 @@ public class IrisObjectScale {
if (origin == null) {
return null;
}
if (!shouldScale()) {
ScaleRequest request = snapshotRequest();
if (!request.shouldScale()) {
return origin;
}
CacheKey key = new CacheKey(origin, size, minimumScale, maximumScale, variations, interpolation);
return cache.computeIfAbsent(key, (k) -> {
KList<IrisObject> c = new KList<>();
int variantIndex = request.selectVariant(rng);
CacheKey key = CACHE.key(origin, request, variantIndex);
CacheLookup lookup = CACHE.lookup(key);
if (lookup.variant() != null) {
return lookup.variant();
}
if (size != 1) {
c.add(origin.scaled(size, interpolation));
return c;
synchronized (CACHE.loadLock(key)) {
CacheLookup afterWait = CACHE.lookup(key);
if (afterWait.variant() != null) {
return afterWait.variant();
}
if (minimumScale == maximumScale) {
c.add(origin.scaled(minimumScale, interpolation));
return c;
}
int vs = Math.max(1, Math.min(variations, 32));
double step = (maximumScale - minimumScale) / (double) vs;
for (int v = 0; v < vs; v++) {
c.add(origin.scaled(minimumScale + step * v, interpolation));
}
return c;
}).getRandom(rng);
IrisObject variant = origin.scaled(request.scaleAt(variantIndex), request.interpolation());
long estimatedBytes = estimateVariant(variant);
return CACHE.putIfCurrent(key, variant, estimatedBytes, lookup.generation());
}
}
public boolean canScaleBeyond() {
return shouldScale() && getMaxScale() > 1;
}
private record CacheKey(IrisObject origin, double size, double minimumScale, double maximumScale, int variations, IrisObjectPlacementScaleInterpolator interpolation) {
static int cacheEntryCount() {
return CACHE.size();
}
static long cacheEstimatedBytes() {
return CACHE.estimatedBytes();
}
static boolean isOriginCached(IrisObject origin) {
return CACHE.containsOrigin(origin);
}
private ScaleRequest snapshotRequest() {
IrisObjectPlacementScaleInterpolator configuredInterpolation = interpolation == null
? IrisObjectPlacementScaleInterpolator.NONE
: interpolation;
return new ScaleRequest(size, minimumScale, maximumScale, variations, configuredInterpolation);
}
private static long estimateVariant(IrisObject variant) {
long width = Math.max(1L, variant.getW());
long height = Math.max(1L, variant.getH());
long depth = Math.max(1L, variant.getD());
long volume = saturatedMultiply(saturatedMultiply(width, height), depth);
long populatedVoxels = saturatedAdd(variant.getBlocks().size(), variant.getStates().size());
long voxelBytes = saturatedMultiply(Math.max(volume, populatedVoxels), CACHE_ESTIMATED_BYTES_PER_VOXEL);
return saturatedAdd(CACHE_ENTRY_ESTIMATED_BYTES,
saturatedAdd(CACHE_VARIANT_ESTIMATED_BYTES, voxelBytes));
}
private static long saturatedAdd(long first, long second) {
if (first >= Long.MAX_VALUE - second) {
return Long.MAX_VALUE;
}
return first + second;
}
private static long saturatedMultiply(long first, long second) {
if (first == 0L || second == 0L) {
return 0L;
}
if (first > Long.MAX_VALUE / second) {
return Long.MAX_VALUE;
}
return first * second;
}
private static long resolveCacheMaximumEstimatedBytes() {
long heapShare = Runtime.getRuntime().maxMemory() / 32L;
return Math.max(CACHE_MIN_ESTIMATED_BYTES, Math.min(CACHE_MAX_ESTIMATED_BYTES, heapShare));
}
record ScaleRequest(double size, double minimumScale, double maximumScale, int variations,
IrisObjectPlacementScaleInterpolator interpolation) {
boolean shouldScale() {
if (size != 1D) {
return true;
}
return variations > 0 && (minimumScale != 1D || maximumScale != 1D);
}
int variantCount() {
if (size != 1D || minimumScale == maximumScale) {
return 1;
}
return Math.max(1, Math.min(variations, 32));
}
int selectVariant(RNG rng) {
int count = variantCount();
return count == 1 ? 0 : rng.nextInt(count);
}
double scaleAt(int index) {
if (size != 1D) {
return size;
}
if (minimumScale == maximumScale) {
return minimumScale;
}
return minimumScale + (((maximumScale - minimumScale) / variantCount()) * index);
}
}
static final class CacheKey extends WeakReference<IrisObject> {
private final IrisData owner;
private final ScaleRequest request;
private final int variantIndex;
private final int originRevision;
private final int loadHashCode;
private final int hashCode;
CacheKey(IrisObject origin, ScaleRequest request, int variantIndex) {
this(origin, request, variantIndex, null);
}
CacheKey(IrisObject origin, ScaleRequest request, int variantIndex,
ReferenceQueue<IrisObject> collectedOrigins) {
super(origin, collectedOrigins);
this.owner = origin.getLoader();
this.request = request;
this.variantIndex = variantIndex;
this.originRevision = originRevision(origin);
int result = System.identityHashCode(origin);
result = (31 * result) + System.identityHashCode(owner);
result = (31 * result) + originRevision;
result = (31 * result) + request.hashCode();
this.loadHashCode = result;
this.hashCode = (31 * result) + variantIndex;
}
boolean belongsTo(IrisData candidate) {
return owner == candidate;
}
boolean hasOrigin(IrisObject candidate) {
return get() == candidate;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CacheKey other)) {
return false;
}
IrisObject origin = get();
return origin != null
&& origin == other.get()
&& owner == other.owner
&& originRevision == other.originRevision
&& request.equals(other.request)
&& variantIndex == other.variantIndex;
}
@Override
public int hashCode() {
return hashCode;
}
int loadHashCode() {
return loadHashCode;
}
private static int originRevision(IrisObject origin) {
origin.readLock.lock();
try {
int result = origin.getW();
result = (31 * result) + origin.getH();
result = (31 * result) + origin.getD();
result = (31 * result) + System.identityHashCode(origin.getBlocks());
result = (31 * result) + System.identityHashCode(origin.getStates());
result = (31 * result) + Long.hashCode(origin.getBlocks().modificationRevision());
result = (31 * result) + Long.hashCode(origin.getStates().modificationRevision());
Vector3i center = origin.getCenter();
if (center != null) {
result = (31 * result) + center.getX();
result = (31 * result) + center.getY();
result = (31 * result) + center.getZ();
}
return result;
} finally {
origin.readLock.unlock();
}
}
}
record CacheLookup(IrisObject variant, long generation) {
}
static final class ScaleCache {
private final long maximumEstimatedBytes;
private final LinkedHashMap<CacheKey, CacheValue> entries;
private final Object[] loadLocks;
private final ReferenceQueue<IrisObject> collectedOrigins;
private long estimatedBytes;
private long generation;
ScaleCache(long maximumEstimatedBytes) {
if (maximumEstimatedBytes <= 0L) {
throw new IllegalArgumentException("maximumEstimatedBytes must be positive");
}
this.maximumEstimatedBytes = maximumEstimatedBytes;
this.entries = new LinkedHashMap<>(256, 0.75F, true);
this.loadLocks = new Object[CACHE_LOAD_LOCK_COUNT];
this.collectedOrigins = new ReferenceQueue<>();
for (int index = 0; index < loadLocks.length; index++) {
loadLocks[index] = new Object();
}
}
CacheKey key(IrisObject origin, ScaleRequest request, int variantIndex) {
return new CacheKey(origin, request, variantIndex, collectedOrigins);
}
Object loadLock(CacheKey key) {
return loadLocks[key.loadHashCode() & (loadLocks.length - 1)];
}
synchronized CacheLookup lookup(CacheKey key) {
drainCollectedOrigins();
CacheValue value = entries.get(key);
return new CacheLookup(value == null ? null : value.variant(), generation);
}
synchronized IrisObject putIfCurrent(CacheKey key, IrisObject variant,
long entryEstimatedBytes, long expectedGeneration) {
drainCollectedOrigins();
CacheValue present = entries.get(key);
if (present != null) {
return present.variant();
}
if (generation != expectedGeneration || entryEstimatedBytes > maximumEstimatedBytes) {
return variant;
}
entries.put(key, new CacheValue(variant, entryEstimatedBytes));
estimatedBytes += entryEstimatedBytes;
evictToBudget();
return variant;
}
synchronized void invalidate(IrisData owner) {
drainCollectedOrigins();
generation++;
Iterator<Map.Entry<CacheKey, CacheValue>> iterator = entries.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<CacheKey, CacheValue> entry = iterator.next();
if (entry.getKey().belongsTo(owner)) {
estimatedBytes -= entry.getValue().estimatedBytes();
iterator.remove();
}
}
}
synchronized int size() {
drainCollectedOrigins();
return entries.size();
}
synchronized long estimatedBytes() {
drainCollectedOrigins();
return estimatedBytes;
}
long maximumEstimatedBytes() {
return maximumEstimatedBytes;
}
synchronized boolean containsOrigin(IrisObject origin) {
drainCollectedOrigins();
for (CacheKey key : entries.keySet()) {
if (key.hasOrigin(origin)) {
return true;
}
}
return false;
}
private void evictToBudget() {
Iterator<Map.Entry<CacheKey, CacheValue>> iterator = entries.entrySet().iterator();
while (estimatedBytes > maximumEstimatedBytes && iterator.hasNext()) {
Map.Entry<CacheKey, CacheValue> entry = iterator.next();
estimatedBytes -= entry.getValue().estimatedBytes();
iterator.remove();
}
}
private void drainCollectedOrigins() {
CacheKey collected;
while ((collected = (CacheKey) collectedOrigins.poll()) != null) {
CacheValue removed = entries.remove(collected);
if (removed != null) {
estimatedBytes -= removed.estimatedBytes();
}
}
}
}
record CacheValue(IrisObject variant, long estimatedBytes) {
}
}
@@ -85,9 +85,12 @@ import java.io.File;
import java.io.PrintWriter;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -524,6 +527,15 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
withExclusiveControl(() -> getEngine().hotload());
}
@Override
public CompletableFuture<Void> hotloadComplexAsync(long acquisitionTimeout, TimeUnit unit) {
Engine activeEngine = getEngine();
if (activeEngine == null) {
return CompletableFuture.completedFuture(null);
}
return withExclusiveControlFuture(activeEngine::hotloadComplex, acquisitionTimeout, unit);
}
static boolean shouldRunStudioHotload(boolean studio, boolean closing, boolean jigsawStudioActive) {
return studio && !closing && !jigsawStudioActive;
}
@@ -634,6 +646,21 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
return future;
}
CompletableFuture<Void> withExclusiveControlFuture(
Runnable operation,
long acquisitionTimeout,
TimeUnit unit
) {
CompletableFuture<Void> future = new CompletableFuture<>();
J.a(() -> completeExclusiveControlFuture(
loadLock,
operation,
future,
acquisitionTimeout,
unit));
return future;
}
static void completeExclusiveControlFuture(
GenerationStageGate gate,
Runnable operation,
@@ -666,6 +693,55 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
}
static void completeExclusiveControlFuture(
GenerationStageGate gate,
Runnable operation,
CompletableFuture<Void> future,
long acquisitionTimeout,
TimeUnit unit
) {
GenerationStageGate activeGate = Objects.requireNonNull(gate, "Exclusive control gate");
Runnable activeOperation = Objects.requireNonNull(operation, "Exclusive control operation");
CompletableFuture<Void> outward = Objects.requireNonNull(future, "Exclusive control future");
TimeUnit activeUnit = Objects.requireNonNull(unit, "Exclusive control timeout unit");
boolean acquired = false;
Throwable failure = null;
try {
acquired = activeGate.tryAcquireExclusive(
acquisitionTimeout,
activeUnit,
outward::isCancelled);
if (!acquired) {
if (!outward.isCancelled()) {
failure = new TimeoutException("Timed out waiting for exclusive Iris generation control after "
+ acquisitionTimeout + " " + activeUnit.name().toLowerCase(Locale.ROOT) + ".");
}
} else if (!outward.isCancelled()) {
activeOperation.run();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (!outward.isCancelled()) {
failure = e;
}
} catch (Throwable e) {
failure = e;
} finally {
if (acquired) {
activeGate.releaseExclusive();
}
}
if (outward.isCancelled()) {
return;
}
if (failure == null) {
outward.complete(null);
} else {
outward.completeExceptionally(failure);
}
}
public void touch(World world) {
getEngine(world);
}
@@ -935,6 +1011,32 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
permits.acquire(permitCount);
}
boolean tryAcquireExclusive(
long timeout,
TimeUnit unit,
BooleanSupplier cancelled
) throws InterruptedException {
if (timeout <= 0L) {
throw new IllegalArgumentException("Exclusive generation control timeout must be positive.");
}
TimeUnit activeUnit = Objects.requireNonNull(unit, "Exclusive generation control timeout unit");
BooleanSupplier cancellation = Objects.requireNonNull(cancelled, "Exclusive generation control cancellation");
long timeoutNanos = activeUnit.toNanos(timeout);
long started = System.nanoTime();
long remainingNanos = timeoutNanos;
long cancellationPollNanos = TimeUnit.MILLISECONDS.toNanos(50L);
while (!cancellation.getAsBoolean() && remainingNanos > 0L) {
if (permits.tryAcquire(
permitCount,
Math.min(remainingNanos, cancellationPollNanos),
TimeUnit.NANOSECONDS)) {
return true;
}
remainingNanos = timeoutNanos - (System.nanoTime() - started);
}
return false;
}
void releaseExclusive() {
permits.release(permitCount);
}
@@ -26,7 +26,9 @@ import art.arcane.iris.util.common.data.DataProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
@Nullable
@@ -47,6 +49,23 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
return CompletableFuture.completedFuture(null);
}
default CompletableFuture<Void> hotloadComplexAsync(long acquisitionTimeout, TimeUnit unit) {
if (acquisitionTimeout <= 0L) {
throw new IllegalArgumentException("Complex hotload acquisition timeout must be positive.");
}
Objects.requireNonNull(unit, "Complex hotload acquisition timeout unit");
Engine activeEngine = getEngine();
if (activeEngine == null) {
return CompletableFuture.completedFuture(null);
}
try {
activeEngine.hotloadComplex();
return CompletableFuture.completedFuture(null);
} catch (Throwable failure) {
return CompletableFuture.failedFuture(failure);
}
}
boolean isStudio();
default boolean isClosing() {
@@ -14,6 +14,11 @@ import java.util.function.Function;
public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
private final Map<Key, Map<Key, T>> map = new KMap<>();
private transient volatile long modificationRevision;
public long modificationRevision() {
return modificationRevision;
}
public int size() {
return map.values().stream().mapToInt(Map::size).sum();
@@ -40,13 +45,23 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
}
public @Nullable T put(@NonNull IrisBlockVector vector, @NonNull T value) {
return map.computeIfAbsent(chunk(vector), k -> new KMap<>())
T previous = map.computeIfAbsent(chunk(vector), k -> new KMap<>())
.put(relative(vector), value);
modificationRevision++;
return previous;
}
public @Nullable T computeIfAbsent(@NonNull IrisBlockVector vector, @NonNull Function<@NonNull IrisBlockVector, @NonNull T> mappingFunction) {
return map.computeIfAbsent(chunk(vector), k -> new KMap<>())
.computeIfAbsent(relative(vector), $ -> mappingFunction.apply(vector));
boolean[] inserted = new boolean[1];
T value = map.computeIfAbsent(chunk(vector), key -> new KMap<>())
.computeIfAbsent(relative(vector), key -> {
inserted[0] = true;
return mappingFunction.apply(vector);
});
if (inserted[0]) {
modificationRevision++;
}
return value;
}
@SuppressWarnings("unchecked")
@@ -61,6 +76,10 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
return chunk.isEmpty() ? null : chunk;
});
if (removed[0] != null) {
modificationRevision++;
}
return (T) removed[0];
}
@@ -69,6 +88,9 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
}
public void clear() {
if (!map.isEmpty()) {
modificationRevision++;
}
map.clear();
}
@@ -193,6 +215,7 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
public void remove() {
if (relativeIterator == null) throw new IllegalStateException("No element to remove");
relativeIterator.remove();
modificationRevision++;
}
}
@@ -230,6 +253,7 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
public void remove() {
if (relativeIterator == null) throw new IllegalStateException("No element to remove");
relativeIterator.remove();
modificationRevision++;
}
@Override
@@ -267,6 +291,7 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
public void remove() {
if (relativeIterator == null) throw new IllegalStateException("No element to remove");
relativeIterator.remove();
modificationRevision++;
}
@Override
@@ -31,12 +31,14 @@ public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStrea
private final ProceduralStream<T> stream;
private final WorldCache2D<T> cache;
private final Engine engine;
private final boolean fastCache;
private final boolean chunked = true;
public CachedStream2D(String name, Engine engine, ProceduralStream<T> stream, int size) {
super();
this.stream = stream;
this.engine = engine;
this.fastCache = Boolean.getBoolean("iris.cache.fast");
cache = WorldCache2D.<T>ofInts(stream::get, size, () -> new ChunkCache2D<>("iris"));
IrisServices.get(PreservationRegistry.class).registerCache(this);
}
@@ -76,6 +78,10 @@ public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStrea
return cache.getMaxSize();
}
public boolean usesFastCache() {
return fastCache;
}
@Override
public boolean isClosed() {
return engine.isClosed();