9
0
mirror of https://github.com/Winds-Studio/Leaf.git synced 2025-12-22 16:39:22 +00:00
Files
Leaf/patches/server/0039-Petal-Multithreaded-Tracker.patch
2024-02-27 23:04:22 -05:00

489 lines
23 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: peaches94 <peachescu94@gmail.com>
Date: Sat, 2 Jul 2022 00:35:56 -0500
Subject: [PATCH] Petal: Multithreaded Tracker
Original code by Bloom-host, licensed under GPL v3
You can find the original code on https://github.com/Bloom-host/Petal
Issues waiting to check:
https://github.com/Bloom-host/Petal/issues/26
https://github.com/Bloom-host/Petal/issues/23
https://github.com/Bloom-host/Petal/issues/12
https://github.com/Bloom-host/Petal/issues/11
https://github.com/Bloom-host/Petal/issues/5
https://github.com/Bloom-host/Petal/issues/3
This patch was ported downstream from the Petal fork, and is derived from
the Airplane fork by Paul Sauve
Based off the Airplane multithreaded tracker, this patch properly handles
concurrent accesses everywhere, as well as being much simpler to maintain
Some things are too unsafe to run off the main thread so we don't attempt to do
that. This multithreaded tracker remains accurate, non-breaking and fast.
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
index 06dfd0b27ac0006a2be07f54a0702519a691c6ec..cc38235d2b5f3fc5113a4d495f14f81cd24d7cb9 100644
--- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
@@ -3,6 +3,7 @@ package io.papermc.paper.plugin.manager;
import com.destroystokyo.paper.event.server.ServerExceptionEvent;
import com.destroystokyo.paper.exception.ServerEventException;
import com.google.common.collect.Sets;
+import net.minecraft.server.MinecraftServer;
import org.bukkit.Server;
import org.bukkit.Warning;
import org.bukkit.event.Event;
@@ -42,7 +43,14 @@ class PaperEventManager {
if (isAsync && onPrimaryThread) {
throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously.");
} else if (!isAsync && !onPrimaryThread && !this.server.isStopping()) {
- throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously.");
+ // Leaf start - petal
+ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled) {
+ MinecraftServer.getServer().scheduleOnMain(event::callEvent);
+ return;
+ } else {
+ throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously.");
+ }
+ // Leaf end - petal
}
// KTP stop - Optimise spigot event bus
}
diff --git a/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java
index 0fd814f1d65c111266a2b20f86561839a4cef755..3fbf6eeb3e835269217a381f00b54a7ec43d5bd2 100644
--- a/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java
+++ b/src/main/java/io/papermc/paper/util/maplist/IteratorSafeOrderedReferenceSet.java
@@ -15,7 +15,7 @@ public final class IteratorSafeOrderedReferenceSet<E> {
/* list impl */
protected E[] listElements;
- protected int listSize;
+ protected int listSize; public int getListSize() { return this.listSize; } // Leaf - petal - expose listSize
protected final double maxFragFactor;
diff --git a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java
index 66721a27cc9a373a12dffb72c4a403473377eff6..2fbab1b4d20c3ead0ed1a117b29679fae6776f22 100644
--- a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java
+++ b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java
@@ -36,7 +36,7 @@ public final class ChunkEntitySlices {
protected final EntityCollectionBySection allEntities;
protected final EntityCollectionBySection hardCollidingEntities;
protected final Reference2ObjectOpenHashMap<Class<? extends Entity>, EntityCollectionBySection> entitiesByClass;
- protected final EntityList entities = new EntityList();
+ public final EntityList entities = new EntityList(); // Leaf - petal - protected -> public
public FullChunkStatus status;
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index 2b9968a8a3d0d66d9db5a83dcf2a44767a9fe412..e95f6053982f9637c195243ceab37061b31355b5 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -1128,8 +1128,35 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
entity.tracker = null; // Paper - We're no longer tracked
}
+ // Leaf start - petal - multithreaded tracker
+ private @Nullable org.dreeam.leaf.async.tracker.MultithreadedTracker multithreadedTracker;
+ private final java.util.concurrent.ConcurrentLinkedQueue<Runnable> trackerMainThreadTasks = new java.util.concurrent.ConcurrentLinkedQueue<>();
+ private boolean tracking = false;
+
+ public void runOnTrackerMainThread(final Runnable runnable) {
+ if (this.tracking) {
+ this.trackerMainThreadTasks.add(runnable);
+ } else {
+ runnable.run();
+ }
+ }
+
// Paper start - optimised tracker
private final void processTrackQueue() {
+ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled) {
+ if (this.multithreadedTracker == null) {
+ this.multithreadedTracker = new org.dreeam.leaf.async.tracker.MultithreadedTracker(this.level.chunkSource.entityTickingChunks, this.trackerMainThreadTasks);
+ }
+
+ this.tracking = true;
+ try {
+ this.multithreadedTracker.processTrackQueue();
+ } finally {
+ this.tracking = false;
+ }
+ return;
+ }
+ // Leaf end - petal
for (TrackedEntity tracker : this.entityMap.values()) {
// update tracker entry
tracker.updatePlayers(tracker.entity.getPlayersInTrackRange());
@@ -1281,10 +1308,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
public class TrackedEntity {
public final ServerEntity serverEntity;
- final Entity entity;
+ public final Entity entity; // Leaf - petal - public
private final int range;
SectionPos lastSectionPos;
- public final Set<ServerPlayerConnection> seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl
+ public final Set<ServerPlayerConnection> seenBy = Sets.newConcurrentHashSet(); // Paper - Perf: optimise map impl // Leaf - Fix tracker NPE
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
@@ -1296,7 +1323,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
// Paper start - use distance map to optimise tracker
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> lastTrackerCandidates;
- final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newTrackerCandidates) {
+ public final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newTrackerCandidates) { // Leaf - petal - public
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> oldTrackerCandidates = this.lastTrackerCandidates;
this.lastTrackerCandidates = newTrackerCandidates;
@@ -1338,14 +1365,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public void broadcast(Packet<?> packet) {
- Iterator iterator = this.seenBy.iterator();
-
- while (iterator.hasNext()) {
- ServerPlayerConnection serverplayerconnection = (ServerPlayerConnection) iterator.next();
-
+ // Leaf start - petal - avoid NPE
+ for (ServerPlayerConnection serverplayerconnection : this.seenBy.toArray(new ServerPlayerConnection[0])) {
serverplayerconnection.send(packet);
}
-
+ // Leaf end - petal
}
public void broadcastAndSend(Packet<?> packet) {
@@ -1357,18 +1381,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public void broadcastRemoved() {
- Iterator iterator = this.seenBy.iterator();
-
- while (iterator.hasNext()) {
- ServerPlayerConnection serverplayerconnection = (ServerPlayerConnection) iterator.next();
-
+ // Leaf start - petal - avoid NPE
+ for (ServerPlayerConnection serverplayerconnection : this.seenBy.toArray(new ServerPlayerConnection[0])) {
this.serverEntity.removePairing(serverplayerconnection.getPlayer());
}
-
+ // Leaf end - petal
}
public void removePlayer(ServerPlayer player) {
- org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot
+ //org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot // Leaf - petal - We can remove async too
if (this.seenBy.remove(player.connection)) {
this.serverEntity.removePairing(player);
}
@@ -1376,7 +1397,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public void updatePlayer(ServerPlayer player) {
- org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot
+ //org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot // Leaf - petal - We can update async
if (player != this.entity) {
// Paper start - remove allocation of Vec3D here
// Vec3 vec3d = player.position().subtract(this.entity.position());
diff --git a/src/main/java/net/minecraft/server/level/ServerBossEvent.java b/src/main/java/net/minecraft/server/level/ServerBossEvent.java
index ca42c2642a729b90d22b968af7258f3aee72e14b..fe03f0f55de978721a9d3d8446bdead2ec82f70d 100644
--- a/src/main/java/net/minecraft/server/level/ServerBossEvent.java
+++ b/src/main/java/net/minecraft/server/level/ServerBossEvent.java
@@ -13,7 +13,7 @@ import net.minecraft.util.Mth;
import net.minecraft.world.BossEvent;
public class ServerBossEvent extends BossEvent {
- private final Set<ServerPlayer> players = Sets.newHashSet();
+ private final Set<ServerPlayer> players = Sets.newConcurrentHashSet(); // Leaf - petal - players can be removed in async tracking
private final Set<ServerPlayer> unmodifiablePlayers = Collections.unmodifiableSet(this.players);
public boolean visible = true;
diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java
index 515e3f444e317de1d82421ad414c1ac10d4668ab..60e8bfc89cfb92e2f7d4860b11c16b3bc5e2f095 100644
--- a/src/main/java/net/minecraft/server/level/ServerEntity.java
+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java
@@ -280,7 +280,11 @@ public class ServerEntity {
public void removePairing(ServerPlayer player) {
this.entity.stopSeenByPlayer(player);
- player.connection.send(new ClientboundRemoveEntitiesPacket(new int[]{this.entity.getId()}));
+ // Leaf start - petal - ensure main thread
+ ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() ->
+ player.connection.send(new ClientboundRemoveEntitiesPacket(this.entity.getId()))
+ );
+ // Leaf end - petal
}
public void addPairing(ServerPlayer player) {
@@ -288,7 +292,7 @@ public class ServerEntity {
Objects.requireNonNull(list);
this.sendPairingData(player, list::add);
- player.connection.send(new ClientboundBundlePacket(list));
+ ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() -> player.connection.send(new ClientboundBundlePacket(list))); // Leaf - petal - Main thread
this.entity.startSeenByPlayer(player);
}
@@ -382,19 +386,29 @@ public class ServerEntity {
if (list != null) {
this.trackedDataValues = datawatcher.getNonDefaultValues();
- this.broadcastAndSend(new ClientboundSetEntityDataPacket(this.entity.getId(), list));
+ // Leaf start - petal - sync
+ ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() ->
+ this.broadcastAndSend(new ClientboundSetEntityDataPacket(this.entity.getId(), list))
+ );
+ // Leaf end - petal
}
if (this.entity instanceof LivingEntity) {
Set<AttributeInstance> set = ((LivingEntity) this.entity).getAttributes().getDirtyAttributes();
if (!set.isEmpty()) {
+ // Leaf start - petal - sync
+ final var copy = Lists.newArrayList(set);
+ ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() -> {
// CraftBukkit start - Send scaled max health
if (this.entity instanceof ServerPlayer) {
- ((ServerPlayer) this.entity).getBukkitEntity().injectScaledMaxHealth(set, false);
+ ((ServerPlayer) this.entity).getBukkitEntity().injectScaledMaxHealth(copy, false);
}
// CraftBukkit end
- this.broadcastAndSend(new ClientboundUpdateAttributesPacket(this.entity.getId(), set));
+ this.broadcastAndSend(new ClientboundUpdateAttributesPacket(this.entity.getId(), copy));
+
+ });
+ // Leaf end - petal
}
set.clear();
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index a3b9068efc38b3eb05e884bcc3fb13532e49308d..f016b5e323f8167d058f61480095721684fd6e42 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -2633,7 +2633,7 @@ public class ServerLevel extends Level implements WorldGenLevel {
@Override
public LevelEntityGetter<Entity> getEntities() {
- org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
+ //org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot // Leaf - petal
return this.entityLookup; // Paper - rewrite chunk system
}
diff --git a/src/main/java/org/dreeam/leaf/async/tracker/MultithreadedTracker.java b/src/main/java/org/dreeam/leaf/async/tracker/MultithreadedTracker.java
new file mode 100644
index 0000000000000000000000000000000000000000..83bd78db3affa7bd7ac646f6b56ec2563051fdc3
--- /dev/null
+++ b/src/main/java/org/dreeam/leaf/async/tracker/MultithreadedTracker.java
@@ -0,0 +1,156 @@
+package org.dreeam.leaf.async.tracker;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet;
+import io.papermc.paper.world.ChunkEntitySlices;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ChunkMap;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.chunk.LevelChunk;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class MultithreadedTracker {
+
+ private enum TrackerStage {
+ UPDATE_PLAYERS,
+ SEND_CHANGES
+ }
+
+ private static final Executor trackerExecutor = new ThreadPoolExecutor(
+ 1,
+ org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerMaxThreads,
+ org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerKeepalive, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(),
+ new ThreadFactoryBuilder()
+ .setNameFormat("petal-async-tracker-thread-%d")
+ .setPriority(Thread.NORM_PRIORITY - 2)
+ .build());
+
+ private final IteratorSafeOrderedReferenceSet<LevelChunk> entityTickingChunks;
+ private final AtomicInteger taskIndex = new AtomicInteger();
+
+ private final ConcurrentLinkedQueue<Runnable> mainThreadTasks;
+ private final AtomicInteger finishedTasks = new AtomicInteger();
+
+ public MultithreadedTracker(IteratorSafeOrderedReferenceSet<LevelChunk> entityTickingChunks, ConcurrentLinkedQueue<Runnable> mainThreadTasks) {
+ this.entityTickingChunks = entityTickingChunks;
+ this.mainThreadTasks = mainThreadTasks;
+ }
+
+ public void processTrackQueue() {
+ int iterator = this.entityTickingChunks.createRawIterator();
+
+ if (iterator == -1) {
+ return;
+ }
+
+ // start with updating players
+ try {
+ this.taskIndex.set(iterator);
+ this.finishedTasks.set(0);
+
+ for (int i = 0; i < org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerMaxThreads; i++) {
+ trackerExecutor.execute(this::runUpdatePlayers);
+ }
+
+ while (this.taskIndex.get() < this.entityTickingChunks.getListSize()) {
+ this.runMainThreadTasks();
+ this.handleChunkUpdates(5); // assist
+ }
+
+ while (this.finishedTasks.get() != org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerMaxThreads) {
+ this.runMainThreadTasks();
+ }
+
+ this.runMainThreadTasks(); // finish any remaining tasks
+ } finally {
+ this.entityTickingChunks.finishRawIterator();
+ }
+
+ // then send changes
+ iterator = this.entityTickingChunks.createRawIterator();
+
+ if (iterator == -1) {
+ return;
+ }
+
+ try {
+ do {
+ LevelChunk chunk = this.entityTickingChunks.rawGet(iterator);
+
+ if (chunk != null) {
+ this.updateChunkEntities(chunk, TrackerStage.SEND_CHANGES);
+ }
+ } while (++iterator < this.entityTickingChunks.getListSize());
+ } finally {
+ this.entityTickingChunks.finishRawIterator();
+ }
+ }
+
+ private void runMainThreadTasks() {
+ try {
+ Runnable task;
+ while ((task = this.mainThreadTasks.poll()) != null) {
+ task.run();
+ }
+ } catch (Throwable throwable) {
+ MinecraftServer.LOGGER.warn("Tasks failed while ticking track queue", throwable);
+ }
+ }
+
+ private void runUpdatePlayers() {
+ try {
+ while (handleChunkUpdates(10));
+ } finally {
+ this.finishedTasks.incrementAndGet();
+ }
+ }
+
+ private boolean handleChunkUpdates(int tasks) {
+ int index = this.taskIndex.getAndAdd(tasks);
+
+ for (int i = index; i < index + tasks && i < this.entityTickingChunks.getListSize(); i++) {
+ LevelChunk chunk = this.entityTickingChunks.rawGet(i);
+ if (chunk != null) {
+ try {
+ this.updateChunkEntities(chunk, TrackerStage.UPDATE_PLAYERS);
+ } catch (Throwable throwable) {
+ MinecraftServer.LOGGER.warn("Ticking tracker failed", throwable);
+ }
+
+ }
+ }
+
+ return index < this.entityTickingChunks.getListSize();
+ }
+
+ private void updateChunkEntities(LevelChunk chunk, TrackerStage trackerStage) {
+ final ChunkEntitySlices entitySlices = chunk.level.getEntityLookup().getChunk(chunk.locX, chunk.locZ);
+ if (entitySlices == null) {
+ return;
+ }
+
+ final Entity[] rawEntities = entitySlices.entities.getRawData();
+ final ChunkMap chunkMap = chunk.level.chunkSource.chunkMap;
+
+ for (Entity entity : rawEntities) {
+ if (entity != null) {
+ ChunkMap.TrackedEntity entityTracker = chunkMap.entityMap.get(entity.getId());
+ if (entityTracker != null) {
+ if (trackerStage == TrackerStage.SEND_CHANGES) {
+ entityTracker.serverEntity.sendChanges();
+ } else if (trackerStage == TrackerStage.UPDATE_PLAYERS) {
+ entityTracker.updatePlayers(entityTracker.entity.getPlayersInTrackRange());
+ }
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java b/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java
new file mode 100644
index 0000000000000000000000000000000000000000..a08da88d257ac8b87b1587c2d01355a443385f58
--- /dev/null
+++ b/src/main/java/org/dreeam/leaf/config/modules/async/MultithreadedTracker.java
@@ -0,0 +1,43 @@
+package org.dreeam.leaf.config.modules.async;
+
+import com.electronwill.nightconfig.core.file.CommentedFileConfig;
+import net.minecraft.server.MinecraftServer;
+import org.dreeam.leaf.config.ConfigInfo;
+import org.dreeam.leaf.config.EnumConfigCategory;
+import org.dreeam.leaf.config.IConfigModule;
+
+public class MultithreadedTracker implements IConfigModule {
+
+ @Override
+ public EnumConfigCategory getCategory() {
+ return EnumConfigCategory.ASYNC;
+ }
+
+ @Override
+ public String getBaseName() {
+ return "async_entity_tracker";
+ }
+
+ @ConfigInfo(baseName = "enabled")
+ public static boolean enabled = false;
+ @ConfigInfo(baseName = "max-threads")
+ public static int asyncEntityTrackerMaxThreads = 0;
+ @ConfigInfo(baseName = "keepalive")
+ public static int asyncEntityTrackerKeepalive = 60;
+
+ @Override
+ public void onLoaded(CommentedFileConfig config) {
+ config.setComment("", """
+ Whether or not async entity tracking should be enabled.
+ You may encounter issues with NPCs""");
+
+ if (asyncEntityTrackerMaxThreads < 0)
+ asyncEntityTrackerMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() + asyncEntityTrackerMaxThreads, 1);
+ else if (asyncEntityTrackerMaxThreads == 0)
+ asyncEntityTrackerMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() / 4, 1);
+ if (!enabled)
+ asyncEntityTrackerMaxThreads = 0;
+ else
+ MinecraftServer.LOGGER.info("Using {} threads for Async Entity Tracker", asyncEntityTrackerMaxThreads);
+ }
+}