From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Fri, 19 Jul 2019 03:29:14 -0700
Subject: [PATCH] Reduce sync loads

This reduces calls to getChunkAt which would load chunks.

This patch also adds a tool to find calls which are doing this, however
it must be enabled by setting the startup flag -Dpaper.debug-sync-loads=true

To get a debug log for sync loads, the command is /paper syncloadinfo

diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
index f657e9b6bb3d24a6c77ef584711a003d1eea0341..eb1e86e8bb0f421e3686ffa02a4015a588107863 100644
--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
@@ -1,6 +1,7 @@
 package com.destroystokyo.paper;
 
 import com.destroystokyo.paper.io.chunk.ChunkTaskManager;
+import com.destroystokyo.paper.io.SyncLoadFinder;
 import com.google.common.base.Functions;
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableSet;
@@ -8,6 +9,10 @@ import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import net.minecraft.resources.MinecraftKey;
+import com.google.gson.JsonObject;
+import com.google.gson.internal.Streams;
+import com.google.gson.stream.JsonWriter;
+import net.minecraft.server.MCUtil;
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.level.ChunkProviderServer;
 import net.minecraft.server.level.PlayerChunk;
@@ -29,6 +34,9 @@ import org.bukkit.craftbukkit.CraftWorld;
 import org.bukkit.entity.Player;
 
 import java.io.File;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+import java.io.StringWriter;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
@@ -44,7 +52,7 @@ import java.util.stream.Collectors;
 
 public class PaperCommand extends Command {
     private static final String BASE_PERM = "bukkit.command.paper.";
-    private static final ImmutableSet<String> SUBCOMMANDS = ImmutableSet.<String>builder().add("heap", "entity", "reload", "version", "debug", "chunkinfo", "dumpwaiting").build();
+    private static final ImmutableSet<String> SUBCOMMANDS = ImmutableSet.<String>builder().add("heap", "entity", "reload", "version", "debug", "chunkinfo", "dumpwaiting", "syncloadinfo").build();
 
     public PaperCommand(String name) {
         super(name);
@@ -162,6 +170,9 @@ public class PaperCommand extends Command {
             case "chunkinfo":
                 doChunkInfo(sender, args);
                 break;
+            case "syncloadinfo":
+                this.doSyncLoadInfo(sender, args);
+                break;
             case "ver":
                 if (!testPermission(sender, "version")) break; // "ver" needs a special check because it's an alias. All other commands are checked up before the switch statement (because they are present in the SUBCOMMANDS set)
             case "version":
@@ -179,6 +190,40 @@ public class PaperCommand extends Command {
         return true;
     }
 
+    private void doSyncLoadInfo(CommandSender sender, String[] args) {
+        if (!SyncLoadFinder.ENABLED) {
+            sender.sendMessage(ChatColor.RED + "This command requires the server startup flag '-Dpaper.debug-sync-loads=true' to be set.");
+            return;
+        }
+        File file = new File(new File(new File("."), "debug"),
+            "sync-load-info" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt");
+        file.getParentFile().mkdirs();
+        sender.sendMessage(ChatColor.GREEN + "Writing sync load info to " + file.toString());
+
+
+        try {
+            final JsonObject data = SyncLoadFinder.serialize();
+
+            StringWriter stringWriter = new StringWriter();
+            JsonWriter jsonWriter = new JsonWriter(stringWriter);
+            jsonWriter.setIndent(" ");
+            jsonWriter.setLenient(false);
+            Streams.write(data, jsonWriter);
+
+            String fileData = stringWriter.toString();
+
+            try (
+                PrintStream out = new PrintStream(new FileOutputStream(file), false, "UTF-8")
+            ) {
+                out.print(fileData);
+            }
+            sender.sendMessage(ChatColor.GREEN + "Successfully written sync load information!");
+        } catch (Throwable thr) {
+            sender.sendMessage(ChatColor.RED + "Failed to write sync load information");
+            thr.printStackTrace();
+        }
+    }
+
     private void doChunkInfo(CommandSender sender, String[] args) {
         List<org.bukkit.World> worlds;
         if (args.length < 2 || args[1].equals("*")) {
diff --git a/src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java b/src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..d381f91cf105bfc01846ada90da8971a3618e784
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java
@@ -0,0 +1,171 @@
+package com.destroystokyo.paper.io;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.mojang.datafixers.util.Pair;
+import it.unimi.dsi.fastutil.longs.Long2IntMap;
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import net.minecraft.world.level.World;
+
+public class SyncLoadFinder {
+
+    public static final boolean ENABLED = Boolean.getBoolean("paper.debug-sync-loads");
+
+    private static final WeakHashMap<World, Object2ObjectOpenHashMap<ThrowableWithEquals, SyncLoadInformation>> SYNC_LOADS = new WeakHashMap<>();
+
+    private static final class SyncLoadInformation {
+
+        public int times;
+
+        public final Long2IntOpenHashMap coordinateTimes = new Long2IntOpenHashMap();
+    }
+
+    public static void logSyncLoad(final World world, final int chunkX, final int chunkZ) {
+        if (!ENABLED) {
+            return;
+        }
+
+        final ThrowableWithEquals stacktrace = new ThrowableWithEquals(Thread.currentThread().getStackTrace());
+
+        SYNC_LOADS.compute(world, (final World keyInMap, Object2ObjectOpenHashMap<ThrowableWithEquals, SyncLoadInformation> map) -> {
+            if (map == null) {
+                map = new Object2ObjectOpenHashMap<>();
+            }
+
+            map.compute(stacktrace, (ThrowableWithEquals keyInMap0, SyncLoadInformation valueInMap) -> {
+                if (valueInMap == null) {
+                    valueInMap = new SyncLoadInformation();
+                }
+
+                ++valueInMap.times;
+
+                valueInMap.coordinateTimes.compute(IOUtil.getCoordinateKey(chunkX, chunkZ), (Long keyInMap1, Integer valueInMap1) -> {
+                    return valueInMap1 == null ? Integer.valueOf(1) : Integer.valueOf(valueInMap1.intValue() + 1);
+                });
+
+                return valueInMap;
+            });
+
+            return map;
+        });
+    }
+
+    public static JsonObject serialize() {
+        final JsonObject ret = new JsonObject();
+
+        final JsonArray worldsData = new JsonArray();
+
+        for (final Map.Entry<World, Object2ObjectOpenHashMap<ThrowableWithEquals, SyncLoadInformation>> entry : SYNC_LOADS.entrySet()) {
+            final World world = entry.getKey();
+
+            final JsonObject worldData = new JsonObject();
+
+            worldData.addProperty("name", world.getWorld().getName());
+
+            final List<Pair<ThrowableWithEquals, SyncLoadInformation>> data = new ArrayList<>();
+
+            entry.getValue().forEach((ThrowableWithEquals stacktrace, SyncLoadInformation times) -> {
+                data.add(new Pair<>(stacktrace, times));
+            });
+
+            data.sort((Pair<ThrowableWithEquals, SyncLoadInformation> pair1, Pair<ThrowableWithEquals, SyncLoadInformation> pair2) -> {
+                return Integer.compare(pair2.getSecond().times, pair1.getSecond().times); // reverse order
+            });
+
+            final JsonArray stacktraces = new JsonArray();
+
+            for (Pair<ThrowableWithEquals, SyncLoadInformation> pair : data) {
+                final JsonObject stacktrace = new JsonObject();
+
+                stacktrace.addProperty("times", pair.getSecond().times);
+
+                final JsonArray traces = new JsonArray();
+
+                for (StackTraceElement element : pair.getFirst().stacktrace) {
+                    traces.add(String.valueOf(element));
+                }
+
+                stacktrace.add("stacktrace", traces);
+
+                final JsonArray coordinates = new JsonArray();
+
+                for (Long2IntMap.Entry coordinate : pair.getSecond().coordinateTimes.long2IntEntrySet()) {
+                    final long key = coordinate.getLongKey();
+                    final int times = coordinate.getIntValue();
+                    coordinates.add("(" + IOUtil.getCoordinateX(key) + "," + IOUtil.getCoordinateZ(key) + "): " + times);
+                }
+
+                stacktrace.add("coordinates", coordinates);
+
+                stacktraces.add(stacktrace);
+            }
+
+
+            worldData.add("stacktraces", stacktraces);
+            worldsData.add(worldData);
+        }
+
+        ret.add("worlds", worldsData);
+
+        return ret;
+    }
+
+    static final class ThrowableWithEquals {
+
+        private final StackTraceElement[] stacktrace;
+        private final int hash;
+
+        public ThrowableWithEquals(final StackTraceElement[] stacktrace) {
+            this.stacktrace = stacktrace;
+            this.hash = ThrowableWithEquals.hash(stacktrace);
+        }
+
+        public static int hash(final StackTraceElement[] stacktrace) {
+            int hash = 0;
+
+            for (int i = 0; i < stacktrace.length; ++i) {
+                hash *= 31;
+                hash += stacktrace[i].hashCode();
+            }
+
+            return hash;
+        }
+
+        @Override
+        public int hashCode() {
+            return this.hash;
+        }
+
+        @Override
+        public boolean equals(final Object obj) {
+            if (obj == null || obj.getClass() != this.getClass()) {
+                return false;
+            }
+
+            final ThrowableWithEquals other = (ThrowableWithEquals)obj;
+            final StackTraceElement[] otherStackTrace = other.stacktrace;
+
+            if (this.stacktrace.length != otherStackTrace.length || this.hash != other.hash) {
+                return false;
+            }
+
+            if (this == obj) {
+                return true;
+            }
+
+            for (int i = 0; i < this.stacktrace.length; ++i) {
+                if (!this.stacktrace[i].equals(otherStackTrace[i])) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+    }
+}
diff --git a/src/main/java/net/minecraft/server/level/ChunkProviderServer.java b/src/main/java/net/minecraft/server/level/ChunkProviderServer.java
index 2a5df37a94b4b609a2d1b045f8e6a7f08d3f8eaa..24201665f8e7fb635839334308a1bad8fc4ac7b9 100644
--- a/src/main/java/net/minecraft/server/level/ChunkProviderServer.java
+++ b/src/main/java/net/minecraft/server/level/ChunkProviderServer.java
@@ -494,6 +494,7 @@ public class ChunkProviderServer extends IChunkProvider {
                 this.world.asyncChunkTaskManager.raisePriority(x, z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY);
                 com.destroystokyo.paper.io.chunk.ChunkTaskManager.pushChunkWait(this.world, x, z);
                 // Paper end
+                com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.world, x, z); // Paper - sync load info
                 this.world.timings.syncChunkLoad.startTiming(); // Paper
             this.serverThreadQueue.awaitTasks(completablefuture::isDone);
                 com.destroystokyo.paper.io.chunk.ChunkTaskManager.popChunkWait(); // Paper - async chunk debug
diff --git a/src/main/java/net/minecraft/server/level/WorldServer.java b/src/main/java/net/minecraft/server/level/WorldServer.java
index 13c99bdb8894d08f297f84ee1f98f50c811f7f4b..fbb550cce96e7e5539c69bae1459326090d0c508 100644
--- a/src/main/java/net/minecraft/server/level/WorldServer.java
+++ b/src/main/java/net/minecraft/server/level/WorldServer.java
@@ -282,6 +282,12 @@ public class WorldServer extends World implements GeneratorAccessSeed {
     };
     public final com.destroystokyo.paper.io.chunk.ChunkTaskManager asyncChunkTaskManager;
     // Paper end
+    // Paper start
+    @Override
+    public boolean isChunkLoaded(int x, int z) {
+        return this.getChunkProvider().getChunkAtIfLoadedImmediately(x, z) != null;
+    }
+    // Paper end
 
     // Add env and gen to constructor, WorldData -> WorldDataServer
     public WorldServer(MinecraftServer minecraftserver, Executor executor, Convertable.ConversionSession convertable_conversionsession, IWorldDataServer iworlddataserver, ResourceKey<World> resourcekey, DimensionManager dimensionmanager, WorldLoadListener worldloadlistener, ChunkGenerator chunkgenerator, boolean flag, long i, List<MobSpawner> list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) {
diff --git a/src/main/java/net/minecraft/world/level/World.java b/src/main/java/net/minecraft/world/level/World.java
index b620d7e0d824c8d0758a66a8fbe872c3e45103d2..6781b25cc8e15be2556bb1bb8dc8c18c106b40ec 100644
--- a/src/main/java/net/minecraft/world/level/World.java
+++ b/src/main/java/net/minecraft/world/level/World.java
@@ -1130,7 +1130,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
 
         for (int i1 = i; i1 <= j; ++i1) {
             for (int j1 = k; j1 <= l; ++j1) {
-                Chunk chunk = ichunkprovider.getChunkAt(i1, j1, false);
+                Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper
 
                 if (chunk != null) {
                     chunk.a(entity, axisalignedbb, list, predicate);
@@ -1151,7 +1151,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
 
         for (int i1 = i; i1 < j; ++i1) {
             for (int j1 = k; j1 < l; ++j1) {
-                Chunk chunk = this.getChunkProvider().getChunkAt(i1, j1, false);
+                Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper
 
                 if (chunk != null) {
                     chunk.a(entitytypes, axisalignedbb, list, predicate);
@@ -1174,7 +1174,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
 
         for (int i1 = i; i1 < j; ++i1) {
             for (int j1 = k; j1 < l; ++j1) {
-                Chunk chunk = ichunkprovider.getChunkAt(i1, j1, false);
+                Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper
 
                 if (chunk != null) {
                     chunk.a(oclass, axisalignedbb, list, predicate);