From 1e9573a2fb6f8757a7f5ef540d162b19a40f0e4b 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.

diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
index b9b0f74..cda516f 100644
--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
@@ -351,4 +351,13 @@ public class PaperWorldConfig {
     private void isHopperPushBased() {
         isHopperPushBased = getBoolean("hopper.push-based", false);
     }
+
+    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;
+        }
+    }
 }
diff --git a/src/main/java/net/minecraft/server/Chunk.java b/src/main/java/net/minecraft/server/Chunk.java
index 98f2cff..88437d7 100644
--- a/src/main/java/net/minecraft/server/Chunk.java
+++ b/src/main/java/net/minecraft/server/Chunk.java
@@ -30,6 +30,7 @@ public class Chunk {
     private boolean j;
     public final World world;
     public final int[] heightMap;
+    public Long scheduledForUnload; // Paper - delay chunk unloads
     public final int locX;
     public final int locZ;
     private boolean m;
diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
index bd44764..7a56a64 100644
--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
@@ -300,6 +300,19 @@ public class ChunkProviderServer implements IChunkProvider {
                     }
                 }
             }
+            // 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.chunkLoader.a();
         }
diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java
index dd40e98..f109e98 100644
--- a/src/main/java/net/minecraft/server/PlayerChunk.java
+++ b/src/main/java/net/minecraft/server/PlayerChunk.java
@@ -32,8 +32,16 @@ public class PlayerChunk {
         public void run() {
             loadInProgress = false;
             PlayerChunk.this.chunk = PlayerChunk.this.playerChunkMap.getWorld().getChunkProviderServer().getOrLoadChunkAt(location.x, location.z);
+            markChunkUsed(); // Paper - delay chunk unloads
         }
     };
+    // Paper start - delay chunk unloads
+    public final void markChunkUsed() {
+        if (chunk != null && chunk.scheduledForUnload != null) {
+            chunk.scheduledForUnload = null;
+        }
+    }
+    // Paper end
     // CraftBukkit end
 
     public PlayerChunk(PlayerChunkMap playerchunkmap, int i, int j) {
@@ -42,6 +50,7 @@ public class PlayerChunk {
         // CraftBukkit start
         loadInProgress = true;
         this.chunk = playerchunkmap.getWorld().getChunkProviderServer().getChunkAt(i, j, loadedRunnable, false);
+        markChunkUsed(); // Paper - delay chunk unloads
         // CraftBukkit end
     }
 
@@ -110,6 +119,7 @@ public class PlayerChunk {
             if (!loadInProgress) {
                 loadInProgress = true;
                 this.chunk = playerChunkMap.getWorld().getChunkProviderServer().getChunkAt(this.location.x, this.location.z, loadedRunnable, flag);
+                markChunkUsed(); // Paper - delay chunk unloads
             }
             // CraftBukkit end
 
diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
index d970e2f..d3c454c 100644
--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
+++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
@@ -483,7 +483,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/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
index 98da2ea..ade5ba0 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -1557,7 +1557,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;
             }
 
-- 
2.9.3