mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Rebase
This commit is contained in:
1 parent
d480abad50
commit
2145e90b1c
46 files changed
+1766
-167
No files matched your search
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.configuration;
|
||||
|
||||
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
||||
import art.arcane.iris.world.IrisPaperLikeBackendMode;
|
||||
import art.arcane.iris.world.IrisRuntimeSchedulerMode;
|
||||
|
||||
@@ -275,7 +276,7 @@ public class IrisSettings {
|
||||
}
|
||||
|
||||
public int getEffectiveResidentTectonicPlates(int worldHeight) {
|
||||
return effectiveResidentTectonicPlates(worldHeight, Runtime.getRuntime().maxMemory());
|
||||
return effectiveResidentTectonicPlates(worldHeight, MantleHeapPressure.memoryBudgetBytes());
|
||||
}
|
||||
|
||||
int effectiveResidentTectonicPlates(int worldHeight, long maximumHeapBytes) {
|
||||
@@ -398,6 +399,7 @@ public class IrisSettings {
|
||||
@Data
|
||||
public static class IrisSettingsStudio {
|
||||
public boolean openVSCode = true;
|
||||
public boolean blendChanges = true;
|
||||
public boolean disableTimeAndWeather = true;
|
||||
public boolean entitySpawning = true;
|
||||
public boolean autoStartDefaultStudio = false;
|
||||
|
||||
@@ -11,6 +11,7 @@ import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
|
||||
import art.arcane.iris.world.history.FloatingBiomeOverlay;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.generation.terrain.Terrain3DColumn;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
@@ -25,6 +26,7 @@ public class ChunkContext {
|
||||
private Hunk<NativeBiome> terrainBiomes;
|
||||
private FloatingBiomeOverlay floatingBiomes;
|
||||
private CaveTerrainSnapshot caveTerrain;
|
||||
private TransitionTerrainColumn[] transitionTerrain;
|
||||
private final int x;
|
||||
private final int z;
|
||||
private final IrisComplex complex;
|
||||
@@ -167,6 +169,17 @@ public class ChunkContext {
|
||||
this.caveTerrain = caveTerrain;
|
||||
}
|
||||
|
||||
public void setTransitionTerrain(int localX, int localZ, Terrain3DColumn solids, int fluidHeight) {
|
||||
if (transitionTerrain == null) {
|
||||
transitionTerrain = new TransitionTerrainColumn[256];
|
||||
}
|
||||
transitionTerrain[(localZ << 4) | localX] = new TransitionTerrainColumn(solids, fluidHeight);
|
||||
}
|
||||
|
||||
public TransitionTerrainColumn getTransitionTerrain(int localX, int localZ) {
|
||||
return transitionTerrain == null ? null : transitionTerrain[(localZ << 4) | localX];
|
||||
}
|
||||
|
||||
public void setNaturalBiome(int localX, int y, int localZ, NativeBiome biome) {
|
||||
if (terrainBiomes != null) {
|
||||
terrainBiomes.setRaw(localX, y, localZ, biome);
|
||||
@@ -280,6 +293,9 @@ public class ChunkContext {
|
||||
}
|
||||
}
|
||||
|
||||
public record TransitionTerrainColumn(Terrain3DColumn solids, int fluidHeight) {
|
||||
}
|
||||
|
||||
private static final class PrefillFillTask implements Runnable {
|
||||
private final ChunkedDataCache<?> dataCache;
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ package art.arcane.iris.generation.decoration;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
@@ -60,7 +62,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
@BlockCoordinates
|
||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||
Hunk<NativeBlockState> data, IrisBiome biome, InferredType inferredType, int height, int max) {
|
||||
int fluidHeight = getEngine().getMantle().getFluidHeight(realX, realZ);
|
||||
int fluidHeight = surfaceFluidHeight(realX, realZ);
|
||||
if (inferredType == InferredType.SHORE && height < fluidHeight) {
|
||||
return;
|
||||
}
|
||||
@@ -100,6 +102,21 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
||||
return height < fluidHeight && inferredType != InferredType.CAVE;
|
||||
}
|
||||
|
||||
int surfaceFluidHeight(int worldX, int worldZ) {
|
||||
IrisContext current = IrisContext.get();
|
||||
ChunkContext chunk = current == null ? null : current.getChunkContext();
|
||||
if (chunk != null && current.getEngine() == getEngine()
|
||||
&& current.getGenerationSessionId() == getEngine().getGenerationSessionId()
|
||||
&& current.getGenerationSessionId() == chunk.getGenerationSessionId()
|
||||
&& worldX >> 4 == chunk.getX() >> 4 && worldZ >> 4 == chunk.getZ() >> 4) {
|
||||
ChunkContext.TransitionTerrainColumn transition = chunk.getTransitionTerrain(worldX & 15, worldZ & 15);
|
||||
if (transition != null) {
|
||||
return transition.fluidHeight();
|
||||
}
|
||||
}
|
||||
return getEngine().getMantle().getFluidHeight(worldX, worldZ);
|
||||
}
|
||||
|
||||
static boolean skipsFluid(InferredType inferredType) {
|
||||
return inferredType == InferredType.CAVE;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static art.arcane.iris.generation.runtime.EngineShutdownSequence.runCleanup;
|
||||
|
||||
/**
|
||||
@@ -224,12 +226,14 @@ final class EngineHotloader {
|
||||
StudioGenerationUpdate update = null;
|
||||
boolean backgroundAdmissionClosed = false;
|
||||
try {
|
||||
long startedAt = System.nanoTime();
|
||||
engine.requireRunning("hotload the Studio pack");
|
||||
GenerationHistory history = router.history();
|
||||
update = StudioGenerationUpdate.prepare(engine, history);
|
||||
if (update == null) {
|
||||
return;
|
||||
}
|
||||
long preparedAt = System.nanoTime();
|
||||
engine.awaitNativeStructureBootstrap("Studio generation cutover");
|
||||
engine.backgroundTasks.closeBackgroundTaskAdmission();
|
||||
backgroundAdmissionClosed = true;
|
||||
@@ -243,15 +247,27 @@ final class EngineHotloader {
|
||||
engine.lifecycleState = LifecycleState.HOTLOADING;
|
||||
}
|
||||
engine.sealForTransition("Studio generation cutover", false);
|
||||
long drainedAt = System.nanoTime();
|
||||
IrisBoundarySignatureSampler.checkpoint(engine);
|
||||
long checkpointedAt = System.nanoTime();
|
||||
synchronized (engine.lifecycleLock) {
|
||||
if (engine.lifecycleState != LifecycleState.HOTLOADING || engine.runtime != previous) {
|
||||
throw new IllegalStateException("Studio engine changed while its generation cutover was draining.");
|
||||
}
|
||||
engine.prepareRuntimeHotload();
|
||||
IrisSettings settings = IrisSettings.get();
|
||||
int transitionWidth = settings.getStudio().isBlendChanges()
|
||||
? settings.getGenerator().getGenerationTransitionWidthBlocks() : 0;
|
||||
history.stageUpdate(update.source(), update.fingerprint(), update.dimensionContract(),
|
||||
update.registryContract(), IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
||||
cutover.promotePending();
|
||||
update.registryContract(), transitionWidth);
|
||||
}
|
||||
long stagedAt = System.nanoTime();
|
||||
cutover.promotePending();
|
||||
long promotedAt = System.nanoTime();
|
||||
synchronized (engine.lifecycleLock) {
|
||||
if (engine.lifecycleState != LifecycleState.HOTLOADING) {
|
||||
throw new IllegalStateException("Studio engine changed while its generation cutover was publishing.");
|
||||
}
|
||||
IrisStructureLocator.invalidate(engine);
|
||||
StructureReachability.invalidate(engine);
|
||||
engine.runtimeBuilder.refreshStudioRuntimeServices();
|
||||
@@ -259,19 +275,27 @@ final class EngineHotloader {
|
||||
engine.getEngineData().getStatistics().hotloaded();
|
||||
engine.getPlatformHooks().applyWorldBoundary(engine);
|
||||
}
|
||||
long completedAt = System.nanoTime();
|
||||
IrisLogging.debug("Studio update timing: world=%s total=%dms prepare=%dms drain=%dms checkpoint=%dms stage=%dms activation=%dms refresh=%dms",
|
||||
engine.getWorld().name(), elapsedMillis(startedAt, completedAt),
|
||||
elapsedMillis(startedAt, preparedAt), elapsedMillis(preparedAt, drainedAt),
|
||||
elapsedMillis(drainedAt, checkpointedAt), elapsedMillis(checkpointedAt, stagedAt),
|
||||
elapsedMillis(stagedAt, promotedAt), elapsedMillis(promotedAt, completedAt));
|
||||
broadcastStudioHotload(false, "");
|
||||
} catch (Throwable failure) {
|
||||
if (previous != null) {
|
||||
synchronized (engine.lifecycleLock) {
|
||||
if (engine.lifecycleState == LifecycleState.HOTLOADING
|
||||
|| engine.lifecycleState == LifecycleState.RUNNING) {
|
||||
boolean unchanged = false;
|
||||
boolean activeGenerationUnchanged = false;
|
||||
try {
|
||||
unchanged = router.history().manifest() == previousManifest;
|
||||
GenerationManifest currentManifest = router.history().manifest();
|
||||
activeGenerationUnchanged = currentManifest.activeActivation().activationId()
|
||||
== previousManifest.activeActivation().activationId();
|
||||
} catch (Throwable historyFailure) {
|
||||
failure.addSuppressed(historyFailure);
|
||||
}
|
||||
if (unchanged) {
|
||||
if (activeGenerationUnchanged) {
|
||||
engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previous);
|
||||
} else {
|
||||
engine.lifecycleState = LifecycleState.FAILED;
|
||||
@@ -305,6 +329,10 @@ final class EngineHotloader {
|
||||
}
|
||||
}
|
||||
|
||||
private static long elapsedMillis(long startedAt, long completedAt) {
|
||||
return TimeUnit.NANOSECONDS.toMillis(completedAt - startedAt);
|
||||
}
|
||||
|
||||
private void requireMutableGenerationRuntime(String operation) {
|
||||
if (engine.getGenerationHistoryRuntimeRouter().isPresent()) {
|
||||
throw new IllegalStateException("Cannot " + operation
|
||||
|
||||
@@ -995,7 +995,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
zone.setCeiling(y - 1);
|
||||
if (zone.isValid(resolver.height)
|
||||
&& (subterrainCell(worldX, floor, worldZ).occupied()
|
||||
|| !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ))) {
|
||||
|| !isNaturalTerrainOpening(getComplex(), worldX, floor, worldZ, context))) {
|
||||
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
|
||||
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
|
||||
}
|
||||
@@ -1112,6 +1112,17 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
&& !getEngine().isAdditionalTerrainOwned(x, y, z);
|
||||
}
|
||||
|
||||
private boolean isNaturalTerrainOpening(IrisComplex complex, int x, int y, int z, ChunkContext context) {
|
||||
ChunkContext.TransitionTerrainColumn transition = context == null ? null
|
||||
: context.getTransitionTerrain(x & 15, z & 15);
|
||||
if (transition == null) {
|
||||
return isNaturalTerrainOpening(complex, x, y, z);
|
||||
}
|
||||
Terrain3DColumn natural = complex.terrainColumn(x, z, complex.sampleHydrologyColumn(x, z));
|
||||
return natural != null && IrisDecorantActuator.isTransitionTerrainOpening(natural, transition, y)
|
||||
&& !getEngine().isAdditionalTerrainOwned(x, y, z);
|
||||
}
|
||||
|
||||
/** The dimension's underground river bed palette, or null when nothing paints. */
|
||||
private IrisRiverMaterialConfig undergroundBedMaterial() {
|
||||
IrisHydrology hydrology = getDimension().getHydrology();
|
||||
|
||||
@@ -174,8 +174,9 @@ public class IrisDecorantActuator extends EngineAssignedActuator<NativeBlockStat
|
||||
if (isFallingWaterfallThroat(hydrology)) {
|
||||
continue;
|
||||
}
|
||||
int surfaceFluidHeight = (int) Math.round(
|
||||
getComplex().getRiverWaterSurfaceStream().get(realX, realZ));
|
||||
ChunkContext.TransitionTerrainColumn transitionTerrain = context.getTransitionTerrain(i, j);
|
||||
int surfaceFluidHeight = transitionTerrain == null ? (int) Math.round(
|
||||
getComplex().getRiverWaterSurfaceStream().get(realX, realZ)) : transitionTerrain.fluidHeight();
|
||||
|
||||
if (biome.getDecorators().isEmpty() && (cave == null || cave.getDecorators().isEmpty())) {
|
||||
continue;
|
||||
@@ -231,7 +232,7 @@ public class IrisDecorantActuator extends EngineAssignedActuator<NativeBlockStat
|
||||
|
||||
Terrain3DColumn terrainColumn = getComplex().terrainColumn(realX, realZ, hydrology);
|
||||
if (terrainColumn != null) {
|
||||
decorateTerrainLedges(output, terrainColumn, biome, i, j, realX, realZ);
|
||||
decorateTerrainLedges(output, terrainColumn, transitionTerrain, biome, i, j, realX, realZ);
|
||||
}
|
||||
|
||||
|
||||
@@ -263,10 +264,15 @@ public class IrisDecorantActuator extends EngineAssignedActuator<NativeBlockStat
|
||||
}
|
||||
|
||||
private void decorateTerrainLedges(Hunk<NativeBlockState> output, Terrain3DColumn column,
|
||||
ChunkContext.TransitionTerrainColumn transition,
|
||||
IrisBiome biome, int localX, int localZ, int worldX, int worldZ) {
|
||||
for (int span = 0; span + 1 < column.spanCount(); span++) {
|
||||
int floorY = column.floor(span);
|
||||
int ceilingY = column.ceiling(span + 1);
|
||||
Terrain3DColumn ledges = transition == null ? column : transition.solids();
|
||||
for (int span = 0; span + 1 < ledges.spanCount(); span++) {
|
||||
int floorY = ledges.floor(span);
|
||||
int ceilingY = ledges.ceiling(span + 1);
|
||||
if (transition != null && resolveTerrainLedge(column, span, transition) == null) {
|
||||
continue;
|
||||
}
|
||||
int headroom = ceilingY - floorY - 1;
|
||||
if (headroom < 1 || !PREDICATE_SOLID.test(output.getRaw(localX, floorY, localZ))) {
|
||||
continue;
|
||||
@@ -280,6 +286,37 @@ public class IrisDecorantActuator extends EngineAssignedActuator<NativeBlockStat
|
||||
}
|
||||
}
|
||||
|
||||
static TerrainLedge resolveTerrainLedge(Terrain3DColumn natural, int span,
|
||||
ChunkContext.TransitionTerrainColumn transition) {
|
||||
if (transition == null) {
|
||||
return new TerrainLedge(natural.floor(span), natural.ceiling(span + 1));
|
||||
}
|
||||
Terrain3DColumn blended = transition.solids();
|
||||
int blendedFloor = blended.floor(span);
|
||||
int blendedCeiling = blended.ceiling(span + 1);
|
||||
for (int candidate = 0; candidate + 1 < natural.spanCount(); candidate++) {
|
||||
if (Math.max(natural.floor(candidate), blendedFloor) + 1
|
||||
< Math.min(natural.ceiling(candidate + 1), blendedCeiling)) {
|
||||
return new TerrainLedge(blendedFloor, blendedCeiling);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static boolean isTransitionTerrainOpening(Terrain3DColumn natural,
|
||||
ChunkContext.TransitionTerrainColumn transition, int y) {
|
||||
Terrain3DColumn blended = transition.solids();
|
||||
for (int span = 0; span + 1 < blended.spanCount(); span++) {
|
||||
if (y > blended.floor(span) && y < blended.ceiling(span + 1)) {
|
||||
return resolveTerrainLedge(natural, span, transition) != null;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
record TerrainLedge(int floorY, int ceilingY) {
|
||||
}
|
||||
|
||||
enum ShorelineDecorationMode {
|
||||
NONE,
|
||||
LEGACY,
|
||||
|
||||
+12
@@ -3,6 +3,7 @@ package art.arcane.iris.generation.stage;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineAssignedComponent;
|
||||
import art.arcane.iris.generation.runtime.EngineStage;
|
||||
import art.arcane.iris.generation.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.BoundaryGeometryInfluence;
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
@@ -85,6 +86,17 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
|
||||
blocks.setRaw(localX, offset, localZ, replacement);
|
||||
}
|
||||
context.setTerrainHeight(localX, localZ, floor);
|
||||
int fluidHeight = floor;
|
||||
while (fluidHeight + 1 < voxels.size()
|
||||
&& voxels.get(fluidHeight + 1).phase() == BoundaryColumnGeometry.Phase.FLUID) {
|
||||
fluidHeight++;
|
||||
}
|
||||
if (fluidHeight == floor) {
|
||||
fluidHeight = -1;
|
||||
}
|
||||
context.setTransitionTerrain(localX, localZ, Terrain3DColumn.fromOccupancy(
|
||||
floor, voxels.size(), offset -> voxels.get(offset).phase() == BoundaryColumnGeometry.Phase.SOLID),
|
||||
fluidHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
private final AtomicReference<CompletableFuture<Void>> watcherHotload = new AtomicReference<>();
|
||||
private volatile Engine engine;
|
||||
private volatile Looper hotloader;
|
||||
private volatile boolean studioUpdatesPaused;
|
||||
private volatile StudioMode lastMode;
|
||||
private volatile UUID lastJigsawStudioRequestId;
|
||||
private volatile boolean jigsawStudioActive;
|
||||
@@ -978,6 +979,18 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> quiesceStudioUpdatesAsync() {
|
||||
studioUpdatesPaused = true;
|
||||
cancelWatcherHotload();
|
||||
Looper activeHotloader = hotloader;
|
||||
hotloader = null;
|
||||
if (activeHotloader != null) {
|
||||
activeHotloader.interrupt();
|
||||
}
|
||||
return withExclusiveControlFuture(() -> { });
|
||||
}
|
||||
|
||||
@Override
|
||||
public void quiesceForServerShutdown() {
|
||||
cancelWatcherHotload();
|
||||
@@ -1071,26 +1084,33 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
|
||||
@Override
|
||||
public void hotload() {
|
||||
if (initialEntryPending || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
|
||||
if (initialEntryPending || studioUpdatesPaused || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
|
||||
return;
|
||||
}
|
||||
|
||||
withExclusiveControl(() -> getEngine().hotload());
|
||||
withExclusiveControl(() -> {
|
||||
if (!studioUpdatesPaused && !closing) {
|
||||
getEngine().hotload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void hotloadFromWatcher() {
|
||||
if (initialEntryPending || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
|
||||
if (initialEntryPending || studioUpdatesPaused || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
|
||||
return;
|
||||
}
|
||||
CompletableFuture<Void> pending = new CompletableFuture<>();
|
||||
long queuedAt = System.nanoTime();
|
||||
watcherHotload.set(pending);
|
||||
try {
|
||||
if (initialEntryPending || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)
|
||||
if (initialEntryPending || studioUpdatesPaused || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)
|
||||
|| Thread.currentThread().isInterrupted()) {
|
||||
pending.cancel(true);
|
||||
} else {
|
||||
J.a(() -> completeExclusiveControlFuture(loadLock, () -> {
|
||||
if (!initialEntryPending && shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
|
||||
if (!initialEntryPending && !studioUpdatesPaused && shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
|
||||
IrisLogging.debug("Studio update admission: world=%s wait=%dms",
|
||||
getTarget().getWorld().name(), TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - queuedAt));
|
||||
getEngine().hotload();
|
||||
}
|
||||
}, pending));
|
||||
|
||||
@@ -66,6 +66,8 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
CompletableFuture<Void> quiesceStudioUpdatesAsync();
|
||||
|
||||
default void quiesceForServerShutdown() {
|
||||
}
|
||||
|
||||
|
||||
@@ -483,6 +483,7 @@ public final class StudioOpenCoordinator {
|
||||
IrisToolbelt.beginWorldMaintenance(world, "studio-close", true);
|
||||
}
|
||||
},
|
||||
() -> provider == null ? CompletableFuture.completedFuture(null) : provider.quiesceStudioUpdatesAsync(),
|
||||
() -> unloadWorldFamily(worldName, world).thenRun(() -> unloadConfirmed.set(true)),
|
||||
() -> provider == null ? CompletableFuture.completedFuture(null) : provider.closeAsync(),
|
||||
() -> deleteFolder
|
||||
@@ -516,6 +517,7 @@ public final class StudioOpenCoordinator {
|
||||
static CompletableFuture<Void> sequenceStudioClose(
|
||||
Supplier<CompletableFuture<Void>> evacuate,
|
||||
Runnable enterMaintenance,
|
||||
Supplier<CompletableFuture<Void>> quiesceUpdates,
|
||||
Supplier<CompletableFuture<Void>> unload,
|
||||
Supplier<CompletableFuture<Void>> closeGenerator,
|
||||
Supplier<CompletableFuture<Void>> deleteFolders
|
||||
@@ -523,8 +525,9 @@ public final class StudioOpenCoordinator {
|
||||
return invokePhase(evacuate)
|
||||
.thenCompose(ignored -> {
|
||||
enterMaintenance.run();
|
||||
return invokePhase(unload);
|
||||
return invokePhase(quiesceUpdates);
|
||||
})
|
||||
.thenCompose(ignored -> invokePhase(unload))
|
||||
.thenCompose(ignored -> invokePhase(closeGenerator))
|
||||
.thenCompose(ignored -> invokePhase(deleteFolders));
|
||||
}
|
||||
|
||||
@@ -20,12 +20,10 @@ package art.arcane.iris.studio.workspace;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.studio.object.StudioOpenCoordinator;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.exceptions.IrisException;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import lombok.Data;
|
||||
import org.bukkit.World;
|
||||
|
||||
@@ -136,8 +134,6 @@ public class IrisProject {
|
||||
);
|
||||
StudioOpenProgressReporter.startStudioOpenReporter(sender, stage, progress, complete, failed);
|
||||
future.whenComplete((result, throwable) -> {
|
||||
World maintenanceWorld = null;
|
||||
boolean maintenanceActive = false;
|
||||
try {
|
||||
if (throwable != null) {
|
||||
failed.set(true);
|
||||
@@ -148,17 +144,7 @@ public class IrisProject {
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.IRIS_PROJECT_STUDIO_OPEN_FAILED, MessageArgument.untrusted("error", String.valueOf(error.getMessage()))));
|
||||
return;
|
||||
}
|
||||
|
||||
maintenanceWorld = result.world();
|
||||
if (maintenanceWorld != null) {
|
||||
IrisToolbelt.beginWorldMaintenance(maintenanceWorld, "studio-open");
|
||||
maintenanceActive = true;
|
||||
}
|
||||
} finally {
|
||||
if (maintenanceActive && maintenanceWorld != null) {
|
||||
World worldToRelease = maintenanceWorld;
|
||||
J.a(() -> IrisToolbelt.endWorldMaintenance(worldToRelease, "studio-open"), 300);
|
||||
}
|
||||
complete.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -365,13 +365,12 @@ public class IrisCreator {
|
||||
worldKey,
|
||||
IrisWorldStorage.levelRoot().getName()
|
||||
);
|
||||
BukkitWorldConfiguration.register(
|
||||
bukkitRegistered = BukkitWorldConfiguration.register(
|
||||
BUKKIT_YML,
|
||||
registrationName,
|
||||
dimension,
|
||||
seed
|
||||
);
|
||||
bukkitRegistered = true;
|
||||
) == BukkitWorldConfiguration.Registration.CREATED;
|
||||
World createdWorld = world;
|
||||
CompletableFuture<Void> multiverseRegistration = J.sfut(
|
||||
() -> IrisServices.get(MultiverseCoreLink.class)
|
||||
|
||||
@@ -22,8 +22,8 @@ public record GenerationTransition(
|
||||
if (algorithmVersion < 1) {
|
||||
throw new IllegalArgumentException("Transition algorithm version must be positive.");
|
||||
}
|
||||
if (widthBlocks < MINIMUM_WIDTH_BLOCKS || widthBlocks > MAXIMUM_WIDTH_BLOCKS) {
|
||||
throw new IllegalArgumentException("Transition width must be between "
|
||||
if (widthBlocks != 0 && (widthBlocks < MINIMUM_WIDTH_BLOCKS || widthBlocks > MAXIMUM_WIDTH_BLOCKS)) {
|
||||
throw new IllegalArgumentException("Transition width must be zero or between "
|
||||
+ MINIMUM_WIDTH_BLOCKS + " and " + MAXIMUM_WIDTH_BLOCKS + " blocks.");
|
||||
}
|
||||
if ((boundaryIdentity == null) != (terrainSignatureIdentity == null)) {
|
||||
|
||||
+48
-18
@@ -4,7 +4,11 @@ import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@@ -47,43 +51,69 @@ public final class IrisBoundarySignatureSampler implements GenerationBoundarySig
|
||||
|
||||
private static final class LiveCapture implements TerrainBoundarySignatureStore.SignatureSampler {
|
||||
private final IrisEngine engine;
|
||||
private final LinkedHashMap<Long, SavedTerrainChunk> chunks = new LinkedHashMap<>(32, 0.75F, true);
|
||||
private final LinkedHashMap<Long, CompletableFuture<SavedTerrainChunk>> chunks = new LinkedHashMap<>(32, 0.75F, true);
|
||||
|
||||
private LiveCapture(IrisEngine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareChunks(List<GenerationBoundary.ChunkCoordinate> coordinates) {
|
||||
if (coordinates.size() > MAXIMUM_CACHED_CHUNKS) {
|
||||
throw new IllegalArgumentException("Terrain capture batches cannot exceed 32 chunks.");
|
||||
}
|
||||
try {
|
||||
for (GenerationBoundary.ChunkCoordinate coordinate : coordinates) {
|
||||
captureChunk(coordinate.chunkX(), coordinate.chunkZ());
|
||||
}
|
||||
} catch (RuntimeException | Error failure) {
|
||||
cancelPendingCaptures();
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException {
|
||||
int chunkX = Math.floorDiv(blockX, 16);
|
||||
int chunkZ = Math.floorDiv(blockZ, 16);
|
||||
long key = ChunkGenerationOwnership.packChunk(chunkX, chunkZ);
|
||||
SavedTerrainChunk terrain = chunks.get(key);
|
||||
if (terrain == null) {
|
||||
terrain = captureChunk(chunkX, chunkZ);
|
||||
chunks.put(key, terrain);
|
||||
if (chunks.size() > MAXIMUM_CACHED_CHUNKS) {
|
||||
Map.Entry<Long, SavedTerrainChunk> oldest = chunks.firstEntry();
|
||||
chunks.remove(oldest.getKey());
|
||||
}
|
||||
}
|
||||
return terrain.column(blockX, blockZ);
|
||||
}
|
||||
|
||||
private SavedTerrainChunk captureChunk(int chunkX, int chunkZ) throws IOException {
|
||||
try {
|
||||
return engine.getPlatformHooks().captureSavedTerrainChunk(engine, chunkX, chunkZ)
|
||||
.get(30, TimeUnit.SECONDS);
|
||||
return captureChunk(chunkX, chunkZ).get(30, TimeUnit.SECONDS).column(blockX, blockZ);
|
||||
} catch (InterruptedException failure) {
|
||||
cancelPendingCaptures();
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted while capturing terrain at " + chunkX + "," + chunkZ, failure);
|
||||
} catch (ExecutionException | TimeoutException failure) {
|
||||
} catch (ExecutionException | TimeoutException | CancellationException failure) {
|
||||
cancelPendingCaptures();
|
||||
throw new IOException("Unable to capture terrain at " + chunkX + "," + chunkZ, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<SavedTerrainChunk> captureChunk(int chunkX, int chunkZ) {
|
||||
long key = ChunkGenerationOwnership.packChunk(chunkX, chunkZ);
|
||||
CompletableFuture<SavedTerrainChunk> terrain = chunks.get(key);
|
||||
if (terrain != null) {
|
||||
return terrain;
|
||||
}
|
||||
terrain = Objects.requireNonNull(engine.getPlatformHooks().captureSavedTerrainChunk(engine, chunkX, chunkZ),
|
||||
"Terrain capture future");
|
||||
chunks.put(key, terrain);
|
||||
if (chunks.size() > MAXIMUM_CACHED_CHUNKS) {
|
||||
Map.Entry<Long, CompletableFuture<SavedTerrainChunk>> oldest = chunks.firstEntry();
|
||||
chunks.remove(oldest.getKey());
|
||||
oldest.getValue().cancel(false);
|
||||
}
|
||||
return terrain;
|
||||
}
|
||||
|
||||
private void cancelPendingCaptures() {
|
||||
for (CompletableFuture<SavedTerrainChunk> capture : chunks.values()) {
|
||||
capture.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
cancelPendingCaptures();
|
||||
try {
|
||||
checkpoint(engine);
|
||||
} finally {
|
||||
|
||||
+51
-16
@@ -66,6 +66,7 @@ public final class TerrainBoundarySignatureStore {
|
||||
private static final int SHARD_FIXED_BODY_BYTES = 12;
|
||||
private static final int CHECKSUM_BYTES = Integer.BYTES;
|
||||
private static final int CELL_SIZE = 32;
|
||||
private static final int PREPARED_CELL_WINDOW = 8;
|
||||
private static final int SUPER_CELL_SIZE = 32;
|
||||
private static final int MAXIMUM_CACHED_SHARDS = 64;
|
||||
private static final int MAX_CELL_ENTRIES = CELL_SIZE * CELL_SIZE;
|
||||
@@ -148,24 +149,34 @@ public final class TerrainBoundarySignatureStore {
|
||||
LongArrays.quickSort(cellKeys, TerrainBoundarySignatureStore::compareCoordinates);
|
||||
Long2ObjectOpenHashMap<CellReference> cells = new Long2ObjectOpenHashMap<>(cellKeys.length);
|
||||
int count = 0;
|
||||
for (long cellKey : cellKeys) {
|
||||
BitSet mask = columns.remove(cellKey);
|
||||
List<TerrainBoundarySignature> signatures = new ArrayList<>(mask.cardinality());
|
||||
for (int index = mask.nextSetBit(0); index >= 0; index = mask.nextSetBit(index + 1)) {
|
||||
int blockX = Math.addExact(Math.multiplyExact(cellX(cellKey), CELL_SIZE), index / CELL_SIZE);
|
||||
int blockZ = Math.addExact(Math.multiplyExact(cellZ(cellKey), CELL_SIZE), index % CELL_SIZE);
|
||||
TerrainBoundarySignature signature = Objects.requireNonNull(
|
||||
sampler.sample(blockX, blockZ), "boundary signature"
|
||||
);
|
||||
if (signature.blockX() != blockX || signature.blockZ() != blockZ) {
|
||||
throw new IOException("Boundary signature coordinates changed while sampling "
|
||||
+ blockX + "," + blockZ + ".");
|
||||
for (int offset = 0; offset < cellKeys.length; offset += PREPARED_CELL_WINDOW) {
|
||||
int end = Math.min(cellKeys.length, offset + PREPARED_CELL_WINDOW);
|
||||
List<GenerationBoundary.ChunkCoordinate> chunks = new ArrayList<>((end - offset) * 4);
|
||||
for (int index = offset; index < end; index++) {
|
||||
long cellKey = cellKeys[index];
|
||||
addSampledChunks(cellKey, columns.get(cellKey), chunks);
|
||||
}
|
||||
sampler.prepareChunks(List.copyOf(chunks));
|
||||
for (int cellIndex = offset; cellIndex < end; cellIndex++) {
|
||||
long cellKey = cellKeys[cellIndex];
|
||||
BitSet mask = columns.remove(cellKey);
|
||||
List<TerrainBoundarySignature> signatures = new ArrayList<>(mask.cardinality());
|
||||
for (int index = mask.nextSetBit(0); index >= 0; index = mask.nextSetBit(index + 1)) {
|
||||
int blockX = Math.addExact(Math.multiplyExact(cellX(cellKey), CELL_SIZE), index / CELL_SIZE);
|
||||
int blockZ = Math.addExact(Math.multiplyExact(cellZ(cellKey), CELL_SIZE), index % CELL_SIZE);
|
||||
TerrainBoundarySignature signature = Objects.requireNonNull(
|
||||
sampler.sample(blockX, blockZ), "boundary signature"
|
||||
);
|
||||
if (signature.blockX() != blockX || signature.blockZ() != blockZ) {
|
||||
throw new IOException("Boundary signature coordinates changed while sampling "
|
||||
+ blockX + "," + blockZ + ".");
|
||||
}
|
||||
signatures.add(canonicalizeSignature(signature));
|
||||
}
|
||||
signatures.add(canonicalizeSignature(signature));
|
||||
String hash = CellShard.publish(directory, signatures, cellX(cellKey), cellZ(cellKey));
|
||||
cells.put(cellKey, new CellReference(hash, signatures.size()));
|
||||
count = Math.addExact(count, signatures.size());
|
||||
}
|
||||
String hash = CellShard.publish(directory, signatures, cellX(cellKey), cellZ(cellKey));
|
||||
cells.put(cellKey, new CellReference(hash, signatures.size()));
|
||||
count = Math.addExact(count, signatures.size());
|
||||
}
|
||||
byte[] identity = computeIdentity(activationId, count, cellKeys, cells);
|
||||
return publishCatalog(activationId, new PublishedCatalog(
|
||||
@@ -174,6 +185,27 @@ public final class TerrainBoundarySignatureStore {
|
||||
}
|
||||
}
|
||||
|
||||
private static void addSampledChunks(long cellKey, BitSet columns,
|
||||
List<GenerationBoundary.ChunkCoordinate> chunks) {
|
||||
int minimumX = Math.multiplyExact(cellX(cellKey), CELL_SIZE);
|
||||
int minimumZ = Math.multiplyExact(cellZ(cellKey), CELL_SIZE);
|
||||
int chunksPerSide = CELL_SIZE / GenerationBoundary.CHUNK_SIZE;
|
||||
int prepared = 0;
|
||||
for (int index = columns.nextSetBit(0); index >= 0; index = columns.nextSetBit(index + 1)) {
|
||||
int localX = index / CELL_SIZE;
|
||||
int localZ = index % CELL_SIZE;
|
||||
int chunkIndex = localX / GenerationBoundary.CHUNK_SIZE * chunksPerSide
|
||||
+ localZ / GenerationBoundary.CHUNK_SIZE;
|
||||
int chunkBit = 1 << chunkIndex;
|
||||
if ((prepared & chunkBit) == 0) {
|
||||
prepared |= chunkBit;
|
||||
chunks.add(new GenerationBoundary.ChunkCoordinate(
|
||||
Math.floorDiv(Math.addExact(minimumX, localX), GenerationBoundary.CHUNK_SIZE),
|
||||
Math.floorDiv(Math.addExact(minimumZ, localZ), GenerationBoundary.CHUNK_SIZE)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Snapshot publishCatalog(long activationId, PublishedCatalog catalog) throws IOException {
|
||||
forceDirectory(directory);
|
||||
Path target = snapshotPath(activationId);
|
||||
@@ -678,6 +710,9 @@ public final class TerrainBoundarySignatureStore {
|
||||
public interface SignatureSampler extends AutoCloseable {
|
||||
TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException;
|
||||
|
||||
default void prepareChunks(List<GenerationBoundary.ChunkCoordinate> chunks) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
default void close() throws IOException {
|
||||
}
|
||||
|
||||
@@ -32,11 +32,11 @@ public final class TransitionGenerationPlan {
|
||||
throw new IllegalArgumentException("Unsupported generation transition algorithm version "
|
||||
+ specification.algorithmVersion());
|
||||
}
|
||||
terrainSampler = new TransitionBoundarySampler(
|
||||
terrainSampler = specification.widthBlocks() == 0 ? null : new TransitionBoundarySampler(
|
||||
specification.widthBlocks(),
|
||||
terrainSignatures
|
||||
);
|
||||
terrainBand = new TransitionBandIndex(boundary, terrainSampler);
|
||||
terrainBand = terrainSampler == null ? null : new TransitionBandIndex(boundary, terrainSampler);
|
||||
newTerrain = TerrainSample.newTerrain(specification.widthBlocks());
|
||||
}
|
||||
|
||||
@@ -97,11 +97,11 @@ public final class TransitionGenerationPlan {
|
||||
}
|
||||
|
||||
public boolean hasTransitionAtChunk(int chunkX, int chunkZ) {
|
||||
return terrainBand.contains(chunkX, chunkZ);
|
||||
return terrainBand != null && terrainBand.contains(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
public BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) {
|
||||
if (!terrainBand.contains(blockX >> 4, blockZ >> 4)) {
|
||||
if (!hasTransitionAtChunk(blockX >> 4, blockZ >> 4)) {
|
||||
return BoundaryGeometryInfluence.none();
|
||||
}
|
||||
return terrainSampler.geometryAt(blockX, blockZ);
|
||||
@@ -111,7 +111,7 @@ public final class TransitionGenerationPlan {
|
||||
if (boundary.isHistoricalBlock(blockX, blockZ)) {
|
||||
return TerrainSample.historicalTerrain();
|
||||
}
|
||||
return terrainBand.contains(blockX >> 4, blockZ >> 4) ? terrainSampler.sample(blockX, blockZ) : newTerrain;
|
||||
return hasTransitionAtChunk(blockX >> 4, blockZ >> 4) ? terrainSampler.sample(blockX, blockZ) : newTerrain;
|
||||
}
|
||||
|
||||
public Optional<String> historicalPhysicalBiomeKeyAt(int blockX, int blockY, int blockZ) {
|
||||
@@ -137,15 +137,15 @@ public final class TransitionGenerationPlan {
|
||||
}
|
||||
|
||||
long terrainCatalogProbeCount() {
|
||||
return terrainSampler.catalogProbeCount();
|
||||
return terrainSampler == null ? 0L : terrainSampler.catalogProbeCount();
|
||||
}
|
||||
|
||||
long terrainShardLoadCount() {
|
||||
return terrainSampler.shardLoadCount();
|
||||
return terrainSampler == null ? 0L : terrainSampler.shardLoadCount();
|
||||
}
|
||||
|
||||
long terrainCandidateBuildCount() {
|
||||
return terrainSampler.candidateBuildCount();
|
||||
return terrainSampler == null ? 0L : terrainSampler.candidateBuildCount();
|
||||
}
|
||||
|
||||
public record Specification(
|
||||
@@ -171,8 +171,8 @@ public final class TransitionGenerationPlan {
|
||||
if (oldEpochId.equals(newEpochId)) {
|
||||
throw new IllegalArgumentException("Transition epoch IDs must differ");
|
||||
}
|
||||
if (widthBlocks <= 0) {
|
||||
throw new IllegalArgumentException("Transition width must be positive");
|
||||
if (widthBlocks < 0) {
|
||||
throw new IllegalArgumentException("Transition width cannot be negative");
|
||||
}
|
||||
if (algorithmVersion < 1) {
|
||||
throw new IllegalArgumentException("Transition algorithm version must be positive");
|
||||
|
||||
@@ -144,7 +144,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
this.chunkAccessMode = chunkAsyncMethodSelection.mode();
|
||||
int detectedWorkerPoolThreads = resolveWorkerPoolThreads();
|
||||
int detectedCpuThreads = Math.max(1, Runtime.getRuntime().availableProcessors());
|
||||
this.pregenWorkerThreads = pregenWorkerThreadTarget(detectedCpuThreads, Runtime.getRuntime().maxMemory(),
|
||||
this.pregenWorkerThreads = pregenWorkerThreadTarget(detectedCpuThreads, MantleHeapPressure.memoryBudgetBytes(),
|
||||
!foliaRuntime && !strictSerial && detectedWorkerPoolThreads > 0);
|
||||
int workerThreadsForCap = foliaRuntime
|
||||
? resolveFoliaConcurrencyWorkerThreads(detectedWorkerPoolThreads, detectedCpuThreads, pregenWorkerThreads)
|
||||
@@ -166,7 +166,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
int configuredThreads = foliaRuntime
|
||||
? computeFoliaRecommendedCap(workerThreadsForCap)
|
||||
: computePaperLikeRecommendedCap(workerThreadsForCap);
|
||||
configuredThreads = heapLimitedConcurrency(configuredThreads, Runtime.getRuntime().maxMemory());
|
||||
configuredThreads = heapLimitedConcurrency(configuredThreads, MantleHeapPressure.memoryBudgetBytes());
|
||||
this.threads = selectConcurrencyCap(configuredThreads, strictSerial);
|
||||
this.workerPoolThreads = detectedWorkerPoolThreads;
|
||||
this.runtimeCpuThreads = detectedCpuThreads;
|
||||
@@ -304,7 +304,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
binding.releaseChunk(world, chunkX, chunkZ);
|
||||
}
|
||||
};
|
||||
return new PregenChunkRetention(tickets, radius, retentionCapacity(Runtime.getRuntime().maxMemory()),
|
||||
return new PregenChunkRetention(tickets, radius, retentionCapacity(MantleHeapPressure.memoryBudgetBytes()),
|
||||
minRegionX, minRegionZ, maxRegionX, maxRegionZ);
|
||||
} catch (Throwable e) {
|
||||
PregenDiagnostics.probeFailed("pregen chunk retention for world " + world.getName(), e);
|
||||
@@ -996,7 +996,14 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
|
||||
PregenAdmissionGate.Wait waited;
|
||||
try {
|
||||
waited = admission.admit(() -> inFlight.get() >= adaptiveInFlightLimit.get(), this::isCancelled);
|
||||
while (true) {
|
||||
waited = admission.admit(() -> inFlight.get() >= adaptiveInFlightLimit.get(), this::isCancelled);
|
||||
if (waited == null || !MantleHeapPressure.overHighWater()) {
|
||||
break;
|
||||
}
|
||||
admission.release();
|
||||
backpressure.awaitHeapHeadroom(MantleHeapPressure::overHighWater, this::reclaimHeapPressure);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
|
||||
@@ -445,7 +445,7 @@ public class IrisPregenerator {
|
||||
try {
|
||||
generator.reclaimMemory();
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError("Pregen could not reclaim completed server chunks under heap pressure.", e);
|
||||
IrisLogging.reportError("Pregen could not reclaim completed server chunks under memory pressure.", e);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.world.pregen;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.diagnostics.ContainerMemory;
|
||||
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.ObjectName;
|
||||
@@ -31,6 +32,8 @@ import java.util.function.LongSupplier;
|
||||
public final class MantleHeapPressure {
|
||||
private static final double HIGH_WATER = 0.92D;
|
||||
private static final double LOW_WATER = 0.82D;
|
||||
private static final double CONTAINER_HIGH_WATER = 0.90D;
|
||||
private static final double CONTAINER_LOW_WATER = 0.80D;
|
||||
private static final long MAXIMUM_HYSTERESIS_MS = 60_000L;
|
||||
private static final ObjectName DIAGNOSTIC_COMMAND = diagnosticCommandName();
|
||||
private static final PanicGcReclaimer PANIC_RECLAIMER = new PanicGcReclaimer(
|
||||
@@ -42,7 +45,7 @@ public final class MantleHeapPressure {
|
||||
// An escalation step in the reclaim policy, rate limited by that policy rather than by
|
||||
// anything the operator did. What is actionable is the wait budget being exceeded.
|
||||
(double fraction) -> IrisLogging.info(
|
||||
"Iris heap remained at %.1f%% after normal panic reclaim; invoking the current JVM's diagnostic full GC to keep generation live.",
|
||||
"Iris memory pressure remained at %.1f%% of its safety budget after normal panic reclaim; invoking the current JVM's diagnostic full GC to keep generation live.",
|
||||
fraction * 100.0D),
|
||||
(String context, Throwable failure) -> IrisLogging.reportError(context, failure)));
|
||||
private static final HeapPressureGate PRESSURE_GATE = new HeapPressureGate(
|
||||
@@ -51,11 +54,47 @@ public final class MantleHeapPressure {
|
||||
MAXIMUM_HYSTERESIS_MS,
|
||||
System::currentTimeMillis,
|
||||
PANIC_RECLAIMER::resetEpisode);
|
||||
private static final HeapPressureGate CONTAINER_PRESSURE_GATE = new HeapPressureGate(
|
||||
CONTAINER_HIGH_WATER,
|
||||
CONTAINER_LOW_WATER,
|
||||
Long.MAX_VALUE,
|
||||
System::currentTimeMillis,
|
||||
PANIC_RECLAIMER::resetEpisode);
|
||||
|
||||
private MantleHeapPressure() {
|
||||
}
|
||||
|
||||
public static double usedFraction() {
|
||||
return pressureFraction(heapUsedFraction(), ContainerMemory.snapshot().pressureFraction());
|
||||
}
|
||||
|
||||
public static long memoryBudgetBytes() {
|
||||
return memoryBudgetBytes(Runtime.getRuntime().maxMemory(), ContainerMemory.snapshot().limitBytes());
|
||||
}
|
||||
|
||||
static long memoryBudgetBytes(long heapBytes, long containerBytes) {
|
||||
return Math.min(heapBytes, containerBytes - containerBytes / 4L);
|
||||
}
|
||||
|
||||
static double pressureFraction(double heapFraction, double containerFraction) {
|
||||
if (containerFraction >= CONTAINER_HIGH_WATER) {
|
||||
return Math.max(heapFraction, HIGH_WATER + (containerFraction - CONTAINER_HIGH_WATER));
|
||||
}
|
||||
double containerPressure = containerFraction <= 0D ? 0D
|
||||
: LOW_WATER + (containerFraction - CONTAINER_LOW_WATER)
|
||||
* (HIGH_WATER - LOW_WATER) / (CONTAINER_HIGH_WATER - CONTAINER_LOW_WATER);
|
||||
return Math.max(heapFraction, containerPressure);
|
||||
}
|
||||
|
||||
public static String describe() {
|
||||
ContainerMemory.Snapshot container = ContainerMemory.snapshot();
|
||||
String heap = Math.round(heapUsedFraction() * 100D) + "% heap";
|
||||
return container.supported()
|
||||
? heap + ", " + Math.round(container.pressureFraction() * 100D) + "% container working memory"
|
||||
: heap;
|
||||
}
|
||||
|
||||
private static double heapUsedFraction() {
|
||||
MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
|
||||
long max = heap.getMax();
|
||||
if (max <= 0L) {
|
||||
@@ -71,7 +110,9 @@ public final class MantleHeapPressure {
|
||||
}
|
||||
|
||||
public static boolean overHighWater() {
|
||||
return PRESSURE_GATE.update(usedFraction());
|
||||
boolean heapPressure = PRESSURE_GATE.update(heapUsedFraction());
|
||||
boolean containerPressure = CONTAINER_PRESSURE_GATE.update(ContainerMemory.snapshot().pressureFraction());
|
||||
return heapPressure || containerPressure;
|
||||
}
|
||||
|
||||
public static double reclaimUrgency(double fraction) {
|
||||
@@ -86,7 +127,7 @@ public final class MantleHeapPressure {
|
||||
|
||||
public static void requestPanicReclaim() {
|
||||
double fraction = usedFraction();
|
||||
PANIC_RECLAIMER.request(fraction, PRESSURE_GATE.update(fraction));
|
||||
PANIC_RECLAIMER.request(fraction, overHighWater());
|
||||
}
|
||||
|
||||
static void invokeHotSpotDiagnosticGc(MBeanServer server) throws Exception {
|
||||
|
||||
@@ -215,16 +215,16 @@ public final class PregenMantleBackpressure {
|
||||
if (elapsed >= nextWarningElapsed) {
|
||||
nextWarningElapsed = elapsed + warningInterval;
|
||||
lastLog = logNow;
|
||||
IrisLogging.warn("Pregen heap pressure is still blocking new chunk submissions after " + elapsed + "ms at "
|
||||
+ Math.round(MantleHeapPressure.usedFraction() * 100.0D) + "% heap; waiting for headroom. "
|
||||
IrisLogging.warn("Pregen memory pressure is still blocking new chunk submissions after " + elapsed + "ms at "
|
||||
+ MantleHeapPressure.describe() + "; waiting for headroom. "
|
||||
+ diagnostics.get());
|
||||
onBudgetTimeout.run();
|
||||
}
|
||||
|
||||
if (logNow - lastLog >= LOG_INTERVAL_MS) {
|
||||
lastLog = logNow;
|
||||
IrisLogging.info("Pregen heap pressure: pausing generation at "
|
||||
+ Math.round(MantleHeapPressure.usedFraction() * 100.0D) + "% heap; evicting tectonic plates and waiting for headroom"
|
||||
IrisLogging.info("Pregen memory pressure: pausing generation at "
|
||||
+ MantleHeapPressure.describe() + "; evicting tectonic plates and waiting for headroom"
|
||||
+ (mantle != null ? " (" + mantle.getLoadedRegionCount() + " plates resident)" : "") + ".");
|
||||
}
|
||||
|
||||
|
||||
@@ -289,6 +289,9 @@ public class J {
|
||||
runnable.run();
|
||||
return true;
|
||||
}
|
||||
if (!canSchedule()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (runRegionImmediate(world, chunkX, chunkZ, runnable)) {
|
||||
return true;
|
||||
@@ -383,6 +386,9 @@ public class J {
|
||||
if (delayTicks <= 0) {
|
||||
return runRegion(world, chunkX, chunkZ, runnable);
|
||||
}
|
||||
if (!canSchedule()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (runRegionDelayed(world, chunkX, chunkZ, runnable, delayTicks)) {
|
||||
return true;
|
||||
|
||||
Reference in new issue
Block a user