9
0
mirror of https://github.com/Winds-Studio/Leaf.git synced 2025-12-22 16:39:22 +00:00
Files
Leaf/patches/server/0004-Pufferfish-Optimize-mob-spawning.patch
2023-06-15 19:40:54 +08:00

326 lines
17 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Kevin Raneri <kevin.raneri@gmail.com>
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/gg/pufferfish/pufferfish/PufferfishLogger.java b/src/main/java/gg/pufferfish/pufferfish/PufferfishLogger.java
new file mode 100644
index 0000000000000000000000000000000000000000..dd5e1381754a158c10704d525a02092008be4c56
--- /dev/null
+++ b/src/main/java/gg/pufferfish/pufferfish/PufferfishLogger.java
@@ -0,0 +1,17 @@
+package gg.pufferfish.pufferfish;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.bukkit.Bukkit;
+
+public class PufferfishLogger extends Logger {
+ public static final PufferfishLogger LOGGER = new PufferfishLogger();
+
+ private PufferfishLogger() {
+ super("Pufferfish", null);
+
+ setParent(Bukkit.getLogger());
+ setLevel(Level.ALL);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/gg/pufferfish/pufferfish/util/AsyncExecutor.java b/src/main/java/gg/pufferfish/pufferfish/util/AsyncExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..23c959cd4075f375887b0d6868120223a65c8980
--- /dev/null
+++ b/src/main/java/gg/pufferfish/pufferfish/util/AsyncExecutor.java
@@ -0,0 +1,74 @@
+package gg.pufferfish.pufferfish.util;
+
+import com.google.common.collect.Queues;
+import gg.pufferfish.pufferfish.PufferfishLogger;
+
+import java.util.Queue;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.logging.Level;
+
+public class AsyncExecutor implements Runnable {
+
+ private final Queue<Runnable> jobs = Queues.newArrayDeque();
+ private final Lock mutex = new ReentrantLock();
+ private final Condition cond = mutex.newCondition();
+ private final Thread thread;
+ private volatile boolean killswitch = false;
+
+ public AsyncExecutor(String threadName) {
+ this.thread = new Thread(this, threadName);
+ }
+
+ public void start() {
+ thread.start();
+ }
+
+ public void kill() {
+ killswitch = true;
+ cond.signalAll();
+ }
+
+ public void submit(Runnable runnable) {
+ mutex.lock();
+ try {
+ jobs.offer(runnable);
+ cond.signalAll();
+ } finally {
+ mutex.unlock();
+ }
+ }
+
+ @Override
+ public void run() {
+ while (!killswitch) {
+ try {
+ Runnable runnable = takeRunnable();
+ if (runnable != null) {
+ runnable.run();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } catch (Exception e) {
+ PufferfishLogger.LOGGER.log(Level.SEVERE, e, () -> "Failed to execute async job for thread " + thread.getName());
+ }
+ }
+ }
+
+ private Runnable takeRunnable() throws InterruptedException {
+ mutex.lock();
+ try {
+ while (jobs.isEmpty() && !killswitch) {
+ cond.await();
+ }
+
+ if (jobs.isEmpty()) return null; // We've set killswitch
+
+ return jobs.remove();
+ } finally {
+ mutex.unlock();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/gg/pufferfish/pufferfish/util/IterableWrapper.java b/src/main/java/gg/pufferfish/pufferfish/util/IterableWrapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..65c3d2989e8140259c5720222c569129034aca6c
--- /dev/null
+++ b/src/main/java/gg/pufferfish/pufferfish/util/IterableWrapper.java
@@ -0,0 +1,20 @@
+package gg.pufferfish.pufferfish.util;
+
+import java.util.Iterator;
+import org.jetbrains.annotations.NotNull;
+
+public class IterableWrapper<T> implements Iterable<T> {
+
+ private final Iterator<T> iterator;
+
+ public IterableWrapper(Iterator<T> iterator) {
+ this.iterator = iterator;
+ }
+
+ @NotNull
+ @Override
+ public Iterator<T> iterator() {
+ return iterator;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 19d1e0dbee516e360381b6d7e1fee83e03ae9034..c62a225a97deca88a15f3d7c61550debdcbfccfe 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -297,6 +297,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public volatile boolean abnormalExit = false; // Paper
public boolean isIteratingOverLevels = false; // Paper
+ public gg.pufferfish.pufferfish.util.AsyncExecutor mobSpawnExecutor = new gg.pufferfish.pufferfish.util.AsyncExecutor("MobSpawning"); // Pufferfish - optimize mob spawning
public static <S extends MinecraftServer> S spin(Function<Thread, S> serverFactory) {
AtomicReference<S> 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 8ba7f4aad1ff28d5f38b895e8eb47e141390e5ea..69c594bbd52335d6779b06f9273a1ef8e8138a67 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
@@ -47,6 +47,7 @@ import net.minecraft.world.level.GameRules;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.entity.SkullBlockEntity;
import net.minecraft.world.level.storage.LevelStorageSource;
+import org.dreeam.leaf.LeafConfig;
import org.galemc.gale.command.GaleCommands;
import org.galemc.gale.configuration.GaleGlobalConfiguration;
import org.slf4j.Logger;
@@ -358,6 +359,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
DedicatedServer.LOGGER.info("JMX monitoring enabled");
}
+ if (LeafConfig.enableAsyncMobSpawning) mobSpawnExecutor.start(); // Pufferfish
return true;
}
}
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
index 3c9ff57cf8cf7e7bfca234e460ff869165bd40d3..9e743d20e9ff979ebad152f209a6ca97a533ea47 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -48,6 +48,7 @@ import net.minecraft.world.level.storage.DimensionDataStorage;
import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.level.storage.LevelStorageSource;
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; // Paper
+import org.dreeam.leaf.LeafConfig;
public class ServerChunkCache extends ChunkSource {
@@ -75,6 +76,9 @@ public class ServerChunkCache extends ChunkSource {
final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<LevelChunk> loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f);
private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4];
+
+ public boolean firstRunSpawnCounts = true; // Pufferfish
+ public final java.util.concurrent.atomic.AtomicBoolean _pufferfish_spawnCountsReady = new java.util.concurrent.atomic.AtomicBoolean(false); // Pufferfish - optimize countmobs
private static int getChunkCacheKey(int x, int z) {
return x & 3 | ((z & 3) << 2);
@@ -553,18 +557,25 @@ public class ServerChunkCache extends ChunkSource {
int l = this.distanceManager.getNaturalSpawnChunkCount();
// Paper start - per player mob spawning
if ((this.spawnFriendlies || this.spawnEnemies) && this.chunkMap.playerMobDistanceMap != null) { // don't count mobs when animals and monsters are disabled
- // re-set mob counts
- for (ServerPlayer player : this.level.players) {
- Arrays.fill(player.mobCounts, 0);
+ // Pufferfish start - moved down when async processing
+ if (!LeafConfig.enableAsyncMobSpawning) {
+ // re-set mob counts
+ for (ServerPlayer player : this.level.players) {
+ Arrays.fill(player.mobCounts, 0);
+ }
+ lastSpawnState = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, null, true);
}
- spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, null, true);
+ // Pufferfish end
} else {
- spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, this.chunkMap.playerMobDistanceMap == null ? new LocalMobCapCalculator(this.chunkMap) : null, false);
+ // Pufferfish start - this is only implemented for per-player mob spawning so this makes everything work if this setting is disabled.
+ lastSpawnState = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, this.chunkMap.playerMobDistanceMap == null ? new LocalMobCapCalculator(this.chunkMap) : null, false);
+ _pufferfish_spawnCountsReady.set(true);
+ // Pufferfish end
}
// Paper end
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;
@@ -604,8 +615,8 @@ public class ServerChunkCache extends ChunkSource {
if ((true || this.level.isNaturalSpawningAllowed(chunkcoordintpair)) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, false)) { // Paper - optimise anyPlayerCloseEnoughForSpawning // Paper - the chunk is known ticking
chunk1.incrementInhabitedTime(j);
- if (flag2AndHasNaturalSpawn && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, true)) { // Spigot // Paper - optimise anyPlayerCloseEnoughForSpawning & optimise chunk tick iteration // Gale - MultiPaper - skip unnecessary mob spawning computations
- NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1);
+ if (flag2AndHasNaturalSpawn && (!LeafConfig.enableAsyncMobSpawning || _pufferfish_spawnCountsReady.get()) && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(holder, chunkcoordintpair, true)) { // Spigot // Paper - optimise anyPlayerCloseEnoughForSpawning & optimise chunk tick iteration // Gale - MultiPaper - skip unnecessary mob spawning computations
+ NaturalSpawner.spawnForChunk(this.level, chunk1, lastSpawnState, this.spawnFriendlies, this.spawnEnemies, flag1);
}
if (true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { // Paper - the chunk is known ticking
@@ -663,6 +674,29 @@ public class ServerChunkCache extends ChunkSource {
}
// Paper end - controlled flush for entity tracker packets
}
+ // Pufferfish start - optimize mob spawning
+ if (LeafConfig.enableAsyncMobSpawning) {
+ for (ServerPlayer player : this.level.players) {
+ Arrays.fill(player.mobCounts, 0);
+ }
+ if (firstRunSpawnCounts) {
+ firstRunSpawnCounts = false;
+ _pufferfish_spawnCountsReady.set(true);
+ }
+ if (chunkMap.playerMobDistanceMap != null && _pufferfish_spawnCountsReady.getAndSet(false)) {
+ net.minecraft.server.MinecraftServer.getServer().mobSpawnExecutor.submit(() -> {
+ int mapped = distanceManager.getNaturalSpawnChunkCount();
+ io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator<Entity> objectiterator =
+ level.entityTickList.entities.iterator(io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS);
+ gg.pufferfish.pufferfish.util.IterableWrapper<Entity> 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<Entity> 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<Entity> 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/LeafConfig.java b/src/main/java/org/dreeam/leaf/LeafConfig.java
index e8b770ad0f7b715deeeb6469a881d65640fd3bf5..00fc9278a61b963382c03c36bc11917361c55612 100644
--- a/src/main/java/org/dreeam/leaf/LeafConfig.java
+++ b/src/main/java/org/dreeam/leaf/LeafConfig.java
@@ -164,6 +164,8 @@ public class LeafConfig {
}
public static String sentryDsn = "";
+ public static boolean enableAsyncMobSpawning;
+ public static boolean asyncMobSpawningInitialized;
private static void performance() {
String sentryEnvironment = System.getenv("SENTRY_DSN");
String sentryConfig = getString("performance.sentry-dsn", sentryDsn, "Sentry DSN for improved error logging, leave blank to disable", "Obtain from https://sentry.io/");
@@ -171,6 +173,18 @@ public class LeafConfig {
if (sentryDsn != null && !sentryDsn.isBlank()) {
gg.pufferfish.pufferfish.sentry.SentryManager.init();
}
+ boolean temp = getBoolean("enable-async-mob-spawning", true,
+ "Async mob spawning is unstable on Leaf, uses as your own risk!",
+ "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;
+ enableAsyncMobSpawning = temp;
+ }
}
private static void network() {