From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Paul Sauve 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 . 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 { /* 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 chunks = world.getChunkProvider().entityTickingChunks; + sender.sendMessage(world.getWorld().getName() + ": " + chunks.size() + " / " + chunks.getListSize() + " (iterators: " + chunks.iteratorCount + ")"); + } + return true; + } + } + + private final IteratorSafeOrderedReferenceSet chunks; + private final ForkJoinPool pool = new ForkJoinPool(Math.max(4, Runtime.getRuntime().availableProcessors() >> 2)); + private final AtomicInteger taskIndex = new AtomicInteger(); + + private final ConcurrentLinkedQueue mainThreadTasks; + + public TrackQueue(IteratorSafeOrderedReferenceSet chunks, ConcurrentLinkedQueue 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 tickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); - final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet entityTickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); + public final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet 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 supplier) { diff --git a/src/main/java/net/minecraft/server/level/EntityPlayer.java b/src/main/java/net/minecraft/server/level/EntityPlayer.java index fb61b6ac167b34486282a24e598020fb96081f28..b2e21e7034ad83a4ba1c99f860be5a0f5ee6a75f 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 removeQueue = new ArrayDeque<>(); // Paper + public final Deque 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 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 914c7a1b18151f29183cfe9474313ce18e7c4ae2..737851cde7752e7cccf226f1868a38d6411bfb31 100644 --- a/src/main/java/net/minecraft/server/level/PlayerChunkMap.java +++ b/src/main/java/net/minecraft/server/level/PlayerChunkMap.java @@ -769,6 +769,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.updatingChunks.getVisibleAsync(i); + } + // Airplane end protected final IntSupplier getPrioritySupplier(long i) { return c(i); } // Paper - OBFHELPER protected IntSupplier c(long i) { @@ -2164,10 +2169,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 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 iterator = this.world.getChunkProvider().entityTickingChunks.iterator(); try { while (iterator.hasNext()) { @@ -2433,7 +2458,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 @@ -2452,7 +2477,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 lastTrackerCandidates; - final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet 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 newTrackerCandidates) { // Airplane com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet oldTrackerCandidates = this.lastTrackerCandidates; this.lastTrackerCandidates = newTrackerCandidates; @@ -2493,7 +2520,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()) { @@ -2505,6 +2538,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); @@ -2531,8 +2570,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 @@ -2553,7 +2592,7 @@ Sections go from 0..16. Now whenever a section is not empty, it can potentially */ int x = this.tracker.chunkX, z = this.tracker.chunkZ; long chunkcoordintpair = ChunkCoordIntPair.pair(x, z); - PlayerChunk playerchunk = PlayerChunkMap.this.getVisibleChunk(chunkcoordintpair); + PlayerChunk playerchunk = PlayerChunkMap.this.trackerGetVisibleChunk(chunkcoordintpair); // 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.someDistanceCalculation(x, z, 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 9ba7c5080ce0cacf438bdd6e11f75cb34fbc5759..cef0462fadb305ebc2b53d37e0c17514626df99d 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); }