aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/1027-Optimise-chunk-tick-iteration.patch
blob: 8a2ed1ca152dd48dfd2186ac42adb78298ccaf6c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
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 a5bd0845a2445fa02561b16fb54a7cf49c114915..380fd05abe191025e12bdd6811e1df90c96e4667 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);
     }
 
     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 2b998bdbe49bf8211b755e0eb7c1bf13ac280eab..5afeb59ff25fed2d565407acacffec8383398006 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -79,11 +79,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
 
@@ -230,7 +238,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();
             }
 
@@ -254,6 +262,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) {
@@ -268,8 +277,19 @@ public class ChunkHolder {
         }
     }
 
+    // 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 - 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 edbd5d4e2dcf5112148dc3d7a9173bb4d0a5d445..0c2617574e21037d94ac56ad08b490f9bca5c5af 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -162,6 +162,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
         // Note: players need to be explicitly added to distance maps before they can be updated
         this.nearbyPlayers.removePlayer(player);
         this.level.playerChunkLoader.removePlayer(player); // Paper - replace chunk loader
+        this.playerMobSpawnMap.remove(player); // Paper - optimise chunk tick iteration
     }
 
     void updateMaps(ServerPlayer player) {
@@ -203,6 +204,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(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
@@ -330,7 +335,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
     }
     // Paper end
 
-    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 68550d4497a5f10bf653482f79be77373df53f27..55f96545d6db95e3e657502a7910d96fded1113e 100644
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
@@ -50,7 +50,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
 
@@ -136,7 +136,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
     }
@@ -150,7 +150,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
         }
@@ -192,13 +192,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 aaaf3a60696389b4e27a45de69743311c58e5829..17b6925b46f8386dcfc561483693de516465ec12 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -553,52 +553,114 @@ public class ServerChunkCache extends ChunkSource {
 
             this.lastSpawnState = spawnercreature_d;
             gameprofilerfiller.popPush("filteringLoadedChunks");
-            List<ServerChunkCache.ChunkAndHolder> list = Lists.newArrayListWithCapacity(l);
-            Iterator iterator = this.chunkMap.getChunks().iterator();
+            // Paper - optimise chunk tick iteration
+            // Paper - optimise chunk tick iteration
             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
 
             gameprofilerfiller.popPush("spawnAndTick");
             boolean flag2 = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit
 
-            Collections.shuffle(list);
-            // Paper start - call player naturally spawn event
-            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 end
-            Iterator iterator1 = list.iterator();
+            // 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 - optimise chunk tick iteration
 
             int chunksTicked = 0; // Paper
+            // Paper start - optimise chunk tick iteration
+            io.papermc.paper.util.player.NearbyPlayers nearbyPlayers = this.chunkMap.getNearbyPlayers(); // Paper - optimise chunk tick iteration
+            Iterator<LevelChunk> iterator1;
+            if (this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
+                iterator1 = this.tickingChunks.iterator();
+            } else {
+                iterator1 = this.tickingChunks.unsafeIterator();
+                List<LevelChunk> shuffled = Lists.newArrayListWithCapacity(this.tickingChunks.size());
+                while (iterator1.hasNext()) {
+                    shuffled.add(iterator1.next());
+                }
+                Collections.shuffle(shuffled);
+                iterator1 = shuffled.iterator();
+            }
+            try {
+            // Paper end - optimise chunk tick iteration
             while (iterator1.hasNext()) {
-                ServerChunkCache.ChunkAndHolder chunkproviderserver_a = (ServerChunkCache.ChunkAndHolder) iterator1.next();
-                LevelChunk chunk1 = chunkproviderserver_a.chunk;
+                LevelChunk chunk1 = iterator1.next(); // Paper - 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;
+                    }
+                }
+                // Paper end - optimise chunk tick iteration
+                if (tick && chunk1.chunkStatus.isOrAfter(net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING)) { // Paper - optimise chunk tick iteration
                     chunk1.incrementInhabitedTime(j);
-                    if (flag2 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair, true)) { // Spigot
+                    if (spawn && flag2 && (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, k);
                         if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper
                     }
                 }
             }
+            // Paper start - optimise chunk tick iteration
+            } finally {
+                if (iterator1 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");
             if (flag2) {
@@ -608,11 +670,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();
             this.chunkMap.tick();
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
index 5d32ce8112562f368e6ebea064181622b22823ed..76157319b79d7fefe38cb2cc10b2d79c12667e3d 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -318,6 +318,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);