mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
stabilize pregeneration
This commit is contained in:
1 parent
4535bf3eed
commit
d196ffe853
32 files changed
+2013
-169
No files matched your search
@@ -6,7 +6,6 @@ import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
@@ -247,27 +246,29 @@ public final class WorldReplacementFilesystem {
|
||||
Path root = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize();
|
||||
requireDirectory(root, "pack root");
|
||||
MessageDigest digest = sha256();
|
||||
List<Path> files;
|
||||
List<FingerprintEntry> entries;
|
||||
try (Stream<Path> stream = Files.walk(root)) {
|
||||
files = stream
|
||||
entries = stream
|
||||
.filter(path -> !path.equals(root))
|
||||
.sorted(Comparator.comparing(path -> root.relativize(path).toString()))
|
||||
.map(path -> fingerprintEntry(root, path))
|
||||
.sorted(Comparator.comparing(FingerprintEntry::relativeString))
|
||||
.toList();
|
||||
}
|
||||
for (Path file : files) {
|
||||
BasicFileAttributes attributes = requireSafeEntry(file);
|
||||
Path relative = root.relativize(file);
|
||||
String separator = root.getFileSystem().getSeparator();
|
||||
byte[] buffer = new byte[8192];
|
||||
for (FingerprintEntry entry : entries) {
|
||||
BasicFileAttributes attributes = requireSafeEntry(entry.path());
|
||||
Path relative = entry.relative();
|
||||
if (isGeneratedPackMetadata(relative, attributes)) {
|
||||
continue;
|
||||
}
|
||||
update(digest, relative.toString().replace(file.getFileSystem().getSeparator(), "/"));
|
||||
update(digest, entry.relativeString().replace(separator, "/"));
|
||||
digest.update((byte) (attributes.isDirectory() ? 1 : 0));
|
||||
if (!attributes.isRegularFile()) {
|
||||
continue;
|
||||
}
|
||||
update(digest, Long.toString(attributes.size()));
|
||||
try (InputStream input = Files.newInputStream(file)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
try (InputStream input = Files.newInputStream(entry.path())) {
|
||||
int read;
|
||||
while ((read = input.read(buffer)) >= 0) {
|
||||
digest.update(buffer, 0, read);
|
||||
@@ -277,6 +278,11 @@ public final class WorldReplacementFilesystem {
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
}
|
||||
|
||||
private static FingerprintEntry fingerprintEntry(Path root, Path path) {
|
||||
Path relative = root.relativize(path);
|
||||
return new FingerprintEntry(path, relative, relative.toString());
|
||||
}
|
||||
|
||||
private static State inspect(ReplacementPaths paths) throws IOException {
|
||||
return new State(
|
||||
directoryPresent(paths.target(), "replacement target"),
|
||||
@@ -405,7 +411,10 @@ public final class WorldReplacementFilesystem {
|
||||
|
||||
private static void update(MessageDigest digest, String value) {
|
||||
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
digest.update(ByteBuffer.allocate(Integer.BYTES).putInt(bytes.length).array());
|
||||
digest.update((byte) (bytes.length >>> 24));
|
||||
digest.update((byte) (bytes.length >>> 16));
|
||||
digest.update((byte) (bytes.length >>> 8));
|
||||
digest.update((byte) bytes.length);
|
||||
digest.update(bytes);
|
||||
}
|
||||
|
||||
@@ -474,4 +483,7 @@ public final class WorldReplacementFilesystem {
|
||||
|
||||
private record State(boolean targetPresent, boolean stagePresent, boolean backupPresent) {
|
||||
}
|
||||
|
||||
private record FingerprintEntry(Path path, Path relative, String relativeString) {
|
||||
}
|
||||
}
|
||||
@@ -463,7 +463,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
|
||||
public synchronized void hotloaded() {
|
||||
StructureGraphCatalog.invalidate(this);
|
||||
IrisObjectScale.invalidate();
|
||||
IrisObjectScale.invalidate(this);
|
||||
closed = false;
|
||||
possibleSnippets = new KMap<>();
|
||||
builder = new GsonBuilder()
|
||||
@@ -529,7 +529,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
|
||||
public void dump() {
|
||||
StructureGraphCatalog.invalidate(this);
|
||||
IrisObjectScale.invalidate();
|
||||
IrisObjectScale.invalidate(this);
|
||||
for (ResourceLoader<?> i : loaders.values()) {
|
||||
i.clearCache();
|
||||
}
|
||||
@@ -763,19 +763,13 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
}
|
||||
|
||||
public void loadPrefetch(Engine engine) {
|
||||
BurstExecutor b = MultiBurst.ioBurst.burst(loaders.size());
|
||||
|
||||
for (ResourceLoader<?> i : loaders.values()) {
|
||||
b.queue(() -> {
|
||||
try {
|
||||
i.loadFirstAccess(engine);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
for (ResourceLoader<?> loader : loaders.values()) {
|
||||
try {
|
||||
loader.loadFirstAccess(engine);
|
||||
} catch (IOException exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
b.complete();
|
||||
IrisLogging.debug("Loaded Prefetch Cache to reduce generation disk use.");
|
||||
}
|
||||
}
|
||||
@@ -52,12 +52,16 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
@@ -79,6 +83,11 @@ import java.util.zip.GZIPOutputStream;
|
||||
public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||
public static final AtomicDouble tlt = new AtomicDouble(0);
|
||||
private static final int CACHE_SIZE = 100000;
|
||||
private static final int PREFETCH_MAGIC = 0x49504632;
|
||||
private static final int PREFETCH_FORMAT_VERSION = 1;
|
||||
private static final int PREFETCH_ENTRY_LIMIT = 1_024;
|
||||
private static final int PREFETCH_KEY_LENGTH_LIMIT = 1_024;
|
||||
private static final long PREFETCH_FILE_SIZE_LIMIT = 4L * 1_024L * 1_024L;
|
||||
private static volatile ExecutorService schemaBuildExecutor;
|
||||
private static final Set<String> schemaBuildQueue = ConcurrentHashMap.newKeySet();
|
||||
protected final AtomicCache<KList<File>> folderCache;
|
||||
@@ -93,6 +102,7 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||
protected IrisData manager;
|
||||
protected AtomicInteger loads;
|
||||
protected ChronoLatch sec;
|
||||
private volatile boolean firstAccessOverflowed;
|
||||
private final Options options;
|
||||
|
||||
public ResourceLoader(
|
||||
@@ -106,6 +116,7 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||
this.options = Objects.requireNonNull(options, "options");
|
||||
this.manager = manager;
|
||||
firstAccess = new KSet<>();
|
||||
firstAccessOverflowed = false;
|
||||
folderCache = new AtomicCache<>();
|
||||
sec = new ChronoLatch(5000);
|
||||
loads = new AtomicInteger();
|
||||
@@ -499,13 +510,28 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||
KSet<String> set = firstAccess;
|
||||
// contains-first: CHM.add takes the bin monitor even when the key is present, and
|
||||
// every generation thread hammers the same few keys through this line.
|
||||
if (set != null && !set.contains(name)) set.add(name);
|
||||
if (set != null && !set.contains(name)) {
|
||||
if (set.size() < prefetchEntryLimit()) {
|
||||
set.add(name);
|
||||
} else {
|
||||
firstAccessOverflowed = true;
|
||||
}
|
||||
}
|
||||
return loadCache.get(name);
|
||||
}
|
||||
|
||||
private File prefetchFile(Engine engine) {
|
||||
String id = "DIM" + Math.abs(engine.getSeedManager().getSeed() + engine.getDimension().getVersion() + engine.getDimension().getLoadKey().hashCode());
|
||||
return IrisPlatforms.get().dataFile("prefetch/" + id + "/" + Math.abs(getFolderName().hashCode()) + ".ipfch");
|
||||
String dimensionIdentity = digestIdentity(
|
||||
root.toPath().toAbsolutePath().normalize().toString(),
|
||||
Long.toString(engine.getSeedManager().getSeed()),
|
||||
Integer.toString(engine.getDimension().getVersion()),
|
||||
Objects.requireNonNullElse(engine.getDimension().getLoadKey(), ""));
|
||||
String loaderIdentity = digestIdentity(getFolderName());
|
||||
return IrisPlatforms.get().dataFile(
|
||||
"prefetch",
|
||||
"v2",
|
||||
dimensionIdentity,
|
||||
loaderIdentity + ".ipfch");
|
||||
}
|
||||
|
||||
public void loadFirstAccess(Engine engine) throws IOException {
|
||||
@@ -515,33 +541,43 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||
return;
|
||||
}
|
||||
|
||||
KList<String> s = new KList<>();
|
||||
if (file.length() > PREFETCH_FILE_SIZE_LIMIT) {
|
||||
discardPrefetch(file, "file exceeds " + PREFETCH_FILE_SIZE_LIMIT + " bytes");
|
||||
return;
|
||||
}
|
||||
|
||||
KList<String> entries = new KList<>();
|
||||
|
||||
try (FileInputStream fin = new FileInputStream(file);
|
||||
GZIPInputStream gzi = new GZIPInputStream(fin);
|
||||
DataInputStream din = new DataInputStream(gzi)) {
|
||||
int m = din.readInt();
|
||||
int magic = din.readInt();
|
||||
int version = din.readInt();
|
||||
int count = din.readInt();
|
||||
|
||||
if (m < 0) {
|
||||
throw new IOException("Bad prefetch count " + m);
|
||||
if (magic != PREFETCH_MAGIC || version != PREFETCH_FORMAT_VERSION) {
|
||||
throw new IOException("unsupported prefetch format");
|
||||
}
|
||||
|
||||
for (int i = 0; i < m; i++) {
|
||||
s.add(din.readUTF());
|
||||
if (count < 0 || count > prefetchEntryLimit()) {
|
||||
throw new IOException("bad prefetch count " + count);
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
String key = din.readUTF();
|
||||
if (key.isBlank() || key.length() > PREFETCH_KEY_LENGTH_LIMIT) {
|
||||
throw new IOException("invalid prefetch key length " + key.length());
|
||||
}
|
||||
entries.add(key);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
IrisLogging.warn("Discarding corrupt prefetch " + file.getPath() + ": " + e.getMessage());
|
||||
|
||||
if (!file.delete()) {
|
||||
IrisLogging.warn("Couldn't delete corrupt prefetch " + file.getPath());
|
||||
}
|
||||
|
||||
discardPrefetch(file, e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
IrisLogging.info("Loading " + s.size() + " prefetch " + getFolderName());
|
||||
IrisLogging.info("Loading " + entries.size() + " prefetch " + getFolderName());
|
||||
firstAccess = null;
|
||||
loadAllParallel(s);
|
||||
for (String entry : entries) {
|
||||
load(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveFirstAccess(Engine engine) throws IOException {
|
||||
@@ -549,6 +585,18 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||
if (set == null) return;
|
||||
KList<String> snapshot = new KList<>(set);
|
||||
File file = prefetchFile(engine);
|
||||
if (firstAccessOverflowed || snapshot.size() > prefetchEntryLimit()) {
|
||||
Files.deleteIfExists(file.toPath());
|
||||
firstAccess = null;
|
||||
return;
|
||||
}
|
||||
for (String key : snapshot) {
|
||||
if (key == null || key.isBlank() || key.length() > PREFETCH_KEY_LENGTH_LIMIT) {
|
||||
Files.deleteIfExists(file.toPath());
|
||||
firstAccess = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
File parent = file.getParentFile();
|
||||
|
||||
if (parent == null) {
|
||||
@@ -565,10 +613,12 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||
try (FileOutputStream fos = new FileOutputStream(temp);
|
||||
GZIPOutputStream gzo = new CustomOutputStream(fos, 9);
|
||||
DataOutputStream dos = new DataOutputStream(gzo)) {
|
||||
dos.writeInt(PREFETCH_MAGIC);
|
||||
dos.writeInt(PREFETCH_FORMAT_VERSION);
|
||||
dos.writeInt(snapshot.size());
|
||||
|
||||
for (String i : snapshot) {
|
||||
dos.writeUTF(i);
|
||||
for (String key : snapshot) {
|
||||
dos.writeUTF(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,6 +634,34 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||
firstAccess = null;
|
||||
}
|
||||
|
||||
private int prefetchEntryLimit() {
|
||||
return (int) Math.min(loadCache.getMaxSize(), PREFETCH_ENTRY_LIMIT);
|
||||
}
|
||||
|
||||
private void discardPrefetch(File file, String reason) {
|
||||
IrisLogging.warn("Discarding corrupt prefetch " + file.getPath() + ": " + reason);
|
||||
if (!file.delete()) {
|
||||
IrisLogging.warn("Couldn't delete corrupt prefetch " + file.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
private static String digestIdentity(String... components) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
for (String component : components) {
|
||||
byte[] value = Objects.requireNonNullElse(component, "").getBytes(StandardCharsets.UTF_8);
|
||||
digest.update((byte) (value.length >>> 24));
|
||||
digest.update((byte) (value.length >>> 16));
|
||||
digest.update((byte) (value.length >>> 8));
|
||||
digest.update((byte) value.length);
|
||||
digest.update(value);
|
||||
}
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("SHA-256 is unavailable", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public KList<File> getFolders() {
|
||||
return folderCache.aquire(() -> {
|
||||
KList<File> fc = new KList<>();
|
||||
|
||||
@@ -20,11 +20,19 @@ package art.arcane.iris.core.pregenerator;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.MeteredCache;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class PregenPerformanceProfile {
|
||||
private static final long HOTLOAD_ACQUISITION_TIMEOUT_SECONDS = 30L;
|
||||
private static final AtomicBoolean JVM_HINT_LOGGED = new AtomicBoolean(false);
|
||||
|
||||
private PregenPerformanceProfile() {
|
||||
@@ -55,10 +63,43 @@ public final class PregenPerformanceProfile {
|
||||
}
|
||||
|
||||
public static void apply(Engine engine) {
|
||||
boolean changed = apply();
|
||||
if (changed && engine != null) {
|
||||
apply();
|
||||
if (requiresNoiseCacheRefresh(engine)) {
|
||||
engine.hotloadComplex();
|
||||
IrisLogging.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast"));
|
||||
logApplied();
|
||||
}
|
||||
}
|
||||
|
||||
public static void applyToGenerator(PlatformChunkGenerator generator) {
|
||||
apply();
|
||||
Engine engine = generator == null ? null : generator.getEngine();
|
||||
if (requiresNoiseCacheRefresh(engine)) {
|
||||
generator.hotloadComplexAsync(HOTLOAD_ACQUISITION_TIMEOUT_SECONDS, TimeUnit.SECONDS).join();
|
||||
logApplied();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean requiresNoiseCacheRefresh(Engine engine) {
|
||||
if (engine == null) {
|
||||
return false;
|
||||
}
|
||||
IrisComplex complex = engine.getComplex();
|
||||
if (complex == null) {
|
||||
return true;
|
||||
}
|
||||
ProceduralStream<Double> heightStream = complex.getHeightStream();
|
||||
if (!(heightStream instanceof MeteredCache cache)) {
|
||||
return true;
|
||||
}
|
||||
ProceduralStream<IrisBiome> caveBiomeStream = complex.getCaveBiomeStream();
|
||||
if (!(caveBiomeStream instanceof CachedStream2D<?> cachedStream) || !cachedStream.usesFastCache()) {
|
||||
return true;
|
||||
}
|
||||
long configuredMaxSize = (long) IrisSettings.get().getPerformance().getNoiseCacheSize() * 256L;
|
||||
return cache.getMaxSize() != configuredMaxSize;
|
||||
}
|
||||
|
||||
private static void logApplied() {
|
||||
IrisLogging.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast"));
|
||||
}
|
||||
}
|
||||
+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();
|
||||
|
||||
Reference in new issue
Block a user