From 2944b419d467efe317df621fe747cb22aaf5c30b Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Wed, 2 Mar 2016 00:52:31 -0600
Subject: [PATCH] Lighting Queue

This provides option to queue lighting updates to ensure they do not cause the server lag

diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
index 145cb274b0..eff9dcf54f 100644
--- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java
+++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
@@ -50,6 +50,8 @@ public class WorldTimingsHandler {
     public final Timing worldSaveLevel;
     public final Timing chunkSaveData;
 
+    public final Timing lightingQueueTimer;
+
     public WorldTimingsHandler(World server) {
         String name = server.worldData.getName() +" - ";
 
@@ -96,6 +98,8 @@ public class WorldTimingsHandler {
         tracker2 = Timings.ofSafe(name + "tracker stage 2");
         doTick = Timings.ofSafe(name + "doTick");
         tickEntities = Timings.ofSafe(name + "tickEntities");
+
+        lightingQueueTimer = Timings.ofSafe(name + "Lighting Queue");
     }
 
     public static Timing getTickList(WorldServer worldserver, String timingsType) {
diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
index 8be079e26e..0968852922 100644
--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
@@ -185,6 +185,13 @@ public class PaperConfig {
         return config.getString(path, config.getString(path));
     }
 
+    public static int maxTickMsLostLightQueue;
+    private static void lightQueue() {
+        int badSetting = config.getInt("queue-light-updates-max-loss", 10);
+        config.set("queue-light-updates-max-loss", null);
+        maxTickMsLostLightQueue = getInt("settings.queue-light-updates-max-loss", badSetting);
+    }
+
     private static void timings() {
         boolean timings = getBoolean("timings.enabled", true);
         boolean verboseTimings = getBoolean("timings.verbose", true);
diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
index 39d565db1f..8f6f0288be 100644
--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
@@ -130,4 +130,12 @@ public class PaperWorldConfig {
         netherVoidTopDamage = getBoolean( "nether-ceiling-void-damage", false );
         log("Top of the nether void damage: " + netherVoidTopDamage);
     }
+
+    public boolean queueLightUpdates;
+    private void queueLightUpdates() {
+        queueLightUpdates = getBoolean("queue-light-updates", false);
+        log("Lighting Queue enabled: " + queueLightUpdates);
+        log("Warning: This feature may help reduce TPS loss from light, but comes at the cost of buggy light data");
+        log("We are working to improve this feature.");
+    }
 }
diff --git a/src/main/java/net/minecraft/server/Chunk.java b/src/main/java/net/minecraft/server/Chunk.java
index af181d4bd7..6fa379e580 100644
--- a/src/main/java/net/minecraft/server/Chunk.java
+++ b/src/main/java/net/minecraft/server/Chunk.java
@@ -90,6 +90,7 @@ public class Chunk implements IChunkAccess {
             return removed;
         }
     }
+    final PaperLightingQueue.LightingQueue lightingQueue = new PaperLightingQueue.LightingQueue(this);
     // Paper end
     public boolean areNeighborsLoaded(final int radius) {
         switch (radius) {
@@ -279,7 +280,7 @@ public class Chunk implements IChunkAccess {
 
     private void g(boolean flag) {
         this.world.methodProfiler.a("recheckGaps");
-        if (this.world.areChunksLoaded(new BlockPosition(this.locX * 16 + 8, 0, this.locZ * 16 + 8), 16)) {
+        if (this.areNeighborsLoaded(1)) { // Paper
             for (int i = 0; i < 16; ++i) {
                 for (int j = 0; j < 16; ++j) {
                     if (this.g[i + j * 16]) {
@@ -330,7 +331,7 @@ public class Chunk implements IChunkAccess {
     }
 
     private void a(int i, int j, int k, int l) {
-        if (l > k && this.world.areChunksLoaded(new BlockPosition(i, 0, j), 16)) {
+        if (l > k && this.areNeighborsLoaded(1)) { // Paper
             for (int i1 = k; i1 < l; ++i1) {
                 this.world.c(EnumSkyBlock.SKY, new BlockPosition(i, i1, j));
             }
@@ -530,6 +531,7 @@ public class Chunk implements IChunkAccess {
                 if (flag1) {
                     this.initLighting();
                 } else {
+                    this.runOrQueueLightUpdate(() -> { // Paper - Queue light update
                     int i1 = iblockdata.b(this.world, blockposition);
                     int j1 = iblockdata1.b(this.world, blockposition);
 
@@ -537,6 +539,7 @@ public class Chunk implements IChunkAccess {
                     if (i1 != j1 && (i1 < j1 || this.getBrightness(EnumSkyBlock.SKY, blockposition) > 0 || this.getBrightness(EnumSkyBlock.BLOCK, blockposition) > 0)) {
                         this.c(i, k);
                     }
+                    }); // Paper
                 }
 
                 TileEntity tileentity;
@@ -1374,6 +1377,16 @@ public class Chunk implements IChunkAccess {
         return this.D == 8;
     }
 
+    // Paper start
+    public void runOrQueueLightUpdate(Runnable runnable) {
+        if (this.world.paperConfig.queueLightUpdates) {
+            lightingQueue.add(runnable);
+        } else {
+            runnable.run();
+        }
+    }
+    // Paper end
+
     public static enum EnumTileEntityState {
 
         IMMEDIATE, QUEUED, CHECK;
diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
index 31ed3e43a5..0b03c6266a 100644
--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
@@ -314,6 +314,7 @@ public class ChunkProviderServer implements IChunkProvider {
             return false;
         }
         save = event.isSaveChunk();
+        chunk.lightingQueue.processUnload(); // Paper
 
         // Update neighbor counts
         for (int x = -2; x < 3; x++) {
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index d6ea4ae532..5086fe4027 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -894,7 +894,7 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati
     protected void a(BooleanSupplier booleansupplier) {
         co.aikar.timings.TimingsManager.FULL_SERVER_TICK.startTiming(); // Paper
         this.slackActivityAccountant.tickStarted(); // Spigot
-        long i = SystemUtils.c();
+        long i = SystemUtils.c(); long startTime = i; // Paper
 
         ++this.ticks;
         if (this.S) {
@@ -952,6 +952,7 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati
         this.methodProfiler.e();
         this.methodProfiler.e();
         org.spigotmc.WatchdogThread.tick(); // Spigot
+        PaperLightingQueue.processQueue(startTime); // Paper
         this.slackActivityAccountant.tickEnded(l); // Spigot
         co.aikar.timings.TimingsManager.FULL_SERVER_TICK.stopTiming(); // Paper
     }
diff --git a/src/main/java/net/minecraft/server/PaperLightingQueue.java b/src/main/java/net/minecraft/server/PaperLightingQueue.java
new file mode 100644
index 0000000000..5fabc5b55c
--- /dev/null
+++ b/src/main/java/net/minecraft/server/PaperLightingQueue.java
@@ -0,0 +1,98 @@
+package net.minecraft.server;
+
+import co.aikar.timings.Timing;
+import com.destroystokyo.paper.PaperConfig;
+import it.unimi.dsi.fastutil.objects.ObjectCollection;
+
+import java.util.ArrayDeque;
+
+class PaperLightingQueue {
+    private static final long MAX_TIME = (long) (1000000 * (50 + PaperConfig.maxTickMsLostLightQueue));
+
+    static void processQueue(long curTime) {
+        final long startTime = System.nanoTime();
+        final long maxTickTime = MAX_TIME - (startTime - curTime);
+
+        if (maxTickTime <= 0) {
+            return;
+        }
+
+        START:
+        for (World world : MinecraftServer.getServer().getWorlds()) {
+            if (!world.paperConfig.queueLightUpdates) {
+                continue;
+            }
+
+            ObjectCollection<Chunk> loadedChunks = ((WorldServer) world).getChunkProviderServer().chunks.values();
+            for (Chunk chunk : loadedChunks.toArray(new Chunk[0])) {
+                if (chunk.lightingQueue.processQueue(startTime, maxTickTime)) {
+                    break START;
+                }
+            }
+        }
+    }
+
+    static class LightingQueue extends ArrayDeque<Runnable> {
+        final private Chunk chunk;
+
+        LightingQueue(Chunk chunk) {
+            super();
+            this.chunk = chunk;
+        }
+
+        /**
+         * Processes the lighting queue for this chunk
+         *
+         * @param startTime If start Time is 0, we will not limit execution time
+         * @param maxTickTime Maximum time to spend processing lighting updates
+         * @return true to abort processing furthur lighting updates
+         */
+        private boolean processQueue(long startTime, long maxTickTime) {
+            if (this.isEmpty()) {
+                return false;
+            }
+            if (isOutOfTime(maxTickTime, startTime)) {
+                return true;
+            }
+            try (Timing ignored = chunk.world.timings.lightingQueueTimer.startTiming()) {
+                Runnable lightUpdate;
+                while ((lightUpdate = this.poll()) != null) {
+                    lightUpdate.run();
+                    if (isOutOfTime(maxTickTime, startTime)) {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        /**
+         * Flushes lighting updates to unload the chunk
+         */
+        void processUnload() {
+            if (!chunk.world.paperConfig.queueLightUpdates) {
+                return;
+            }
+            processQueue(0, 0); // No timeout
+
+            final int radius = 1;
+            for (int x = chunk.locX - radius; x <= chunk.locX + radius; ++x) {
+                for (int z = chunk.locZ - radius; z <= chunk.locZ + radius; ++z) {
+                    if (x == chunk.locX && z == chunk.locZ) {
+                        continue;
+                    }
+
+                    Chunk neighbor = chunk.world.getChunkIfLoaded(x, z);
+                    if (neighbor != null) {
+                        neighbor.lightingQueue.processQueue(0, 0); // No timeout
+                    }
+                }
+            }
+        }
+    }
+
+    private static boolean isOutOfTime(long maxTickTime, long startTime) {
+        return startTime > 0 && System.nanoTime() - startTime > maxTickTime;
+    }
+}
diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java
index 499d64ea2c..e06da6bef9 100644
--- a/src/main/java/net/minecraft/server/World.java
+++ b/src/main/java/net/minecraft/server/World.java
@@ -335,7 +335,7 @@ public abstract class World implements IEntityAccess, GeneratorAccess, IIBlockAc
 
                 if (iblockdata2.b(this, blockposition) != iblockdata1.b(this, blockposition) || iblockdata2.e() != iblockdata1.e()) {
                     this.methodProfiler.a("checkLight");
-                    this.r(blockposition);
+                    chunk.runOrQueueLightUpdate(() -> this.r(blockposition)); // Paper - Queue light update
                     this.methodProfiler.e();
                 }
 
-- 
2.19.0