From 8ccaac5d548336c1d48d88cae6afabf81e7c299b Mon Sep 17 00:00:00 2001 From: ishland Date: Mon, 12 Jan 2026 00:38:14 +0800 Subject: [PATCH 1/5] new: batching ticket ticks --- .../org/popcraft/chunky/GenerationTask.java | 9 +- .../org/popcraft/chunky/platform/Batcher.java | 7 + .../org/popcraft/chunky/platform/World.java | 5 + .../platform/impl/AbstractNMSBatcher.java | 136 ++++++++++++++++++ .../chunky/platform/impl/NOOPBatcher.java | 18 +++ .../popcraft/chunky/mixin/ChunkMapMixin.java | 5 + .../chunky/mixin/MinecraftServerMixin.java | 2 +- .../chunky/platform/FabricBatcher.java | 31 ++++ .../popcraft/chunky/platform/FabricWorld.java | 42 ++++-- 9 files changed, 238 insertions(+), 17 deletions(-) create mode 100644 common/src/main/java/org/popcraft/chunky/platform/Batcher.java create mode 100644 common/src/main/java/org/popcraft/chunky/platform/impl/AbstractNMSBatcher.java create mode 100644 common/src/main/java/org/popcraft/chunky/platform/impl/NOOPBatcher.java create mode 100644 fabric/src/main/java/org/popcraft/chunky/platform/FabricBatcher.java diff --git a/common/src/main/java/org/popcraft/chunky/GenerationTask.java b/common/src/main/java/org/popcraft/chunky/GenerationTask.java index dfdc6ab22..cc10ee81d 100644 --- a/common/src/main/java/org/popcraft/chunky/GenerationTask.java +++ b/common/src/main/java/org/popcraft/chunky/GenerationTask.java @@ -6,6 +6,7 @@ import org.popcraft.chunky.event.task.GenerationTaskUpdateEvent; import org.popcraft.chunky.iterator.ChunkIterator; import org.popcraft.chunky.iterator.ChunkIteratorFactory; +import org.popcraft.chunky.platform.Batcher; import org.popcraft.chunky.platform.Sender; import org.popcraft.chunky.shape.Shape; import org.popcraft.chunky.shape.ShapeFactory; @@ -22,7 +23,7 @@ import java.util.concurrent.atomic.AtomicLong; public class GenerationTask implements Runnable { - private static final int MAX_WORKING_COUNT = Input.tryInteger(System.getProperty("chunky.maxWorkingCount")).orElse(50); + public static final int MAX_WORKING_COUNT = Input.tryInteger(System.getProperty("chunky.maxWorkingCount")).orElse(50); private static final double SAMPLE_INTERVAL = 1000d * Math.max(Input.tryInteger(System.getProperty("chunky.sampleInterval")).orElse(30), 30); private static final double SAMPLE_SUB_INTERVAL = SAMPLE_INTERVAL / 30; private final Chunky chunky; @@ -123,6 +124,8 @@ public void run() { } final Semaphore working = new Semaphore(MAX_WORKING_COUNT); final boolean forceLoadExistingChunks = chunky.getConfig().isForceLoadExistingChunks(); + final Batcher batcher = selection.world().getBatcher(); + batcher.resume(); startTime.set(System.currentTimeMillis()); while (!stopped && chunkIterator.hasNext()) { final ChunkCoordinate chunk = chunkIterator.next(); @@ -154,10 +157,14 @@ public void run() { return selection.world().getChunkAtAsync(chunk.x(), chunk.z()); } }).whenComplete((ignored, throwable) -> { + if (throwable != null) { + throwable.printStackTrace(); + } working.release(); update(chunk.x(), chunk.z(), true); }); } + batcher.shutdown(); if (stopped) { chunky.getServer().getConsole().sendMessagePrefixed(TranslationKey.TASK_STOPPED, selection.world().getName()); } else { diff --git a/common/src/main/java/org/popcraft/chunky/platform/Batcher.java b/common/src/main/java/org/popcraft/chunky/platform/Batcher.java new file mode 100644 index 000000000..b2ff637d1 --- /dev/null +++ b/common/src/main/java/org/popcraft/chunky/platform/Batcher.java @@ -0,0 +1,7 @@ +package org.popcraft.chunky.platform; + +public interface Batcher { + void resume(); + + void shutdown(); +} diff --git a/common/src/main/java/org/popcraft/chunky/platform/World.java b/common/src/main/java/org/popcraft/chunky/platform/World.java index 3c0270040..b0b618ef3 100644 --- a/common/src/main/java/org/popcraft/chunky/platform/World.java +++ b/common/src/main/java/org/popcraft/chunky/platform/World.java @@ -1,5 +1,6 @@ package org.popcraft.chunky.platform; +import org.popcraft.chunky.platform.impl.NOOPBatcher; import org.popcraft.chunky.platform.util.Location; import java.nio.file.Path; @@ -38,6 +39,10 @@ default CompletableFuture getElevationAtAsync(int x, int z) { Optional getDirectory(String name); + default Batcher getBatcher() { + return NOOPBatcher.INSTANCE; + } + default Optional getEntitiesDirectory() { return getDirectory("entities"); } diff --git a/common/src/main/java/org/popcraft/chunky/platform/impl/AbstractNMSBatcher.java b/common/src/main/java/org/popcraft/chunky/platform/impl/AbstractNMSBatcher.java new file mode 100644 index 000000000..59c610622 --- /dev/null +++ b/common/src/main/java/org/popcraft/chunky/platform/impl/AbstractNMSBatcher.java @@ -0,0 +1,136 @@ +package org.popcraft.chunky.platform.impl; + +import org.popcraft.chunky.GenerationTask; +import org.popcraft.chunky.platform.Batcher; +import org.popcraft.chunky.util.Input; + +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +public abstract class AbstractNMSBatcher implements Batcher { + public static final int BATCH_DIVISOR = Input.tryInteger(System.getProperty("chunky.batchDivisor")).orElse(4); + + protected final ConcurrentLinkedQueue ticketAddTasks = new ConcurrentLinkedQueue<>(); + protected final ArrayDeque futureFetchTasks = new ArrayDeque<>(); + protected final ArrayDeque ticketRemoveTasks = new ArrayDeque<>(); + protected final Executor ticketAddExecutor = command -> { + if (this.shutdown) { + this.executeSyncRaw(command); + } else { + this.ticketAddTasks.add(command); + this.checkScheduleAsync(); + } + }; + protected final Executor futureFetchExecutor = command -> { + if (this.shutdown) { + this.executeSyncRaw(() -> { + this.tickTickets(); // execute because no more batches + command.run(); + }); + } else { + this.futureFetchTasks.add(command); + // it is expected that tasks in ticketAddExecutor to schedule here, don't schedule again + } + }; + protected final Executor ticketRemoveExecutor = command -> { + if (this.shutdown) { + this.executeSyncRaw(command); + } else { + this.ticketRemoveTasks.add(command); + this.checkScheduleSync(); + } + }; + protected final int batchMinSize; + protected final AtomicBoolean scheduled = new AtomicBoolean(false); + protected volatile boolean shutdown = true; + + public AbstractNMSBatcher(int maxWorkingCount) { + this.batchMinSize = maxWorkingCount / BATCH_DIVISOR; + } + + public AbstractNMSBatcher() { + this(GenerationTask.MAX_WORKING_COUNT); + } + + protected abstract void tickTickets(); + + protected abstract void executeSyncRaw(Runnable command); + + protected void runImpl() { + boolean wasShutdown = this.shutdown; + try { + drainQueue(this.ticketAddTasks); + drainQueue(this.ticketRemoveTasks); + this.tickTickets(); + drainQueue(this.futureFetchTasks); + } finally { + this.scheduled.set(false); + } + this.checkScheduleAsync(); + this.checkScheduleSync(); + if (wasShutdown ^ this.shutdown) { + this.schedule(); // flush the last raced tasks + } + } + + private void schedule() { + if (this.scheduled.compareAndSet(false, true)) { + this.executeSyncRaw(this::runImpl); + } + } + + private void checkScheduleAsync() { + if (this.ticketAddTasks.size() >= this.batchMinSize) { + this.schedule(); + } + } + + private void checkScheduleSync() { + if (this.ticketRemoveTasks.size() >= this.batchMinSize) { // no futureFetchTasks check, see comments above + this.schedule(); + } + } + + @Override + public void shutdown() { + if (this.shutdown) { + throw new IllegalStateException("Batcher is already shutdown"); + } + this.shutdown = true; + this.schedule(); + } + + @Override + public void resume() { + if (!this.shutdown) { + throw new IllegalStateException("Batcher is running"); + } + this.shutdown = false; + } + + public Executor getTicketAddExecutor() { + return this.ticketAddExecutor; + } + + public Executor getFutureFetchExecutor() { + return this.futureFetchExecutor; + } + + public Executor getTicketRemoveExecutor() { + return this.ticketRemoveExecutor; + } + + protected static void drainQueue(Queue queue) { + Runnable r; + while ((r = queue.poll()) != null) { + try { + r.run(); + } catch (Throwable t) { + t.printStackTrace(); + } + } + } +} diff --git a/common/src/main/java/org/popcraft/chunky/platform/impl/NOOPBatcher.java b/common/src/main/java/org/popcraft/chunky/platform/impl/NOOPBatcher.java new file mode 100644 index 000000000..f821860b0 --- /dev/null +++ b/common/src/main/java/org/popcraft/chunky/platform/impl/NOOPBatcher.java @@ -0,0 +1,18 @@ +package org.popcraft.chunky.platform.impl; + +import org.popcraft.chunky.platform.Batcher; + +public class NOOPBatcher implements Batcher { + public static final NOOPBatcher INSTANCE = new NOOPBatcher(); + + private NOOPBatcher() { + } + + @Override + public void resume() { + } + + @Override + public void shutdown() { + } +} diff --git a/fabric/src/main/java/org/popcraft/chunky/mixin/ChunkMapMixin.java b/fabric/src/main/java/org/popcraft/chunky/mixin/ChunkMapMixin.java index dbc4d9d58..0b5207f2f 100644 --- a/fabric/src/main/java/org/popcraft/chunky/mixin/ChunkMapMixin.java +++ b/fabric/src/main/java/org/popcraft/chunky/mixin/ChunkMapMixin.java @@ -3,8 +3,10 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkMap; +import net.minecraft.util.thread.BlockableEventLoop; import net.minecraft.world.level.ChunkPos; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; import org.spongepowered.asm.mixin.gen.Invoker; import java.util.Optional; @@ -22,4 +24,7 @@ public interface ChunkMapMixin { @Invoker void invokeTick(BooleanSupplier booleanSupplier); + + @Accessor + BlockableEventLoop getMainThreadExecutor(); } diff --git a/fabric/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java b/fabric/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java index 093bf870c..b53d8c95b 100644 --- a/fabric/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java +++ b/fabric/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java @@ -32,7 +32,7 @@ private void tickPaused(BooleanSupplier booleanSupplier, CallbackInfo ci) { public void chunky$runChunkSystemHousekeeping(BooleanSupplier haveTime) { if (this.chunky$needChunkSystemHousekeeping.compareAndSet(true, false)) { for (ServerLevel level : this.getAllLevels()) { - ((ChunkMapMixin) level.getChunkSource().chunkMap).invokeTick(haveTime); + ((ChunkMapMixin) level.getChunkSource().chunkMap).invokeTick(() -> true); // push the vanilla chunk system to unload unneeded chunks ASAP if (!ChunkyFabric.ENABLE_MOONRISE_WORKAROUNDS) { // note: Moonrise destroys the vanilla entity system, so skip it here if it's present ((ServerLevelMixin) level).getEntityManager().tick(); diff --git a/fabric/src/main/java/org/popcraft/chunky/platform/FabricBatcher.java b/fabric/src/main/java/org/popcraft/chunky/platform/FabricBatcher.java new file mode 100644 index 000000000..93ce51fc1 --- /dev/null +++ b/fabric/src/main/java/org/popcraft/chunky/platform/FabricBatcher.java @@ -0,0 +1,31 @@ +package org.popcraft.chunky.platform; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.popcraft.chunky.mixin.ServerChunkCacheMixin; +import org.popcraft.chunky.platform.impl.AbstractNMSBatcher; + +public class FabricBatcher extends AbstractNMSBatcher { + private final ServerLevel world; + + public FabricBatcher(int maxWorkingCount, ServerLevel world) { + super(maxWorkingCount); + this.world = world; + } + + public FabricBatcher(ServerLevel world) { + super(); + this.world = world; + } + + @Override + protected void tickTickets() { + ((ServerChunkCacheMixin) this.world.getChunkSource()).invokeRunDistanceManagerUpdates(); + } + + @Override + protected void executeSyncRaw(Runnable command) { + MinecraftServer server = this.world.getServer(); + server.schedule(server.wrapRunnable(command)); + } +} diff --git a/fabric/src/main/java/org/popcraft/chunky/platform/FabricWorld.java b/fabric/src/main/java/org/popcraft/chunky/platform/FabricWorld.java index 4d2bedc46..550f2cecf 100644 --- a/fabric/src/main/java/org/popcraft/chunky/platform/FabricWorld.java +++ b/fabric/src/main/java/org/popcraft/chunky/platform/FabricWorld.java @@ -42,10 +42,12 @@ public class FabricWorld implements World { private static final boolean UPDATE_CHUNK_NBT = Boolean.getBoolean("chunky.updateChunkNbt"); private final ServerLevel world; private final Border worldBorder; + private final FabricBatcher batcher; public FabricWorld(final ServerLevel world) { this.world = world; this.worldBorder = new FabricBorder(world.getWorldBorder()); + this.batcher = new FabricBatcher(world); } @Override @@ -95,7 +97,7 @@ public CompletableFuture isChunkGenerated(final int x, final int z) { @Override public CompletableFuture getChunkAtAsync(final int x, final int z) { if (Thread.currentThread() != world.getServer().getRunningThread()) { - return CompletableFuture.supplyAsync(() -> getChunkAtAsync(x, z), world.getServer()).thenCompose(Function.identity()); + return CompletableFuture.supplyAsync(() -> getChunkAtAsync(x, z), this.batcher.getTicketAddExecutor()).thenCompose(Function.identity()); } else { final ChunkPos chunkPos = new ChunkPos(x, z); final ServerChunkCache serverChunkCache = world.getChunkSource(); @@ -103,20 +105,25 @@ public CompletableFuture getChunkAtAsync(final int x, final int z) { if (TICKING_LOAD_DURATION > 0) { serverChunkCache.addTicketWithRadius(CHUNKY_TICKING, chunkPos, 1); } - ((ServerChunkCacheMixin) serverChunkCache).invokeRunDistanceManagerUpdates(); - // note: when Moonrise is present, holders do not get created most of the time even after explicit distance manager update - // so we force `create = true` *only if* Moonrise is present, as it breaks pausing for everyone else - boolean create = ChunkyFabric.ENABLE_MOONRISE_WORKAROUNDS; - return ((ServerChunkCacheMixin) world.getChunkSource()).invokeGetChunkFutureMainThread(x, z, ChunkStatus.FULL, create) - .whenCompleteAsync((ignored, throwable) -> { - serverChunkCache.removeTicketWithRadius(CHUNKY, chunkPos, 0); - ((MinecraftServerExtension) world.getServer()).chunky$markChunkSystemHousekeeping(); - if (ChunkyFabric.ENABLE_MOONRISE_WORKAROUNDS) { - // note: to prevent pausing on dedicated server when Moonrise is present - ((MinecraftServerAccess) world.getServer()).setEmptyTicks(0); - } - }, world.getServer()) - .thenApply(ignored -> null); + return CompletableFuture.supplyAsync(() -> { + // note: when Moonrise is present, holders do not get created most of the time even after explicit distance manager update + // so we force `create = true` *only if* Moonrise is present, as it breaks pausing for everyone else + boolean create = ChunkyFabric.ENABLE_MOONRISE_WORKAROUNDS; + return ((ServerChunkCacheMixin) world.getChunkSource()).invokeGetChunkFutureMainThread(x, z, ChunkStatus.FULL, create) + .thenApplyAsync(Function.identity(), ((ChunkMapMixin) serverChunkCache.chunkMap).getMainThreadExecutor()) // workaround to prevent memory leaks in vanilla chunk system + .whenCompleteAsync((ignored, throwable) -> { + if (throwable != null) { + throwable.printStackTrace(); + } + serverChunkCache.removeTicketWithRadius(CHUNKY, chunkPos, 0); + ((MinecraftServerExtension) world.getServer()).chunky$markChunkSystemHousekeeping(); + if (ChunkyFabric.ENABLE_MOONRISE_WORKAROUNDS) { + // note: to prevent pausing on dedicated server when Moonrise is present + ((MinecraftServerAccess) world.getServer()).setEmptyTicks(0); + } + }, this.batcher.getTicketRemoveExecutor()) + .thenApply(ignored -> (Void) null); + }, this.batcher.getFutureFetchExecutor()).thenCompose(Function.identity()); } } @@ -193,6 +200,11 @@ public Optional getDirectory(final String name) { return Files.exists(directory) ? Optional.of(directory) : Optional.empty(); } + @Override + public Batcher getBatcher() { + return this.batcher; + } + public ServerLevel getWorld() { return world; } From b05af24392605fbe9072465e1aa2d5dd48b53989 Mon Sep 17 00:00:00 2001 From: ishland Date: Thu, 22 Jan 2026 18:32:50 +0800 Subject: [PATCH 2/5] change: port to neoforge --- .../chunky/mixin/MinecraftServerMixin.java | 2 +- .../chunky/platform/NeoForgeBatcher.java | 30 +++++++++++++++ .../chunky/platform/NeoForgeWorld.java | 37 +++++++++++-------- .../resources/META-INF/accesstransformer.cfg | 1 + 4 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeBatcher.java diff --git a/neoforge/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java b/neoforge/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java index 84c19ae45..d03b2fa44 100644 --- a/neoforge/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java +++ b/neoforge/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java @@ -31,7 +31,7 @@ private void tickPaused(BooleanSupplier booleanSupplier, CallbackInfo ci) { public void chunky$runChunkSystemHousekeeping(BooleanSupplier haveTime) { if (this.chunky$needChunkSystemHousekeeping.compareAndSet(true, false)) { for (ServerLevel level : this.getAllLevels()) { - level.getChunkSource().chunkMap.tick(haveTime); + level.getChunkSource().chunkMap.tick(() -> true); // push the vanilla chunk system to unload unneeded chunks ASAP if (!ChunkyNeoForge.ENABLE_MOONRISE_WORKAROUNDS) { // note: Moonrise destroys the vanilla entity system, so skip it here if it's present level.entityManager.tick(); diff --git a/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeBatcher.java b/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeBatcher.java new file mode 100644 index 000000000..505fff428 --- /dev/null +++ b/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeBatcher.java @@ -0,0 +1,30 @@ +package org.popcraft.chunky.platform; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.popcraft.chunky.platform.impl.AbstractNMSBatcher; + +public class NeoForgeBatcher extends AbstractNMSBatcher { + private final ServerLevel world; + + public NeoForgeBatcher(int maxWorkingCount, ServerLevel world) { + super(maxWorkingCount); + this.world = world; + } + + public NeoForgeBatcher(ServerLevel world) { + super(); + this.world = world; + } + + @Override + protected void tickTickets() { + this.world.getChunkSource().runDistanceManagerUpdates(); + } + + @Override + protected void executeSyncRaw(Runnable command) { + MinecraftServer server = this.world.getServer(); + server.schedule(server.wrapRunnable(command)); + } +} diff --git a/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeWorld.java b/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeWorld.java index d8e375fda..327b5d018 100644 --- a/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeWorld.java +++ b/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeWorld.java @@ -39,10 +39,12 @@ public class NeoForgeWorld implements World { private static final boolean UPDATE_CHUNK_NBT = Boolean.getBoolean("chunky.updateChunkNbt"); private final ServerLevel world; private final Border worldBorder; + private final NeoForgeBatcher batcher; public NeoForgeWorld(final ServerLevel world) { this.world = world; this.worldBorder = new NeoForgeBorder(world.getWorldBorder()); + this.batcher = new NeoForgeBatcher(world); } @Override @@ -91,7 +93,7 @@ public CompletableFuture isChunkGenerated(final int x, final int z) { @Override public CompletableFuture getChunkAtAsync(final int x, final int z) { if (Thread.currentThread() != world.getServer().getRunningThread()) { - return CompletableFuture.supplyAsync(() -> getChunkAtAsync(x, z), world.getServer()).thenCompose(Function.identity()); + return CompletableFuture.supplyAsync(() -> getChunkAtAsync(x, z), this.batcher.getTicketAddExecutor()).thenCompose(Function.identity()); } else { final ChunkPos chunkPos = new ChunkPos(x, z); final ServerChunkCache serverChunkCache = world.getChunkSource(); @@ -99,20 +101,25 @@ public CompletableFuture getChunkAtAsync(final int x, final int z) { if (TICKING_LOAD_DURATION > 0) { serverChunkCache.addTicketWithRadius(CHUNKY_TICKING, chunkPos, 1); } - serverChunkCache.runDistanceManagerUpdates(); - // note: when Moonrise is present, holders do not get created most of the time even after explicit distance manager update - // so we force `create = true` *only if* Moonrise is present, as it breaks pausing for everyone else - boolean create = ChunkyNeoForge.ENABLE_MOONRISE_WORKAROUNDS; - return serverChunkCache.getChunkFutureMainThread(x, z, ChunkStatus.FULL, create) - .whenCompleteAsync((ignored, throwable) -> { - serverChunkCache.removeTicketWithRadius(CHUNKY, chunkPos, 0); - ((MinecraftServerExtension) world.getServer()).chunky$markChunkSystemHousekeeping(); - if (ChunkyNeoForge.ENABLE_MOONRISE_WORKAROUNDS) { - // note: to prevent pausing on dedicated server when Moonrise is present - world.getServer().emptyTicks = 0; - } - }, world.getServer()) - .thenApply(ignored -> null); + return CompletableFuture.supplyAsync(() -> { + // note: when Moonrise is present, holders do not get created most of the time even after explicit distance manager update + // so we force `create = true` *only if* Moonrise is present, as it breaks pausing for everyone else + boolean create = ChunkyNeoForge.ENABLE_MOONRISE_WORKAROUNDS; + return world.getChunkSource().getChunkFutureMainThread(x, z, ChunkStatus.FULL, create) + .thenApplyAsync(Function.identity(), serverChunkCache.chunkMap.mainThreadExecutor) // workaround to prevent memory leaks in vanilla chunk system + .whenCompleteAsync((ignored, throwable) -> { + if (throwable != null) { + throwable.printStackTrace(); + } + serverChunkCache.removeTicketWithRadius(CHUNKY, chunkPos, 0); + ((MinecraftServerExtension) world.getServer()).chunky$markChunkSystemHousekeeping(); + if (ChunkyNeoForge.ENABLE_MOONRISE_WORKAROUNDS) { + // note: to prevent pausing on dedicated server when Moonrise is present + world.getServer().emptyTicks = 0; + } + }, this.batcher.getTicketRemoveExecutor()) + .thenApply(ignored -> (Void) null); + }, this.batcher.getFutureFetchExecutor()).thenCompose(Function.identity()); } } diff --git a/neoforge/src/main/resources/META-INF/accesstransformer.cfg b/neoforge/src/main/resources/META-INF/accesstransformer.cfg index a56529a85..c1075e870 100644 --- a/neoforge/src/main/resources/META-INF/accesstransformer.cfg +++ b/neoforge/src/main/resources/META-INF/accesstransformer.cfg @@ -4,6 +4,7 @@ public net.minecraft.server.MinecraftServer emptyTicks public net.minecraft.server.level.ChunkMap readChunk(Lnet/minecraft/world/level/ChunkPos;)Ljava/util/concurrent/CompletableFuture; public net.minecraft.server.level.ChunkMap getVisibleChunkIfPresent(J)Lnet/minecraft/server/level/ChunkHolder; public net.minecraft.server.level.ChunkMap tick(Ljava/util/function/BooleanSupplier;)V +public net.minecraft.server.level.ChunkMap mainThreadExecutor # ServerChunkCache public net.minecraft.server.level.ServerChunkCache getChunkFutureMainThread(IILnet/minecraft/world/level/chunk/status/ChunkStatus;Z)Ljava/util/concurrent/CompletableFuture; public net.minecraft.server.level.ServerChunkCache runDistanceManagerUpdates()Z From 8d8b43da4dec15890c5539707cd39a95f53760f6 Mon Sep 17 00:00:00 2001 From: ishland Date: Thu, 22 Jan 2026 19:07:45 +0800 Subject: [PATCH 3/5] change: port to forge --- .../java/org/popcraft/chunky/ChunkyForge.java | 1 + .../chunky/platform/ForgeBatcher.java | 30 +++++++++++++++++ .../popcraft/chunky/platform/ForgeWorld.java | 33 +++++++++++++------ .../resources/META-INF/accesstransformer.cfg | 2 ++ 4 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 forge/src/main/java/org/popcraft/chunky/platform/ForgeBatcher.java diff --git a/forge/src/main/java/org/popcraft/chunky/ChunkyForge.java b/forge/src/main/java/org/popcraft/chunky/ChunkyForge.java index 1d937f230..9fb278e32 100644 --- a/forge/src/main/java/org/popcraft/chunky/ChunkyForge.java +++ b/forge/src/main/java/org/popcraft/chunky/ChunkyForge.java @@ -46,6 +46,7 @@ @Mod(ChunkyForge.MOD_ID) public class ChunkyForge { public static final String MOD_ID = "chunky"; + public static final boolean ENABLE_MOONRISE_WORKAROUNDS = false; private Chunky chunky; private final Map bossBars = new ConcurrentHashMap<>(); diff --git a/forge/src/main/java/org/popcraft/chunky/platform/ForgeBatcher.java b/forge/src/main/java/org/popcraft/chunky/platform/ForgeBatcher.java new file mode 100644 index 000000000..72abc0ce6 --- /dev/null +++ b/forge/src/main/java/org/popcraft/chunky/platform/ForgeBatcher.java @@ -0,0 +1,30 @@ +package org.popcraft.chunky.platform; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.popcraft.chunky.platform.impl.AbstractNMSBatcher; + +public class ForgeBatcher extends AbstractNMSBatcher { + private final ServerLevel world; + + public ForgeBatcher(int maxWorkingCount, ServerLevel world) { + super(maxWorkingCount); + this.world = world; + } + + public ForgeBatcher(ServerLevel world) { + super(); + this.world = world; + } + + @Override + protected void tickTickets() { + this.world.getChunkSource().runDistanceManagerUpdates(); + } + + @Override + protected void executeSyncRaw(Runnable command) { + MinecraftServer server = this.world.getServer(); + server.schedule(server.wrapRunnable(command)); + } +} diff --git a/forge/src/main/java/org/popcraft/chunky/platform/ForgeWorld.java b/forge/src/main/java/org/popcraft/chunky/platform/ForgeWorld.java index ef263c2c3..4c93897eb 100644 --- a/forge/src/main/java/org/popcraft/chunky/platform/ForgeWorld.java +++ b/forge/src/main/java/org/popcraft/chunky/platform/ForgeWorld.java @@ -20,6 +20,7 @@ import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.storage.LevelResource; +import org.popcraft.chunky.ChunkyForge; import org.popcraft.chunky.ducks.MinecraftServerExtension; import org.popcraft.chunky.platform.util.Location; import org.popcraft.chunky.util.Input; @@ -38,10 +39,12 @@ public class ForgeWorld implements World { private static final boolean UPDATE_CHUNK_NBT = Boolean.getBoolean("chunky.updateChunkNbt"); private final ServerLevel world; private final Border worldBorder; + private final ForgeBatcher batcher; public ForgeWorld(final ServerLevel world) { this.world = world; this.worldBorder = new ForgeBorder(world.getWorldBorder()); + this.batcher = new ForgeBatcher(world); } @Override @@ -94,7 +97,7 @@ public CompletableFuture isChunkGenerated(final int x, final int z) { @Override public CompletableFuture getChunkAtAsync(final int x, final int z) { if (Thread.currentThread() != world.getServer().getRunningThread()) { - return CompletableFuture.supplyAsync(() -> getChunkAtAsync(x, z), world.getServer()).thenCompose(Function.identity()); + return CompletableFuture.supplyAsync(() -> getChunkAtAsync(x, z), this.batcher.getTicketAddExecutor()).thenCompose(Function.identity()); } else { final ChunkPos chunkPos = new ChunkPos(x, z); final ServerChunkCache serverChunkCache = world.getChunkSource(); @@ -102,15 +105,25 @@ public CompletableFuture getChunkAtAsync(final int x, final int z) { if (TICKING_LOAD_DURATION > 0) { serverChunkCache.addTicketWithRadius(CHUNKY_TICKING, chunkPos, 1); } - serverChunkCache.runDistanceManagerUpdates(); - final ChunkMap chunkManager = serverChunkCache.chunkMap; - final ChunkHolder chunkHolder = chunkManager.getVisibleChunkIfPresent(chunkPos.toLong()); - final CompletableFuture chunkFuture = chunkHolder == null ? CompletableFuture.completedFuture(null) : CompletableFuture.allOf(chunkHolder.scheduleChunkGenerationTask(ChunkStatus.FULL, chunkManager)); - chunkFuture.whenCompleteAsync((ignored, throwable) -> { - serverChunkCache.removeTicketWithRadius(CHUNKY, chunkPos, 0); - ((MinecraftServerExtension) world.getServer()).chunky$markChunkSystemHousekeeping(); - }, world.getServer()); - return chunkFuture; + return CompletableFuture.supplyAsync(() -> { + // note: when Moonrise is present, holders do not get created most of the time even after explicit distance manager update + // so we force `create = true` *only if* Moonrise is present, as it breaks pausing for everyone else + boolean create = ChunkyForge.ENABLE_MOONRISE_WORKAROUNDS; + return world.getChunkSource().getChunkFutureMainThread(x, z, ChunkStatus.FULL, create) + .thenApplyAsync(Function.identity(), serverChunkCache.chunkMap.mainThreadExecutor) // workaround to prevent memory leaks in vanilla chunk system + .whenCompleteAsync((ignored, throwable) -> { + if (throwable != null) { + throwable.printStackTrace(); + } + serverChunkCache.removeTicketWithRadius(CHUNKY, chunkPos, 0); + ((MinecraftServerExtension) world.getServer()).chunky$markChunkSystemHousekeeping(); + if (ChunkyForge.ENABLE_MOONRISE_WORKAROUNDS) { + // note: to prevent pausing on dedicated server when Moonrise is present + world.getServer().emptyTicks = 0; + } + }, this.batcher.getTicketRemoveExecutor()) + .thenApply(ignored -> (Void) null); + }, this.batcher.getFutureFetchExecutor()).thenCompose(Function.identity()); } } diff --git a/forge/src/main/resources/META-INF/accesstransformer.cfg b/forge/src/main/resources/META-INF/accesstransformer.cfg index 2e4f59033..c89a3e928 100644 --- a/forge/src/main/resources/META-INF/accesstransformer.cfg +++ b/forge/src/main/resources/META-INF/accesstransformer.cfg @@ -5,7 +5,9 @@ public net.minecraft.server.level.ChunkMap readChunk(Lnet/minecraft/world/level/ public net.minecraft.server.level.ChunkMap getVisibleChunkIfPresent(J)Lnet/minecraft/server/level/ChunkHolder; public net.minecraft.server.level.ChunkMap pendingUnloads public net.minecraft.server.level.ChunkMap tick(Ljava/util/function/BooleanSupplier;)V +public net.minecraft.server.level.ChunkMap mainThreadExecutor # ServerChunkCache public net.minecraft.server.level.ServerChunkCache runDistanceManagerUpdates()Z +public net.minecraft.server.level.ServerChunkCache getChunkFutureMainThread(IILnet/minecraft/world/level/chunk/status/ChunkStatus;Z)Ljava/util/concurrent/CompletableFuture; # ServerLevel public net.minecraft.server.level.ServerLevel entityManager From 5c128c66a1876a6e64b5f70065f6446b6860823c Mon Sep 17 00:00:00 2001 From: ishland Date: Fri, 23 Jan 2026 21:42:41 +0800 Subject: [PATCH 4/5] fix: broadcast changes memory leak --- .../java/org/popcraft/chunky/mixin/MinecraftServerMixin.java | 2 ++ .../java/org/popcraft/chunky/mixin/ServerChunkCacheMixin.java | 4 ++++ .../java/org/popcraft/chunky/mixin/MinecraftServerMixin.java | 2 ++ forge/src/main/resources/META-INF/accesstransformer.cfg | 1 + .../java/org/popcraft/chunky/mixin/MinecraftServerMixin.java | 2 ++ neoforge/src/main/resources/META-INF/accesstransformer.cfg | 1 + 6 files changed, 12 insertions(+) diff --git a/fabric/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java b/fabric/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java index b53d8c95b..0f19c5458 100644 --- a/fabric/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java +++ b/fabric/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java @@ -2,6 +2,7 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.profiling.InactiveProfiler; import org.popcraft.chunky.ChunkyFabric; import org.popcraft.chunky.ChunkyProvider; import org.popcraft.chunky.ducks.MinecraftServerExtension; @@ -33,6 +34,7 @@ private void tickPaused(BooleanSupplier booleanSupplier, CallbackInfo ci) { if (this.chunky$needChunkSystemHousekeeping.compareAndSet(true, false)) { for (ServerLevel level : this.getAllLevels()) { ((ChunkMapMixin) level.getChunkSource().chunkMap).invokeTick(() -> true); // push the vanilla chunk system to unload unneeded chunks ASAP + ((ServerChunkCacheMixin) level.getChunkSource()).invokeBroadcastChangedChunks(InactiveProfiler.INSTANCE); if (!ChunkyFabric.ENABLE_MOONRISE_WORKAROUNDS) { // note: Moonrise destroys the vanilla entity system, so skip it here if it's present ((ServerLevelMixin) level).getEntityManager().tick(); diff --git a/fabric/src/main/java/org/popcraft/chunky/mixin/ServerChunkCacheMixin.java b/fabric/src/main/java/org/popcraft/chunky/mixin/ServerChunkCacheMixin.java index 4d8a149c6..65a6b77f3 100644 --- a/fabric/src/main/java/org/popcraft/chunky/mixin/ServerChunkCacheMixin.java +++ b/fabric/src/main/java/org/popcraft/chunky/mixin/ServerChunkCacheMixin.java @@ -2,6 +2,7 @@ import net.minecraft.server.level.ChunkResult; import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.status.ChunkStatus; import org.spongepowered.asm.mixin.Mixin; @@ -20,4 +21,7 @@ public CompletableFuture> invokeGetChunkFutureMainThrea @Invoker boolean invokeRunDistanceManagerUpdates(); + + @Invoker + void invokeBroadcastChangedChunks(ProfilerFiller arg); } diff --git a/forge/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java b/forge/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java index e9242b457..8cfaef7bd 100644 --- a/forge/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java +++ b/forge/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java @@ -2,6 +2,7 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.profiling.InactiveProfiler; import org.popcraft.chunky.ducks.MinecraftServerExtension; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -31,6 +32,7 @@ private void tickPaused(BooleanSupplier booleanSupplier, CallbackInfo ci) { if (this.chunky$needChunkSystemHousekeeping.compareAndSet(true, false)) { for (ServerLevel level : this.getAllLevels()) { level.getChunkSource().chunkMap.tick(haveTime); + level.getChunkSource().broadcastChangedChunks(InactiveProfiler.INSTANCE); level.entityManager.tick(); } } diff --git a/forge/src/main/resources/META-INF/accesstransformer.cfg b/forge/src/main/resources/META-INF/accesstransformer.cfg index c89a3e928..50837d068 100644 --- a/forge/src/main/resources/META-INF/accesstransformer.cfg +++ b/forge/src/main/resources/META-INF/accesstransformer.cfg @@ -6,6 +6,7 @@ public net.minecraft.server.level.ChunkMap getVisibleChunkIfPresent(J)Lnet/minec public net.minecraft.server.level.ChunkMap pendingUnloads public net.minecraft.server.level.ChunkMap tick(Ljava/util/function/BooleanSupplier;)V public net.minecraft.server.level.ChunkMap mainThreadExecutor +public net.minecraft.server.level.ServerChunkCache broadcastChangedChunks(Lnet/minecraft/util/profiling/ProfilerFiller;)V # ServerChunkCache public net.minecraft.server.level.ServerChunkCache runDistanceManagerUpdates()Z public net.minecraft.server.level.ServerChunkCache getChunkFutureMainThread(IILnet/minecraft/world/level/chunk/status/ChunkStatus;Z)Ljava/util/concurrent/CompletableFuture; diff --git a/neoforge/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java b/neoforge/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java index d03b2fa44..4c45ee26a 100644 --- a/neoforge/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java +++ b/neoforge/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java @@ -2,6 +2,7 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.profiling.InactiveProfiler; import org.popcraft.chunky.ChunkyNeoForge; import org.popcraft.chunky.ducks.MinecraftServerExtension; import org.spongepowered.asm.mixin.Mixin; @@ -32,6 +33,7 @@ private void tickPaused(BooleanSupplier booleanSupplier, CallbackInfo ci) { if (this.chunky$needChunkSystemHousekeeping.compareAndSet(true, false)) { for (ServerLevel level : this.getAllLevels()) { level.getChunkSource().chunkMap.tick(() -> true); // push the vanilla chunk system to unload unneeded chunks ASAP + level.getChunkSource().broadcastChangedChunks(InactiveProfiler.INSTANCE); if (!ChunkyNeoForge.ENABLE_MOONRISE_WORKAROUNDS) { // note: Moonrise destroys the vanilla entity system, so skip it here if it's present level.entityManager.tick(); diff --git a/neoforge/src/main/resources/META-INF/accesstransformer.cfg b/neoforge/src/main/resources/META-INF/accesstransformer.cfg index c1075e870..ee627ef9c 100644 --- a/neoforge/src/main/resources/META-INF/accesstransformer.cfg +++ b/neoforge/src/main/resources/META-INF/accesstransformer.cfg @@ -5,6 +5,7 @@ public net.minecraft.server.level.ChunkMap readChunk(Lnet/minecraft/world/level/ public net.minecraft.server.level.ChunkMap getVisibleChunkIfPresent(J)Lnet/minecraft/server/level/ChunkHolder; public net.minecraft.server.level.ChunkMap tick(Ljava/util/function/BooleanSupplier;)V public net.minecraft.server.level.ChunkMap mainThreadExecutor +public net.minecraft.server.level.ServerChunkCache broadcastChangedChunks(Lnet/minecraft/util/profiling/ProfilerFiller;)V # ServerChunkCache public net.minecraft.server.level.ServerChunkCache getChunkFutureMainThread(IILnet/minecraft/world/level/chunk/status/ChunkStatus;Z)Ljava/util/concurrent/CompletableFuture; public net.minecraft.server.level.ServerChunkCache runDistanceManagerUpdates()Z From 96a14ee1e8b8afef2dc25ca52c0c892714382f87 Mon Sep 17 00:00:00 2001 From: Christopher Bohn Date: Sun, 25 Jan 2026 19:40:34 -0800 Subject: [PATCH 5/5] refactor --- .../org/popcraft/chunky/GenerationTask.java | 3 - .../org/popcraft/chunky/platform/World.java | 10 +- .../platform/impl/AbstractNMSBatcher.java | 136 ------------------ .../impl/batcher/AbstractBatcher.java | 114 +++++++++++++++ .../impl/{ => batcher}/NOOPBatcher.java | 2 +- .../chunky/mixin/MinecraftServerMixin.java | 1 - .../chunky/platform/FabricBatcher.java | 16 +-- .../popcraft/chunky/platform/FabricWorld.java | 5 +- .../chunky/platform/ForgeBatcher.java | 16 +-- .../popcraft/chunky/platform/ForgeWorld.java | 5 +- .../chunky/platform/NeoForgeBatcher.java | 16 +-- .../chunky/platform/NeoForgeWorld.java | 5 +- 12 files changed, 138 insertions(+), 191 deletions(-) delete mode 100644 common/src/main/java/org/popcraft/chunky/platform/impl/AbstractNMSBatcher.java create mode 100644 common/src/main/java/org/popcraft/chunky/platform/impl/batcher/AbstractBatcher.java rename common/src/main/java/org/popcraft/chunky/platform/impl/{ => batcher}/NOOPBatcher.java (85%) diff --git a/common/src/main/java/org/popcraft/chunky/GenerationTask.java b/common/src/main/java/org/popcraft/chunky/GenerationTask.java index cc10ee81d..c760b9988 100644 --- a/common/src/main/java/org/popcraft/chunky/GenerationTask.java +++ b/common/src/main/java/org/popcraft/chunky/GenerationTask.java @@ -157,9 +157,6 @@ public void run() { return selection.world().getChunkAtAsync(chunk.x(), chunk.z()); } }).whenComplete((ignored, throwable) -> { - if (throwable != null) { - throwable.printStackTrace(); - } working.release(); update(chunk.x(), chunk.z(), true); }); diff --git a/common/src/main/java/org/popcraft/chunky/platform/World.java b/common/src/main/java/org/popcraft/chunky/platform/World.java index b0b618ef3..7dc42c3ff 100644 --- a/common/src/main/java/org/popcraft/chunky/platform/World.java +++ b/common/src/main/java/org/popcraft/chunky/platform/World.java @@ -1,6 +1,6 @@ package org.popcraft.chunky.platform; -import org.popcraft.chunky.platform.impl.NOOPBatcher; +import org.popcraft.chunky.platform.impl.batcher.NOOPBatcher; import org.popcraft.chunky.platform.util.Location; import java.nio.file.Path; @@ -39,10 +39,6 @@ default CompletableFuture getElevationAtAsync(int x, int z) { Optional getDirectory(String name); - default Batcher getBatcher() { - return NOOPBatcher.INSTANCE; - } - default Optional getEntitiesDirectory() { return getDirectory("entities"); } @@ -54,4 +50,8 @@ default Optional getPOIDirectory() { default Optional getRegionDirectory() { return getDirectory("region"); } + + default Batcher getBatcher() { + return NOOPBatcher.INSTANCE; + } } diff --git a/common/src/main/java/org/popcraft/chunky/platform/impl/AbstractNMSBatcher.java b/common/src/main/java/org/popcraft/chunky/platform/impl/AbstractNMSBatcher.java deleted file mode 100644 index 59c610622..000000000 --- a/common/src/main/java/org/popcraft/chunky/platform/impl/AbstractNMSBatcher.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.popcraft.chunky.platform.impl; - -import org.popcraft.chunky.GenerationTask; -import org.popcraft.chunky.platform.Batcher; -import org.popcraft.chunky.util.Input; - -import java.util.ArrayDeque; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; - -public abstract class AbstractNMSBatcher implements Batcher { - public static final int BATCH_DIVISOR = Input.tryInteger(System.getProperty("chunky.batchDivisor")).orElse(4); - - protected final ConcurrentLinkedQueue ticketAddTasks = new ConcurrentLinkedQueue<>(); - protected final ArrayDeque futureFetchTasks = new ArrayDeque<>(); - protected final ArrayDeque ticketRemoveTasks = new ArrayDeque<>(); - protected final Executor ticketAddExecutor = command -> { - if (this.shutdown) { - this.executeSyncRaw(command); - } else { - this.ticketAddTasks.add(command); - this.checkScheduleAsync(); - } - }; - protected final Executor futureFetchExecutor = command -> { - if (this.shutdown) { - this.executeSyncRaw(() -> { - this.tickTickets(); // execute because no more batches - command.run(); - }); - } else { - this.futureFetchTasks.add(command); - // it is expected that tasks in ticketAddExecutor to schedule here, don't schedule again - } - }; - protected final Executor ticketRemoveExecutor = command -> { - if (this.shutdown) { - this.executeSyncRaw(command); - } else { - this.ticketRemoveTasks.add(command); - this.checkScheduleSync(); - } - }; - protected final int batchMinSize; - protected final AtomicBoolean scheduled = new AtomicBoolean(false); - protected volatile boolean shutdown = true; - - public AbstractNMSBatcher(int maxWorkingCount) { - this.batchMinSize = maxWorkingCount / BATCH_DIVISOR; - } - - public AbstractNMSBatcher() { - this(GenerationTask.MAX_WORKING_COUNT); - } - - protected abstract void tickTickets(); - - protected abstract void executeSyncRaw(Runnable command); - - protected void runImpl() { - boolean wasShutdown = this.shutdown; - try { - drainQueue(this.ticketAddTasks); - drainQueue(this.ticketRemoveTasks); - this.tickTickets(); - drainQueue(this.futureFetchTasks); - } finally { - this.scheduled.set(false); - } - this.checkScheduleAsync(); - this.checkScheduleSync(); - if (wasShutdown ^ this.shutdown) { - this.schedule(); // flush the last raced tasks - } - } - - private void schedule() { - if (this.scheduled.compareAndSet(false, true)) { - this.executeSyncRaw(this::runImpl); - } - } - - private void checkScheduleAsync() { - if (this.ticketAddTasks.size() >= this.batchMinSize) { - this.schedule(); - } - } - - private void checkScheduleSync() { - if (this.ticketRemoveTasks.size() >= this.batchMinSize) { // no futureFetchTasks check, see comments above - this.schedule(); - } - } - - @Override - public void shutdown() { - if (this.shutdown) { - throw new IllegalStateException("Batcher is already shutdown"); - } - this.shutdown = true; - this.schedule(); - } - - @Override - public void resume() { - if (!this.shutdown) { - throw new IllegalStateException("Batcher is running"); - } - this.shutdown = false; - } - - public Executor getTicketAddExecutor() { - return this.ticketAddExecutor; - } - - public Executor getFutureFetchExecutor() { - return this.futureFetchExecutor; - } - - public Executor getTicketRemoveExecutor() { - return this.ticketRemoveExecutor; - } - - protected static void drainQueue(Queue queue) { - Runnable r; - while ((r = queue.poll()) != null) { - try { - r.run(); - } catch (Throwable t) { - t.printStackTrace(); - } - } - } -} diff --git a/common/src/main/java/org/popcraft/chunky/platform/impl/batcher/AbstractBatcher.java b/common/src/main/java/org/popcraft/chunky/platform/impl/batcher/AbstractBatcher.java new file mode 100644 index 000000000..9e78dd55d --- /dev/null +++ b/common/src/main/java/org/popcraft/chunky/platform/impl/batcher/AbstractBatcher.java @@ -0,0 +1,114 @@ +package org.popcraft.chunky.platform.impl.batcher; + +import org.popcraft.chunky.GenerationTask; +import org.popcraft.chunky.platform.Batcher; +import org.popcraft.chunky.util.Input; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +public abstract class AbstractBatcher implements Batcher { + public static final int BATCH_DIVISOR = Input.tryInteger(System.getProperty("chunky.batchDivisor")).orElse(4); + protected final ConcurrentLinkedQueue ticketAddTasks = new ConcurrentLinkedQueue<>(); + protected final ConcurrentLinkedQueue ticketRemoveTasks = new ConcurrentLinkedQueue<>(); + protected final ConcurrentLinkedQueue chunkLoadTasks = new ConcurrentLinkedQueue<>(); + protected final Executor ticketAddExecutor = command -> { + if (this.shutdown.get()) { + this.runSync(command); + } else { + this.ticketAddTasks.add(command); + this.scheduleIfReady(); + } + }; + protected final Executor ticketRemoveExecutor = command -> { + if (this.shutdown.get()) { + this.runSync(command); + } else { + this.ticketRemoveTasks.add(command); + this.scheduleIfReady(); + } + }; + protected final Executor chunkLoadExecutor = command -> { + if (this.shutdown.get()) { + this.runSync(() -> { + this.tickTickets(); + command.run(); + }); + } else { + this.chunkLoadTasks.add(command); + } + }; + protected final int batchSize = Math.max(1, GenerationTask.MAX_WORKING_COUNT / BATCH_DIVISOR); + protected final AtomicBoolean scheduled = new AtomicBoolean(false); + protected final AtomicBoolean shutdown = new AtomicBoolean(true); + + protected abstract void tickTickets(); + + protected abstract void runSync(Runnable command); + + @Override + public void shutdown() { + if (this.shutdown.get()) { + throw new IllegalStateException("Batcher is already shutdown"); + } + this.shutdown.set(true); + this.schedule(); + } + + @Override + public void resume() { + if (!this.shutdown.get()) { + throw new IllegalStateException("Batcher is running"); + } + this.shutdown.set(false); + } + + public Executor getTicketAddExecutor() { + return this.ticketAddExecutor; + } + + public Executor getTicketRemoveExecutor() { + return this.ticketRemoveExecutor; + } + + public Executor getChunkLoadExecutor() { + return this.chunkLoadExecutor; + } + + private void processTaskQueue() { + final boolean wasShutdown = this.shutdown.get(); + try { + runTasks(this.ticketAddTasks); + runTasks(this.ticketRemoveTasks); + this.tickTickets(); + runTasks(this.chunkLoadTasks); + } finally { + this.scheduled.set(false); + } + this.scheduleIfReady(); + if (!wasShutdown && this.shutdown.get()) { + this.schedule(); + } + } + + private void runTasks(final Queue queue) { + Runnable r; + while ((r = queue.poll()) != null) { + r.run(); + } + } + + private void schedule() { + if (this.scheduled.compareAndSet(false, true)) { + this.runSync(this::processTaskQueue); + } + } + + private void scheduleIfReady() { + if (this.ticketRemoveTasks.size() >= this.batchSize || this.ticketAddTasks.size() >= this.batchSize) { + this.schedule(); + } + } +} diff --git a/common/src/main/java/org/popcraft/chunky/platform/impl/NOOPBatcher.java b/common/src/main/java/org/popcraft/chunky/platform/impl/batcher/NOOPBatcher.java similarity index 85% rename from common/src/main/java/org/popcraft/chunky/platform/impl/NOOPBatcher.java rename to common/src/main/java/org/popcraft/chunky/platform/impl/batcher/NOOPBatcher.java index f821860b0..224bf8a3b 100644 --- a/common/src/main/java/org/popcraft/chunky/platform/impl/NOOPBatcher.java +++ b/common/src/main/java/org/popcraft/chunky/platform/impl/batcher/NOOPBatcher.java @@ -1,4 +1,4 @@ -package org.popcraft.chunky.platform.impl; +package org.popcraft.chunky.platform.impl.batcher; import org.popcraft.chunky.platform.Batcher; diff --git a/fabric/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java b/fabric/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java index 0f19c5458..52630c38f 100644 --- a/fabric/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java +++ b/fabric/src/main/java/org/popcraft/chunky/mixin/MinecraftServerMixin.java @@ -4,7 +4,6 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.util.profiling.InactiveProfiler; import org.popcraft.chunky.ChunkyFabric; -import org.popcraft.chunky.ChunkyProvider; import org.popcraft.chunky.ducks.MinecraftServerExtension; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; diff --git a/fabric/src/main/java/org/popcraft/chunky/platform/FabricBatcher.java b/fabric/src/main/java/org/popcraft/chunky/platform/FabricBatcher.java index 93ce51fc1..4d171ba0f 100644 --- a/fabric/src/main/java/org/popcraft/chunky/platform/FabricBatcher.java +++ b/fabric/src/main/java/org/popcraft/chunky/platform/FabricBatcher.java @@ -3,18 +3,12 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import org.popcraft.chunky.mixin.ServerChunkCacheMixin; -import org.popcraft.chunky.platform.impl.AbstractNMSBatcher; +import org.popcraft.chunky.platform.impl.batcher.AbstractBatcher; -public class FabricBatcher extends AbstractNMSBatcher { +public class FabricBatcher extends AbstractBatcher { private final ServerLevel world; - public FabricBatcher(int maxWorkingCount, ServerLevel world) { - super(maxWorkingCount); - this.world = world; - } - - public FabricBatcher(ServerLevel world) { - super(); + public FabricBatcher(final ServerLevel world) { this.world = world; } @@ -24,8 +18,8 @@ protected void tickTickets() { } @Override - protected void executeSyncRaw(Runnable command) { - MinecraftServer server = this.world.getServer(); + protected void runSync(final Runnable command) { + final MinecraftServer server = this.world.getServer(); server.schedule(server.wrapRunnable(command)); } } diff --git a/fabric/src/main/java/org/popcraft/chunky/platform/FabricWorld.java b/fabric/src/main/java/org/popcraft/chunky/platform/FabricWorld.java index 550f2cecf..b0a48fb10 100644 --- a/fabric/src/main/java/org/popcraft/chunky/platform/FabricWorld.java +++ b/fabric/src/main/java/org/popcraft/chunky/platform/FabricWorld.java @@ -112,9 +112,6 @@ public CompletableFuture getChunkAtAsync(final int x, final int z) { return ((ServerChunkCacheMixin) world.getChunkSource()).invokeGetChunkFutureMainThread(x, z, ChunkStatus.FULL, create) .thenApplyAsync(Function.identity(), ((ChunkMapMixin) serverChunkCache.chunkMap).getMainThreadExecutor()) // workaround to prevent memory leaks in vanilla chunk system .whenCompleteAsync((ignored, throwable) -> { - if (throwable != null) { - throwable.printStackTrace(); - } serverChunkCache.removeTicketWithRadius(CHUNKY, chunkPos, 0); ((MinecraftServerExtension) world.getServer()).chunky$markChunkSystemHousekeeping(); if (ChunkyFabric.ENABLE_MOONRISE_WORKAROUNDS) { @@ -123,7 +120,7 @@ public CompletableFuture getChunkAtAsync(final int x, final int z) { } }, this.batcher.getTicketRemoveExecutor()) .thenApply(ignored -> (Void) null); - }, this.batcher.getFutureFetchExecutor()).thenCompose(Function.identity()); + }, this.batcher.getChunkLoadExecutor()).thenCompose(Function.identity()); } } diff --git a/forge/src/main/java/org/popcraft/chunky/platform/ForgeBatcher.java b/forge/src/main/java/org/popcraft/chunky/platform/ForgeBatcher.java index 72abc0ce6..b7d6f6437 100644 --- a/forge/src/main/java/org/popcraft/chunky/platform/ForgeBatcher.java +++ b/forge/src/main/java/org/popcraft/chunky/platform/ForgeBatcher.java @@ -2,18 +2,12 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; -import org.popcraft.chunky.platform.impl.AbstractNMSBatcher; +import org.popcraft.chunky.platform.impl.batcher.AbstractBatcher; -public class ForgeBatcher extends AbstractNMSBatcher { +public class ForgeBatcher extends AbstractBatcher { private final ServerLevel world; - public ForgeBatcher(int maxWorkingCount, ServerLevel world) { - super(maxWorkingCount); - this.world = world; - } - - public ForgeBatcher(ServerLevel world) { - super(); + public ForgeBatcher(final ServerLevel world) { this.world = world; } @@ -23,8 +17,8 @@ protected void tickTickets() { } @Override - protected void executeSyncRaw(Runnable command) { - MinecraftServer server = this.world.getServer(); + protected void runSync(final Runnable command) { + final MinecraftServer server = this.world.getServer(); server.schedule(server.wrapRunnable(command)); } } diff --git a/forge/src/main/java/org/popcraft/chunky/platform/ForgeWorld.java b/forge/src/main/java/org/popcraft/chunky/platform/ForgeWorld.java index 4c93897eb..a64207b04 100644 --- a/forge/src/main/java/org/popcraft/chunky/platform/ForgeWorld.java +++ b/forge/src/main/java/org/popcraft/chunky/platform/ForgeWorld.java @@ -112,9 +112,6 @@ public CompletableFuture getChunkAtAsync(final int x, final int z) { return world.getChunkSource().getChunkFutureMainThread(x, z, ChunkStatus.FULL, create) .thenApplyAsync(Function.identity(), serverChunkCache.chunkMap.mainThreadExecutor) // workaround to prevent memory leaks in vanilla chunk system .whenCompleteAsync((ignored, throwable) -> { - if (throwable != null) { - throwable.printStackTrace(); - } serverChunkCache.removeTicketWithRadius(CHUNKY, chunkPos, 0); ((MinecraftServerExtension) world.getServer()).chunky$markChunkSystemHousekeeping(); if (ChunkyForge.ENABLE_MOONRISE_WORKAROUNDS) { @@ -123,7 +120,7 @@ public CompletableFuture getChunkAtAsync(final int x, final int z) { } }, this.batcher.getTicketRemoveExecutor()) .thenApply(ignored -> (Void) null); - }, this.batcher.getFutureFetchExecutor()).thenCompose(Function.identity()); + }, this.batcher.getChunkLoadExecutor()).thenCompose(Function.identity()); } } diff --git a/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeBatcher.java b/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeBatcher.java index 505fff428..e0e947429 100644 --- a/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeBatcher.java +++ b/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeBatcher.java @@ -2,18 +2,12 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; -import org.popcraft.chunky.platform.impl.AbstractNMSBatcher; +import org.popcraft.chunky.platform.impl.batcher.AbstractBatcher; -public class NeoForgeBatcher extends AbstractNMSBatcher { +public class NeoForgeBatcher extends AbstractBatcher { private final ServerLevel world; - public NeoForgeBatcher(int maxWorkingCount, ServerLevel world) { - super(maxWorkingCount); - this.world = world; - } - - public NeoForgeBatcher(ServerLevel world) { - super(); + public NeoForgeBatcher(final ServerLevel world) { this.world = world; } @@ -23,8 +17,8 @@ protected void tickTickets() { } @Override - protected void executeSyncRaw(Runnable command) { - MinecraftServer server = this.world.getServer(); + protected void runSync(final Runnable command) { + final MinecraftServer server = this.world.getServer(); server.schedule(server.wrapRunnable(command)); } } diff --git a/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeWorld.java b/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeWorld.java index 327b5d018..faa648f55 100644 --- a/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeWorld.java +++ b/neoforge/src/main/java/org/popcraft/chunky/platform/NeoForgeWorld.java @@ -108,9 +108,6 @@ public CompletableFuture getChunkAtAsync(final int x, final int z) { return world.getChunkSource().getChunkFutureMainThread(x, z, ChunkStatus.FULL, create) .thenApplyAsync(Function.identity(), serverChunkCache.chunkMap.mainThreadExecutor) // workaround to prevent memory leaks in vanilla chunk system .whenCompleteAsync((ignored, throwable) -> { - if (throwable != null) { - throwable.printStackTrace(); - } serverChunkCache.removeTicketWithRadius(CHUNKY, chunkPos, 0); ((MinecraftServerExtension) world.getServer()).chunky$markChunkSystemHousekeeping(); if (ChunkyNeoForge.ENABLE_MOONRISE_WORKAROUNDS) { @@ -119,7 +116,7 @@ public CompletableFuture getChunkAtAsync(final int x, final int z) { } }, this.batcher.getTicketRemoveExecutor()) .thenApply(ignored -> (Void) null); - }, this.batcher.getFutureFetchExecutor()).thenCompose(Function.identity()); + }, this.batcher.getChunkLoadExecutor()).thenCompose(Function.identity()); } }