From ed99479d1b81272b059aa5e8b102a4dae3a0ebb4 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
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 ff1a2046f6..0cd15c17e8 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 28ec8628dc..234c9eb1c3 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.Type, HeightMap> 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 71ddaf591e..39ac032b0b 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<Chunk> {
                 }
             }
         }
+        // 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 42d0731f0a..e32c9f9a62 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 ac0e90eeca..2fd8fa30ee 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 1318ddd997..bc67565bf0 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 8aa4138a2e..225f7574d2 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 24e504c279..f0e974dffc 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -1757,7 +1757,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 d08ef3fe10..081789a8fe 100644
--- a/src/main/java/org/spigotmc/ActivationRange.java
+++ b/src/main/java/org/spigotmc/ActivationRange.java
@@ -323,6 +323,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