diff --git a/paper-server/patches/features/0033-Optimise-EntityScheduler-ticking.patch b/paper-server/patches/features/0033-Optimise-EntityScheduler-ticking.patch new file mode 100644 index 0000000000..6a1ba697e8 --- /dev/null +++ b/paper-server/patches/features/0033-Optimise-EntityScheduler-ticking.patch @@ -0,0 +1,83 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 24 Jun 2025 07:05:51 -0700 +Subject: [PATCH] Optimise EntityScheduler ticking + +The vast majority of the time, there are no tasks scheduled to +the EntityScheduler. We can avoid iterating the entire entity list +by tracking which schedulers have any tasks scheduled. + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +index 5f2deeb5cc01d8bbeb7449bd4e59c466b3dfdf57..82824ae7ffbced513a8bcace684af94916135e84 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +@@ -96,6 +96,7 @@ public final class ServerEntityLookup extends EntityLookup { + if (entity instanceof ThrownEnderpearl enderpearl) { + this.addEnderPearl(CoordinateUtils.getChunkKey(enderpearl.chunkPosition())); + } ++ entity.registerScheduler(); // Paper - optimise Folia entity scheduler + } + + @Override +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 0a260fdf6b198a8ab52e60bf6db2fb5eab719c48..b8d864b9a05ba2822b6610a2ebd4ef5d2d96bd9a 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1654,33 +1654,21 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop serverPlayer1.connection.suspendFlushing()); + this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit +- // Paper start - Folia scheduler API +- ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) org.bukkit.Bukkit.getGlobalRegionScheduler()).tick(); +- for (ServerPlayer player : this.playerList.players) { +- if (!this.playerList.players.contains(player)) { ++ // Paper start - optimise Folia entity scheduler ++ for (io.papermc.paper.threadedregions.EntityScheduler scheduler : this.entitySchedulerTickList.getAllSchedulers()) { ++ if (scheduler.isRetired()) { + continue; + } +- final org.bukkit.craftbukkit.entity.CraftEntity bukkit = player.getBukkitEntityRaw(); +- if (bukkit != null) { +- bukkit.taskScheduler.executeTick(); +- } ++ ++ scheduler.executeTick(); + } +- getAllLevels().forEach(level -> { +- for (final net.minecraft.world.entity.Entity entity : io.papermc.paper.FeatureHooks.getAllEntities(level)) { +- if (entity.isRemoved() || entity instanceof ServerPlayer) { +- continue; +- } +- final org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); +- if (bukkit != null) { +- bukkit.taskScheduler.executeTick(); +- } +- } +- }); +- // Paper end - Folia scheduler API ++ // Paper end - optimise Folia entity scheduler + io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper + profilerFiller.push("commandFunctions"); + this.getFunctions().tick(); +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 45cdbea0bbf12697ffd1fb2193c2eafe34142ea9..81413ac0de7b3c7a72bc606fe5ae6fb4ae7055e3 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -5175,6 +5175,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.getBukkitEntity().taskScheduler.retire(); + } + // Paper end - Folia schedulers ++ // Paper start - optimise Folia entity scheduler ++ public final void registerScheduler() { ++ this.getBukkitEntity().taskScheduler.registerTo(net.minecraft.server.MinecraftServer.getServer().entitySchedulerTickList); ++ } ++ // Paper end - optimise Folia entity scheduler + + @Override + public void setLevelCallback(EntityInLevelCallback levelCallback) { diff --git a/paper-server/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java b/paper-server/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java index c03608fec9..12a135ff0a 100644 --- a/paper-server/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java +++ b/paper-server/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java @@ -1,6 +1,7 @@ package io.papermc.paper.threadedregions; import ca.spottedleaf.concurrentutil.util.Validate; +import ca.spottedleaf.moonrise.common.list.ReferenceList; import ca.spottedleaf.moonrise.common.util.TickThread; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import net.minecraft.world.entity.Entity; @@ -9,6 +10,8 @@ import org.bukkit.craftbukkit.entity.CraftEntity; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.StampedLock; import java.util.function.Consumer; /** @@ -43,6 +46,8 @@ public final class EntityScheduler { private static final long RETIRED_TICK_COUNT = -1L; private final Object stateLock = new Object(); private final Long2ObjectOpenHashMap> oneTimeDelayed = new Long2ObjectOpenHashMap<>(); + private EntitySchedulerTickList scheduledList; + private boolean insideScheduledList; private final ArrayDeque currentlyExecuting = new ArrayDeque<>(); @@ -50,6 +55,51 @@ public final class EntityScheduler { this.entity = Validate.notNull(entity); } + // must own state lock + private boolean hasTasks() { + return !this.currentlyExecuting.isEmpty() || !this.oneTimeDelayed.isEmpty(); + } + + public void registerTo(final EntitySchedulerTickList newTickList) { + synchronized (this.stateLock) { + final EntitySchedulerTickList prevList = this.scheduledList; + if (prevList == newTickList) { + return; + } + this.scheduledList = newTickList; + + // make sure tasks scheduled before registration can be ticked + if (prevList == null && this.hasTasks()) { + this.insideScheduledList = true; + } + + // transfer to new list + if (this.insideScheduledList) { + if (prevList != null) { + prevList.remove(this); + } + if (newTickList != null) { + newTickList.add(this); + } else { + // retired + this.insideScheduledList = false; + } + } + } + } + + /** + * Returns whether this scheduler is retired. + * + *

+ * Note: This should only be invoked on the owning thread for the entity. + *

+ * @return whether this scheduler is retired. + */ + public boolean isRetired() { + return this.tickCount == RETIRED_TICK_COUNT; + } + /** * Retires the scheduler, preventing new tasks from being scheduled and invoking the retired callback * on all currently scheduled tasks. @@ -66,6 +116,7 @@ public final class EntityScheduler { throw new IllegalStateException("Already retired"); } this.tickCount = RETIRED_TICK_COUNT; + this.registerTo(null); } final Entity thisEntity = this.entity.getHandleRaw(); @@ -127,6 +178,11 @@ public final class EntityScheduler { this.oneTimeDelayed.computeIfAbsent(this.tickCount + Math.max(1L, delay), (final long keyInMap) -> { return new ArrayList<>(); }).add(task); + + if (!this.insideScheduledList && this.scheduledList != null) { + this.scheduledList.add(this); + this.insideScheduledList = true; + } } return true; @@ -147,6 +203,12 @@ public final class EntityScheduler { throw new IllegalStateException("Ticking retired scheduler"); } ++this.tickCount; + + if (this.scheduledList != null && !this.hasTasks()) { + this.scheduledList.remove(this); + this.insideScheduledList = false; + } + if (this.oneTimeDelayed.isEmpty()) { toRun = null; } else { @@ -178,4 +240,34 @@ public final class EntityScheduler { } } } + + public static final class EntitySchedulerTickList { + + private static final EntityScheduler[] ENTITY_SCHEDULER_ARRAY = new EntityScheduler[0]; + + private final ReferenceList entitySchedulers = new ReferenceList<>(ENTITY_SCHEDULER_ARRAY); + + public boolean add(final EntityScheduler scheduler) { + synchronized (this) { + return this.entitySchedulers.add(scheduler); + } + } + + public void remove(final EntityScheduler scheduler) { + synchronized (this) { + this.entitySchedulers.remove(scheduler); + } + } + + public EntityScheduler[] getAllSchedulers() { + EntityScheduler[] ret = new EntityScheduler[this.entitySchedulers.size()]; + synchronized (this) { + if (ret.length != this.entitySchedulers.size()) { + ret = new EntityScheduler[this.entitySchedulers.size()]; + } + System.arraycopy(this.entitySchedulers.getRawDataUnchecked(), 0, ret, 0, this.entitySchedulers.size()); + return ret; + } + } + } }