From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Samsuik Date: Mon, 27 May 2024 18:02:27 +0100 Subject: [PATCH] Async Entity Tracking diff --git a/src/main/java/me/samsuik/sakura/player/tracking/AsyncEntityTracker.java b/src/main/java/me/samsuik/sakura/player/tracking/AsyncEntityTracker.java new file mode 100644 index 0000000000000000000000000000000000000000..54422e6e4bf0860ea504414141821cca7c072b28 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/player/tracking/AsyncEntityTracker.java @@ -0,0 +1,101 @@ +package me.samsuik.sakura.player.tracking; + +import com.google.common.collect.Iterators; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.server.level.ChunkMap; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +public final class AsyncEntityTracker { + private static final int AVAILABLE_THREADS = Math.max(Runtime.getRuntime().availableProcessors() / 5 * 2, 1); // 0.4 + private static final ThreadFactory THREAD_FACTORY = EntityTrackerThread::new; + private static ExecutorService TRACKER_EXECUTOR = null; + + private final ChunkMap chunkMap; + private volatile boolean processingTick; + + public AsyncEntityTracker(ChunkMap chunkMap) { + this.chunkMap = chunkMap; + } + + private static void tryStartService() { + if (TRACKER_EXECUTOR == null) { + TRACKER_EXECUTOR = Executors.newFixedThreadPool(AVAILABLE_THREADS, THREAD_FACTORY); + } + } + + public void cycle() { + if (this.processingTick) { + return; // uh oh. + } + + tryStartService(); + this.processingTick = true; + + TrackedEntities trackedEntities = new TrackedEntities(this.chunkMap); + this.updatePlayersInParallel(trackedEntities); + + TRACKER_EXECUTOR.execute(() -> { + try { + this.processEntities(trackedEntities); + } finally { + this.processingTick = false; + } + }); + } + + private void updatePlayersInParallel(TrackedEntities trackedEntities) { + trackedEntities.getEntities().parallelStream().forEach(tracker -> { + if (!tracker.isActive) return; // removed entity + synchronized (tracker.entity) { + if (tracker.shouldLookForPlayers()) { + tracker.updatePlayers(tracker.entity.getPlayersInTrackRange()); + } + } + if (tracker.entity.isPassengersDirty.getAndSet(false)) { + trackedEntities.markEntityAsUpdated(tracker); + } + }); + } + + private void processEntities(TrackedEntities trackedEntities) { + for (ChunkMap.TrackedEntity tracker : trackedEntities) { + if (!tracker.isActive) continue; // removed entity + tracker.seenByLock.readLock().lock(); + synchronized (tracker.entity) { + tracker.serverEntity.sendChanges(); + } + tracker.seenByLock.readLock().unlock(); + } + } + + private static class TrackedEntities implements Iterable { + private final List entities; + private final Set alreadyUpdated; + + public TrackedEntities(ChunkMap chunkMap) { + this.entities = new ArrayList<>(chunkMap.entityMap.values()); + this.alreadyUpdated = new ReferenceOpenHashSet<>(); + } + + public void markEntityAsUpdated(ChunkMap.TrackedEntity trackedEntity) { + this.alreadyUpdated.add(trackedEntity); + } + + public List getEntities() { + return this.entities; + } + + @Override + public @NotNull Iterator iterator() { + return Iterators.filter(this.entities.iterator(), e -> !this.alreadyUpdated.contains(e)); + } + } +} diff --git a/src/main/java/me/samsuik/sakura/player/tracking/EntityTrackerThread.java b/src/main/java/me/samsuik/sakura/player/tracking/EntityTrackerThread.java new file mode 100644 index 0000000000000000000000000000000000000000..a4736257bee2f2c6c51363ca54269d477ae42464 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/player/tracking/EntityTrackerThread.java @@ -0,0 +1,12 @@ +package me.samsuik.sakura.player.tracking; + +import java.util.concurrent.atomic.AtomicInteger; + +public final class EntityTrackerThread extends Thread { + private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(0); + + public EntityTrackerThread(Runnable runnable) { + super(runnable); + this.setName("Entity Tracker Thread " + THREAD_COUNTER.getAndIncrement()); + } +} diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java index 64bf4398a8a0b429e5a7483cf8a24a02c58b7fb3..c9c1f69f942d58304ec592e62d678cf595905bb8 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -1163,8 +1163,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } } // Paper end - optimised tracker - + // Sakura start - async entity tracking + private final me.samsuik.sakura.player.tracking.AsyncEntityTracker asyncEntityTracker = new me.samsuik.sakura.player.tracking.AsyncEntityTracker(this); protected void tick() { + if (this.level.sakuraConfig().players.entityTracker.asyncTracking) { + this.asyncEntityTracker.cycle(); + return; + } + // Sakura end - async entity tracking // Paper start - optimized tracker if (true) { this.processTrackQueue(); @@ -1308,12 +1314,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public class TrackedEntity { public final ServerEntity serverEntity; - final Entity entity; + public final Entity entity; // Sakura - package-protected -> public private final int range; SectionPos lastSectionPos; public final Set seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl private final int playerSearchInterval; // Sakura - reduce entity tracker player updates private Vec3 entityPosition; // Sakura - reduce entity tracker player updates + public final java.util.concurrent.locks.ReentrantReadWriteLock seenByLock = new java.util.concurrent.locks.ReentrantReadWriteLock(); // Sakura - async entity tracking + public volatile boolean isActive = true; // Sakura - async entity tracking public TrackedEntity(Entity entity, int i, int j, boolean flag) { this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit @@ -1343,7 +1351,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // 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 final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newTrackerCandidates) { // Sakura - async entity tracking com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet oldTrackerCandidates = this.lastTrackerCandidates; this.lastTrackerCandidates = newTrackerCandidates; @@ -1355,7 +1363,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider continue; } ServerPlayer player = (ServerPlayer)raw; - this.updatePlayer(player); + this.sakura_updatePlayer(player); // Sakura - async entity tracking } } @@ -1370,7 +1378,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider for (ServerPlayerConnection conn : this.seenBy.toArray(new ServerPlayerConnection[0])) { // avoid CME if (newTrackerCandidates == null || !newTrackerCandidates.contains(conn.getPlayer())) { - this.updatePlayer(conn.getPlayer()); + this.sakura_updatePlayer(conn.getPlayer()); // Sakura - async entity tracking } } } @@ -1412,18 +1420,26 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.serverEntity.removePairing(serverplayerconnection.getPlayer()); } + this.isActive = false; // Sakura - async entity tracking } public void removePlayer(ServerPlayer player) { org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot + this.seenByLock.writeLock().lock(); // Sakura - async entity tracking if (this.seenBy.remove(player.connection)) { this.serverEntity.removePairing(player); } + this.seenByLock.writeLock().unlock(); // Sakura - async entity tracking } public void updatePlayer(ServerPlayer player) { org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot + // Sakura start - async entity tracking + this.sakura_updatePlayer(player); + } + private void sakura_updatePlayer(ServerPlayer player) { + // Sakura end - async entity tracking if (player != this.entity) { // Paper start - remove allocation of Vec3D here // Vec3 vec3d = player.position().subtract(this.entity.position()); @@ -1466,6 +1482,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider flag = false; } // CraftBukkit end + this.seenByLock.writeLock().lock(); // Sakura - async entity tracking if (flag) { if (this.seenBy.add(player.connection)) { // Paper start - entity tracking events @@ -1477,6 +1494,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } else if (this.seenBy.remove(player.connection)) { this.serverEntity.removePairing(player); } + this.seenByLock.writeLock().unlock(); // Sakura - async entity tracking } } diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java index b37f568fbd39be15f08e00f4ea5f28738a1d99fe..aab41f2bc51d2a3766170293e6262f9bb7d3072a 100644 --- a/src/main/java/net/minecraft/server/level/ServerEntity.java +++ b/src/main/java/net/minecraft/server/level/ServerEntity.java @@ -94,6 +94,11 @@ public class ServerEntity { } public void sendChanges() { + // Sakura start - async entity tracking + this.sendChanges(this.entity.isPassengersDirty.getAndSet(false)); + } + private void updateDirtyPassengers() { + // Sakura end - async entity tracking List list = this.entity.getPassengers(); if (!list.equals(this.lastPassengers)) { @@ -108,7 +113,13 @@ public class ServerEntity { }); this.lastPassengers = list; } - + // Sakura start - async entity tracking + } + public final void sendChanges(boolean isPassengersDirty) { + if (isPassengersDirty) { + this.updateDirtyPassengers(); + } + // Sakura end - async entity tracking Entity entity = this.entity; if (!this.trackedPlayers.isEmpty() && entity instanceof ItemFrame) { // Paper - Perf: Only tick item frames if players can see it diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index 2a9a6a9f00343f614a0d2430095a17088861eb1f..8d27289aa81aa60ba6bdde57b74fc4a0a76df4e5 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -923,7 +923,11 @@ public class ServerLevel extends Level implements WorldGenLevel { // Sakura end gameprofilerfiller.push("tick"); + // Sakura start - async entity tracking + synchronized (entity) { this.guardEntityTick(this::tickNonPassenger, entity); + } + // Sakura end - async entity tracking gameprofilerfiller.pop(); } } diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java index d6e60e4e7b5410f30b47e6b9b57b390837368dfc..3583a897912f4ae15111c909284c9b98aa55954c 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -335,8 +335,10 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl this.player.xo = this.player.getX(); this.player.yo = this.player.getY(); this.player.zo = this.player.getZ(); + synchronized (this.player) { // Sakura - async entity tracking this.player.doTick(); this.player.absMoveTo(this.firstGoodX, this.firstGoodY, this.firstGoodZ, this.player.getYRot(), this.player.getXRot()); + } // Sakura - async entity tracking ++this.tickCount; this.knownMovePacketCount = this.receivedMovePacketCount; if (this.clientIsFloating && !this.player.isSleeping() && !this.player.isPassenger() && !this.player.isDeadOrDying()) { @@ -2090,11 +2092,13 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl this.player.disconnect(); // Paper start - Adventure + synchronized (this.player) { // Sakura - async entity tracking quitMessage = quitMessage == null ? this.server.getPlayerList().remove(this.player) : this.server.getPlayerList().remove(this.player, quitMessage); // Paper - pass in quitMessage to fix kick message not being used if ((quitMessage != null) && !quitMessage.equals(net.kyori.adventure.text.Component.empty())) { this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(quitMessage), false); // Paper end } + } // Sakura - async entity tracking // CraftBukkit end this.player.getTextFilter().leave(); } diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index 08d33295967f66051b9e846d854ffac6fe885281..520f158dba52c22a33166e0ad88cffab31c426b4 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -301,6 +301,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S private int id; public boolean blocksBuilding; public ImmutableList passengers; + public final java.util.concurrent.atomic.AtomicBoolean isPassengersDirty = new java.util.concurrent.atomic.AtomicBoolean(); // Sakura - aysnc entity tracking protected int boardingCooldown; @Nullable private Entity vehicle; @@ -3421,6 +3422,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S this.passengers = ImmutableList.copyOf(list); } + this.isPassengersDirty.set(true); // Sakura - async entity tracking this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); } } @@ -3469,6 +3471,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S } entity.boardingCooldown = 60; + this.isPassengersDirty.set(true); // Sakura - async entity tracking this.gameEvent(GameEvent.ENTITY_DISMOUNT, entity); } return true; // CraftBukkit diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java index 45269115e63cfc3bd7dc740a5694e2cc7c35bcb1..fd782a72f934e2de943d664568ccc7293b980e69 100644 --- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java @@ -63,7 +63,7 @@ public class MapItemSavedData extends SavedData { public final List carriedBy = Lists.newArrayList(); public final Map carriedByPlayers = Maps.newHashMap(); private final Map bannerMarkers = Maps.newHashMap(); - public final Map decorations = Maps.newLinkedHashMap(); + public final Map decorations = java.util.Collections.synchronizedMap(Maps.newLinkedHashMap()); // Sakura - async entity tracking private final Map frameMarkers = Maps.newHashMap(); private int trackedDecorationCount; private org.bukkit.craftbukkit.map.RenderData vanillaRender = new org.bukkit.craftbukkit.map.RenderData(); // Paper @@ -584,6 +584,7 @@ public class MapItemSavedData extends SavedData { // Paper start private void addSeenPlayers(java.util.Collection icons) { org.bukkit.entity.Player player = (org.bukkit.entity.Player) this.player.getBukkitEntity(); + synchronized (MapItemSavedData.this.decorations) { // Sakura - async entity tracking MapItemSavedData.this.decorations.forEach((name, mapIcon) -> { // If this cursor is for a player check visibility with vanish system org.bukkit.entity.Player other = org.bukkit.Bukkit.getPlayerExact(name); // Spigot @@ -591,6 +592,7 @@ public class MapItemSavedData extends SavedData { icons.add(mapIcon); } }); + } // Sakura - async entity tracking } private boolean shouldUseVanillaMap() { return mapView.getRenderers().size() == 1 && mapView.getRenderers().get(0).getClass() == org.bukkit.craftbukkit.map.CraftMapRenderer.class; diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java index 4731d10dd5e493af9564d38d8bf1ff223390bd75..b1904549b1b1180ebae822e63871f6e80d9100cc 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -715,9 +715,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { ChunkMap.TrackedEntity entityTracker = world.getChunkSource().chunkMap.entityMap.get(this.getEntityId()); if (entityTracker != null) { + entityTracker.seenByLock.readLock().lock(); // Sakura - async entity tracking for (ServerPlayerConnection connection : entityTracker.seenBy) { players.add(connection.getPlayer().getBukkitEntity()); } + entityTracker.seenByLock.readLock().unlock(); // Sakura - async entity tracking } return players.build(); @@ -1013,9 +1015,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { } // Paper start, resend possibly desynced entity instead of add entity packet + entityTracker.seenByLock.readLock().lock(); // Sakura - async entity tracking for (ServerPlayerConnection playerConnection : entityTracker.seenBy) { this.getHandle().getEntityData().resendPossiblyDesyncedEntity(playerConnection.getPlayer()); } + entityTracker.seenByLock.readLock().unlock(); // Sakura - async entity tracking // Paper end } @@ -1183,10 +1187,12 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { return java.util.Collections.emptySet(); } + this.entity.tracker.seenByLock.readLock().lock(); // Sakura - async entity tracking Set set = new java.util.HashSet<>(this.entity.tracker.seenBy.size()); for (net.minecraft.server.network.ServerPlayerConnection connection : this.entity.tracker.seenBy) { set.add(connection.getPlayer().getBukkitEntity().getPlayer()); } + this.entity.tracker.seenByLock.readLock().unlock(); // Sakura - async entity tracking return set; } // Paper end - tracked players API diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index 29f0c4c3fd9185bf8768572c135b50a9db34dbbe..cd462589ab90bf8dfd4a213f49c1257c8152e137 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -2060,9 +2060,11 @@ public class CraftPlayer extends CraftHumanEntity implements Player { } ChunkMap.TrackedEntity entry = tracker.entityMap.get(other.getId()); + entry.seenByLock.readLock().lock(); // Sakura - async entity tracking if (entry != null && !entry.seenBy.contains(this.getHandle().connection)) { entry.updatePlayer(this.getHandle()); } + entry.seenByLock.readLock().unlock(); // Sakura - async entity tracking this.server.getPluginManager().callEvent(new PlayerShowEntityEvent(this, entity)); } diff --git a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java index 15e9dd8844f893de5e8372b847c9e8295d6f69ca..8948474a27b70ed00a2eb0fc6db3beadd2a083ee 100644 --- a/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java +++ b/src/main/java/org/bukkit/craftbukkit/map/CraftMapRenderer.java @@ -34,6 +34,7 @@ public class CraftMapRenderer extends MapRenderer { cursors.removeCursor(cursors.getCursor(0)); } + synchronized (this.worldMap.decorations) { // Sakura - async entity tracking for (String key : this.worldMap.decorations.keySet()) { // If this cursor is for a player check visibility with vanish system Player other = Bukkit.getPlayerExact((String) key); @@ -44,6 +45,7 @@ public class CraftMapRenderer extends MapRenderer { MapDecoration decoration = this.worldMap.decorations.get(key); cursors.addCursor(decoration.x(), decoration.y(), (byte) (decoration.rot() & 15), decoration.type().getIcon(), true, decoration.name() == null ? null : io.papermc.paper.adventure.PaperAdventure.asAdventure(decoration.name())); // Paper } + } // Sakura - async entity tracking } }