mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
stabilize pregeneration
This commit is contained in:
@@ -121,6 +121,9 @@ dependencies {
|
||||
|
||||
tasks.named('test').configure {
|
||||
maxHeapSize = '1g'
|
||||
systemProperty('iris.packBenchmarkingSource', file('src/main/java/art/arcane/iris/core/tools/IrisPackBenchmarking.java').absolutePath)
|
||||
systemProperty('iris.irisToolbeltSource', file('src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java').absolutePath)
|
||||
systemProperty('iris.bukkitChunkGeneratorSource', file('src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java').absolutePath)
|
||||
}
|
||||
|
||||
java {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
+54
-48
@@ -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();
|
||||
|
||||
@@ -289,6 +289,90 @@ public class WorldReplacementBootstrapTest {
|
||||
assertTrue(published.stream().allMatch(transaction -> transaction.phase() == Phase.PUBLISHED));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publishesPreparedOverworldAndUnderworldIntoExactVanillaSlotsInOneColdReconcile()
|
||||
throws Exception {
|
||||
long overworldSeed = 81818181L;
|
||||
WorldSlotKey overworldKey = WorldSlotKey.minecraft("overworld");
|
||||
ExactWorldSlotPathPolicy.Target overworldTarget = ExactWorldSlotPathPolicy.resolve(levelRoot, overworldKey);
|
||||
WorldGeneratorSnapshot overworldOriginal = BukkitWorldConfiguration.snapshot(
|
||||
bukkitConfiguration.toFile(),
|
||||
"world"
|
||||
);
|
||||
UUID overworldId = UUID.randomUUID();
|
||||
ReplacementPaths overworldPaths = WorldReplacementFilesystem.paths(overworldTarget, overworldId);
|
||||
writeOriginalTarget(overworldPaths, "overworld-original");
|
||||
Path overworldDimension = overworldPaths.stage().resolve("iris/pack/dimensions/overworld.json");
|
||||
Files.createDirectories(overworldDimension.getParent());
|
||||
Files.writeString(overworldDimension, "overworld-replacement");
|
||||
String overworldFingerprint = WorldReplacementFilesystem.fingerprintPack(
|
||||
overworldPaths.stage().resolve("iris/pack")
|
||||
);
|
||||
Transaction overworld = new Transaction(
|
||||
overworldId,
|
||||
overworldKey,
|
||||
"world",
|
||||
overworldTarget.levelRoot(),
|
||||
"overworld",
|
||||
overworldSeed,
|
||||
overworldFingerprint,
|
||||
overworldOriginal,
|
||||
true,
|
||||
Phase.ARMED
|
||||
);
|
||||
WorldReplacementJournal.write(dataDirectory, overworld);
|
||||
configureReplacement(overworld);
|
||||
|
||||
Transaction nether = stagedTransaction(Phase.ARMED, true, "nether-original");
|
||||
configureReplacement(nether);
|
||||
|
||||
WorldReplacementBootstrap.ReconcileResult result = reconcile();
|
||||
|
||||
assertEquals(2, result.transactions());
|
||||
assertEquals(2, result.published());
|
||||
assertEquals(0, result.rolledBack());
|
||||
assertEquals(0, result.retained());
|
||||
assertEquals(0, result.skipped());
|
||||
assertEquals(
|
||||
levelRoot.toRealPath().resolve("dimensions/minecraft/overworld"),
|
||||
overworldTarget.worldDirectory()
|
||||
);
|
||||
assertEquals(
|
||||
levelRoot.toRealPath().resolve("dimensions/minecraft/the_nether"),
|
||||
target.worldDirectory()
|
||||
);
|
||||
assertEquals("overworld-replacement", Files.readString(
|
||||
overworldTarget.worldDirectory().resolve("iris/pack/dimensions/overworld.json")
|
||||
));
|
||||
assertEquals("replacement", replacementContent(target.worldDirectory()));
|
||||
assertEquals("overworld-original", Files.readString(overworldPaths.backup().resolve("original.txt")));
|
||||
assertEquals("nether-original", Files.readString(backup(nether).resolve("original.txt")));
|
||||
|
||||
WorldGeneratorSnapshot overworldConfiguration = BukkitWorldConfiguration.snapshot(
|
||||
bukkitConfiguration.toFile(),
|
||||
"world"
|
||||
);
|
||||
WorldGeneratorSnapshot netherConfiguration = BukkitWorldConfiguration.snapshot(
|
||||
bukkitConfiguration.toFile(),
|
||||
"world_nether"
|
||||
);
|
||||
assertEquals("Iris:overworld", overworldConfiguration.generator());
|
||||
assertEquals(Long.valueOf(overworldSeed), overworldConfiguration.seed());
|
||||
assertEquals("Iris:underworld", netherConfiguration.generator());
|
||||
assertEquals(Long.valueOf(SEED), netherConfiguration.seed());
|
||||
|
||||
List<Transaction> published = WorldReplacementJournal.load(dataDirectory, levelRoot);
|
||||
assertEquals(2, published.size());
|
||||
assertTrue(published.stream().anyMatch(transaction ->
|
||||
transaction.worldKey().equals(overworldKey)
|
||||
&& transaction.dimension().equals("overworld")
|
||||
&& transaction.phase() == Phase.PUBLISHED));
|
||||
assertTrue(published.stream().anyMatch(transaction ->
|
||||
transaction.worldKey().equals(WORLD_KEY)
|
||||
&& transaction.dimension().equals("underworld")
|
||||
&& transaction.phase() == Phase.PUBLISHED));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripsBlankAndWhitespaceOriginalGenerators() throws Exception {
|
||||
for (String generator : List.of("", " ")) {
|
||||
|
||||
+39
-1
@@ -322,8 +322,9 @@ public class WorldReplacementFilesystemTest {
|
||||
public void rejectsSymlinkInsidePackAndNonDirectoryArtifacts() throws Exception {
|
||||
WorldReplacementFilesystem.ReplacementPaths linkedPaths = paths("inside-pack-link", TRANSACTION_ID);
|
||||
Path pack = Files.createDirectories(linkedPaths.stage().resolve("iris/pack"));
|
||||
Path nestedObjects = Files.createDirectories(pack.resolve("objects/oak"));
|
||||
Path outside = temporaryFolder.newFile("outside-pack.txt").toPath();
|
||||
Files.createSymbolicLink(pack.resolve("linked.json"), outside);
|
||||
Files.createSymbolicLink(nestedObjects.resolve("linked.json"), outside);
|
||||
|
||||
assertThrows(IOException.class, () -> WorldReplacementFilesystem.fingerprintPack(pack));
|
||||
|
||||
@@ -335,6 +336,20 @@ public class WorldReplacementFilesystemTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fingerprintRemainsCompatibleAcrossNestedCreationOrder() throws Exception {
|
||||
Path firstPack = temporaryFolder.newFolder("fingerprint-order-first").toPath();
|
||||
Path secondPack = temporaryFolder.newFolder("fingerprint-order-second").toPath();
|
||||
writeFingerprintFixture(firstPack, false);
|
||||
writeFingerprintFixture(secondPack, true);
|
||||
|
||||
String firstFingerprint = WorldReplacementFilesystem.fingerprintPack(firstPack);
|
||||
String secondFingerprint = WorldReplacementFilesystem.fingerprintPack(secondPack);
|
||||
|
||||
assertEquals("215206c4a5e74c7731fbd8d2da9265332bfb06a0f74b32e47d02dab3dd878b4c", firstFingerprint);
|
||||
assertEquals(firstFingerprint, secondFingerprint);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoresGeneratedAuthoringMetadataWithoutIgnoringNestedPackContent() throws Exception {
|
||||
Path pack = temporaryFolder.newFolder("generated-metadata").toPath();
|
||||
@@ -448,6 +463,29 @@ public class WorldReplacementFilesystemTest {
|
||||
Files.writeString(paths.target().resolve("data/minecraft/world_gen_settings.dat"), "generation");
|
||||
}
|
||||
|
||||
private void writeFingerprintFixture(Path pack, boolean reverseOrder) throws Exception {
|
||||
if (reverseOrder) {
|
||||
Files.createDirectories(pack.resolve("regions"));
|
||||
Files.writeString(pack.resolve("regions/default.json"), "{}\n");
|
||||
Files.createDirectories(pack.resolve("empty"));
|
||||
Files.createDirectories(pack.resolve("objects/oak"));
|
||||
Files.write(pack.resolve("objects/oak/tree.iob"), new byte[]{0, 1, 2, (byte) 255});
|
||||
Files.createDirectories(pack.resolve("dimensions"));
|
||||
Files.writeString(pack.resolve("dimensions/overworld.json"), "{\"name\":\"world\"}");
|
||||
} else {
|
||||
Files.createDirectories(pack.resolve("dimensions"));
|
||||
Files.writeString(pack.resolve("dimensions/overworld.json"), "{\"name\":\"world\"}");
|
||||
Files.createDirectories(pack.resolve("objects/oak"));
|
||||
Files.write(pack.resolve("objects/oak/tree.iob"), new byte[]{0, 1, 2, (byte) 255});
|
||||
Files.createDirectories(pack.resolve("empty"));
|
||||
Files.createDirectories(pack.resolve("regions"));
|
||||
Files.writeString(pack.resolve("regions/default.json"), "{}\n");
|
||||
}
|
||||
Files.createDirectories(pack.resolve(".iris/schema"));
|
||||
Files.writeString(pack.resolve(".iris/schema/dimensions-schema.json"), "generated");
|
||||
Files.writeString(pack.resolve("editor.code-workspace"), "generated");
|
||||
}
|
||||
|
||||
private String readPackContent(Path worldDirectory) throws Exception {
|
||||
return Files.readString(packContent(worldDirectory));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package art.arcane.iris.core.loader;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.SeedManager;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.Answers;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ResourceLoaderPrefetchTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private IrisPlatform previousPlatform;
|
||||
private IrisSettings previousSettings;
|
||||
private File platformRoot;
|
||||
private File packRoot;
|
||||
private IrisData manager;
|
||||
private Engine engine;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
previousSettings = IrisSettings.settings;
|
||||
IrisPlatforms.unbind();
|
||||
platformRoot = temporaryFolder.newFolder("platform");
|
||||
packRoot = temporaryFolder.newFolder("pack");
|
||||
IrisPlatform platform = mock(IrisPlatform.class, Answers.CALLS_REAL_METHODS);
|
||||
when(platform.dataFolder()).thenReturn(platformRoot);
|
||||
when(platform.dataFile(any(String[].class))).thenAnswer(invocation -> {
|
||||
File file = platformRoot;
|
||||
for (Object segment : invocation.getArguments()) {
|
||||
file = new File(file, String.valueOf(segment));
|
||||
}
|
||||
return file;
|
||||
});
|
||||
IrisPlatforms.bind(platform);
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
|
||||
manager = mock(IrisData.class);
|
||||
when(manager.getId()).thenReturn(7);
|
||||
when(manager.getDataFolder()).thenReturn(packRoot);
|
||||
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
dimension.setLoadKey("overworld");
|
||||
dimension.setVersion(9);
|
||||
engine = mock(Engine.class);
|
||||
when(engine.getSeedManager()).thenReturn(new SeedManager(42L));
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
IrisSettings.settings = previousSettings;
|
||||
IrisPlatforms.unbind();
|
||||
if (previousPlatform != null) {
|
||||
IrisPlatforms.bind(previousPlatform);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oversizedHistoricalSetIsNotAdmittedOnReopen() throws Exception {
|
||||
CountingLoader writer = loader(32);
|
||||
KSet<String> history = new KSet<>();
|
||||
for (int index = 0; index < 4_096; index++) {
|
||||
history.add("resource-" + index);
|
||||
}
|
||||
writer.setFirstAccess(history);
|
||||
|
||||
writer.saveFirstAccess(engine);
|
||||
CountingLoader reader = loader(32);
|
||||
reader.loadFirstAccess(engine);
|
||||
|
||||
assertTrue(reader.loadedKeys().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liveHistoryStopsGrowingAtTheAdmissionLimit() {
|
||||
CountingLoader loader = loader(4_096);
|
||||
|
||||
for (int index = 0; index < 4_096; index++) {
|
||||
loader.load("resource-" + index, false);
|
||||
}
|
||||
|
||||
assertEquals(1_024, loader.getFirstAccess().size());
|
||||
assertTrue(loader.isFirstAccessOverflowed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void admittedHistoryLoadsWithoutPerEntryTaskFanout() throws Exception {
|
||||
CountingLoader writer = loader(32);
|
||||
KSet<String> history = new KSet<>();
|
||||
for (int index = 0; index < 32; index++) {
|
||||
history.add("resource-" + index);
|
||||
}
|
||||
writer.setFirstAccess(history);
|
||||
writer.saveFirstAccess(engine);
|
||||
|
||||
CountingLoader reader = loader(32);
|
||||
String loadingThread = Thread.currentThread().getName();
|
||||
reader.loadFirstAccess(engine);
|
||||
|
||||
assertEquals(32, reader.loadedKeys().size());
|
||||
assertEquals(Set.of(loadingThread), reader.loadingThreads());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dataManagerLoadsHistoriesOnTheCallingThread() throws Exception {
|
||||
CountingLoader writer = loader(32);
|
||||
writer.setFirstAccess(new KSet<>("resource"));
|
||||
writer.saveFirstAccess(engine);
|
||||
CountingLoader reader = loader(32);
|
||||
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
|
||||
loaders.put(TestRegistrant.class, reader);
|
||||
IrisData data = mock(IrisData.class, Answers.CALLS_REAL_METHODS);
|
||||
data.setLoaders(loaders);
|
||||
String loadingThread = Thread.currentThread().getName();
|
||||
|
||||
data.loadPrefetch(engine);
|
||||
|
||||
assertEquals(List.of("resource"), reader.loadedKeys());
|
||||
assertEquals(Set.of(loadingThread), reader.loadingThreads());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyIdentityIsNotRead() throws Exception {
|
||||
String identity = "DIM" + Math.abs(42L + 9L + "overworld".hashCode());
|
||||
File legacy = new File(platformRoot,
|
||||
"prefetch/" + identity + "/" + Math.abs("test-resources".hashCode()) + ".ipfch");
|
||||
assertTrue(legacy.getParentFile().mkdirs());
|
||||
try (FileOutputStream output = new FileOutputStream(legacy);
|
||||
GZIPOutputStream gzip = new GZIPOutputStream(output);
|
||||
DataOutputStream data = new DataOutputStream(gzip)) {
|
||||
data.writeInt(1);
|
||||
data.writeUTF("legacy-resource");
|
||||
}
|
||||
|
||||
CountingLoader reader = loader(32);
|
||||
reader.loadFirstAccess(engine);
|
||||
|
||||
assertTrue(reader.loadedKeys().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalDimensionIdentitiesDoNotCrossPackRoots() throws Exception {
|
||||
CountingLoader writer = loader(32);
|
||||
writer.setFirstAccess(new KSet<>("source-only"));
|
||||
writer.saveFirstAccess(engine);
|
||||
File otherRoot = temporaryFolder.newFolder("other-pack");
|
||||
IrisData otherManager = mock(IrisData.class);
|
||||
when(otherManager.getId()).thenReturn(8);
|
||||
when(otherManager.getDataFolder()).thenReturn(otherRoot);
|
||||
|
||||
CountingLoader reader = loader(otherRoot, otherManager, 32);
|
||||
reader.loadFirstAccess(engine);
|
||||
|
||||
assertTrue(reader.loadedKeys().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void binaryObjectLoaderDoesNotContributeToJsonPrefetchHistory() {
|
||||
ObjectResourceLoader loader = new ObjectResourceLoader(
|
||||
packRoot,
|
||||
manager,
|
||||
"objects",
|
||||
"Object",
|
||||
ResourceLoader.Options.datapackCompiler());
|
||||
|
||||
IrisObject object = loader.load("missing", false);
|
||||
|
||||
assertNull(object);
|
||||
assertTrue(loader.getFirstAccess().isEmpty());
|
||||
}
|
||||
|
||||
private CountingLoader loader(int cacheSize) {
|
||||
return loader(packRoot, manager, cacheSize);
|
||||
}
|
||||
|
||||
private CountingLoader loader(File root, IrisData data, int cacheSize) {
|
||||
return new CountingLoader(
|
||||
root,
|
||||
data,
|
||||
new ResourceLoader.Options(cacheSize, false, true));
|
||||
}
|
||||
|
||||
private static final class CountingLoader extends ResourceLoader<TestRegistrant> {
|
||||
private final List<String> loaded = Collections.synchronizedList(new ArrayList<>());
|
||||
private final Set<String> threads = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private CountingLoader(File root, IrisData manager, Options options) {
|
||||
super(root, manager, "test-resources", "Test Resource", TestRegistrant.class, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TestRegistrant load(String name) {
|
||||
loaded.add(name);
|
||||
threads.add(Thread.currentThread().getName());
|
||||
return new TestRegistrant();
|
||||
}
|
||||
|
||||
private List<String> loadedKeys() {
|
||||
return List.copyOf(loaded);
|
||||
}
|
||||
|
||||
private Set<String> loadingThreads() {
|
||||
return Set.copyOf(threads);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class TestRegistrant extends IrisRegistrant {
|
||||
@Override
|
||||
public String getFolderName() {
|
||||
return "test-resources";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return "Test Resource";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisEnvironment;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
@@ -120,6 +122,69 @@ public class PackDownloaderTest {
|
||||
assertTrue(PackDownloader.isBuiltInPack("underworld"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sequentialBuiltInPackInstallsPublishBothBootstrapResultsBeforeRestart() throws Exception {
|
||||
File packsFolder = IrisPlatforms.get().packsFolder();
|
||||
File overworldSource = writePack(
|
||||
temp.newFolder("shipping-overworld-source").toPath(),
|
||||
"overworld",
|
||||
"overworld",
|
||||
IrisEnvironment.NORMAL
|
||||
);
|
||||
File underworldSource = writePack(
|
||||
temp.newFolder("shipping-underworld-source").toPath(),
|
||||
"underworld",
|
||||
"underworld",
|
||||
IrisEnvironment.NETHER
|
||||
);
|
||||
|
||||
PackDownloader.PackInstallResult overworld = PackDownloader.installExtractedPack(
|
||||
packsFolder,
|
||||
overworldSource,
|
||||
false,
|
||||
"overworld",
|
||||
ignored -> {
|
||||
}
|
||||
);
|
||||
PackDownloader.PackInstallResult underworld = PackDownloader.installExtractedPack(
|
||||
packsFolder,
|
||||
underworldSource,
|
||||
false,
|
||||
"underworld",
|
||||
ignored -> {
|
||||
}
|
||||
);
|
||||
|
||||
assertNotNull(overworld);
|
||||
assertEquals("overworld", overworld.key());
|
||||
assertTrue(overworld.changed());
|
||||
assertTrue(overworld.restartRequired());
|
||||
assertNotNull(underworld);
|
||||
assertEquals("underworld", underworld.key());
|
||||
assertTrue(underworld.changed());
|
||||
assertTrue(underworld.restartRequired());
|
||||
assertTrue(Files.isRegularFile(packsFolder.toPath().resolve("overworld/dimensions/overworld.json")));
|
||||
assertTrue(Files.isRegularFile(packsFolder.toPath().resolve("underworld/dimensions/underworld.json")));
|
||||
assertTrue(PackValidationRegistry.requireLoadable("overworld").isLoadable());
|
||||
assertTrue(PackValidationRegistry.requireLoadable("underworld").isLoadable());
|
||||
IrisData overworldData = IrisData.openDatapackCompiler(new File(packsFolder, "overworld"));
|
||||
IrisData underworldData = IrisData.openDatapackCompiler(new File(packsFolder, "underworld"));
|
||||
try {
|
||||
IrisDimension overworldDimension = overworldData.getDimensionLoader().load("overworld");
|
||||
IrisDimension underworldDimension = underworldData.getDimensionLoader().load("underworld");
|
||||
assertNotNull(overworldDimension);
|
||||
assertEquals("overworld", overworldDimension.getLoadKey());
|
||||
assertEquals(IrisEnvironment.NORMAL, overworldDimension.getEnvironment());
|
||||
assertNotNull(underworldDimension);
|
||||
assertEquals("underworld", underworldDimension.getLoadKey());
|
||||
assertEquals(IrisEnvironment.NETHER, underworldDimension.getEnvironment());
|
||||
} finally {
|
||||
overworldData.close();
|
||||
underworldData.close();
|
||||
}
|
||||
assertTransactionStateClean(packsFolder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recognizesOnlyHttpZipUrlsAsDirectSources() {
|
||||
assertTrue(PackDownloader.isDirectZipUrl("https://packs.example.test/overworld.zip"));
|
||||
@@ -654,6 +719,23 @@ public class PackDownloaderTest {
|
||||
return root.toFile();
|
||||
}
|
||||
|
||||
private static File writePack(
|
||||
Path root,
|
||||
String key,
|
||||
String state,
|
||||
IrisEnvironment environment
|
||||
) throws IOException {
|
||||
File pack = writePack(root, key, state);
|
||||
Files.writeString(
|
||||
root.resolve("dimensions/" + key + ".json"),
|
||||
"{\"name\":\"" + key + "\",\"environment\":\"" + environment.name()
|
||||
+ "\",\"regions\":[\"local\"],\"logicalHeight\":256,"
|
||||
+ "\"dimensionHeight\":{\"min\":-64,\"max\":320}}",
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
return pack;
|
||||
}
|
||||
|
||||
private static void writeDimension(Path root, String key) throws IOException {
|
||||
Files.writeString(
|
||||
root.resolve("dimensions/" + key + ".json"),
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package art.arcane.iris.core.pregenerator;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class PregenPerformanceProfileTest {
|
||||
@Test
|
||||
public void liveGeneratorUsesItsPlatformHotloadBoundary() {
|
||||
IrisSettings previousSettings = IrisSettings.settings;
|
||||
String previousFastCache = System.getProperty("iris.cache.fast");
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
System.clearProperty("iris.cache.fast");
|
||||
Engine engine = engineWithProfile(1_024, false);
|
||||
PlatformChunkGenerator generator = generatorFor(engine);
|
||||
try {
|
||||
PregenPerformanceProfile.applyToGenerator(generator);
|
||||
|
||||
assertEquals(4_096, IrisSettings.get().getPerformance().getNoiseCacheSize());
|
||||
assertTrue(Boolean.getBoolean("iris.cache.fast"));
|
||||
verify(generator).hotloadComplexAsync(30L, TimeUnit.SECONDS);
|
||||
verify(engine, never()).hotloadComplex();
|
||||
} finally {
|
||||
restore(previousSettings, previousFastCache);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eachExistingEngineRefreshesAgainstTheAppliedProfile() {
|
||||
IrisSettings previousSettings = IrisSettings.settings;
|
||||
String previousFastCache = System.getProperty("iris.cache.fast");
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
IrisSettings.get().getPerformance().setNoiseCacheSize(4_096);
|
||||
System.clearProperty("iris.cache.fast");
|
||||
Engine overworldEngine = engineWithProfile(4_096, false);
|
||||
Engine netherEngine = engineWithProfile(4_096, false);
|
||||
PlatformChunkGenerator overworldGenerator = generatorFor(overworldEngine);
|
||||
PlatformChunkGenerator netherGenerator = generatorFor(netherEngine);
|
||||
try {
|
||||
PregenPerformanceProfile.applyToGenerator(overworldGenerator);
|
||||
PregenPerformanceProfile.applyToGenerator(netherGenerator);
|
||||
|
||||
verify(overworldGenerator).hotloadComplexAsync(30L, TimeUnit.SECONDS);
|
||||
verify(netherGenerator).hotloadComplexAsync(30L, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
restore(previousSettings, previousFastCache);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void profileAppliedBeforeWorldCreationAvoidsLiveHotload() {
|
||||
IrisSettings previousSettings = IrisSettings.settings;
|
||||
String previousFastCache = System.getProperty("iris.cache.fast");
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
System.clearProperty("iris.cache.fast");
|
||||
Engine engine = engineWithProfile(4_096, true);
|
||||
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
|
||||
when(generator.getEngine()).thenReturn(engine);
|
||||
try {
|
||||
PregenPerformanceProfile.apply();
|
||||
PregenPerformanceProfile.applyToGenerator(generator);
|
||||
|
||||
verify(generator, never()).hotloadComplexAsync(30L, TimeUnit.SECONDS);
|
||||
verify(engine, never()).hotloadComplex();
|
||||
} finally {
|
||||
restore(previousSettings, previousFastCache);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Engine engineWithProfile(int cacheSize, boolean fastCache) {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
CachedDoubleStream2D heightStream = mock(CachedDoubleStream2D.class);
|
||||
CachedStream2D<IrisBiome> caveBiomeStream = mock(CachedStream2D.class);
|
||||
when(heightStream.getMaxSize()).thenReturn((long) cacheSize * 256L);
|
||||
when(caveBiomeStream.usesFastCache()).thenReturn(fastCache);
|
||||
when(complex.getHeightStream()).thenReturn(heightStream);
|
||||
when(complex.getCaveBiomeStream()).thenReturn(caveBiomeStream);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static PlatformChunkGenerator generatorFor(Engine engine) {
|
||||
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
|
||||
when(generator.getEngine()).thenReturn(engine);
|
||||
when(generator.hotloadComplexAsync(30L, TimeUnit.SECONDS))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
return generator;
|
||||
}
|
||||
|
||||
private static void restore(IrisSettings settings, String fastCache) {
|
||||
IrisSettings.settings = settings;
|
||||
if (fastCache == null) {
|
||||
System.clearProperty("iris.cache.fast");
|
||||
} else {
|
||||
System.setProperty("iris.cache.fast", fastCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-3
@@ -2,7 +2,12 @@ package art.arcane.iris.core.pregenerator.methods;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public class AsyncPregenMethodConcurrencyCapTest {
|
||||
@Test
|
||||
@@ -23,11 +28,13 @@ public class AsyncPregenMethodConcurrencyCapTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void paperLikeConcurrencyProvisionsForWorldGenThreadBump() {
|
||||
assertEquals(32, AsyncPregenMethod.resolvePaperLikeConcurrencyWorkerThreads(4, 16, 32));
|
||||
assertEquals(16, AsyncPregenMethod.resolvePaperLikeConcurrencyWorkerThreads(4, 16, 16));
|
||||
public void paperLikeConcurrencyUsesDetectedWorkerPoolWhenAvailable() {
|
||||
assertEquals(4, AsyncPregenMethod.resolvePaperLikeConcurrencyWorkerThreads(4, 16, 32));
|
||||
assertEquals(4, AsyncPregenMethod.resolvePaperLikeConcurrencyWorkerThreads(4, 16, 16));
|
||||
assertEquals(24, AsyncPregenMethod.resolvePaperLikeConcurrencyWorkerThreads(-1, 16, 24));
|
||||
assertEquals(16, AsyncPregenMethod.resolvePaperLikeConcurrencyWorkerThreads(-1, 16, 8));
|
||||
assertEquals(32, AsyncPregenMethod.computePaperLikeRecommendedCap(
|
||||
AsyncPregenMethod.resolvePaperLikeConcurrencyWorkerThreads(4, 16, 16)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -42,4 +49,32 @@ public class AsyncPregenMethodConcurrencyCapTest {
|
||||
assertEquals(128, AsyncPregenMethod.selectConcurrencyCap(128, false));
|
||||
assertEquals(1, AsyncPregenMethod.selectConcurrencyCap(0, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void slowRequestObservationDoesNotCompleteOrReplacePendingFuture() {
|
||||
CompletableFuture<String> request = new CompletableFuture<>();
|
||||
AtomicInteger slowRequests = new AtomicInteger();
|
||||
|
||||
CompletableFuture<String> observed = AsyncPregenMethod.observeSlowRequest(
|
||||
request,
|
||||
Runnable::run,
|
||||
slowRequests::incrementAndGet);
|
||||
|
||||
assertSame(request, observed);
|
||||
assertFalse(request.isDone());
|
||||
assertEquals(1, slowRequests.get());
|
||||
request.complete("generated");
|
||||
assertEquals("generated", observed.join());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void slowRequestObservationIgnoresCompletedFuture() {
|
||||
CompletableFuture<String> request = CompletableFuture.completedFuture("generated");
|
||||
AtomicInteger slowRequests = new AtomicInteger();
|
||||
|
||||
AsyncPregenMethod.observeSlowRequest(request, Runnable::run, slowRequests::incrementAndGet);
|
||||
|
||||
assertEquals(0, slowRequests.get());
|
||||
assertEquals("generated", request.join());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,14 @@ public class EngineMaintenanceTest {
|
||||
assertFalse(plan.heapPressure());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioDisablesOnlyRoutineMaintenance() {
|
||||
assertFalse(EngineMaintenance.shouldRunStudioMaintenance(true, false, false));
|
||||
assertTrue(EngineMaintenance.shouldRunStudioMaintenance(true, false, true));
|
||||
assertTrue(EngineMaintenance.shouldRunStudioMaintenance(true, true, false));
|
||||
assertTrue(EngineMaintenance.shouldRunStudioMaintenance(false, false, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedMantleClosedFailureIsRecognized() {
|
||||
IllegalStateException cause = new IllegalStateException("Mantle is closed");
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package art.arcane.iris.core.tools;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisPackBenchmarkingLifecycleContractTest {
|
||||
@Test
|
||||
public void benchmarkAppliesProfileBeforeCreatingItsWorld() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.packBenchmarkingSource")));
|
||||
String runBenchmark = method(source, "private void runBenchmark()");
|
||||
|
||||
assertBefore(runBenchmark, "IrisToolbelt.applyPregenPerformanceProfile()", "createBenchmark()");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liveEngineProfileUsesTheBoundPlatformGenerator() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.irisToolbeltSource")));
|
||||
String applyProfile = method(source, "public static void applyPregenPerformanceProfile(Engine engine)");
|
||||
|
||||
assertBefore(applyProfile, "PregenPerformanceProfile.applyToGenerator(generator)", "PregenPerformanceProfile.apply(engine)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bukkitComplexHotloadUsesExclusiveGenerationControl() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource")));
|
||||
String hotload = method(source, "public CompletableFuture<Void> hotloadComplexAsync(long acquisitionTimeout, TimeUnit unit)");
|
||||
|
||||
assertTrue(hotload.contains("withExclusiveControlFuture(activeEngine::hotloadComplex, acquisitionTimeout, unit)"));
|
||||
}
|
||||
|
||||
private static void assertBefore(String source, String first, String second) {
|
||||
int firstIndex = source.indexOf(first);
|
||||
int secondIndex = source.indexOf(second);
|
||||
assertTrue(firstIndex >= 0);
|
||||
assertTrue(secondIndex > firstIndex);
|
||||
}
|
||||
|
||||
private static String method(String source, String signature) {
|
||||
int start = source.indexOf(signature);
|
||||
assertTrue(start >= 0);
|
||||
int openingBrace = source.indexOf('{', start);
|
||||
assertTrue(openingBrace >= 0);
|
||||
int depth = 0;
|
||||
for (int index = openingBrace; index < source.length(); index++) {
|
||||
char character = source.charAt(index);
|
||||
if (character == '{') {
|
||||
depth++;
|
||||
} else if (character == '}') {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
return source.substring(start, index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Method body did not close: " + signature);
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package art.arcane.iris.engine.data.cache;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class LazyBoundedCacheTest {
|
||||
@Test
|
||||
public void backingStorageIsLazy() {
|
||||
LazyBoundedCache<String, Object> cache = new LazyBoundedCache<>(8);
|
||||
|
||||
assertFalse(cache.isInitialized());
|
||||
assertEquals(0, cache.size());
|
||||
assertNull(cache.computeIfAbsent("missing", ignored -> null));
|
||||
assertFalse(cache.isInitialized());
|
||||
|
||||
Object expected = new Object();
|
||||
assertSame(expected, cache.computeIfAbsent("present", ignored -> expected));
|
||||
assertTrue(cache.isInitialized());
|
||||
assertEquals(1, cache.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void capacityUsesAccessOrder() {
|
||||
LazyBoundedCache<Integer, Object> cache = new LazyBoundedCache<>(8);
|
||||
Object first = new Object();
|
||||
cache.computeIfAbsent(0, ignored -> first);
|
||||
for (int key = 1; key < 8; key++) {
|
||||
cache.computeIfAbsent(key, ignored -> new Object());
|
||||
}
|
||||
|
||||
assertSame(first, cache.computeIfAbsent(0, ignored -> new Object()));
|
||||
cache.computeIfAbsent(8, ignored -> new Object());
|
||||
|
||||
AtomicInteger reloads = new AtomicInteger();
|
||||
cache.computeIfAbsent(1, ignored -> {
|
||||
reloads.incrementAndGet();
|
||||
return new Object();
|
||||
});
|
||||
assertEquals(1, reloads.get());
|
||||
assertEquals(8, cache.size());
|
||||
assertSame(first, cache.computeIfAbsent(0, ignored -> new Object()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentSameKeyComputesOnce() throws Exception {
|
||||
int threadCount = 16;
|
||||
LazyBoundedCache<String, Object> cache = new LazyBoundedCache<>(8);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
CountDownLatch resolverEntered = new CountDownLatch(1);
|
||||
CountDownLatch releaseResolver = new CountDownLatch(1);
|
||||
AtomicInteger computations = new AtomicInteger();
|
||||
Object expected = new Object();
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||
List<Future<Object>> futures = new ArrayList<>(threadCount);
|
||||
try {
|
||||
for (int index = 0; index < threadCount; index++) {
|
||||
futures.add(executor.submit(() -> {
|
||||
start.await();
|
||||
return cache.computeIfAbsent("shared", ignored -> {
|
||||
computations.incrementAndGet();
|
||||
resolverEntered.countDown();
|
||||
await(releaseResolver);
|
||||
return expected;
|
||||
});
|
||||
}));
|
||||
}
|
||||
start.countDown();
|
||||
assertTrue(resolverEntered.await(5L, TimeUnit.SECONDS));
|
||||
releaseResolver.countDown();
|
||||
for (Future<Object> future : futures) {
|
||||
assertSame(expected, future.get(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
} finally {
|
||||
releaseResolver.countDown();
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
assertEquals(1, computations.get());
|
||||
assertEquals(1, cache.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void keysRetainIdentityEqualitySemantics() {
|
||||
Object firstIdentity = new String("same");
|
||||
Object secondIdentity = new String("same");
|
||||
LazyBoundedCache<IdentityKey, Object> cache = new LazyBoundedCache<>(8);
|
||||
Object firstValue = new Object();
|
||||
|
||||
assertSame(firstValue, cache.computeIfAbsent(new IdentityKey(firstIdentity), ignored -> firstValue));
|
||||
assertSame(firstValue, cache.computeIfAbsent(new IdentityKey(firstIdentity), ignored -> new Object()));
|
||||
assertNotSame(firstValue, cache.computeIfAbsent(new IdentityKey(secondIdentity), ignored -> new Object()));
|
||||
assertEquals(2, cache.size());
|
||||
}
|
||||
|
||||
private static void await(CountDownLatch latch) {
|
||||
try {
|
||||
if (!latch.await(5L, TimeUnit.SECONDS)) {
|
||||
throw new IllegalStateException("Timed out waiting for cache test latch");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class IdentityKey {
|
||||
private final Object identity;
|
||||
|
||||
private IdentityKey(Object identity) {
|
||||
this.identity = identity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof IdentityKey key && identity == key.identity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return System.identityHashCode(identity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class IrisObjectScaleCacheTest {
|
||||
private static final IrisObjectScale.ScaleRequest REQUEST = new IrisObjectScale.ScaleRequest(
|
||||
2D,
|
||||
1D,
|
||||
1D,
|
||||
7,
|
||||
IrisObjectPlacementScaleInterpolator.NONE
|
||||
);
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
IrisObjectScale.invalidate(null);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
IrisObjectScale.invalidate(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mutatedOriginCreatesStableReplacementAndCanBeReleased() {
|
||||
IrisObject origin = new IrisObject(2, 2, 2);
|
||||
IrisObjectScale scale = new IrisObjectScale().setSize(2D);
|
||||
|
||||
IrisObject first = scale.get(new RNG(1L), origin);
|
||||
IrisObjectScale.ScaleCache keyCache = new IrisObjectScale.ScaleCache(10L);
|
||||
IrisObjectScale.CacheKey originalKey = keyCache.key(origin, REQUEST, 0);
|
||||
int originalHash = originalKey.hashCode();
|
||||
|
||||
origin.setW(3);
|
||||
IrisObject second = scale.get(new RNG(1L), origin);
|
||||
|
||||
assertEquals(originalHash, originalKey.hashCode());
|
||||
assertNotEquals(originalKey, keyCache.key(origin, REQUEST, 0));
|
||||
assertNotSame(first, second);
|
||||
assertNotEquals(first.getW(), second.getW());
|
||||
assertEquals(2, IrisObjectScale.cacheEntryCount());
|
||||
|
||||
IrisObjectScale.invalidate(null);
|
||||
|
||||
assertEquals(0, IrisObjectScale.cacheEntryCount());
|
||||
assertEquals(0L, IrisObjectScale.cacheEstimatedBytes());
|
||||
assertFalse(IrisObjectScale.isOriginCached(origin));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void voxelMutationReplacesCachedVariants() {
|
||||
IrisObject origin = new IrisObject(2, 2, 2);
|
||||
IrisObjectScale scale = new IrisObjectScale().setSize(2D);
|
||||
IrisObject empty = scale.get(new RNG(1L), origin);
|
||||
|
||||
origin.setUnsigned(0, 0, 0, mock(PlatformBlockState.class));
|
||||
IrisObject populated = scale.get(new RNG(1L), origin);
|
||||
|
||||
assertEquals(0, empty.getBlocks().size());
|
||||
assertTrue(populated.getBlocks().size() > 0);
|
||||
assertNotSame(empty, populated);
|
||||
assertEquals(2, IrisObjectScale.cacheEntryCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rangedScaleBuildsSelectedVariantsLazilyWithStableSequence() {
|
||||
IrisObject origin = new IrisObject(10, 10, 10);
|
||||
IrisObjectScale scale = new IrisObjectScale()
|
||||
.setVariations(4)
|
||||
.setMinimumScale(1D)
|
||||
.setMaximumScale(2D);
|
||||
SequenceRng rng = new SequenceRng();
|
||||
|
||||
IrisObject first = scale.get(rng, origin);
|
||||
IrisObject second = scale.get(rng, origin);
|
||||
IrisObject third = scale.get(rng, origin);
|
||||
IrisObject fourth = scale.get(rng, origin);
|
||||
IrisObject repeated = scale.get(rng, origin);
|
||||
|
||||
assertSame(first, repeated);
|
||||
assertNotEquals(first.getW(), second.getW());
|
||||
assertNotEquals(second.getW(), third.getW());
|
||||
assertNotEquals(third.getW(), fourth.getW());
|
||||
assertEquals(4, IrisObjectScale.cacheEntryCount());
|
||||
assertTrue(IrisObjectScale.cacheEstimatedBytes() > 0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overweightScaledVariantIsNotRetained() {
|
||||
OverweightScaleObject origin = new OverweightScaleObject();
|
||||
IrisObjectScale scale = new IrisObjectScale().setSize(2D);
|
||||
|
||||
IrisObject result = scale.get(new RNG(1L), origin);
|
||||
|
||||
assertEquals(1_000, result.getW());
|
||||
assertEquals(1, origin.scaleCalls.get());
|
||||
assertEquals(0, IrisObjectScale.cacheEntryCount());
|
||||
assertEquals(0L, IrisObjectScale.cacheEstimatedBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentSameVariantScalesOnlyOnce() throws Exception {
|
||||
BlockingScaleObject origin = new BlockingScaleObject();
|
||||
IrisObjectScale scale = new IrisObjectScale().setSize(2D);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
CountDownLatch secondInvoked = new CountDownLatch(1);
|
||||
AtomicReference<Thread> secondThread = new AtomicReference<>();
|
||||
|
||||
try {
|
||||
Future<IrisObject> first = executor.submit(() -> scale.get(new RNG(1L), origin));
|
||||
assertTrue(origin.firstScaleEntered.await(5L, TimeUnit.SECONDS));
|
||||
Future<IrisObject> second = executor.submit(() -> {
|
||||
secondThread.set(Thread.currentThread());
|
||||
secondInvoked.countDown();
|
||||
return scale.get(new RNG(1L), origin);
|
||||
});
|
||||
assertTrue(secondInvoked.await(5L, TimeUnit.SECONDS));
|
||||
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||
while (secondThread.get().getState() != Thread.State.BLOCKED
|
||||
&& origin.scaleCalls.get() == 1
|
||||
&& System.nanoTime() < deadline) {
|
||||
LockSupport.parkNanos(100_000L);
|
||||
}
|
||||
|
||||
assertEquals(1, origin.scaleCalls.get());
|
||||
assertEquals(Thread.State.BLOCKED, secondThread.get().getState());
|
||||
origin.releaseScale.countDown();
|
||||
assertSame(first.get(5L, TimeUnit.SECONDS), second.get(5L, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
origin.releaseScale.countDown();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedEvictionKeepsStrictBudgetAndUsesRecentAccess() {
|
||||
IrisObjectScale.ScaleCache cache = new IrisObjectScale.ScaleCache(10L);
|
||||
IrisObject firstOrigin = new IrisObject(1, 1, 1);
|
||||
IrisObject secondOrigin = new IrisObject(1, 1, 1);
|
||||
IrisObject thirdOrigin = new IrisObject(1, 1, 1);
|
||||
IrisObjectScale.CacheKey firstKey = key(firstOrigin);
|
||||
IrisObjectScale.CacheKey secondKey = key(secondOrigin);
|
||||
IrisObjectScale.CacheKey thirdKey = key(thirdOrigin);
|
||||
|
||||
put(cache, firstKey, new IrisObject(1, 1, 1), 4L);
|
||||
put(cache, secondKey, new IrisObject(1, 1, 1), 4L);
|
||||
assertTrue(cache.lookup(firstKey).variant() != null);
|
||||
put(cache, thirdKey, new IrisObject(1, 1, 1), 4L);
|
||||
|
||||
assertEquals(2, cache.size());
|
||||
assertEquals(8L, cache.estimatedBytes());
|
||||
assertTrue(cache.estimatedBytes() <= cache.maximumEstimatedBytes());
|
||||
assertTrue(cache.lookup(firstKey).variant() != null);
|
||||
assertNull(cache.lookup(secondKey).variant());
|
||||
assertTrue(cache.lookup(thirdKey).variant() != null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overweightEntryIsReturnedWithoutRetention() {
|
||||
IrisObjectScale.ScaleCache cache = new IrisObjectScale.ScaleCache(10L);
|
||||
IrisObject origin = new IrisObject(1, 1, 1);
|
||||
IrisObjectScale.CacheKey key = key(origin);
|
||||
IrisObject variant = new IrisObject(1, 1, 1);
|
||||
IrisObjectScale.CacheLookup lookup = cache.lookup(key);
|
||||
|
||||
IrisObject result = cache.putIfCurrent(key, variant, 11L, lookup.generation());
|
||||
|
||||
assertSame(variant, result);
|
||||
assertEquals(0, cache.size());
|
||||
assertEquals(0L, cache.estimatedBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidationReleasesOnlyTheRequestedRuntime() {
|
||||
IrisObjectScale.ScaleCache cache = new IrisObjectScale.ScaleCache(20L);
|
||||
IrisData firstOwner = mock(IrisData.class);
|
||||
IrisData secondOwner = mock(IrisData.class);
|
||||
IrisObject firstOrigin = new IrisObject(1, 1, 1);
|
||||
IrisObject secondOrigin = new IrisObject(1, 1, 1);
|
||||
firstOrigin.setLoader(firstOwner);
|
||||
secondOrigin.setLoader(secondOwner);
|
||||
IrisObjectScale.CacheKey firstKey = key(firstOrigin);
|
||||
IrisObjectScale.CacheKey secondKey = key(secondOrigin);
|
||||
|
||||
put(cache, firstKey, new IrisObject(1, 1, 1), 5L);
|
||||
put(cache, secondKey, new IrisObject(1, 1, 1), 5L);
|
||||
|
||||
cache.invalidate(firstOwner);
|
||||
|
||||
assertNull(cache.lookup(firstKey).variant());
|
||||
assertTrue(cache.lookup(secondKey).variant() != null);
|
||||
assertEquals(1, cache.size());
|
||||
assertEquals(5L, cache.estimatedBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidationRejectsAnInFlightStaleInsertion() {
|
||||
IrisObjectScale.ScaleCache cache = new IrisObjectScale.ScaleCache(10L);
|
||||
IrisData owner = mock(IrisData.class);
|
||||
IrisObject origin = new IrisObject(1, 1, 1);
|
||||
origin.setLoader(owner);
|
||||
IrisObjectScale.CacheKey key = key(origin);
|
||||
IrisObjectScale.CacheLookup lookup = cache.lookup(key);
|
||||
|
||||
cache.invalidate(owner);
|
||||
cache.putIfCurrent(key, new IrisObject(1, 1, 1), 5L, lookup.generation());
|
||||
|
||||
assertEquals(0, cache.size());
|
||||
assertEquals(0L, cache.estimatedBytes());
|
||||
assertNull(cache.lookup(key).variant());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectedOriginReleasesScaledValueWithOwnerReference() {
|
||||
IrisObjectScale.ScaleCache cache = new IrisObjectScale.ScaleCache(20L);
|
||||
IrisData owner = mock(IrisData.class);
|
||||
IrisObject origin = new IrisObject(1, 1, 1);
|
||||
IrisObject variant = new IrisObject(1, 1, 1);
|
||||
origin.setLoader(owner);
|
||||
variant.setLoader(owner);
|
||||
IrisObjectScale.CacheKey collectedKey = cache.key(origin, REQUEST, 0);
|
||||
put(cache, collectedKey, variant, 5L);
|
||||
|
||||
collectedKey.clear();
|
||||
assertTrue(collectedKey.enqueue());
|
||||
|
||||
assertEquals(0, cache.size());
|
||||
assertEquals(0L, cache.estimatedBytes());
|
||||
}
|
||||
|
||||
private static IrisObjectScale.CacheKey key(IrisObject origin) {
|
||||
return new IrisObjectScale.CacheKey(origin, REQUEST, 0);
|
||||
}
|
||||
|
||||
private static void put(IrisObjectScale.ScaleCache cache, IrisObjectScale.CacheKey key,
|
||||
IrisObject variant, long estimatedBytes) {
|
||||
IrisObjectScale.CacheLookup lookup = cache.lookup(key);
|
||||
cache.putIfCurrent(key, variant, estimatedBytes, lookup.generation());
|
||||
}
|
||||
|
||||
private static final class BlockingScaleObject extends IrisObject {
|
||||
private final AtomicInteger scaleCalls = new AtomicInteger();
|
||||
private final CountDownLatch firstScaleEntered = new CountDownLatch(1);
|
||||
private final CountDownLatch releaseScale = new CountDownLatch(1);
|
||||
|
||||
private BlockingScaleObject() {
|
||||
super(2, 2, 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IrisObject scaled(double scale, IrisObjectPlacementScaleInterpolator interpolation) {
|
||||
scaleCalls.incrementAndGet();
|
||||
firstScaleEntered.countDown();
|
||||
try {
|
||||
if (!releaseScale.await(5L, TimeUnit.SECONDS)) {
|
||||
throw new AssertionError("Timed out waiting to release scale computation");
|
||||
}
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError(exception);
|
||||
}
|
||||
return new IrisObject(4, 4, 4);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OverweightScaleObject extends IrisObject {
|
||||
private final AtomicInteger scaleCalls = new AtomicInteger();
|
||||
|
||||
private OverweightScaleObject() {
|
||||
super(2, 2, 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IrisObject scaled(double scale, IrisObjectPlacementScaleInterpolator interpolation) {
|
||||
scaleCalls.incrementAndGet();
|
||||
return new IrisObject(1_000, 1_000, 1_000);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SequenceRng extends RNG {
|
||||
private int next;
|
||||
|
||||
@Override
|
||||
public int nextInt(int bound) {
|
||||
int value = next % bound;
|
||||
next++;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -16,6 +16,7 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
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;
|
||||
@@ -23,6 +24,7 @@ import java.util.concurrent.locks.LockSupport;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -311,6 +313,73 @@ public class BukkitChunkGeneratorGenerationStageGateTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timedExclusiveControlDoesNotReleaseAnUnacquiredPermit() throws Exception {
|
||||
AtomicBoolean closing = new AtomicBoolean(false);
|
||||
BukkitChunkGenerator.GenerationStageGate gate =
|
||||
new BukkitChunkGenerator.GenerationStageGate(1, closing::get);
|
||||
BukkitChunkGenerator.GenerationStagePermit stage = gate.acquireStage("timeout-holder");
|
||||
CompletableFuture<Void> outward = new CompletableFuture<>();
|
||||
AtomicBoolean operationRan = new AtomicBoolean(false);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
try {
|
||||
Future<?> operation = executor.submit(() ->
|
||||
BukkitChunkGenerator.completeExclusiveControlFuture(
|
||||
gate,
|
||||
() -> operationRan.set(true),
|
||||
outward,
|
||||
50L,
|
||||
TimeUnit.MILLISECONDS));
|
||||
|
||||
ExecutionException failure = assertThrows(
|
||||
ExecutionException.class,
|
||||
() -> outward.get(2L, TimeUnit.SECONDS));
|
||||
operation.get(2L, TimeUnit.SECONDS);
|
||||
|
||||
assertTrue(failure.getCause() instanceof TimeoutException);
|
||||
assertFalse(operationRan.get());
|
||||
assertEquals(0, gate.availablePermits());
|
||||
} finally {
|
||||
stage.close();
|
||||
assertEquals(1, gate.availablePermits());
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelledExclusiveControlStopsWaitingWithoutReleasingAStagePermit() throws Exception {
|
||||
AtomicBoolean closing = new AtomicBoolean(false);
|
||||
BukkitChunkGenerator.GenerationStageGate gate =
|
||||
new BukkitChunkGenerator.GenerationStageGate(1, closing::get);
|
||||
BukkitChunkGenerator.GenerationStagePermit stage = gate.acquireStage("cancellation-holder");
|
||||
CompletableFuture<Void> outward = new CompletableFuture<>();
|
||||
AtomicBoolean operationRan = new AtomicBoolean(false);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
try {
|
||||
Future<?> operation = executor.submit(() ->
|
||||
BukkitChunkGenerator.completeExclusiveControlFuture(
|
||||
gate,
|
||||
() -> operationRan.set(true),
|
||||
outward,
|
||||
30L,
|
||||
TimeUnit.SECONDS));
|
||||
awaitQueueLength(gate, 1);
|
||||
|
||||
assertTrue(outward.cancel(true));
|
||||
operation.get(2L, TimeUnit.SECONDS);
|
||||
|
||||
assertTrue(outward.isCancelled());
|
||||
assertFalse(operationRan.get());
|
||||
assertEquals(0, gate.availablePermits());
|
||||
} finally {
|
||||
stage.close();
|
||||
assertEquals(1, gate.availablePermits());
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queuedStageIsRejectedAfterCloseBegins() throws Exception {
|
||||
AtomicBoolean closing = new AtomicBoolean(false);
|
||||
|
||||
Reference in New Issue
Block a user