diff --git a/gale-server/minecraft-patches/sources/net/minecraft/server/MinecraftServer.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/server/MinecraftServer.java.patch index 1e8e9d4c..537d5186 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/server/MinecraftServer.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/server/MinecraftServer.java.patch @@ -1,183 +1,60 @@ --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java -@@ -332,6 +_,7 @@ +@@ -332,6 +_,9 @@ public volatile boolean abnormalExit; // Paper - Improved watchdog support public volatile Thread shutdownThread; // Paper - Improved watchdog support public final io.papermc.paper.configuration.PaperConfigurations paperConfigurations; // Paper - add paper configuration files -+ public final org.galemc.gale.configuration.GaleConfigurations galeConfigurations; // Gale - Configuration - Add configuration files ++ // Gale start - Parallel world ticking ++ public org.galemc.gale.tick.GaleWorldTickExecutor galeWorldTickExecutor; // Gale - Parallel world ticking ++ // Gale end - Parallel world ticking public boolean isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked private final Set pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping public static final long SERVER_INIT = System.nanoTime(); // Paper - Lag compensation -@@ -576,6 +_,7 @@ +@@ -576,6 +_,18 @@ Runtime.getRuntime().addShutdownHook(new org.bukkit.craftbukkit.util.ServerShutdownThread(this)); // CraftBukkit end this.paperConfigurations = services.paper().configurations(); // Paper - add paper configuration files -+ this.galeConfigurations = services.paper().galeConfigurations(); // Gale - Configuration - Add configuration files ++ // Gale start - Parallel world ticking - Capture main thread + init executor ++ org.galemc.gale.tick.GaleThreadMarkers.captureMainServerThread(); ++ { ++ org.galemc.gale.configuration.GaleGlobalConfiguration galeGlobalCfg = org.galemc.gale.configuration.GaleGlobalConfiguration.get(); ++ if (galeGlobalCfg != null) { ++ org.galemc.gale.configuration.GaleGlobalConfiguration.Performance perf = galeGlobalCfg.performance; ++ if (perf != null && perf.parallelWorldTicking) { ++ this.galeWorldTickExecutor = org.galemc.gale.tick.GaleWorldTickExecutor.create(true, perf.parallelWorldTickingMaxThreads); ++ } ++ } ++ } ++ // Gale end - Parallel world ticking - Capture main thread + init executor } protected abstract boolean initServer() throws IOException; -@@ -1347,6 +_,11 @@ - - try (Profiler.Scope ignored = Profiler.use(this.createProfiler())) { - this.processPacketsAndTick(sprinting); -+ // Gale start - Speculative execution based on next tick simulation - Simulate next tick -+ org.galemc.gale.async.SimulationFlag.REAL = false; -+ this.processPacketsAndTick(true); -+ org.galemc.gale.async.SimulationFlag.REAL = true; -+ // Gale end - Speculative execution based on next tick simulation - Simulate next tick - ProfilerFiller profiler = Profiler.get(); - profiler.push("nextTickWait"); - this.mayHaveDelayedTasks = true; -@@ -1578,8 +_,9 @@ - } - - protected void tickServer(final BooleanSupplier haveTime) { -- org.spigotmc.WatchdogThread.tick(); // Spigot -+ if (org.galemc.gale.async.SimulationFlag.REAL) org.spigotmc.WatchdogThread.tick(); // Spigot // Gale - Speculative execution based on next tick simulation - Simulate next tick - long nano = Util.getNanos(); -+ if (org.galemc.gale.async.SimulationFlag.REAL) { // Gale - Speculative execution based on next tick simulation - Simulate next tick - int emptyTickThreshold = this.pauseWhenEmptySeconds() * 20; - this.removeDisabledPluginsBlockingSleep(); // Paper - API to allow/disallow tick sleeping - if (emptyTickThreshold > 0) { -@@ -1618,7 +_,9 @@ - new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events - this.tickCount++; - this.tickRateManager.tick(); -+ } // Gale - Speculative execution based on next tick simulation - Simulate next tick - this.tickChildren(haveTime); -+ if (org.galemc.gale.async.SimulationFlag.REAL) { // Gale - Speculative execution based on next tick simulation - Simulate next tick - if (nano - this.lastServerStatus >= STATUS_EXPIRE_TIME_NANOS) { - this.lastServerStatus = nano; - this.status = this.buildServerStatus(); -@@ -1668,6 +_,7 @@ - this.smoothedTickTimeMillis = this.smoothedTickTimeMillis * 0.8F + (float)tickTime / (float)TimeUtil.NANOSECONDS_PER_MILLISECOND * 0.19999999F; - this.logTickMethodTime(nano); - profiler.pop(); -+ } // Gale - Speculative execution based on next tick simulation - Simulate next tick - } - - protected void processPacketsAndTick(final boolean sprinting) { -@@ -1675,7 +_,7 @@ - profiler.push("tick"); - this.tickFrame.start(); - // Paper - improve tick loop - moved into runAllTasksAtTickStart -- this.runAllTasksAtTickStart(); // Paper - improve tick loop -+ if (org.galemc.gale.async.SimulationFlag.REAL) this.runAllTasksAtTickStart(); // Paper - improve tick loop // Gale - Speculative execution based on next tick simulation - Simulate next tick - this.tickServer(sprinting ? () -> false : this::haveTime); - // Paper start - rewrite chunk system - final Throwable crash = this.chunkSystemCrash; -@@ -1685,7 +_,7 @@ - } - // Paper end - rewrite chunk system - this.tickFrame.end(); -- this.recordEndOfTick(); // Paper - improve tick loop -+ if (org.galemc.gale.async.SimulationFlag.REAL) this.recordEndOfTick(); // Paper - improve tick loop // Gale - Speculative execution based on next tick simulation - Simulate next tick - profiler.pop(); - } - -@@ -1764,6 +_,7 @@ - - protected void tickChildren(final BooleanSupplier haveTime) { - ProfilerFiller profiler = Profiler.get(); -+ if (org.galemc.gale.async.SimulationFlag.REAL) { // Gale - Speculative execution based on next tick simulation - Simulate next tick - this.getPlayerList().getPlayers().forEach(playerx -> playerx.connection.suspendFlushing()); - this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit - // Paper start - optimise Folia entity scheduler -@@ -1779,10 +_,13 @@ - // Paper end - Folia scheduler API - io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.ADVENTURE_CLICK_MANAGER.handleQueue(this.tickCount); // Paper - io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.DIALOG_CLICK_MANAGER.handleQueue(this.tickCount); // Paper -+ } // Gale - Speculative execution based on next tick simulation - Simulate next tick - profiler.push("commandFunctions"); - this.getFunctions().tick(); - profiler.pop(); -+ boolean clocksNeedRewind = false; // Gale - Speculative execution based on next tick simulation - Simulate next tick - if (this.tickRateManager.runsNormally()) { -+ clocksNeedRewind = true; // Gale - Speculative execution based on next tick simulation - Simulate next tick - profiler.push("clocks"); - // Paper start - per-world time - if (io.papermc.paper.configuration.GlobalConfiguration.get().time.affectsAllWorlds) { -@@ -1796,6 +_,7 @@ - profiler.pop(); - } - -+ if (org.galemc.gale.async.SimulationFlag.REAL) { // Gale - Speculative execution based on next tick simulation - Simulate next tick - if (this.tickCount % 20 == 0) { - profiler.push("timeSync"); - this.forceGameTimeSynchronization(); -@@ -1812,7 +_,9 @@ +@@ -1811,7 +_,29 @@ this.updateEffectiveRespawnData(); this.isIteratingOverLevels = true; // Paper - Throw exception on world create while being ticked -+ } // Gale - Speculative execution based on next tick simulation - Simulate next tick ++ // Gale start - Parallel world ticking - Tick different worlds on separate CPU cores ++ java.util.ArrayList gale$parallelLevels = new java.util.ArrayList<>(); ++ for (ServerLevel gale$lvl : this.getAllLevels()) gale$parallelLevels.add(gale$lvl); ++ org.galemc.gale.tick.GaleWorldTickExecutor gale$exec = this.galeWorldTickExecutor; ++ final boolean gale$useParallel = gale$exec != null ++ && gale$parallelLevels.size() > 1; ++ if (gale$useParallel) { ++ for (int gale$i = 0; gale$i < gale$parallelLevels.size(); gale$i++) { ++ ServerLevel gale$level = gale$parallelLevels.get(gale$i); ++ gale$level.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent ++ gale$level.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent ++ gale$level.updateLagCompensationTick(); // Paper - lag compensation ++ } ++ gale$exec.tickWorldsParallel(haveTime, gale$parallelLevels); ++ } ++ // Gale end - Parallel world ticking - Tick different worlds on separate CPU cores for (ServerLevel level : this.getAllLevels()) { -+ if (org.galemc.gale.async.SimulationFlag.REAL) { // Gale - Speculative execution based on next tick simulation - Simulate next tick ++ // Gale start - Parallel world ticking - Skip per-world sequential loop when already ran in parallel ++ if (gale$useParallel) { ++ break; ++ } ++ // Gale end - Parallel world ticking - Skip per-world sequential loop when already ran in parallel level.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent level.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent level.updateLagCompensationTick(); // Paper - lag compensation -@@ -1820,6 +_,7 @@ - profiler.push(() -> level + " " + level.dimension().identifier()); - profiler.push("tick"); - -+ } // Gale - Speculative execution based on next tick simulation - Simulate next tick - try { - level.tick(haveTime); - } catch (Throwable t) { -@@ -1827,11 +_,14 @@ - level.fillReportDetails(report); - throw new ReportedException(report); - } -+ if (org.galemc.gale.async.SimulationFlag.REAL) { // Gale - Speculative execution based on next tick simulation - Simulate next tick - - profiler.pop(); - profiler.pop(); - level.explosionDensityCache.clear(); // Paper - Optimize explosions -+ } // Gale - Speculative execution based on next tick simulation - Simulate next tick - } -+ if (org.galemc.gale.async.SimulationFlag.REAL) { // Gale - Speculative execution based on next tick simulation - Simulate next tick - this.isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked - - profiler.popPush("connection"); -@@ -1860,6 +_,21 @@ - - profiler.pop(); - this.serverActivityMonitor.tick(); -+ // Gale start - Speculative execution based on next tick simulation - Simulate next tick -+ } else { -+ if (clocksNeedRewind) { -+ // Paper start - per-world time -+ if (io.papermc.paper.configuration.GlobalConfiguration.get().time.affectsAllWorlds) { -+ this.clockManager.rewind(); -+ } else { -+ for (ServerLevel level : this.getAllLevels()) { -+ level.clockManager().rewind(); -+ } -+ } -+ // Paper end - per-world time -+ } -+ } -+ // Gale end - Speculative execution based on next tick simulation - Simulate next tick - } - - // Paper start - per world respawn data -@@ -2032,7 +_,19 @@ - new io.papermc.paper.event.world.WorldDifficultyChangeEvent( - level.getWorld(), source, org.bukkit.craftbukkit.util.CraftDifficulty.toBukkit(difficulty) - ).callEvent(); -- worldData.setDifficulty(worldData.isHardcore() ? Difficulty.HARD : difficulty); -+ // Gale start - Event-driven - Mob.mustDespawnBecauseOfPeacefulDifficulty -+ Difficulty oldDifficulty = worldData.getDifficulty(); -+ Difficulty newDifficulty = worldData.isHardcore() ? Difficulty.HARD : difficulty; -+ if (oldDifficulty == newDifficulty) { -+ return; -+ } -+ worldData.setDifficulty(newDifficulty); -+ if ((oldDifficulty == Difficulty.PEACEFUL) != (newDifficulty == Difficulty.PEACEFUL)) { -+ for (net.minecraft.world.entity.Mob mob : level.getEntities(net.minecraft.world.level.entity.EntityTypeTest.forClass(net.minecraft.world.entity.Mob.class), _ -> true)) { -+ mob.gale$eventDriven_mustDespawnBecauseOfPeacefulDifficulty_update(); -+ } -+ } -+ // Gale end - Event-driven - Mob.mustDespawnBecauseOfPeacefulDifficulty - level.setSpawnSettings(level.isSpawningMonsters()); - level.players().forEach(this::sendDifficultyUpdate); - // Paper end - per level difficulty diff --git a/gale-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/CraftServer.java.patch b/gale-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/CraftServer.java.patch deleted file mode 100644 index 11411499..00000000 --- a/gale-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/CraftServer.java.patch +++ /dev/null @@ -1,85 +0,0 @@ ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -16,7 +_,6 @@ - import io.papermc.paper.util.MCUtil; - import io.papermc.paper.world.PaperWorldLoader; - import io.papermc.paper.world.migration.WorldFolderMigration; --import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; - import java.awt.image.BufferedImage; - import java.io.ByteArrayOutputStream; - import java.io.File; -@@ -153,7 +_,6 @@ - import org.bukkit.craftbukkit.ban.CraftProfileBanList; - import org.bukkit.craftbukkit.block.data.CraftBlockData; - import org.bukkit.craftbukkit.boss.CraftBossBar; --import org.bukkit.craftbukkit.boss.CraftKeyedBossbar; - import org.bukkit.craftbukkit.command.CraftCommandMap; - import org.bukkit.craftbukkit.command.VanillaCommandWrapper; - import org.bukkit.craftbukkit.entity.CraftEntityFactory; -@@ -276,7 +_,7 @@ - private final StructureManager structureManager; - final DedicatedServer console; - private final DedicatedPlayerList playerList; -- private final Map worlds = new LinkedHashMap<>(); -+ private final Map worlds = new it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap<>(); // Gale - Collections - CraftServer.worlds (use fastutil) - private YamlConfiguration configuration; - private YamlConfiguration commandsConfiguration; - private final Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); -@@ -284,7 +_,10 @@ - private final EntityMetadataStore entityMetadata = new EntityMetadataStore(); - private final PlayerMetadataStore playerMetadata = new PlayerMetadataStore(); - private final WorldMetadataStore worldMetadata = new WorldMetadataStore(); -- private final Object2IntOpenHashMap spawnCategoryLimit = new Object2IntOpenHashMap<>(); -+ // Gale start - Collections - CraftServer.spawnCategoryLimit (use array) -+ private static final SpawnCategory[] gale$SPAWN_CATEGORIES = SpawnCategory.values(); -+ private final int[] spawnCategoryLimit = new int[gale$SPAWN_CATEGORIES.length]; -+ // Gale end - Collections - CraftServer.spawnCategoryLimit (use array) - private File container; - private WarningState warningState = WarningState.DEFAULT; - public ApiVersion minimumAPI; -@@ -463,6 +_,7 @@ - this.saveCommandsConfig(); - this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); - this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); -+ java.util.Arrays.fill(this.spawnCategoryLimit, -1); // Gale - Collections - CraftServer.spawnCategoryLimit (use array) - this.overrideSpawnLimits(); - console.autosavePeriod = this.configuration.getInt("ticks-per.autosave"); - this.warningState = WarningState.value(this.configuration.getString("settings.deprecated-verbose")); -@@ -494,9 +_,9 @@ - } - - private void overrideSpawnLimits() { -- for (SpawnCategory spawnCategory : SpawnCategory.values()) { -+ for (SpawnCategory spawnCategory : gale$SPAWN_CATEGORIES) { // Gale - Collections - CraftServer.spawnCategoryLimit (use array) - if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { -- this.spawnCategoryLimit.put(spawnCategory, this.configuration.getInt(CraftSpawnCategory.getConfigNameSpawnLimit(spawnCategory))); -+ this.spawnCategoryLimit[spawnCategory.ordinal()] = this.configuration.getInt(CraftSpawnCategory.getConfigNameSpawnLimit(spawnCategory)); // Gale - Collections - CraftServer.spawnCategoryLimit (use array) - } - } - } -@@ -900,7 +_,7 @@ - - @Override - public List getWorlds() { -- return new ArrayList(this.worlds.values()); -+ return new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(this.worlds.values()); // Gale - Collections - CraftServer.getWorlds() (use fastutil) - } - - @Override -@@ -985,6 +_,7 @@ - - org.spigotmc.SpigotConfig.init((File) this.console.options.valueOf("spigot-settings")); // Spigot - this.console.paperConfigurations.reloadConfigs(this.console); -+ // this.console.galeConfigurations.reloadConfigs(this.console); // Gale - Configuration - Reload Gale configuration files // TODO Gale configuration temp disabled - for (ServerLevel world : this.console.getAllLevels()) { - // world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty - world.setSpawnSettings(world.isSpawningMonsters()); // Paper - per level difficulty (from MinecraftServer#setDifficulty(ServerLevel, Difficulty, boolean)) -@@ -2303,7 +_,7 @@ - } - - public int getSpawnLimitUnsafe(final SpawnCategory spawnCategory) { -- return this.spawnCategoryLimit.getOrDefault(spawnCategory, -1); -+ return this.spawnCategoryLimit[spawnCategory.ordinal()]; // Gale - Collections - CraftServer.spawnCategoryLimit (use array) - } - - @Override diff --git a/gale-server/src/main/java/org/galemc/gale/configuration/GaleGlobalConfiguration.java b/gale-server/src/main/java/org/galemc/gale/configuration/GaleGlobalConfiguration.java index 5c6c9ab2..1d1a43ca 100644 --- a/gale-server/src/main/java/org/galemc/gale/configuration/GaleGlobalConfiguration.java +++ b/gale-server/src/main/java/org/galemc/gale/configuration/GaleGlobalConfiguration.java @@ -2,6 +2,7 @@ import io.papermc.paper.configuration.Configuration; import io.papermc.paper.configuration.ConfigurationPart; +import org.spongepowered.configurate.objectmapping.meta.Comment; import org.spongepowered.configurate.objectmapping.meta.Setting; public class GaleGlobalConfiguration extends ConfigurationPart { @@ -16,4 +17,25 @@ static void set(GaleGlobalConfiguration instance) { @Setting(Configuration.VERSION_FIELD) public int version = CURRENT_VERSION; + + public Performance performance = new Performance(); + + public static class Performance extends ConfigurationPart { + + @Comment("Enables parallel ticking of different worlds (Overworld, Nether, End, etc.) on separate CPU threads.\n" + + "This can dramatically improve TPS on servers with multiple worlds or many dimensions by utilizing\n" + + "additional CPU cores that would otherwise be idle. Plugin compatibility is 100% because each\n" + + "world's tick still runs on its own dedicated \"main-like\" thread.\n" + + "Requires at least 2 worlds loaded to have any effect.\n" + + "Default: false (conservative - enable to benefit from multi-core CPUs)") + @Setting("parallel-world-ticking") + public boolean parallelWorldTicking = false; + + @Comment("Maximum number of threads to use for parallel world ticking. The actual value is also capped by\n" + + "(available CPU cores - 1) and the number of loaded worlds. Values above 16 are ignored to prevent\n" + + "excessive thread overhead.\n" + + "Default: 8 (good for modern 8c/16t+ CPUs)") + @Setting("parallel-world-ticking-max-threads") + public int parallelWorldTickingMaxThreads = 8; + } } diff --git a/gale-server/src/main/java/org/galemc/gale/tick/GaleThreadMarkers.java b/gale-server/src/main/java/org/galemc/gale/tick/GaleThreadMarkers.java new file mode 100644 index 00000000..e5e56488 --- /dev/null +++ b/gale-server/src/main/java/org/galemc/gale/tick/GaleThreadMarkers.java @@ -0,0 +1,53 @@ +package org.galemc.gale.tick; + +import net.minecraft.server.level.ServerLevel; + +public final class GaleThreadMarkers { + + private static final ThreadLocal CURRENT_WORLD_MAIN_THREAD = new ThreadLocal<>(); + private static volatile boolean parallelTickActive = false; + private static volatile Thread mainServerThread; + + public static void captureMainServerThread() { + if (mainServerThread == null) { + mainServerThread = Thread.currentThread(); + } + } + + public static Thread getMainServerThread() { + return mainServerThread; + } + + public static boolean isMainServerThread(Thread t) { + Thread main = mainServerThread; + return main != null && main == t; + } + + public static void markAsWorldMainThread(ServerLevel level) { + CURRENT_WORLD_MAIN_THREAD.set(level); + } + + public static void clearWorldMainThreadMarker() { + CURRENT_WORLD_MAIN_THREAD.remove(); + } + + public static ServerLevel getCurrentWorldMainThread() { + return CURRENT_WORLD_MAIN_THREAD.get(); + } + + public static boolean isWorldMainThread(ServerLevel level) { + ServerLevel marked = CURRENT_WORLD_MAIN_THREAD.get(); + return marked != null && marked == level; + } + + public static void setParallelTickActive(boolean active) { + parallelTickActive = active; + } + + public static boolean isParallelTickActive() { + return parallelTickActive; + } + + private GaleThreadMarkers() { + } +} diff --git a/gale-server/src/main/java/org/galemc/gale/tick/GaleWorldTickExecutor.java b/gale-server/src/main/java/org/galemc/gale/tick/GaleWorldTickExecutor.java new file mode 100644 index 00000000..c756e892 --- /dev/null +++ b/gale-server/src/main/java/org/galemc/gale/tick/GaleWorldTickExecutor.java @@ -0,0 +1,145 @@ +package org.galemc.gale.tick; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; +import net.minecraft.CrashReport; +import net.minecraft.ReportedException; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.TimeUtil; +import net.minecraft.util.profiling.Profiler; +import net.minecraft.util.profiling.ProfilerFiller; + +public final class GaleWorldTickExecutor { + + private static final long TICK_TIMEOUT_NANOS = 60L * TimeUtil.NANOSECONDS_PER_SECOND; + + private final ExecutorService worldThreadPool; + private final int parallelism; + + public GaleWorldTickExecutor(int maxThreads) { + int available = Runtime.getRuntime().availableProcessors(); + int desired = Math.min(maxThreads, available - 1); + this.parallelism = Math.max(2, Math.min(desired, 16)); + this.worldThreadPool = Executors.newFixedThreadPool(this.parallelism, new WorldTickThreadFactory()); + } + + public static GaleWorldTickExecutor create(boolean enabled, int maxThreads) { + if (!enabled) { + return null; + } + return new GaleWorldTickExecutor(Math.max(2, Math.min(maxThreads, 16))); + } + + public int getParallelism() { + return parallelism; + } + + public void tickWorldsParallel(BooleanSupplier haveTime, List levels) { + if (levels.size() <= 1) { + for (int i = 0; i < levels.size(); i++) { + tickSingleWorldSequentially(levels.get(i), haveTime); + } + return; + } + + CompletableFuture[] futures = new CompletableFuture[levels.size()]; + GaleThreadMarkers.setParallelTickActive(true); + try { + for (int i = 0; i < levels.size(); i++) { + final ServerLevel level = levels.get(i); + final BooleanSupplier haveTimeCapture = haveTime; + futures[i] = CompletableFuture.runAsync(() -> { + GaleThreadMarkers.markAsWorldMainThread(level); + try { + tickSingleWorldSequentially(level, haveTimeCapture); + } finally { + GaleThreadMarkers.clearWorldMainThreadMarker(); + } + }, worldThreadPool); + } + + try { + CompletableFuture.allOf(futures).get(TICK_TIMEOUT_NANOS, TimeUnit.NANOSECONDS); + } catch (TimeoutException e) { + cancelAllFutures(futures); + throw new RuntimeException("Parallel world tick exceeded 60s timeout", e); + } catch (ExecutionException e) { + cancelAllFutures(futures); + Throwable cause = e.getCause(); + if (cause == null) { + throw new RuntimeException("Parallel world tick failed with no cause", e); + } + if (cause instanceof ReportedException re) throw re; + if (cause instanceof RuntimeException re) throw re; + if (cause instanceof Error err) throw err; + throw new RuntimeException(cause); + } catch (InterruptedException e) { + cancelAllFutures(futures); + Thread.currentThread().interrupt(); + throw new RuntimeException("Parallel world tick interrupted", e); + } + } finally { + GaleThreadMarkers.setParallelTickActive(false); + } + } + + private static void cancelAllFutures(CompletableFuture[] futures) { + if (futures == null) return; + for (CompletableFuture f : futures) { + if (f != null) { + f.cancel(false); + } + } + } + + private static void tickSingleWorldSequentially(ServerLevel level, BooleanSupplier haveTime) { + ProfilerFiller profiler = Profiler.get(); + profiler.push(() -> level + " " + level.dimension().identifier()); + profiler.push("tick"); + try { + level.tick(haveTime); + } catch (Throwable t) { + CrashReport report = CrashReport.forThrowable(t, "Exception ticking world " + level.dimension().identifier()); + level.fillReportDetails(report); + throw new ReportedException(report); + } finally { + profiler.pop(); + profiler.pop(); + level.explosionDensityCache.clear(); + } + } + + public void shutdown() { + worldThreadPool.shutdown(); + try { + if (!worldThreadPool.awaitTermination(5, TimeUnit.SECONDS)) { + worldThreadPool.shutdownNow(); + if (!worldThreadPool.awaitTermination(3, TimeUnit.SECONDS)) { + } + } + } catch (InterruptedException e) { + worldThreadPool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + private static final class WorldTickThreadFactory implements ThreadFactory { + private final AtomicInteger counter = new AtomicInteger(0); + + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, "Gale-World-Tick-" + counter.incrementAndGet()); + thread.setDaemon(true); + thread.setPriority(Thread.NORM_PRIORITY); + return thread; + } + } +}