Yatopia/patches/Purpur/patches/server/0001-Multithreaded-entity-t...

464 lines
24 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Paul Sauve <paul@technove.co>
Date: Fri, 12 Feb 2021 16:29:41 -0600
Subject: [PATCH] Multithreaded entity tracking
Adds a configuration option to run the tracker on multiple threads.
This will generally be faster than the single threaded approach,
unless you don't have a lot of entities/players.
Airplane
Copyright (C) 2020 Technove LLC
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
diff --git a/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java
index be408aebbccbda46e8aa82ef337574137cfa0096..739839314fd8a88b5fca8b9678e1df07a166c35d 100644
--- a/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java
+++ b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java
@@ -16,11 +16,11 @@ public final class IteratorSafeOrderedReferenceSet<E> {
/* list impl */
protected E[] listElements;
- protected int listSize;
+ protected int listSize; public int getListSize() { return this.listSize; } // Airplane - getter
protected final double maxFragFactor;
- protected int iteratorCount;
+ public int iteratorCount; // Airplane - public for debug
private final boolean threadRestricted;
diff --git a/src/main/java/gg/airplane/AirplaneConfig.java b/src/main/java/gg/airplane/AirplaneConfig.java
index 7ec84ef1d1cbb1fabf4c590a2f2c1da3cc181010..b9118cc08ac38e0813d0677700d3d7dcf9b74159 100644
--- a/src/main/java/gg/airplane/AirplaneConfig.java
+++ b/src/main/java/gg/airplane/AirplaneConfig.java
@@ -102,4 +102,17 @@ public class AirplaneConfig {
}
+ public static boolean multithreadedEntityTracker = false;
+ public static boolean entityTrackerAsyncPackets = false;
+
+ private static void entityTracker() {
+ config.setComment("tracker", "Options to improve the performance of the entity tracker");
+
+ multithreadedEntityTracker = config.getBoolean("tracker.multithreaded", multithreadedEntityTracker,
+ "This enables the multithreading of the tracker.");
+ entityTrackerAsyncPackets = config.getBoolean("tracker.unsafe-async-packets", entityTrackerAsyncPackets,
+ "This option can break plugins that assume packets from the",
+ "entity tracker will be sent sync.");
+ }
+
}
diff --git a/src/main/java/gg/airplane/commands/AirplaneCommands.java b/src/main/java/gg/airplane/commands/AirplaneCommands.java
index 66b20250a26d005427601b1cdee43bdd9eba70cc..f84e26b2d8ab9a9b2d9e92e18002483127121135 100644
--- a/src/main/java/gg/airplane/commands/AirplaneCommands.java
+++ b/src/main/java/gg/airplane/commands/AirplaneCommands.java
@@ -2,11 +2,13 @@ package gg.airplane.commands;
import gg.airplane.AirplaneCommand;
import gg.airplane.flare.FlareCommand;
+import gg.airplane.structs.TrackQueue;
import net.minecraft.server.MinecraftServer;
public class AirplaneCommands {
public static void init() {
MinecraftServer.getServer().server.getCommandMap().register("airplane", "Airplane", new AirplaneCommand());
MinecraftServer.getServer().server.getCommandMap().register("flare", "Airplane", new FlareCommand());
+ TrackQueue.TrackQueueDebugCommand.register();
}
}
diff --git a/src/main/java/gg/airplane/structs/TrackQueue.java b/src/main/java/gg/airplane/structs/TrackQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..4419fbe94041f4b8a0ea848880798289d063b8e2
--- /dev/null
+++ b/src/main/java/gg/airplane/structs/TrackQueue.java
@@ -0,0 +1,109 @@
+package gg.airplane.structs;
+
+import com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet;
+import net.minecraft.server.level.WorldServer;
+import net.minecraft.world.level.chunk.Chunk;
+import net.minecraft.server.MinecraftServer;
+import org.apache.logging.log4j.Level;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Helper class to handle processing a track queue.
+ */
+public class TrackQueue {
+
+ public static class TrackQueueDebugCommand extends Command {
+ protected TrackQueueDebugCommand() {
+ super("trackqueuedebug");
+ }
+
+ public static void register() {
+ MinecraftServer.getServer().server.getCommandMap().register("trackqueuedebug", "Airplane", new TrackQueueDebugCommand());
+ }
+
+ @Override
+ public boolean execute(CommandSender sender, String commandLabel, String[] args) {
+ if (!sender.isOp()) {
+ return false;
+ }
+ for (WorldServer world : MinecraftServer.getServer().getWorlds()) {
+ IteratorSafeOrderedReferenceSet<Chunk> chunks = world.getChunkProvider().entityTickingChunks;
+ sender.sendMessage(world.getWorld().getName() + ": " + chunks.size() + " / " + chunks.getListSize() + " (iterators: " + chunks.iteratorCount + ")");
+ }
+ return true;
+ }
+ }
+
+ private final IteratorSafeOrderedReferenceSet<Chunk> chunks;
+ private final ForkJoinPool pool = new ForkJoinPool(Math.max(4, Runtime.getRuntime().availableProcessors() >> 2));
+ private final AtomicInteger taskIndex = new AtomicInteger();
+
+ private final ConcurrentLinkedQueue<Runnable> mainThreadTasks;
+
+ public TrackQueue(IteratorSafeOrderedReferenceSet<Chunk> chunks, ConcurrentLinkedQueue<Runnable> mainThreadTasks) {
+ this.chunks = chunks;
+ this.mainThreadTasks = mainThreadTasks;
+ }
+
+ public void start() {
+ int iterator = this.chunks.createRawIterator();
+ if (iterator == -1) {
+ return;
+ }
+ try {
+ this.taskIndex.set(iterator);
+
+ for (int i = 0; i < this.pool.getParallelism(); i++) {
+ this.pool.execute(this::run);
+ }
+
+ while (this.taskIndex.get() < this.chunks.getListSize()) {
+ this.runMainThreadTasks();
+ this.handleTask(); // assist
+ }
+ this.runMainThreadTasks(); // finish tasks
+ } finally {
+ this.chunks.finishRawIterator();
+ }
+ }
+
+ private void runMainThreadTasks() {
+ Runnable task;
+ while ((task = this.mainThreadTasks.poll()) != null) {
+ try {
+ task.run();
+ } catch (Throwable t) {
+ MinecraftServer.LOGGER.log(Level.WARN, "Tasks failed while ticking track queue", t);
+ }
+ }
+
+ }
+
+ private void run() {
+ while (handleTask());
+ }
+
+ private boolean handleTask() {
+ int index;
+ while ((index = this.taskIndex.getAndIncrement()) < this.chunks.getListSize()) {
+ Chunk chunk = this.chunks.rawGet(index);
+ if (chunk != null) {
+ try {
+ chunk.entityTracker.run();
+ } catch (Throwable t) {
+ MinecraftServer.LOGGER.log(Level.WARN, "Ticking tracker failed", t);
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/src/main/java/net/minecraft/server/level/ChunkProviderServer.java b/src/main/java/net/minecraft/server/level/ChunkProviderServer.java
index 207a9c3928aad7c6e89a120b54d87e003ebd232c..424cd048f905cd0ed3f7a4545a26ffde8da1f91f 100644
--- a/src/main/java/net/minecraft/server/level/ChunkProviderServer.java
+++ b/src/main/java/net/minecraft/server/level/ChunkProviderServer.java
@@ -388,7 +388,7 @@ public class ChunkProviderServer extends IChunkProvider {
}
final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<Chunk> tickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
- final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<Chunk> entityTickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
+ public final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<Chunk> entityTickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); // Airplane - public for debug
// Tuinity end
public ChunkProviderServer(WorldServer worldserver, Convertable.ConversionSession convertable_conversionsession, DataFixer datafixer, DefinedStructureManager definedstructuremanager, Executor executor, ChunkGenerator chunkgenerator, int i, boolean flag, WorldLoadListener worldloadlistener, Supplier<WorldPersistentData> supplier) {
diff --git a/src/main/java/net/minecraft/server/level/EntityPlayer.java b/src/main/java/net/minecraft/server/level/EntityPlayer.java
index 62b95dcba8606330fbb3239e74c5eaf8baa3c51d..fd6dbbf619b828c4ef3d00f8dc30d3893007db7b 100644
--- a/src/main/java/net/minecraft/server/level/EntityPlayer.java
+++ b/src/main/java/net/minecraft/server/level/EntityPlayer.java
@@ -182,7 +182,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
public NetworkManager networkManager; // Paper
public final MinecraftServer server;
public final PlayerInteractManager playerInteractManager;
- public final Deque<Integer> removeQueue = new ArrayDeque<>(); // Paper
+ public final Deque<Integer> removeQueue = new java.util.concurrent.ConcurrentLinkedDeque<>(); // Paper // Airplane concurrent deque
private final AdvancementDataPlayer advancementDataPlayer;
private final ServerStatisticManager serverStatisticManager;
private float lastHealthScored = Float.MIN_VALUE;
diff --git a/src/main/java/net/minecraft/server/level/EntityTrackerEntry.java b/src/main/java/net/minecraft/server/level/EntityTrackerEntry.java
index 67ca28463f5add7c18f7f16b918c3f36f8feeeda..37e64e24ca3a90370cdf12e5ff9cd1fceede796b 100644
--- a/src/main/java/net/minecraft/server/level/EntityTrackerEntry.java
+++ b/src/main/java/net/minecraft/server/level/EntityTrackerEntry.java
@@ -75,6 +75,10 @@ public class EntityTrackerEntry {
* Requested in https://github.com/PaperMC/Paper/issues/1537 to allow intercepting packets
*/
public void sendPlayerPacket(EntityPlayer player, Packet packet) {
+ if (!gg.airplane.AirplaneConfig.entityTrackerAsyncPackets && !org.bukkit.Bukkit.isPrimaryThread()) {
+ this.b.chunkProvider.playerChunkMap.trackerEnsureMain(() -> sendPlayerPacket(player, packet));
+ return;
+ }
player.playerConnection.sendPacket(packet);
}
@@ -103,7 +107,7 @@ public class EntityTrackerEntry {
public final void tick() { this.a(); } // Paper - OBFHELPER
public void a() {
- com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Tracker update"); // Tuinity
+ //com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Tracker update"); // Tuinity // Airplane - allow multithreaded
List<Entity> list = this.tracker.passengers; // Paper - do not copy list
if (!list.equals(this.p)) {
@@ -116,6 +120,8 @@ public class EntityTrackerEntry {
ItemStack itemstack = entityitemframe.getItem();
if (this.tickCounter % 10 == 0 && itemstack.getItem() instanceof ItemWorldMap) { // CraftBukkit - Moved this.tickCounter % 10 logic here so item frames do not enter the other blocks
+ // Airplane start - process maps on main
+ this.b.chunkProvider.playerChunkMap.trackerEnsureMain(() -> {
WorldMap worldmap = ItemWorldMap.getSavedMap(itemstack, this.b);
Iterator iterator = this.trackedPlayers.iterator(); // CraftBukkit
@@ -129,6 +135,8 @@ public class EntityTrackerEntry {
entityplayer.playerConnection.sendPacket(packet);
}
}
+ });
+ // Airplane end
}
this.c();
@@ -263,18 +271,25 @@ public class EntityTrackerEntry {
// CraftBukkit start - Create PlayerVelocity event
boolean cancelled = false;
- if (this.tracker instanceof EntityPlayer) {
+ if (this.tracker instanceof EntityPlayer && PlayerVelocityEvent.getHandlerList().getRegisteredListeners().length > 0) { // Airplane - ensure there's listeners
+ // Airplane start - run on main thread
+ this.b.chunkProvider.playerChunkMap.trackerEnsureMain(() -> {
Player player = (Player) this.tracker.getBukkitEntity();
org.bukkit.util.Vector velocity = player.getVelocity();
PlayerVelocityEvent event = new PlayerVelocityEvent(player, velocity.clone());
this.tracker.world.getServer().getPluginManager().callEvent(event);
- if (event.isCancelled()) {
- cancelled = true;
- } else if (!velocity.equals(event.getVelocity())) {
+ if (!event.isCancelled() && !velocity.equals(event.getVelocity())) {
player.setVelocity(event.getVelocity());
}
+ if (!event.isCancelled()) {
+ this.broadcastIncludingSelf(new PacketPlayOutEntityVelocity(this.tracker)); // duplicate from !cancelled below
+ }
+
+ });
+ cancelled = true; // don't broadcast until the event has finished
+ // Airplane end
}
if (!cancelled) {
@@ -358,7 +373,9 @@ public class EntityTrackerEntry {
if (!list.isEmpty()) {
consumer.accept(new PacketPlayOutEntityEquipment(this.tracker.getId(), list));
}
+ this.b.chunkProvider.playerChunkMap.trackerEnsureMain(() -> { // Airplane
((EntityLiving) this.tracker).updateEquipment(); // CraftBukkit - SPIGOT-3789: sync again immediately after sending
+ }); // Airplane
}
// CraftBukkit start - Fix for nonsensical head yaw
@@ -436,6 +453,10 @@ public class EntityTrackerEntry {
// Paper end
private void broadcastIncludingSelf(Packet<?> packet) {
+ if (!gg.airplane.AirplaneConfig.entityTrackerAsyncPackets && !org.bukkit.Bukkit.isPrimaryThread()) {
+ this.b.chunkProvider.playerChunkMap.trackerEnsureMain(() -> broadcastIncludingSelf(packet));
+ return;
+ }
this.f.accept(packet);
if (this.tracker instanceof EntityPlayer) {
((EntityPlayer) this.tracker).playerConnection.sendPacket(packet);
diff --git a/src/main/java/net/minecraft/server/level/PlayerChunkMap.java b/src/main/java/net/minecraft/server/level/PlayerChunkMap.java
index b28995ecfd7f45e6b6197be96c418aa0d05d3383..bb5ebacf99238223b84f3663af3ab9c6c60332eb 100644
--- a/src/main/java/net/minecraft/server/level/PlayerChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/PlayerChunkMap.java
@@ -763,6 +763,11 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
return this.updatingChunks.getVisibleAsync(i);
// Tuinity end - Don't copy
}
+ // Airplane start - since neither map can be updated during tracker tick, it's safe to allow direct retrieval here
+ private PlayerChunk trackerGetVisibleChunk(long i) {
+ return this.hasPendingVisibleUpdate ? this.pendingVisibleChunks.get(i) : ((ProtectedVisibleChunksMap) this.visibleChunks).safeGet(i);
+ }
+ // Airplane end
protected final IntSupplier getPrioritySupplier(long i) { return c(i); } // Paper - OBFHELPER
protected IntSupplier c(long i) {
@@ -2158,10 +2163,30 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
entity.tracker = null; // Paper - We're no longer tracked
}
+ // Airplane start - tools to ensure main thread
+ private final java.util.concurrent.ConcurrentLinkedQueue<Runnable> trackerMainQueue = new java.util.concurrent.ConcurrentLinkedQueue<>();
+ private gg.airplane.structs.TrackQueue trackQueue;
+
+ void trackerEnsureMain(Runnable runnable) {
+ if (this.world.serverThread == Thread.currentThread()) {
+ runnable.run();
+ } else {
+ this.trackerMainQueue.add(runnable);
+ }
+ }
+ // Airplane end
+
// Paper start - optimised tracker
private final void processTrackQueue() {
this.world.timings.tracker1.startTiming();
try {
+ // Airplane start - multithreaded tracker
+ if (this.trackQueue == null) this.trackQueue = new gg.airplane.structs.TrackQueue(this.world.getChunkProvider().entityTickingChunks, trackerMainQueue);
+ if (gg.airplane.AirplaneConfig.multithreadedEntityTracker) {
+ this.trackQueue.start();
+ return;
+ }
+ // Airplane end
com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator<Chunk> iterator = this.world.getChunkProvider().entityTickingChunks.iterator();
try {
while (iterator.hasNext()) {
@@ -2427,7 +2452,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
public class EntityTracker {
final EntityTrackerEntry trackerEntry; // Paper - private -> package private
- private final Entity tracker;
+ public final Entity tracker; // Airplane - public for chunk
private final int trackingDistance;
private SectionPosition e;
// Paper start
@@ -2446,7 +2471,9 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
// Paper start - use distance map to optimise tracker
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> lastTrackerCandidates;
- final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newTrackerCandidates) {
+ public synchronized final void tickEntry() { this.trackerEntry.tick(); } // Airplane - move entry tick into sync block
+
+ public synchronized final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newTrackerCandidates) { // Airplane
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> oldTrackerCandidates = this.lastTrackerCandidates;
this.lastTrackerCandidates = newTrackerCandidates;
@@ -2487,7 +2514,13 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
return this.tracker.getId();
}
- public void broadcast(Packet<?> packet) {
+ public synchronized void broadcast(Packet<?> packet) { // Airplane - synchronized for tracked player
+ // Airplane start
+ if (!gg.airplane.AirplaneConfig.entityTrackerAsyncPackets && !org.bukkit.Bukkit.isPrimaryThread()) {
+ trackerEnsureMain(() -> broadcast(packet));
+ return;
+ }
+ // Airplane end
Iterator iterator = this.trackedPlayers.iterator();
while (iterator.hasNext()) {
@@ -2499,6 +2532,12 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
}
public void broadcastIncludingSelf(Packet<?> packet) {
+ // Airplane start
+ if (!gg.airplane.AirplaneConfig.entityTrackerAsyncPackets && !org.bukkit.Bukkit.isPrimaryThread()) {
+ trackerEnsureMain(() -> broadcastIncludingSelf(packet));
+ return;
+ }
+ // Airplane end
this.broadcast(packet);
if (this.tracker instanceof EntityPlayer) {
((EntityPlayer) this.tracker).playerConnection.sendPacket(packet);
@@ -2525,8 +2564,8 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
}
- public void updatePlayer(EntityPlayer entityplayer) {
- org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot
+ public synchronized void updatePlayer(EntityPlayer entityplayer) { // Airplane - sync for access to map
+ //org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot // Airplane - allow sync for tracker
if (entityplayer != this.tracker) {
// Paper start - remove allocation of Vec3D here
//Vec3D vec3d = entityplayer.getPositionVector().d(this.tracker.getPositionVector()); // MC-155077, SPIGOT-5113
@@ -2542,7 +2581,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially
if (!flag1) {
ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(this.tracker.chunkX, this.tracker.chunkZ);
- PlayerChunk playerchunk = PlayerChunkMap.this.getVisibleChunk(chunkcoordintpair.pair());
+ PlayerChunk playerchunk = PlayerChunkMap.this.trackerGetVisibleChunk(chunkcoordintpair.pair()); // Airplane
if (playerchunk != null && playerchunk.getSendingChunk() != null && PlayerChunkMap.this.playerChunkManager.isChunkSent(entityplayer, MathHelper.floor(this.tracker.locX()) >> 4, MathHelper.floor(this.tracker.locZ()) >> 4)) { // Paper - no-tick view distance // Tuinity - don't broadcast in chunks the player hasn't received
flag1 = PlayerChunkMap.b(chunkcoordintpair, entityplayer, false) <= PlayerChunkMap.this.viewDistance;
diff --git a/src/main/java/net/minecraft/world/level/chunk/Chunk.java b/src/main/java/net/minecraft/world/level/chunk/Chunk.java
index 8f5809756b4fb358f1207c1d61c5cbe6df3fff00..2e8eb0bb8fb4f7ce6b92fe01a81327da30e614ae 100644
--- a/src/main/java/net/minecraft/world/level/chunk/Chunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/Chunk.java
@@ -111,6 +111,26 @@ public class Chunk implements IChunkAccess {
}
// Airplane end
+ // Airplane start - entity tracker runnable
+ // prevents needing to allocate new lambda in processTrackQueue
+ public final Runnable entityTracker = new Runnable() {
+ @Override
+ public void run() {
+ Entity[] entities = Chunk.this.entities.getRawData();
+ for (int i = 0, len = Chunk.this.entities.size(); i < len; ++i) {
+ Entity entity = entities[i];
+ if (entity != null) {
+ PlayerChunkMap.EntityTracker tracker = ((WorldServer) Chunk.this.getWorld()).getChunkProvider().playerChunkMap.trackedEntities.get(entity.getId());
+ if (tracker != null) {
+ tracker.updatePlayers(tracker.tracker.getPlayersInTrackRange());
+ tracker.tickEntry();
+ }
+ }
+ }
+ }
+ };
+ // Airplane end
+
public Chunk(World world, ChunkCoordIntPair chunkcoordintpair, BiomeStorage biomestorage) {
this(world, chunkcoordintpair, biomestorage, ChunkConverter.a, TickListEmpty.b(), TickListEmpty.b(), 0L, (ChunkSection[]) null, (Consumer) null);
}