From 7ac07ac07ac07ac07ac07ac07ac07ac07ac07ac0 Mon Sep 17 00:00:00 2001 From: Aikar Date: Sat, 18 Jun 2016 23:22:12 -0400 Subject: [PATCH] Delay Chunk Unloads based on Player Movement When players are moving in the world, doing things such as building or exploring, they will commonly go back and forth in a small area. This causes a ton of chunk load and unload activity on the edge chunks of their view distance. A simple back and forth movement in 6 blocks could spam a chunk to thrash a loading and unload cycle over and over again. This is very wasteful. This system introduces a delay of inactivity on a chunk before it actually unloads, which is maintained separately from ChunkGC. This allows servers with smaller worlds who do less long distance exploring to stop wasting cpu cycles on saving/unloading/reloading chunks repeatedly. This also makes the Chunk GC System useless, by auto scheduling unload as soon as a spare chunk is added to the server thats outside of view distance. diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java index 7ac07ac07ac0..7ac07ac07ac0 100644 --- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java @@ -296,4 +296,18 @@ public class PaperWorldConfig { preventTntFromMovingInWater = getBoolean("prevent-tnt-from-moving-in-water", false); log("Prevent TNT from moving in water: " + preventTntFromMovingInWater); } + + public long delayChunkUnloadsBy; + private void delayChunkUnloadsBy() { + delayChunkUnloadsBy = PaperConfig.getSeconds(getString("delay-chunk-unloads-by", "10s")); + if (delayChunkUnloadsBy > 0) { + log("Delaying chunk unloads by " + delayChunkUnloadsBy + " seconds"); + delayChunkUnloadsBy *= 1000; + } + } + + public boolean skipEntityTickingInChunksScheduledForUnload = true; + private void skipEntityTickingInChunksScheduledForUnload() { + skipEntityTickingInChunksScheduledForUnload = getBoolean("skip-entity-ticking-in-chunks-scheduled-for-unload", skipEntityTickingInChunksScheduledForUnload); + } } diff --git a/src/main/java/net/minecraft/server/Chunk.java b/src/main/java/net/minecraft/server/Chunk.java index 7ac07ac07ac0..7ac07ac07ac0 100644 --- a/src/main/java/net/minecraft/server/Chunk.java +++ b/src/main/java/net/minecraft/server/Chunk.java @@ -36,6 +36,7 @@ public class Chunk implements IChunkAccess { private boolean i;public boolean isLoaded() { return i; } // Paper - OBFHELPER public final World world; public final Map heightMap; + public Long scheduledForUnload; // Paper - delay chunk unloads public final int locX; public final int locZ; private boolean l; diff --git a/src/main/java/net/minecraft/server/ChunkMap.java b/src/main/java/net/minecraft/server/ChunkMap.java index 7ac07ac07ac0..7ac07ac07ac0 100644 --- a/src/main/java/net/minecraft/server/ChunkMap.java +++ b/src/main/java/net/minecraft/server/ChunkMap.java @@ -48,6 +48,15 @@ public class ChunkMap extends Long2ObjectOpenHashMap { } } } + // Paper start - if this is a spare chunk (not part of any players view distance), go ahead and queue it for unload. + if (!((WorldServer)chunk.world).getPlayerChunkMap().isChunkInUse(chunk.locX, chunk.locZ)) { + if (chunk.world.paperConfig.delayChunkUnloadsBy > 0) { + chunk.scheduledForUnload = System.currentTimeMillis(); + } else { + ((WorldServer) chunk.world).getChunkProviderServer().unload(chunk); + } + } + // Paper end chunk.world.timings.syncChunkLoadPostTimer.stopTiming(); // Paper // CraftBukkit end diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java index 7ac07ac07ac0..7ac07ac07ac0 100644 --- a/src/main/java/net/minecraft/server/ChunkProviderServer.java +++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java @@ -306,6 +306,19 @@ public class ChunkProviderServer implements IChunkProvider { } activityAccountant.endActivity(); // Spigot } + // Paper start - delayed chunk unloads + long now = System.currentTimeMillis(); + long unloadAfter = world.paperConfig.delayChunkUnloadsBy; + if (unloadAfter > 0) { + //noinspection Convert2streamapi + for (Chunk chunk : chunks.values()) { + if (chunk.scheduledForUnload != null && now - chunk.scheduledForUnload > unloadAfter) { + chunk.scheduledForUnload = null; + unload(chunk); + } + } + } + // Paper end this.chunkScheduler.a(booleansupplier); } diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java index 7ac07ac07ac0..7ac07ac07ac0 100644 --- a/src/main/java/net/minecraft/server/PlayerChunk.java +++ b/src/main/java/net/minecraft/server/PlayerChunk.java @@ -33,8 +33,22 @@ public class PlayerChunk { public void run() { loadInProgress = false; PlayerChunk.this.chunk = PlayerChunk.this.playerChunkMap.getWorld().getChunkProviderServer().getChunkAt(location.x, location.z, true, true); + markChunkUsed(); // Paper - delay chunk unloads } }; + // Paper start - delay chunk unloads + private void markChunkUsed() { + if (chunk == null) { + return; + } + if (chunkHasPlayers) { + chunk.scheduledForUnload = null; + } else if (chunk.scheduledForUnload == null) { + chunk.scheduledForUnload = System.currentTimeMillis(); + } + } + private boolean chunkHasPlayers = false; + // Paper end // CraftBukkit end public PlayerChunk(PlayerChunkMap playerchunkmap, int i, int j) { @@ -44,6 +58,7 @@ public class PlayerChunk { chunkproviderserver.a(i, j); this.chunk = chunkproviderserver.getChunkAt(i, j, true, false); + markChunkUsed(); // Paper - delay chunk unloads } public ChunkCoordIntPair a() { @@ -56,6 +71,8 @@ public class PlayerChunk { } else { if (this.c.isEmpty()) { this.i = this.playerChunkMap.getWorld().getTime(); + chunkHasPlayers = true; // Paper - delay chunk unloads + markChunkUsed(); // Paper - delay chunk unloads } this.c.add(entityplayer); @@ -74,6 +91,8 @@ public class PlayerChunk { this.c.remove(entityplayer); if (this.c.isEmpty()) { + chunkHasPlayers = false; // Paper - delay chunk unloads + markChunkUsed(); // Paper - delay chunk unloads this.playerChunkMap.b(this); } @@ -85,6 +104,7 @@ public class PlayerChunk { return true; } else { this.chunk = this.playerChunkMap.getWorld().getChunkProviderServer().getChunkAt(this.location.x, this.location.z, true, flag); + markChunkUsed(); // Paper - delay chunk unloads return this.chunk != null; } } diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java index 7ac07ac07ac0..7ac07ac07ac0 100644 --- a/src/main/java/net/minecraft/server/PlayerChunkMap.java +++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java @@ -460,7 +460,13 @@ public class PlayerChunkMap { Chunk chunk = playerchunk.f(); if (chunk != null) { - this.getWorld().getChunkProviderServer().unload(chunk); + // Paper start - delay chunk unloads + if (world.paperConfig.delayChunkUnloadsBy <= 0) { + this.getWorld().getChunkProviderServer().unload(chunk); + } else { + chunk.scheduledForUnload = System.currentTimeMillis(); + } + // Paper end } } diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java index 7ac07ac07ac0..7ac07ac07ac0 100644 --- a/src/main/java/net/minecraft/server/World.java +++ b/src/main/java/net/minecraft/server/World.java @@ -1342,7 +1342,13 @@ public abstract class World implements IEntityAccess, GeneratorAccess, IIBlockAc if (!tileentity.x() && tileentity.hasWorld()) { BlockPosition blockposition = tileentity.getPosition(); - if (this.isLoaded(blockposition) && this.K.a(blockposition)) { + // Paper start - Skip ticking in chunks scheduled for unload + net.minecraft.server.Chunk chunk = this.getChunkIfLoaded(blockposition); + boolean shouldTick = chunk != null; + if(this.paperConfig.skipEntityTickingInChunksScheduledForUnload) + shouldTick = shouldTick && chunk.scheduledForUnload == null; + if (shouldTick && this.K.a(blockposition)) { + // Paper end try { this.methodProfiler.a(() -> { return String.valueOf(TileEntityTypes.a(tileentity.C())); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 7ac07ac07ac0..7ac07ac07ac0 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -1633,7 +1633,7 @@ public class CraftWorld implements World { ChunkProviderServer cps = world.getChunkProviderServer(); for (net.minecraft.server.Chunk chunk : cps.chunks.values()) { // If in use, skip it - if (isChunkInUse(chunk.locX, chunk.locZ)) { + if (isChunkInUse(chunk.locX, chunk.locZ) || chunk.scheduledForUnload != null) { // Paper - delayed chunk unloads continue; } diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java index 7ac07ac07ac0..7ac07ac07ac0 100644 --- a/src/main/java/org/spigotmc/ActivationRange.java +++ b/src/main/java/org/spigotmc/ActivationRange.java @@ -319,6 +319,11 @@ public class ActivationRange { isActive = false; } + // Paper start - Skip ticking in chunks scheduled for unload + else if (entity.world.paperConfig.skipEntityTickingInChunksScheduledForUnload && (chunk == null || chunk.scheduledForUnload != null)) { + isActive = false; + } + // Paper end return isActive; } } -- 2.19.1