From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Kevin Raneri Date: Wed, 10 Nov 2021 00:37:03 -0500 Subject: [PATCH] Pufferfish: Optimize mob spawning Original license: GPL v3 Original project: https://github.com/pufferfish-gg/Pufferfish This patch aims to reduce the main-thread impact of mob spawning by offloading as much work as possible to other threads. It is possible for inconsistencies to come up, but when they happen they never interfere with the server's operation (they don't produce errors), and side effects are limited to more or less mobs being spawned in any particular tick. It is possible to disable this optimization if it is not required or if it interferes with any plugins. On servers with thousands of entities, this can result in performance gains of up to 15%, which is significant and, in my opinion, worth the low risk of minor mob-spawning-related inconsistencies. diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 9e7119152664e785e23f08e3a702f0bc60d817a0..a001fd51a7eb4ff97a84965679170f2da6619a9b 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -297,6 +297,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { AtomicReference atomicreference = new AtomicReference(); Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java index bebafdf670ef1783ccae3b93fcda7d0eaef38d3e..b676d1605236b75cf6095a28c2a7a76f3466eea8 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java @@ -351,6 +351,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface DedicatedServer.LOGGER.info("JMX monitoring enabled"); } + if (org.dreeam.leaf.config.modules.async.AsyncMobSpawning.enabled) mobSpawnExecutor.start(); // Pufferfish return true; } } diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java index d8a3b28b38f2ac62fbf0f87f8b5ff811873f8701..42869d35abb8d006fb720ca7190a770ce5cbf778 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -243,7 +243,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper end // Paper start - optimise chunk tick iteration public final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet needsChangeBroadcasting = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); - public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); + public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap = new gg.pufferfish.pufferfish.util.AsyncPlayerAreaMap(this.pooledLinkedPlayerHashSets); // Pufferfish // Paper end - optimise chunk tick iteration public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) { diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index 68ad7cb06d85c428f3560f95ed46bca32187e729..6587ce30d88983cb42822e6ff3e012047d3ce16d 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -76,6 +76,9 @@ public class ServerChunkCache extends ChunkSource { private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4]; // Paper end + public boolean firstRunSpawnCounts = true; // Pufferfish + public final java.util.concurrent.atomic.AtomicBoolean _pufferfish_spawnCountsReady = new java.util.concurrent.atomic.AtomicBoolean(false); // Pufferfish - optimize countmobs + public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory) { this.level = world; this.mainThreadProcessor = new ServerChunkCache.MainThreadExecutor(world); @@ -510,6 +513,7 @@ public class ServerChunkCache extends ChunkSource { // Paper start - Optional per player mob spawns int naturalSpawnChunkCount = k; if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled + if (!org.dreeam.leaf.config.modules.async.AsyncMobSpawning.enabled) { // Pufferfish - moved down when async processing // re-set mob counts for (ServerPlayer player : this.level.players) { // Paper start - per player mob spawning backoff @@ -524,17 +528,21 @@ public class ServerChunkCache extends ChunkSource { } // Paper end - per player mob spawning backoff } - spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); + lastSpawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); // Pufferfish - async mob spawning + } // Pufferfish - (endif) moved down when async processing } else { - spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); + // Pufferfish start - async mob spawning + lastSpawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); + _pufferfish_spawnCountsReady.set(true); + // Pufferfish end } // Paper end - Optional per player mob spawns this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings - this.lastSpawnState = spawnercreature_d; + //this.lastSpawnState = spawnercreature_d; // Pufferfish - this is managed asynchronously // Gale start - MultiPaper - skip unnecessary mob spawning computations } else { - spawnercreature_d = null; + lastSpawnState = null; // Leaf - Pufferfish - Optimize mob spawning } // Gale end - MultiPaper - skip unnecessary mob spawning computations @@ -624,8 +632,8 @@ public class ServerChunkCache extends ChunkSource { if (tick && chunk1.chunkStatus.isOrAfter(net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING)) { // Paper end - optimise chunk tick iteration chunk1.incrementInhabitedTime(j); - if (spawn && flagAndHasNaturalSpawn && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair)) { // Spigot // Paper - optimise chunk tick iteration // Gale - MultiPaper - skip unnecessary mob spawning computations - NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1); + if (spawn && flagAndHasNaturalSpawn && (!org.dreeam.leaf.config.modules.async.AsyncMobSpawning.enabled || _pufferfish_spawnCountsReady.get()) && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair)) { // Spigot // Paper - optimise chunk tick iteration // Gale - MultiPaper - skip unnecessary mob spawning computations // Pufferfish + NaturalSpawner.spawnForChunk(this.level, chunk1, lastSpawnState, this.spawnFriendlies, this.spawnEnemies, flag1); // Pufferfish } if (true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { // Paper - optimise chunk tick iteration @@ -668,6 +676,40 @@ public class ServerChunkCache extends ChunkSource { this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing // Paper - optimise chunk tick iteration } + + // Pufferfish start - optimize mob spawning + if (org.dreeam.leaf.config.modules.async.AsyncMobSpawning.enabled) { + for (ServerPlayer player : this.level.players) { + // Paper start - per player mob spawning backoff + for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { + player.mobCounts[ii] = 0; + + int newBackoff = player.mobBackoffCounts[ii] - 1; // TODO make configurable bleed // TODO use nonlinear algorithm? + if (newBackoff < 0) { + newBackoff = 0; + } + player.mobBackoffCounts[ii] = newBackoff; + } + // Paper end - per player mob spawning backoff + } + if (firstRunSpawnCounts) { + firstRunSpawnCounts = false; + _pufferfish_spawnCountsReady.set(true); + } + if (_pufferfish_spawnCountsReady.getAndSet(false)) { + net.minecraft.server.MinecraftServer.getServer().mobSpawnExecutor.submit(() -> { + int mapped = distanceManager.getNaturalSpawnChunkCount(); + io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator objectiterator = + level.entityTickList.entities.iterator(io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); + gg.pufferfish.pufferfish.util.IterableWrapper wrappedIterator = + new gg.pufferfish.pufferfish.util.IterableWrapper<>(objectiterator); + lastSpawnState = NaturalSpawner.createState(mapped, wrappedIterator, this::getFullChunk, null, true); + objectiterator.finishedIterating(); + _pufferfish_spawnCountsReady.set(true); + }); + } + } + // Pufferfish end } // Gale start - MultiPaper - skip unnecessary mob spawning computations diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java index 4cdfc433df67afcd455422e9baf56f167dd712ae..57fcf3910f45ce371ac2e237b277b1034caaac4e 100644 --- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java +++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java @@ -8,7 +8,7 @@ import javax.annotation.Nullable; import net.minecraft.world.entity.Entity; public class EntityTickList { - private final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entities = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Paper - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking? + public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entities = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Paper - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking? // Pufferfish - private->public private void ensureActiveIsNotIterated() { // Paper - replace with better logic, do not delay removals diff --git a/src/main/java/org/dreeam/leaf/config/modules/async/AsyncMobSpawning.java b/src/main/java/org/dreeam/leaf/config/modules/async/AsyncMobSpawning.java new file mode 100644 index 0000000000000000000000000000000000000000..67c575439f7d60046586972dfc3212bb36a3d033 --- /dev/null +++ b/src/main/java/org/dreeam/leaf/config/modules/async/AsyncMobSpawning.java @@ -0,0 +1,41 @@ +package org.dreeam.leaf.config.modules.async; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import org.dreeam.leaf.config.ConfigInfo; +import org.dreeam.leaf.config.DoNotLoad; +import org.dreeam.leaf.config.EnumConfigCategory; +import org.dreeam.leaf.config.IConfigModule; + +public class AsyncMobSpawning implements IConfigModule { + + @Override + public EnumConfigCategory getCategory() { + return EnumConfigCategory.ASYNC; + } + + @Override + public String getBaseName() { + return "async_mob_spawning"; + } + + @ConfigInfo(baseName = "enabled") + public static boolean enabled = true; + @DoNotLoad + public static boolean asyncMobSpawningInitialized; + + @Override + public void onLoaded(CommentedFileConfig config) { + config.setComment("async.async_mob_spawning", """ + Whether or not asynchronous mob spawning should be enabled. + On servers with many entities, this can improve performance by up to 15%. You must have + paper's per-player-mob-spawns setting set to true for this to work. + One quick note - this does not actually spawn mobs async (that would be very unsafe). + This just offloads some expensive calculations that are required for mob spawning."""); + + // This prevents us from changing the value during a reload. + if (!asyncMobSpawningInitialized) { + asyncMobSpawningInitialized = true; + this.get("async.async_mob_spawning.enabled", enabled, config); + } + } +}