aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/1018-Optimise-chunk-tick-iteration.patch
diff options
context:
space:
mode:
authorJake Potrebic <[email protected]>2024-05-29 13:58:57 -0700
committerGitHub <[email protected]>2024-05-29 22:58:57 +0200
commita31dc90741ed9c121a13a3c124c9ebf5bafd0da7 (patch)
tree81963bd94fe7772ee08bd21710230de21b6b25da /patches/server/1018-Optimise-chunk-tick-iteration.patch
parented85aac53cfd93d29fa24e6071dbdddd0e49624b (diff)
downloadPaper-a31dc90741ed9c121a13a3c124c9ebf5bafd0da7.tar.gz
Paper-a31dc90741ed9c121a13a3c124c9ebf5bafd0da7.zip
Several fixes and new api for experience merging/stacking (#9242)
Diffstat (limited to 'patches/server/1018-Optimise-chunk-tick-iteration.patch')
-rw-r--r--patches/server/1018-Optimise-chunk-tick-iteration.patch380
1 files changed, 380 insertions, 0 deletions
diff --git a/patches/server/1018-Optimise-chunk-tick-iteration.patch b/patches/server/1018-Optimise-chunk-tick-iteration.patch
new file mode 100644
index 0000000000..6b17ae655d
--- /dev/null
+++ b/patches/server/1018-Optimise-chunk-tick-iteration.patch
@@ -0,0 +1,380 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <[email protected]>
+Date: Sat, 23 Sep 2023 21:36:36 -0700
+Subject: [PATCH] Optimise chunk tick iteration
+
+When per-player mob spawning is enabled we do not need to randomly
+shuffle the chunk list. Additionally, we can use the NearbyPlayers
+class to quickly retrieve nearby players instead of possible
+searching all players on the server.
+
+diff --git a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java
+index c3ce8a42dddd76b7189ad5685b23f9d9f8ccadb3..f164256d59b761264876ca0c85f812d101bfd5de 100644
+--- a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java
++++ b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java
+@@ -17,7 +17,8 @@ public final class NearbyPlayers {
+ GENERAL_SMALL,
+ GENERAL_REALLY_SMALL,
+ TICK_VIEW_DISTANCE,
+- VIEW_DISTANCE;
++ VIEW_DISTANCE, // Paper - optimise chunk iteration
++ SPAWN_RANGE, // Paper - optimise chunk iteration
+ }
+
+ private static final NearbyMapType[] MOB_TYPES = NearbyMapType.values();
+@@ -26,10 +27,12 @@ public final class NearbyPlayers {
+ private static final int GENERAL_AREA_VIEW_DISTANCE = 33;
+ private static final int GENERAL_SMALL_VIEW_DISTANCE = 10;
+ private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3;
++ private static final int SPAWN_RANGE_VIEW_DISTANCE = net.minecraft.server.level.DistanceManager.MOB_SPAWN_RANGE; // Paper - optimise chunk iteration
+
+ public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4);
+ public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4);
+ public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4);
++ public static final int SPAWN_RANGE_VIEW_DISTANCE_BLOCKS = (SPAWN_RANGE_VIEW_DISTANCE << 4); // Paper - optimise chunk iteration
+
+ private final ServerLevel world;
+ private final Reference2ReferenceOpenHashMap<ServerPlayer, TrackedPlayer[]> players = new Reference2ReferenceOpenHashMap<>();
+@@ -80,6 +83,7 @@ public final class NearbyPlayers {
+ players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE);
+ players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player));
+ players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getLoadViewDistance(player));
++ players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, SPAWN_RANGE_VIEW_DISTANCE); // Paper - optimise chunk iteration
+ }
+
+ public TrackedChunk getChunk(final ChunkPos pos) {
+diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+index 13d15a135dd0373bef4a5ac9ffb56dbbf53353a0..472b9494f8a34a8ba90d6a2936b0db7530a229ad 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+@@ -77,11 +77,19 @@ public class ChunkHolder {
+
+ // Paper start
+ public void onChunkAdd() {
+-
++ // Paper start - optimise chunk tick iteration
++ if (this.needsBroadcastChanges()) {
++ this.chunkMap.needsChangeBroadcasting.add(this);
++ }
++ // Paper end - optimise chunk tick iteration
+ }
+
+ public void onChunkRemove() {
+-
++ // Paper start - optimise chunk tick iteration
++ if (this.needsBroadcastChanges()) {
++ this.chunkMap.needsChangeBroadcasting.remove(this);
++ }
++ // Paper end - optimise chunk tick iteration
+ }
+ // Paper end
+
+@@ -216,7 +224,7 @@ public class ChunkHolder {
+
+ if (i < 0 || i >= this.changedBlocksPerSection.length) return; // CraftBukkit - SPIGOT-6086, SPIGOT-6296
+ if (this.changedBlocksPerSection[i] == null) {
+- this.hasChangedSections = true;
++ this.hasChangedSections = true; this.addToBroadcastMap(); // Paper - optimise chunk tick iteration
+ this.changedBlocksPerSection[i] = new ShortOpenHashSet();
+ }
+
+@@ -236,6 +244,7 @@ public class ChunkHolder {
+ int k = this.lightEngine.getMaxLightSection();
+
+ if (y >= j && y <= k) {
++ this.addToBroadcastMap(); // Paper - optimise chunk tick iteration
+ int l = y - j;
+
+ if (lightType == LightLayer.SKY) {
+@@ -254,9 +263,19 @@ public class ChunkHolder {
+ this.broadcast(this.getPlayers(onChunkViewEdge), packet); // Paper - rewrite chunk system
+ }
+ // Paper end - starlight
++ // Paper start - optimise chunk tick iteration
++ public final boolean needsBroadcastChanges() {
++ return this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty();
++ }
++
++ private void addToBroadcastMap() {
++ io.papermc.paper.util.TickThread.ensureTickThread(this.chunkMap.level, this.pos, "Asynchronous ChunkHolder update is not allowed");
++ this.chunkMap.needsChangeBroadcasting.add(this);
++ }
++ // Paper end - optimise chunk tick iteration
+
+ public void broadcastChanges(LevelChunk chunk) {
+- if (this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) {
++ if (this.needsBroadcastChanges()) { // Paper - optimise chunk tick iteration; moved into above, other logic needs to call
+ Level world = chunk.getLevel();
+ List list;
+
+diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
+index 48f7997e8a20f5a5a77516cbde990d0aacc2078a..dbe9df1e1973db133f7c8516256697ef7c968137 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
+@@ -194,6 +194,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ this.playerEntityTrackerTrackMaps[i].remove(player);
+ }
+ // Paper end - use distance map to optimise tracker
++ this.playerMobSpawnMap.remove(player); // Paper - optimise chunk tick iteration
+ }
+
+ void updateMaps(ServerPlayer player) {
+@@ -243,6 +244,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+ public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers;
+ // Paper end
++ // Paper start - optimise chunk tick iteration
++ public final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<ChunkHolder> needsChangeBroadcasting = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>();
++ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets);
++ // Paper end - optimise chunk tick iteration
+
+ public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
+ super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
+@@ -411,7 +416,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+ // Paper end - Optional per player mob spawns
+
+- private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) {
++ public static double euclideanDistanceSquared(ChunkPos pos, Entity entity) { // Paper - optimise chunk iteration; public
+ double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8);
+ double d1 = (double) SectionPos.sectionToBlockCoord(pos.z, 8);
+ double d2 = d0 - entity.getX();
+diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
+index ed5154e41ca858f4d6b4d1c276c66831c038d2a6..cdb3c2cde5d9133ef60cf96d91762e6a7c8aeb4a 100644
+--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
+@@ -49,7 +49,7 @@ public abstract class DistanceManager {
+ private static final int INITIAL_TICKET_LIST_CAPACITY = 4;
+ final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap();
+ // Paper - rewrite chunk system
+- private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8);
++ public static final int MOB_SPAWN_RANGE = 8; //private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); // Paper - optimise chunk tick iteration
+ // Paper - rewrite chunk system
+ private final ChunkMap chunkMap; // Paper
+
+@@ -135,7 +135,7 @@ public abstract class DistanceManager {
+ long i = chunkcoordintpair.toLong();
+
+ // Paper - no longer used
+- this.naturalSpawnChunkCounter.update(i, 0, true);
++ //this.naturalSpawnChunkCounter.update(i, 0, true); // Paper - optimise chunk tick iteration
+ //this.playerTicketManager.update(i, 0, true); // Paper - no longer used
+ //this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used
+ }
+@@ -149,7 +149,7 @@ public abstract class DistanceManager {
+ if (objectset != null) objectset.remove(player); // Paper - some state corruption happens here, don't crash, clean up gracefully
+ if (objectset == null || objectset.isEmpty()) { // Paper
+ this.playersPerChunk.remove(i);
+- this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false);
++ //this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false); // Paper - optimise chunk tick iteration
+ //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used
+ //this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used
+ }
+@@ -191,13 +191,11 @@ public abstract class DistanceManager {
+ }
+
+ public int getNaturalSpawnChunkCount() {
+- this.naturalSpawnChunkCounter.runAllUpdates();
+- return this.naturalSpawnChunkCounter.chunks.size();
++ return this.chunkMap.playerMobSpawnMap.size(); // Paper - optimise chunk tick iteration
+ }
+
+ public boolean hasPlayersNearby(long chunkPos) {
+- this.naturalSpawnChunkCounter.runAllUpdates();
+- return this.naturalSpawnChunkCounter.chunks.containsKey(chunkPos);
++ return this.chunkMap.playerMobSpawnMap.getObjectsInRange(chunkPos) != null; // Paper - optimise chunk tick iteration
+ }
+
+ public String getDebugStatus() {
+diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+index 7fbeebe63f755624b967374072aa2e0565ce8c35..36caf354634d6675a3f1ec6829f4778e1d0623bc 100644
+--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+@@ -499,18 +499,10 @@ public class ServerChunkCache extends ChunkSource {
+
+ gameprofilerfiller.push("pollingChunks");
+ gameprofilerfiller.push("filteringLoadedChunks");
+- List<ServerChunkCache.ChunkAndHolder> list = Lists.newArrayListWithCapacity(this.chunkMap.size());
+- Iterator iterator = this.chunkMap.getChunks().iterator();
++ // Paper - optimise chunk tick iteration
+ if (this.level.getServer().tickRateManager().runsNormally()) this.level.timings.chunkTicks.startTiming(); // Paper
+
+- while (iterator.hasNext()) {
+- ChunkHolder playerchunk = (ChunkHolder) iterator.next();
+- LevelChunk chunk = playerchunk.getTickingChunk();
+-
+- if (chunk != null) {
+- list.add(new ServerChunkCache.ChunkAndHolder(chunk, playerchunk));
+- }
+- }
++ // Paper - optimise chunk tick iteration
+
+ if (this.level.tickRateManager().runsNormally()) {
+ gameprofilerfiller.popPush("naturalSpawnCount");
+@@ -545,38 +537,109 @@ public class ServerChunkCache extends ChunkSource {
+ gameprofilerfiller.popPush("spawnAndTick");
+ boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit
+
+- Util.shuffle(list, this.level.random);
+- // Paper start - PlayerNaturallySpawnCreaturesEvent
+- int chunkRange = level.spigotConfig.mobSpawnRange;
+- chunkRange = (chunkRange > level.spigotConfig.viewDistance) ? (byte) level.spigotConfig.viewDistance : chunkRange;
+- chunkRange = Math.min(chunkRange, 8);
+- for (ServerPlayer entityPlayer : this.level.players()) {
+- entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange);
+- entityPlayer.playerNaturallySpawnedEvent.callEvent();
++ // Paper start - optimise chunk tick iteration
++ ChunkMap playerChunkMap = this.chunkMap;
++ for (ServerPlayer player : this.level.players) {
++ if (!player.affectsSpawning || player.isSpectator()) {
++ playerChunkMap.playerMobSpawnMap.remove(player);
++ player.playerNaturallySpawnedEvent = null;
++ player.lastEntitySpawnRadiusSquared = -1.0;
++ continue;
++ }
++
++ int viewDistance = io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player);
++
++ // copied and modified from isOutisdeRange
++ int chunkRange = (int)level.spigotConfig.mobSpawnRange;
++ chunkRange = (chunkRange > viewDistance) ? viewDistance : chunkRange;
++ chunkRange = (chunkRange > DistanceManager.MOB_SPAWN_RANGE) ? DistanceManager.MOB_SPAWN_RANGE : chunkRange;
++
++ com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(player.getBukkitEntity(), (byte)chunkRange);
++ event.callEvent();
++ if (event.isCancelled() || event.getSpawnRadius() < 0) {
++ playerChunkMap.playerMobSpawnMap.remove(player);
++ player.playerNaturallySpawnedEvent = null;
++ player.lastEntitySpawnRadiusSquared = -1.0;
++ continue;
++ }
++
++ int range = Math.min(event.getSpawnRadius(), DistanceManager.MOB_SPAWN_RANGE); // limit to max spawn range
++ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkCoordinate(player.getX());
++ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkCoordinate(player.getZ());
++
++ playerChunkMap.playerMobSpawnMap.addOrUpdate(player, chunkX, chunkZ, range);
++ player.lastEntitySpawnRadiusSquared = (double)((range << 4) * (range << 4)); // used in anyPlayerCloseEnoughForSpawning
++ player.playerNaturallySpawnedEvent = event;
+ }
+- // Paper end - PlayerNaturallySpawnCreaturesEvent
++ // Paper end - optimise chunk tick iteration
+ int l = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING);
+ boolean flag1 = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit
+- Iterator iterator1 = list.iterator();
++ // Paper - optimise chunk tick iteration
+
+ int chunksTicked = 0; // Paper
+- while (iterator1.hasNext()) {
+- ServerChunkCache.ChunkAndHolder chunkproviderserver_a = (ServerChunkCache.ChunkAndHolder) iterator1.next();
+- LevelChunk chunk1 = chunkproviderserver_a.chunk;
++ // Paper start - optimise chunk tick iteration
++ io.papermc.paper.util.player.NearbyPlayers nearbyPlayers = this.chunkMap.getNearbyPlayers(); // Paper - optimise chunk tick iteration
++ Iterator<LevelChunk> chunkIterator;
++ if (this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
++ chunkIterator = this.tickingChunks.iterator();
++ } else {
++ chunkIterator = this.tickingChunks.unsafeIterator();
++ List<LevelChunk> shuffled = Lists.newArrayListWithCapacity(this.tickingChunks.size());
++ while (chunkIterator.hasNext()) {
++ shuffled.add(chunkIterator.next());
++ }
++ Util.shuffle(shuffled, this.level.random);
++ chunkIterator = shuffled.iterator();
++ }
++ try {
++ // Paper end - optimise chunk tick iteration
++ while (chunkIterator.hasNext()) {
++ LevelChunk chunk1 = chunkIterator.next();
++ // Paper end - optimise chunk tick iteration
+ ChunkPos chunkcoordintpair = chunk1.getPos();
+
+- if (this.level.isNaturalSpawningAllowed(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair)) {
++ // Paper start - optimise chunk tick iteration
++ com.destroystokyo.paper.util.maplist.ReferenceList<ServerPlayer> playersNearby
++ = nearbyPlayers.getPlayers(chunkcoordintpair, io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.SPAWN_RANGE);
++ if (playersNearby == null) {
++ continue;
++ }
++ Object[] rawData = playersNearby.getRawData();
++ boolean spawn = false;
++ boolean tick = false;
++ for (int itr = 0, len = playersNearby.size(); itr < len; ++itr) {
++ ServerPlayer player = (ServerPlayer)rawData[itr];
++ if (player.isSpectator()) {
++ continue;
++ }
++
++ double distance = ChunkMap.euclideanDistanceSquared(chunkcoordintpair, player);
++ spawn |= player.lastEntitySpawnRadiusSquared >= distance;
++ tick |= ((double)io.papermc.paper.util.player.NearbyPlayers.SPAWN_RANGE_VIEW_DISTANCE_BLOCKS) * ((double)io.papermc.paper.util.player.NearbyPlayers.SPAWN_RANGE_VIEW_DISTANCE_BLOCKS) >= distance;
++ if (spawn & tick) {
++ break;
++ }
++ }
++ if (tick && chunk1.chunkStatus.isOrAfter(net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING)) {
++ // Paper end - optimise chunk tick iteration
+ chunk1.incrementInhabitedTime(j);
+- if (flag && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair, true)) { // Spigot
++ if (spawn && flag && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair)) { // Spigot // Paper - optimise chunk tick iteration
+ NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1);
+ }
+
+- if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) {
++ if (true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { // Paper - optimise chunk tick iteration
+ this.level.tickChunk(chunk1, l);
+ if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper
+ }
+ }
+ }
++ // Paper start - optimise chunk tick iteration
++ } finally {
++ if (chunkIterator instanceof io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator safeIterator) {
++ safeIterator.finishedIterating();
++ }
++ }
++ // Paper end - optimise chunk tick iteration
+ this.level.timings.chunkTicks.stopTiming(); // Paper
+
+ gameprofilerfiller.popPush("customSpawners");
+@@ -588,11 +651,23 @@ public class ServerChunkCache extends ChunkSource {
+ }
+
+ gameprofilerfiller.popPush("broadcast");
+- list.forEach((chunkproviderserver_a1) -> {
++ // Paper - optimise chunk tick iteration
+ this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing
+- chunkproviderserver_a1.holder.broadcastChanges(chunkproviderserver_a1.chunk);
++ // Paper start - optimise chunk tick iteration
++ if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) {
++ it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<ChunkHolder> copy = this.chunkMap.needsChangeBroadcasting.clone();
++ this.chunkMap.needsChangeBroadcasting.clear();
++ for (ChunkHolder holder : copy) {
++ holder.broadcastChanges(holder.getFullChunkNowUnchecked()); // LevelChunks are NEVER unloaded
++ if (holder.needsBroadcastChanges()) {
++ // I DON'T want to KNOW what DUMB plugins might be doing.
++ this.chunkMap.needsChangeBroadcasting.add(holder);
++ }
++ }
++ }
++ // Paper end - optimise chunk tick iteration
+ this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing
+- });
++ // Paper - optimise chunk tick iteration
+ gameprofilerfiller.pop();
+ gameprofilerfiller.pop();
+ }
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index 09a80b6816e50ea0d733725c59164520136308d6..6a4637eef14cbd84bbe26ef16f004b8f93367a3d 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -342,6 +342,9 @@ public class ServerPlayer extends Player {
+ });
+ }
+ // Paper end - replace player chunk loader
++ // Paper start - optimise chunk tick iteration
++ public double lastEntitySpawnRadiusSquared = -1.0;
++ // Paper end - optimise chunk tick iteration
+
+ public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
+ super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);