diff --git a/patches/server/0053-Asynchronous-chunk-IO.patch b/patches/server/0052-Asynchronous-chunk-IO-and-loading.patch similarity index 55% rename from patches/server/0053-Asynchronous-chunk-IO.patch rename to patches/server/0052-Asynchronous-chunk-IO-and-loading.patch index f3e44b34b..c98493657 100644 --- a/patches/server/0053-Asynchronous-chunk-IO.patch +++ b/patches/server/0052-Asynchronous-chunk-IO-and-loading.patch @@ -1,33 +1,147 @@ -From 6a1a8b6aafe4641484ae13615a21920c863586ae Mon Sep 17 00:00:00 2001 +From da939406c7900f95e4c1888e8672968e38afe69a Mon Sep 17 00:00:00 2001 From: Spottedleaf -Date: Tue, 9 Jul 2019 03:38:23 -0700 -Subject: [PATCH] Asynchronous chunk IO +Date: Sat, 13 Jul 2019 09:23:10 -0700 +Subject: [PATCH] Asynchronous chunk IO and loading --- - .../paper/io/ConcreteFileIOThread.java | 652 ++++++++++++++++++ + .../co/aikar/timings/WorldTimingsHandler.java | 14 + + .../ChunkPacketBlockControllerAntiXray.java | 43 +- + .../paper/io/ConcreteFileIOThread.java | 661 ++++++++++++++++++ .../com/destroystokyo/paper/io/IOUtil.java | 62 ++ - .../paper/io/PrioritizedTaskQueue.java | 262 +++++++ - .../paper/io/QueueExecutorThread.java | 200 ++++++ - .../minecraft/server/ChunkProviderServer.java | 5 + + .../paper/io/PrioritizedTaskQueue.java | 258 +++++++ + .../paper/io/QueueExecutorThread.java | 216 ++++++ + .../paper/io/chunk/ChunkLoadTask.java | 104 +++ + .../paper/io/chunk/ChunkLoadTaskManager.java | 119 ++++ + .../minecraft/server/ChunkProviderServer.java | 135 ++++ + .../minecraft/server/ChunkRegionLoader.java | 51 +- + .../net/minecraft/server/ChunkStatus.java | 1 + + .../net/minecraft/server/IChunkLoader.java | 29 +- + .../java/net/minecraft/server/MCUtil.java | 5 + .../net/minecraft/server/MinecraftServer.java | 1 + - .../net/minecraft/server/PlayerChunkMap.java | 152 +++- - .../java/net/minecraft/server/RegionFile.java | 6 +- + .../net/minecraft/server/PlayerChunkMap.java | 204 +++++- + .../java/net/minecraft/server/RegionFile.java | 2 +- .../net/minecraft/server/RegionFileCache.java | 6 +- - .../minecraft/server/RegionFileSection.java | 57 +- - .../net/minecraft/server/VillagePlace.java | 64 +- - .../net/minecraft/server/WorldServer.java | 72 ++ - 12 files changed, 1501 insertions(+), 38 deletions(-) + .../minecraft/server/RegionFileSection.java | 56 +- + .../java/net/minecraft/server/TicketType.java | 1 + + .../net/minecraft/server/VillagePlace.java | 66 +- + .../net/minecraft/server/WorldServer.java | 74 ++ + .../org/bukkit/craftbukkit/CraftWorld.java | 36 +- + 22 files changed, 2066 insertions(+), 78 deletions(-) create mode 100644 src/main/java/com/destroystokyo/paper/io/ConcreteFileIOThread.java create mode 100644 src/main/java/com/destroystokyo/paper/io/IOUtil.java create mode 100644 src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java create mode 100644 src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java + create mode 100644 src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java + create mode 100644 src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTaskManager.java +diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java +index 366de66657..7f623270f9 100644 +--- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java ++++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java +@@ -51,6 +51,13 @@ public class WorldTimingsHandler { + public final Timing worldSaveChunks; + public final Timing worldSaveLevel; + public final Timing chunkSaveData; ++ public final Timing poiUnload; ++ public final Timing chunkUnload; ++ public final Timing poiSaveDataSerialization; ++ public final Timing chunkSave; ++ public final Timing chunkSaveOverwriteCheck; ++ public final Timing chunkSaveDataSerialization; ++ public final Timing chunkSaveIOWait; + + public WorldTimingsHandler(World server) { + String name = server.worldData.getName() +" - "; +@@ -99,6 +106,13 @@ public class WorldTimingsHandler { + tracker2 = Timings.ofSafe(name + "tracker stage 2"); + doTick = Timings.ofSafe(name + "doTick"); + tickEntities = Timings.ofSafe(name + "tickEntities"); ++ poiUnload = Timings.ofSafe(name + "Chunk unload - POI"); ++ chunkUnload = Timings.ofSafe(name + "Chunk unload - Chunk"); ++ poiSaveDataSerialization = Timings.ofSafe(name + "Chunk save - POI Data serialization"); ++ chunkSave = Timings.ofSafe(name + "Chunk save - Chunk"); ++ chunkSaveOverwriteCheck = Timings.ofSafe(name + "Chunk save - Chunk Overwrite check"); ++ chunkSaveDataSerialization = Timings.ofSafe(name + "Chunk save - Chunk Data serialization"); ++ chunkSaveIOWait = Timings.ofSafe(name + "Chunk save - Chunk IO wait"); + } + + public static Timing getTickList(WorldServer worldserver, String timingsType) { +diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java +index 9d8bee5cac..c2dd59f573 100644 +--- a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java ++++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java +@@ -150,6 +150,12 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll + + private final AtomicInteger xrayRequests = new AtomicInteger(); + ++ // Paper start - async chunk api ++ private Integer nextTicketHold() { ++ return Integer.valueOf(this.xrayRequests.getAndIncrement()); ++ } ++ // Paper end ++ + private Integer addXrayTickets(final int x, final int z, final ChunkProviderServer chunkProvider) { + final Integer hold = Integer.valueOf(this.xrayRequests.getAndIncrement()); + +@@ -181,6 +187,33 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll + chunk.world.getChunkAt(locX, locZ + 1); + } + ++ // Paper start - async chunk api ++ private void loadNeighbourAsync(ChunkProviderServer chunkProvider, int chunkX, int chunkZ, int[] counter, java.util.function.Consumer onNeighourLoad, Runnable onAllNeighboursLoad) { ++ chunkProvider.getChunkAtAsynchronously(chunkX, chunkZ, true, (Chunk neighbour) -> { ++ onNeighourLoad.accept(neighbour); ++ if (++counter[0] == 4) { ++ onAllNeighboursLoad.run(); ++ } ++ }); ++ } ++ ++ private void loadNeighboursAsync(Chunk chunk, java.util.function.Consumer onNeighourLoad, Runnable onAllNeighboursLoad) { ++ int[] loaded = new int[1]; ++ ++ int locX = chunk.getPos().x; ++ int locZ = chunk.getPos().z; ++ ++ onNeighourLoad.accept(chunk); ++ ++ ChunkProviderServer chunkProvider = ((WorldServer)chunk.world).getChunkProvider(); ++ ++ this.loadNeighbourAsync(chunkProvider, locX - 1, locZ, loaded, onNeighourLoad, onAllNeighboursLoad); ++ this.loadNeighbourAsync(chunkProvider, locX + 1, locZ, loaded, onNeighourLoad, onAllNeighboursLoad); ++ this.loadNeighbourAsync(chunkProvider, locX, locZ - 1, loaded, onNeighourLoad, onAllNeighboursLoad); ++ this.loadNeighbourAsync(chunkProvider, locX, locZ + 1, loaded, onNeighourLoad, onAllNeighboursLoad); ++ } ++ // Paper end ++ + @Override + public boolean onChunkPacketCreate(Chunk chunk, int chunkSectionSelector, boolean force) { + int locX = chunk.getPos().x; +@@ -256,11 +289,15 @@ public class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockControll + + if (chunks[0] == null || chunks[1] == null || chunks[2] == null || chunks[3] == null) { + // we need to load +- MinecraftServer.getServer().scheduleOnMain(() -> { +- Integer ticketHold = this.addXrayTickets(locX, locZ, world.getChunkProvider()); +- this.loadNeighbours(chunk); ++ // Paper start - async chunk api ++ Integer ticketHold = this.nextTicketHold(); ++ this.loadNeighboursAsync(chunk, (Chunk neighbour) -> { // when a neighbour is loaded ++ ((WorldServer)neighbour.world).getChunkProvider().addTicket(TicketType.ANTIXRAY, neighbour.getPos(), 0, ticketHold); ++ }, ++ () -> { // once neighbours get loaded + this.modifyBlocks(packetPlayOutMapChunk, chunkPacketInfo, false, ticketHold); + }); ++ // Paper end + return; + } + diff --git a/src/main/java/com/destroystokyo/paper/io/ConcreteFileIOThread.java b/src/main/java/com/destroystokyo/paper/io/ConcreteFileIOThread.java new file mode 100644 -index 0000000000..13194d1978 +index 0000000000..8cbe522f23 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/io/ConcreteFileIOThread.java -@@ -0,0 +1,652 @@ +@@ -0,0 +1,661 @@ +package com.destroystokyo.paper.io; + +import net.minecraft.server.ChunkCoordIntPair; @@ -49,17 +163,17 @@ index 0000000000..13194d1978 + * Prioritized singleton thread responsible for all chunk IO that occurs in a minecraft server. + * + *

-+ * Singleton access: {@link Holder#INSTANCE} ++ * Singleton access: {@link Holder#INSTANCE} + *

+ * + *

-+ * All functions provided are MT-Safe, however certain ordering constraints are (but not enforced): -+ *

  • -+ * Chunk saves may not occur for unloaded chunks. -+ *
  • -+ *
  • -+ * Tasks must be scheduled on the main thread. -+ *
  • ++ * All functions provided are MT-Safe, however certain ordering constraints are (but not enforced): ++ *
  • ++ * Chunk saves may not occur for unloaded chunks. ++ *
  • ++ *
  • ++ * Tasks must be scheduled on the main thread. ++ *
  • + *

    + * + * @see Holder#INSTANCE @@ -68,7 +182,7 @@ index 0000000000..13194d1978 + */ +public final class ConcreteFileIOThread extends QueueExecutorThread { + -+ private static final Logger LOGGER = MinecraftServer.LOGGER; ++ public static final Logger LOGGER = MinecraftServer.LOGGER; + public static final NBTTagCompound FAILURE_VALUE = new NBTTagCompound(); + + public static final class Holder { @@ -83,7 +197,7 @@ index 0000000000..13194d1978 + private final AtomicLong writeCounter = new AtomicLong(); + + private ConcreteFileIOThread() { -+ super(new PrioritizedTaskQueue<>(), (int) (1.0e6)); // 1.0ms spinwait time ++ super(new PrioritizedTaskQueue<>(), (int)(1.0e6)); // 1.0ms spinwait time + this.setName("Concrete RegionFile IO Thread"); + this.setPriority(Thread.NORM_PRIORITY - 1); // we keep priority close to normal because threads can wait on us + this.setUncaughtExceptionHandler((final Thread unused, final Throwable thr) -> { @@ -113,10 +227,9 @@ index 0000000000..13194d1978 + + /** + * Attempts to bump the priority of all IO tasks for the given chunk coordinates. This has no effect if no tasks are queued. -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate + * @param priority Priority level to try to bump to + */ + public void bumpPriority(final WorldServer world, final int chunkX, final int chunkZ, final int priority) { @@ -138,18 +251,17 @@ index 0000000000..13194d1978 + } + + // Hack start -+ + /** + * if {@code waitForRead} is true, then this task will wait on an available read task, else it will wait on an available + * write task + * if {@code poiTask} is true, then this task will wait on a poi task, else it will wait on chunk data task -+ * -+ * @return whether the task succeeded, or {@code null} if there is no task + * @deprecated API is garbage and will only work for main thread queueing of tasks (which is vanilla), plugins messing -+ * around asynchronously will give unexpected results ++ * around asynchronously will give unexpected results ++ * @return whether the task succeeded, or {@code null} if there is no task + */ + @Deprecated -+ public Boolean waitForIOToComplete(final WorldServer world, final int chunkX, final int chunkZ, boolean waitForRead, boolean poiTask) { ++ public Boolean waitForIOToComplete(final WorldServer world, final int chunkX, final int chunkZ, final boolean waitForRead, ++ final boolean poiTask) { + final ChunkDataTask task; + + final Long key = IOUtil.getCoordinateKey(chunkX, chunkZ); @@ -180,12 +292,29 @@ index 0000000000..13194d1978 + } + // Hack end + ++ public NBTTagCompound getPendingWrite(final WorldServer world, final int chunkX, final int chunkZ, final boolean poiData) { ++ final ChunkDataController taskController = poiData ? world.poiDataController : world.chunkDataController; ++ ++ final ChunkDataTask dataTask = taskController.tasks.get(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ))); ++ ++ if (dataTask == null) { ++ return null; ++ } ++ ++ final ChunkDataController.InProgressWrite write = dataTask.inProgressWrite; ++ ++ if (write == null) { ++ return null; ++ } ++ ++ return write.data; ++ } ++ + /** + * Sets the priority of all IO tasks for the given chunk coordinates. This has no effect if no tasks are queued. -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate + * @param priority Priority level to set to + */ + public void setPriority(final WorldServer world, final int chunkX, final int chunkZ, final int priority) { @@ -209,24 +338,23 @@ index 0000000000..13194d1978 + /** + * Schedules the chunk data to be written asynchronously. + *

    -+ * Impl notes: ++ * Impl notes: + *

    + *
  • -+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means -+ * saves must be scheduled before a chunk is unloaded. ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. + *
  • + *
  • -+ * Writes may be called concurrently, although only the "later" write will go through. ++ * Writes may be called concurrently, although only the "later" write will go through. + *
  • -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param poiData Chunk point of interest data. If {@code null}, then no poi data is saved. ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param poiData Chunk point of interest data. If {@code null}, then no poi data is saved. + * @param chunkData Chunk data. If {@code null}, then no chunk data is saved. -+ * @param priority Priority level for this task. See {@link PrioritizedTaskQueue} ++ * @param priority Priority level for this task. See {@link PrioritizedTaskQueue} + * @throws IllegalArgumentException If both {@code poiData} and {@code chunkData} are {@code null}. -+ * @throws IllegalStateException If the file io thread has shutdown. ++ * @throws IllegalStateException If the file io thread has shutdown. + */ + public void scheduleSave(final WorldServer world, final int chunkX, final int chunkZ, + final NBTTagCompound poiData, final NBTTagCompound chunkData, @@ -290,7 +418,7 @@ index 0000000000..13194d1978 + * a {@link CompletableFuture} which is potentially completed ASYNCHRONOUSLY ON THE FILE IO THREAD when the load task + * has completed. + *

    -+ * Note that if the chunk fails to load the returned future is completed with {@code null}. ++ * Note that if the chunk fails to load the returned future is completed with {@code null}. + *

    + */ + public CompletableFuture loadChunkDataAsyncFuture(final WorldServer world, final int chunkX, final int chunkZ, @@ -304,30 +432,29 @@ index 0000000000..13194d1978 + /** + * Schedules a load to be executed asynchronously. + *

    -+ * Impl notes: ++ * Impl notes: + *

    + *
  • -+ * If a chunk fails to load, the {@code onComplete} parameter is completed with {@code null}. ++ * If a chunk fails to load, the {@code onComplete} parameter is completed with {@code null}. + *
  • + *
  • -+ * It is possible for the {@code onComplete} parameter to be given {@link ChunkData} containing data -+ * this call did not request. ++ * It is possible for the {@code onComplete} parameter to be given {@link ChunkData} containing data ++ * this call did not request. + *
  • + *
  • -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. + *
  • -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param priority Priority level for this task. See {@link PrioritizedTaskQueue} -+ * @param onComplete Consumer to execute once this task has completed -+ * @param readPoiData Whether to read point of interest data. If {@code false}, the {@code NBTTagCompound} will be {@code null}. ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param priority Priority level for this task. See {@link PrioritizedTaskQueue} ++ * @param onComplete Consumer to execute once this task has completed ++ * @param readPoiData Whether to read point of interest data. If {@code false}, the {@code NBTTagCompound} will be {@code null}. + * @param readChunkData Whether to read chunk data. If {@code false}, the {@code NBTTagCompound} will be {@code null}. + * @return The {@link PrioritizedTaskQueue.PrioritizedTask} associated with this task. Note that this task does not support -+ * cancellation. ++ * cancellation. + */ + public void loadChunkDataAsync(final WorldServer world, final int chunkX, final int chunkZ, + final int priority, final Consumer onComplete, @@ -342,7 +469,7 @@ index 0000000000..13194d1978 + } + + final ChunkData complete = new ChunkData(); -+ final boolean[] requireCompletion = new boolean[]{readPoiData, readChunkData}; ++ final boolean[] requireCompletion = new boolean[] { readPoiData, readChunkData }; + + if (readPoiData) { + this.scheduleRead(world.poiDataController, world, chunkX, chunkZ, (final NBTTagCompound poiData) -> { @@ -401,7 +528,7 @@ index 0000000000..13194d1978 + // not scheduled + + final Boolean shouldSchedule = intendingToBlock ? dataController.computeForRegionFile(chunkX, chunkZ, tryLoadFunction) : -+ dataController.computeForRegionFileIfLoaded(chunkX, chunkZ, tryLoadFunction); ++ dataController.computeForRegionFileIfLoaded(chunkX, chunkZ, tryLoadFunction); + + if (shouldSchedule == Boolean.FALSE) { + // not on disk @@ -436,7 +563,6 @@ index 0000000000..13194d1978 + /** + * Same as {@link #loadChunkDataAsync(WorldServer, int, int, int, Consumer, boolean, boolean, boolean)}, except this function returns + * the {@link ChunkData} associated with the specified chunk when the task is complete. -+ * + * @return The chunk data, or {@code null} if the chunk failed to load. + */ + public ChunkData loadChunkData(final WorldServer world, final int chunkX, final int chunkZ, final int priority, @@ -447,7 +573,7 @@ index 0000000000..13194d1978 + /** + * Schedules the given task at the specified priority to be executed on the IO thread. + *

    -+ * Internal api. Do not use. ++ * Internal api. Do not use. + *

    + */ + public void runTask(final int priority, final Runnable runnable) { @@ -469,7 +595,7 @@ index 0000000000..13194d1978 + this.run.run(); + } catch (final Throwable throwable) { + if (throwable instanceof ThreadDeath) { -+ throw (ThreadDeath) throwable; ++ throw (ThreadDeath)throwable; + } + LOGGER.fatal("Failed to execute general task on IO thread " + IOUtil.genericToString(this.run), throwable); + } @@ -481,8 +607,7 @@ index 0000000000..13194d1978 + public NBTTagCompound poiData; + public NBTTagCompound chunkData; + -+ public ChunkData() { -+ } ++ public ChunkData() {} + + public ChunkData(final NBTTagCompound poiData, final NBTTagCompound chunkData) { + this.poiData = poiData; @@ -496,11 +621,9 @@ index 0000000000..13194d1978 + public final ConcurrentHashMap tasks = new ConcurrentHashMap<>(64, 0.5f); + + public abstract void writeData(final int x, final int z, final NBTTagCompound compound) throws IOException; -+ + public abstract NBTTagCompound readData(final int x, final int z) throws IOException; + + public abstract T computeForRegionFile(final int chunkX, final int chunkZ, final Function function); -+ + public abstract T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function); + + public static final class InProgressWrite { @@ -576,7 +699,7 @@ index 0000000000..13194d1978 + compound = this.taskController.readData(this.x, this.z); + } catch (final Throwable thr) { + if (thr instanceof ThreadDeath) { -+ throw (ThreadDeath) thr; ++ throw (ThreadDeath)thr; + } + LOGGER.fatal("Failed to read chunk data for (" + this.x + "," + this.z + ")", thr); + // fall through to complete with null data @@ -619,7 +742,7 @@ index 0000000000..13194d1978 + return; + } + -+ for (; ; ) { ++ for (;;) { + final long writeCounter; + final NBTTagCompound data; + @@ -635,14 +758,14 @@ index 0000000000..13194d1978 + this.taskController.writeData(this.x, this.z, data); + } catch (final Throwable thr) { + if (thr instanceof ThreadDeath) { -+ throw (ThreadDeath) thr; ++ throw (ThreadDeath)thr; + } + LOGGER.fatal("Failed to write chunk data for (" + this.x + "," + this.z + ")", thr); + failedWrite = true; + } + + boolean finalFailWrite = failedWrite; -+ boolean[] returnFailWrite = new boolean[]{false}; ++ boolean[] returnFailWrite = new boolean[] { false }; + + ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final Long keyInMap, final ChunkDataTask valueInMap) -> { + if (valueInMap == null) { @@ -682,7 +805,7 @@ index 0000000000..13194d1978 +} diff --git a/src/main/java/com/destroystokyo/paper/io/IOUtil.java b/src/main/java/com/destroystokyo/paper/io/IOUtil.java new file mode 100644 -index 0000000000..7898cab62f +index 0000000000..5af0ac3d9e --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/io/IOUtil.java @@ -0,0 +1,62 @@ @@ -695,15 +818,15 @@ index 0000000000..7898cab62f + /* Copied from concrete or concurrentutil */ + + public static long getCoordinateKey(final int x, final int z) { -+ return ((long) z << 32) | (x & 0xFFFFFFFFL); ++ return ((long)z << 32) | (x & 0xFFFFFFFFL); + } + + public static int getCoordinateX(final long key) { -+ return (int) key; ++ return (int)key; + } + + public static int getCoordinateZ(final long key) { -+ return (int) (key >>> 32); ++ return (int)(key >>> 32); + } + + public static int getRegionCoordinate(final int chunkCoordinate) { @@ -744,17 +867,16 @@ index 0000000000..7898cab62f + + @SuppressWarnings("unchecked") + public static void rethrow(final Throwable throwable) throws T { -+ throw (T) throwable; ++ throw (T)throwable; + } + +} -\ No newline at end of file diff --git a/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java b/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java new file mode 100644 -index 0000000000..3722991e1b +index 0000000000..c3ca3c4a1c --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java -@@ -0,0 +1,262 @@ +@@ -0,0 +1,258 @@ +package com.destroystokyo.paper.io; + +import java.util.concurrent.ConcurrentLinkedQueue; @@ -770,42 +892,42 @@ index 0000000000..3722991e1b + /** + * Priority value indicating the task has completed or is being completed. + */ -+ public static final int COMPLETING_PRIORITY = -1; ++ public static final int COMPLETING_PRIORITY = -1; + + /** + * Highest priority, should only be used for main thread tasks or tasks that are blocking the main thread. + */ -+ public static final int HIGHEST_PRIORITY = 0; ++ public static final int HIGHEST_PRIORITY = 0; + + /** + * Should be only used in an IO task so that chunk loads do not wait on other IO tasks. + * This only exists because IO tasks are scheduled before chunk load tasks to decrease IO waiting times. + */ -+ public static final int HIGHER_PRIORITY = 1; ++ public static final int HIGHER_PRIORITY = 1; + + /** + * Should be used for scheduling chunk loads/generation that would increase response times to users. + */ -+ public static final int HIGH_PRIORITY = 2; ++ public static final int HIGH_PRIORITY = 2; + + /** + * Default priority. + */ -+ public static final int NORMAL_PRIORITY = 3; ++ public static final int NORMAL_PRIORITY = 3; + + /** + * Use for tasks not at all critical and can potentially be delayed. + */ -+ public static final int LOW_PRIORITY = 4; ++ public static final int LOW_PRIORITY = 4; + + /** + * Use for tasks that should "eventually" execute. + */ -+ public static final int LOWEST_PRIORITY = 5; ++ public static final int LOWEST_PRIORITY = 5; + -+ private static final int TOTAL_PRIORITIES = 6; ++ private static final int TOTAL_PRIORITIES = 6; + -+ final ConcurrentLinkedQueue[] queues = (ConcurrentLinkedQueue[]) new ConcurrentLinkedQueue[TOTAL_PRIORITIES]; ++ final ConcurrentLinkedQueue[] queues = (ConcurrentLinkedQueue[])new ConcurrentLinkedQueue[TOTAL_PRIORITIES]; + + private final AtomicBoolean shutdown = new AtomicBoolean(); + @@ -824,7 +946,6 @@ index 0000000000..3722991e1b + + /** + * Queues a task. -+ * + * @throws IllegalStateException If the task has already been queued. Use {@link PrioritizedTask#raisePriority(int)} to + * raise a task's priority. + * This can also be thrown if the queue has shutdown. @@ -862,12 +983,11 @@ index 0000000000..3722991e1b + * Prevent further additions to this queue. Attempts to add after this call has completed (potentially during) will + * result in {@link IllegalStateException} being thrown. + *

    -+ * This operation is atomic with respect to other shutdown calls ++ * This operation is atomic with respect to other shutdown calls + *

    + *

    -+ * After this call has completed, regardless of return value, this queue will be shutdown. ++ * After this call has completed, regardless of return value, this queue will be shutdown. + *

    -+ * + * @return {@code true} if the queue was shutdown, {@code false} if it has shut down already + */ + public boolean shutdown() { @@ -899,8 +1019,15 @@ index 0000000000..3722991e1b + return this.priority.get(); + } + ++ /** ++ * Returns whether this task is scheduled to execute, or has been already executed. ++ */ ++ public boolean isScheduled() { ++ return this.queue.get() != null; ++ } ++ + final int tryComplete(final int minPriority) { -+ for (int curr = this.getPriorityVolatile(); ; ) { ++ for (int curr = this.getPriorityVolatile();;) { + if (curr == COMPLETING_PRIORITY) { + return COMPLETING_PRIORITY; + } @@ -918,7 +1045,6 @@ index 0000000000..3722991e1b + + /** + * Forces this task to be completed. -+ * + * @return {@code true} if the task was cancelled, {@code false} if the task has already completed or is being completed. + */ + public boolean cancel() { @@ -927,7 +1053,6 @@ index 0000000000..3722991e1b + + /** + * Attempts to raise the priority to the priority level specified. -+ * + * @param priority Priority specified + * @return {@code true} if successful, {@code false} otherwise. + */ @@ -936,13 +1061,7 @@ index 0000000000..3722991e1b + throw new IllegalArgumentException("Invalid priority"); + } + -+ final PrioritizedTaskQueue queue = this.queue.get(); -+ -+ if (queue == null) { -+ throw new IllegalStateException("Not queued"); -+ } -+ -+ for (int curr = this.getPriorityVolatile(); ; ) { ++ for (int curr = this.getPriorityVolatile();;) { + if (curr == COMPLETING_PRIORITY) { + return false; + } @@ -951,8 +1070,11 @@ index 0000000000..3722991e1b + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority))) { -+ //noinspection unchecked -+ queue.queues[priority].add(this); // silently fail on shutdown ++ PrioritizedTaskQueue queue = this.queue.get(); ++ if (queue != null) { ++ //noinspection unchecked ++ queue.queues[priority].add(this); // silently fail on shutdown ++ } + return true; + } + continue; @@ -961,7 +1083,6 @@ index 0000000000..3722991e1b + + /** + * Attempts to set this task's priority level to the level specified. -+ * + * @param priority Specified priority level. + * @return {@code true} if successful, {@code false} if this task is completing or has completed. + */ @@ -970,13 +1091,7 @@ index 0000000000..3722991e1b + throw new IllegalArgumentException("Invalid priority"); + } + -+ final PrioritizedTaskQueue queue = this.queue.get(); -+ -+ if (queue == null) { -+ throw new IllegalStateException("Not queued"); -+ } -+ -+ for (int curr = this.getPriorityVolatile(); ; ) { ++ for (int curr = this.getPriorityVolatile();;) { + if (curr == COMPLETING_PRIORITY) { + return false; + } @@ -985,8 +1100,11 @@ index 0000000000..3722991e1b + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority))) { -+ //noinspection unchecked -+ queue.queues[priority].add(this); // silently fail on shutdown ++ PrioritizedTaskQueue queue = this.queue.get(); ++ if (queue != null) { ++ //noinspection unchecked ++ queue.queues[priority].add(this); // silently fail on shutdown ++ } + return true; + } + continue; @@ -1017,13 +1135,12 @@ index 0000000000..3722991e1b + } + } +} -\ No newline at end of file diff --git a/src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java b/src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java new file mode 100644 -index 0000000000..1b64cec885 +index 0000000000..609b2038f2 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java -@@ -0,0 +1,200 @@ +@@ -0,0 +1,216 @@ +package com.destroystokyo.paper.io; + +import net.minecraft.server.MinecraftServer; @@ -1047,7 +1164,7 @@ index 0000000000..1b64cec885 + protected volatile int flushCounter; + + public QueueExecutorThread(final PrioritizedTaskQueue queue) { -+ this(queue, (int) (1.e6)); // 1.0ms ++ this(queue, (int)(1.e6)); // 1.0ms + } + + public QueueExecutorThread(final PrioritizedTaskQueue queue, final long spinWaitTime) { // in ms @@ -1059,14 +1176,14 @@ index 0000000000..1b64cec885 + public void run() { + final long spinWaitTime = this.spinWaitTime; + main_loop: -+ for (; ; ) { ++ for (;;) { + this.pollTasks(); + + // spinwait + + final long start = System.nanoTime(); + -+ for (; ; ) { ++ for (;;) { + // If we are interrpted for any reason, park() will always return immediately. Clear so that we don't needlessly use cpu in such an event. + Thread.interrupted(); + LockSupport.parkNanos("Spinwaiting on tasks", 1000L); // 1us @@ -1116,13 +1233,13 @@ index 0000000000..1b64cec885 + Runnable task; + boolean ret = false; + -+ while ((task = (Runnable) this.queue.poll()) != null) { ++ while ((task = (Runnable)this.queue.poll()) != null) { + ret = true; + try { + task.run(); + } catch (final Throwable throwable) { + if (throwable instanceof ThreadDeath) { -+ throw (ThreadDeath) throwable; ++ throw (ThreadDeath)throwable; + } + LOGGER.fatal("Exception thrown from prioritized runnable task in thread " + this.getName() + ": " + IOUtil.genericToString(task), throwable); + } @@ -1145,13 +1262,30 @@ index 0000000000..1b64cec885 + + } + -+ protected void queueTask(final T task) { -+ this.queue.add(task); ++ public boolean isWaitingForTasks() { ++ return this.parked.get(); ++ } ++ ++ public boolean notifyTasks() { ++ if (this.parked.get() && this.parked.getAndSet(false)) { ++ LockSupport.unpark(this); ++ return true; ++ } ++ return false; ++ } ++ ++ public void onTaskQueue() { + if (this.parked.get() && this.parked.getAndSet(false)) { + LockSupport.unpark(this); + } + } + ++ protected void queueTask(final T task) { ++ this.queue.add(task); ++ this.onTaskQueue(); ++ } ++ ++ + /** + * Waits until this thread's queue is empty. + * @@ -1206,12 +1340,11 @@ index 0000000000..1b64cec885 + /** + * Closes this queue executor's queue and optionally waits for it to empty. + *

    -+ * If wait is {@code true}, then the queue will be empty by the time this call completes. ++ * If wait is {@code true}, then the queue will be empty by the time this call completes. + *

    + *

    -+ * This function is MT-Safe. ++ * This function is MT-Safe. + *

    -+ * + * @param wait If this call is to wait until the queue is empty + * @return whether this thread shut down the queue + */ @@ -1224,12 +1357,382 @@ index 0000000000..1b64cec885 + return ret; + } +} -\ No newline at end of file +diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java +new file mode 100644 +index 0000000000..566d1684a5 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java +@@ -0,0 +1,104 @@ ++package com.destroystokyo.paper.io.chunk; ++ ++import com.destroystokyo.paper.io.ConcreteFileIOThread; ++import com.destroystokyo.paper.io.IOUtil; ++import com.destroystokyo.paper.io.PrioritizedTaskQueue; ++import net.minecraft.server.ChunkCoordIntPair; ++import net.minecraft.server.ChunkRegionLoader; ++import net.minecraft.server.PlayerChunkMap; ++import net.minecraft.server.WorldServer; ++ ++import java.io.IOException; ++import java.util.ArrayDeque; ++import java.util.function.Consumer; ++ ++public class ChunkLoadTask extends PrioritizedTaskQueue.PrioritizedTask implements Runnable { ++ ++ public final WorldServer world; ++ public final int chunkX; ++ public final int chunkZ; ++ public final ChunkLoadTaskManager taskManager; ++ ++ final Consumer onComplete; ++ ++ public ConcreteFileIOThread.ChunkData chunkData; ++ ++ public ChunkLoadTask(final WorldServer world, final int chunkX, final int chunkZ, final int priority, ++ final Consumer onComplete, ++ final ChunkLoadTaskManager taskManager) { ++ super(priority); ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.taskManager = taskManager; ++ this.onComplete = onComplete; ++ } ++ ++ private static final ArrayDeque EMPTY_QUEUE = new ArrayDeque<>(); ++ ++ private static ChunkRegionLoader.InProgressChunkHolder createEmptyHolder() { ++ return new ChunkRegionLoader.InProgressChunkHolder(null, EMPTY_QUEUE); ++ } ++ ++ @Override ++ public void run() { ++ // either executed synchronously or asynchronously ++ final ConcreteFileIOThread.ChunkData chunkData = this.chunkData; ++ ++ if (chunkData.poiData == ConcreteFileIOThread.FAILURE_VALUE || chunkData.chunkData == ConcreteFileIOThread.FAILURE_VALUE) { ++ ConcreteFileIOThread.LOGGER.error("Could not load chunk at (" + this.chunkX + "," + this.chunkZ + ")"); ++ this.complete(ChunkLoadTask.createEmptyHolder()); ++ return; ++ } ++ ++ if (chunkData.chunkData == null) { ++ ChunkRegionLoader.InProgressChunkHolder ret = new ChunkRegionLoader.InProgressChunkHolder(null, EMPTY_QUEUE); ++ ret.poiData = chunkData.poiData; ++ this.complete(ret); ++ return; ++ } ++ ++ final ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(this.chunkX, this.chunkZ); ++ ++ final PlayerChunkMap chunkManager = this.world.getChunkProvider().playerChunkMap; ++ ++ final ChunkRegionLoader.InProgressChunkHolder chunkHolder = ChunkRegionLoader.loadChunk(this.world, ++ chunkManager.definedStructureManager, chunkManager.getVillagePlace(), chunkPos, ++ chunkData.chunkData, true); ++ ++ // apply fixes ++ ++ try { ++ chunkData.chunkData = chunkManager.getChunkData(this.world.getWorldProvider().getDimensionManager(), ++ chunkManager.getWorldPersistentDataSupplier(), chunkData.chunkData, chunkPos, this.world); ++ } catch (IOException ex) { ++ ConcreteFileIOThread.LOGGER.error("Could not apply datafixers for chunk at (" + this.chunkX + "," + this.chunkZ + ")", ex); ++ this.complete(ChunkLoadTask.createEmptyHolder()); ++ } ++ ++ this.complete(chunkHolder); ++ } ++ ++ private void complete(final ChunkRegionLoader.InProgressChunkHolder holder) { ++ this.onComplete.accept(holder); ++ this.taskManager.tasks.compute(Long.valueOf(IOUtil.getCoordinateKey(this.chunkX, this.chunkZ)), (final Long keyInMap, final ChunkLoadTask valueInMap) -> { ++ if (valueInMap != ChunkLoadTask.this) { ++ throw new IllegalStateException("Expected this task to be scheduled, but another was! Other:" + valueInMap); ++ } ++ return null; ++ }); ++ } ++ ++ @Override ++ public boolean raisePriority(final int priority) { ++ ConcreteFileIOThread.Holder.INSTANCE.bumpPriority(this.world, this.chunkX, this.chunkZ, priority); ++ return super.raisePriority(priority); ++ } ++ ++ @Override ++ public boolean updatePriority(final int priority) { ++ ConcreteFileIOThread.Holder.INSTANCE.setPriority(this.world, this.chunkX, this.chunkZ, priority); ++ return super.updatePriority(priority); ++ } ++ ++} +diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTaskManager.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTaskManager.java +new file mode 100644 +index 0000000000..8dbaaba3cd +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTaskManager.java +@@ -0,0 +1,119 @@ ++package com.destroystokyo.paper.io.chunk; ++ ++import com.destroystokyo.paper.io.ConcreteFileIOThread; ++import com.destroystokyo.paper.io.IOUtil; ++import com.destroystokyo.paper.io.PrioritizedTaskQueue; ++import com.destroystokyo.paper.io.QueueExecutorThread; ++import net.minecraft.server.ChunkRegionLoader; ++import net.minecraft.server.IAsyncTaskHandler; ++import net.minecraft.server.WorldServer; ++import org.spigotmc.AsyncCatcher; ++ ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.function.Consumer; ++ ++public final class ChunkLoadTaskManager { ++ ++ private final QueueExecutorThread[] workers; ++ private final WorldServer world; ++ ++ private final PrioritizedTaskQueue queue = new PrioritizedTaskQueue<>(); ++ ++ final ConcurrentHashMap tasks = new ConcurrentHashMap<>(64, 0.5f); ++ ++ public ChunkLoadTaskManager(final WorldServer world, final int threads) { ++ this.world = world; ++ this.workers = threads <= 0 ? null : new QueueExecutorThread[threads]; ++ ++ for (int i = 0; i < threads; ++i) { ++ this.workers[i] = new QueueExecutorThread<>(this.queue, (long)0.10e6); //0.1ms ++ this.workers[i].setName("Async chunk loader thread for world: " + world.getWorldData().getName()); ++ this.workers[i].setPriority(Thread.NORM_PRIORITY - 1); ++ this.workers[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> { ++ ConcreteFileIOThread.LOGGER.fatal("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable); ++ }); ++ this.workers[i].start(); ++ } ++ ++ } ++ ++ public ChunkLoadTask scheduleChunkLoad(final int chunkX, final int chunkZ, final int priority, ++ final Consumer onComplete, ++ final boolean intendingToBlock) { ++ AsyncCatcher.catchOp("Async chunk load schedule"); ++ final WorldServer world = this.world; ++ ++ return this.tasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkLoadTask valueInMap) -> { ++ if (valueInMap != null) { ++ throw new IllegalStateException("Double scheduling chunk load"); ++ } ++ ++ final ChunkLoadTask ret = new ChunkLoadTask(world, chunkX, chunkZ, priority, onComplete, ChunkLoadTaskManager.this); ++ ++ ConcreteFileIOThread.Holder.INSTANCE.loadChunkDataAsync(world, chunkX, chunkZ, priority, (final ConcreteFileIOThread.ChunkData chunkData) -> { ++ ret.chunkData = chunkData; ++ ChunkLoadTaskManager.this.internalSchedule(ret); // only schedule to the worker threads here ++ }, true, true, intendingToBlock); ++ ++ return ret; ++ }); ++ } ++ ++ public void flush() { ++ ConcreteFileIOThread.Holder.INSTANCE.flush(); ++ if (this.workers == null) { ++ return; ++ } ++ for (final QueueExecutorThread worker : this.workers) { ++ worker.flush(); ++ } ++ } ++ ++ public void shutdown(final boolean wait) { ++ if (wait) { ++ ConcreteFileIOThread.Holder.INSTANCE.flush(); ++ } ++ ++ if (this.workers == null) { ++ return; ++ } ++ ++ for (final QueueExecutorThread worker : this.workers) { ++ worker.close(false); ++ } ++ ++ if (wait) { ++ this.flush(); ++ } ++ } ++ ++ public void raisePriority(final int chunkX, final int chunkZ, final int priority) { ++ ChunkLoadTask task = this.tasks.get(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ))); ++ if (task != null) { ++ task.raisePriority(priority); ++ this.internalScheduleNotify(); ++ } ++ } ++ ++ protected void internalSchedule(final ChunkLoadTask task) { ++ if (this.workers == null) { ++ ((IAsyncTaskHandler)this.world.getChunkProvider().serverThreadQueue).addTask(task); ++ return; ++ } ++ ++ // It's important we order the task to be executed before notifying. ++ this.queue.add(task); ++ if (task.isScheduled()) { ++ this.internalScheduleNotify(); ++ } ++ } ++ ++ protected void internalScheduleNotify() { ++ for (final QueueExecutorThread worker : this.workers) { ++ if (worker.notifyTasks()) { ++ break; ++ } ++ } ++ } ++ ++} diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java -index b46285ecdc..f6a6421140 100644 +index db9113994e..4f7c442264 100644 --- a/src/main/java/net/minecraft/server/ChunkProviderServer.java +++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java -@@ -286,6 +286,7 @@ public class ChunkProviderServer extends IChunkProvider { +@@ -147,11 +147,143 @@ public class ChunkProviderServer extends IChunkProvider { + return playerChunk.getAvailableChunkNow(); + + } ++ ++ private long asyncLoadSeqCounter; ++ ++ public void getChunkAtAsynchronously(int x, int z, boolean gen, java.util.function.Consumer onComplete) { ++ if (Thread.currentThread() != this.serverThread) { ++ this.serverThreadQueue.execute(() -> { ++ this.getChunkAtAsynchronously(x, z, gen, onComplete); ++ }); ++ return; ++ } ++ ++ long k = ChunkCoordIntPair.pair(x, z); ++ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z); ++ ++ IChunkAccess ichunkaccess; ++ ++ // try cache ++ for (int l = 0; l < 4; ++l) { ++ if (k == this.cachePos[l] && ChunkStatus.FULL == this.cacheStatus[l]) { ++ ichunkaccess = this.cacheChunk[l]; ++ if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime ++ ++ // move to first in cache ++ ++ for (int i1 = 3; i1 > 0; --i1) { ++ this.cachePos[i1] = this.cachePos[i1 - 1]; ++ this.cacheStatus[i1] = this.cacheStatus[i1 - 1]; ++ this.cacheChunk[i1] = this.cacheChunk[i1 - 1]; ++ } ++ ++ this.cachePos[0] = k; ++ this.cacheStatus[0] = ChunkStatus.FULL; ++ this.cacheChunk[0] = ichunkaccess; ++ ++ onComplete.accept((Chunk)ichunkaccess); ++ ++ return; ++ } ++ } ++ } ++ ++ if (gen) { ++ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); ++ return; ++ } ++ ++ IChunkAccess current = this.getChunkAtImmediately(x, z); // we want to bypass ticket restrictions ++ if (current != null) { ++ if (!(current instanceof ProtoChunkExtension) && !(current instanceof net.minecraft.server.Chunk)) { ++ onComplete.accept(null); // the chunk is not gen'd ++ return; ++ } ++ // we know the chunk is at full status here (either in read-only mode or the real thing) ++ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); ++ return; ++ } else { ++ // Paper start - async io ++ ChunkStatus status = world.getChunkProvider().playerChunkMap.getStatusOnDiskNoLoad(x, z); // Paper - async io - move to own method ++ ++ if (status == ChunkStatus.EMPTY) { ++ // does not exist on disk ++ onComplete.accept(null); ++ return; ++ } ++ ++ if (status == ChunkStatus.FULL) { ++ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); ++ return; ++ } else if (status != null) { ++ onComplete.accept(null); ++ return; // not full status on disk ++ } ++ // status is null here ++ // Paper end ++ ++ // at this stage we don't know what status the chunk is in ++ } ++ ++ // here we don't know what status it is and we're not supposed to generate ++ // so we asynchronously load empty status ++ ++ this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.EMPTY, (IChunkAccess chunk) -> { ++ if (!(chunk instanceof ProtoChunkExtension) && !(chunk instanceof net.minecraft.server.Chunk)) { ++ // the chunk on disk was not a full status chunk ++ onComplete.accept(null); ++ return; ++ } ++ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); // bring to full status if required ++ }); ++ } ++ ++ private void bringToFullStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, java.util.function.Consumer onComplete) { ++ this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.FULL, (java.util.function.Consumer)onComplete); ++ } ++ ++ private void bringToStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, ChunkStatus status, java.util.function.Consumer onComplete) { ++ CompletableFuture> future = this.getChunkFutureMainThread(x, z, status, true); ++ long identifier = this.asyncLoadSeqCounter++; ++ int ticketLevel = MCUtil.getTicketLevelFor(status); ++ this.addTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier); ++ ++ future.whenCompleteAsync((Either either, Throwable throwable) -> { ++ // either left -> success ++ // either right -> failure ++ ++ if (throwable != null) { ++ throw new RuntimeException(throwable); ++ } ++ ++ this.removeTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier); ++ this.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); // allow unloading ++ ++ Optional failure = either.right(); ++ ++ if (failure.isPresent()) { ++ // failure ++ throw new IllegalStateException("Chunk failed to load: " + failure.get().toString()); ++ } ++ ++ onComplete.accept(either.left().get()); ++ ++ }, this.serverThreadQueue); ++ } ++ ++ public void addTicketAtLevel(TicketType ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) { ++ this.chunkMapDistance.addTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier); ++ } ++ ++ public void removeTicketAtLevel(TicketType ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) { ++ this.chunkMapDistance.removeTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier); ++ } + // Paper end + @Nullable @Override public IChunkAccess getChunkAt(int i, int j, ChunkStatus chunkstatus, boolean flag) { @@ -1237,17 +1740,224 @@ index b46285ecdc..f6a6421140 100644 if (Thread.currentThread() != this.serverThread) { return (IChunkAccess) CompletableFuture.supplyAsync(() -> { return this.getChunkAt(i, j, chunkstatus, flag); -@@ -307,6 +308,10 @@ public class ChunkProviderServer extends IChunkProvider { +@@ -173,6 +305,9 @@ public class ChunkProviderServer extends IChunkProvider { CompletableFuture> completablefuture = this.getChunkFutureMainThread(i, j, chunkstatus, flag); if (!completablefuture.isDone()) { // Paper -+ // Paper start - async chunk io -+ com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.bumpPriority(this.world, x, z, -+ com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY); ++ // Paper start - async chunk io // Paper start - async chunk loading ++ this.world.asyncLoadManager.raisePriority(x, z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY); + // Paper end this.world.timings.chunkAwait.startTiming(); // Paper this.serverThreadQueue.awaitTasks(completablefuture::isDone); this.world.timings.chunkAwait.stopTiming(); // Paper +diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java +index f88e3d957f..bee1330763 100644 +--- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java ++++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java +@@ -6,6 +6,7 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet; + import it.unimi.dsi.fastutil.longs.LongSet; + import it.unimi.dsi.fastutil.shorts.ShortList; + import it.unimi.dsi.fastutil.shorts.ShortListIterator; ++import java.util.ArrayDeque; // Paper + import java.util.Arrays; + import java.util.BitSet; + import java.util.EnumSet; +@@ -22,7 +23,29 @@ public class ChunkRegionLoader { + + private static final Logger LOGGER = LogManager.getLogger(); + ++ // Paper start ++ public static final class InProgressChunkHolder { ++ ++ public final ProtoChunk protoChunk; ++ public final ArrayDeque tasks; ++ ++ public NBTTagCompound poiData; ++ ++ public InProgressChunkHolder(final ProtoChunk protoChunk, final ArrayDeque tasks) { ++ this.protoChunk = protoChunk; ++ this.tasks = tasks; ++ } ++ } ++ + public static ProtoChunk loadChunk(WorldServer worldserver, DefinedStructureManager definedstructuremanager, VillagePlace villageplace, ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) { ++ InProgressChunkHolder holder = loadChunk(worldserver, definedstructuremanager, villageplace, chunkcoordintpair, nbttagcompound, true); ++ holder.tasks.forEach(Runnable::run); ++ return holder.protoChunk; ++ } ++ ++ public static InProgressChunkHolder loadChunk(WorldServer worldserver, DefinedStructureManager definedstructuremanager, VillagePlace villageplace, ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound, boolean distinguish) { ++ ArrayDeque tasksToExecuteOnMain = new ArrayDeque<>(); ++ // Paper end + ChunkGenerator chunkgenerator = worldserver.getChunkProvider().getChunkGenerator(); + WorldChunkManager worldchunkmanager = chunkgenerator.getWorldChunkManager(); + NBTTagCompound nbttagcompound1 = nbttagcompound.getCompound("Level"); +@@ -66,7 +89,9 @@ public class ChunkRegionLoader { + LightEngine lightengine = chunkproviderserver.getLightEngine(); + + if (flag) { +- lightengine.b(chunkcoordintpair, true); ++ tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main ++ lightengine.b(chunkcoordintpair, true); ++ }); // Paper - delay this task since we're executing off-main + } + + for (int k = 0; k < nbttaglist.size(); ++k) { +@@ -82,16 +107,30 @@ public class ChunkRegionLoader { + achunksection[b0] = chunksection; + } + +- villageplace.a(chunkcoordintpair, chunksection); ++ tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main ++ villageplace.a(chunkcoordintpair, chunksection); ++ }); // Paper - delay this task since we're executing off-main + } + + if (flag) { + if (nbttagcompound2.hasKeyOfType("BlockLight", 7)) { +- lightengine.a(EnumSkyBlock.BLOCK, SectionPosition.a(chunkcoordintpair, b0), new NibbleArray(nbttagcompound2.getByteArray("BlockLight"))); ++ // Paper start - delay this task since we're executing off-main ++ NibbleArray blockLight = new NibbleArray(nbttagcompound2.getByteArray("BlockLight")); ++ // Note: We move the block light nibble array creation here for perf & in case the compound is modified ++ tasksToExecuteOnMain.add(() -> { ++ lightengine.a(EnumSkyBlock.BLOCK, SectionPosition.a(chunkcoordintpair, b0), blockLight); ++ }); ++ // Paper end + } + + if (flag2 && nbttagcompound2.hasKeyOfType("SkyLight", 7)) { +- lightengine.a(EnumSkyBlock.SKY, SectionPosition.a(chunkcoordintpair, b0), new NibbleArray(nbttagcompound2.getByteArray("SkyLight"))); ++ // Paper start - delay this task since we're executing off-main ++ NibbleArray skyLight = new NibbleArray(nbttagcompound2.getByteArray("SkyLight")); ++ // Note: We move the block light nibble array creation here for perf & in case the compound is modified ++ tasksToExecuteOnMain.add(() -> { ++ lightengine.a(EnumSkyBlock.SKY, SectionPosition.a(chunkcoordintpair, b0), skyLight); ++ }); ++ // Paper end + } + } + } +@@ -194,7 +233,7 @@ public class ChunkRegionLoader { + } + + if (chunkstatus_type == ChunkStatus.Type.LEVELCHUNK) { +- return new ProtoChunkExtension((Chunk) object); ++ return new InProgressChunkHolder(new ProtoChunkExtension((Chunk) object), tasksToExecuteOnMain); // Paper - Async chunk loading + } else { + ProtoChunk protochunk1 = (ProtoChunk) object; + +@@ -233,7 +272,7 @@ public class ChunkRegionLoader { + protochunk1.a(worldgenstage_features, BitSet.valueOf(nbttagcompound5.getByteArray(s1))); + } + +- return protochunk1; ++ return new InProgressChunkHolder(protochunk1, tasksToExecuteOnMain); // Paper - Async chunk loading + } + } + +diff --git a/src/main/java/net/minecraft/server/ChunkStatus.java b/src/main/java/net/minecraft/server/ChunkStatus.java +index e324989b46..abb0d69d2f 100644 +--- a/src/main/java/net/minecraft/server/ChunkStatus.java ++++ b/src/main/java/net/minecraft/server/ChunkStatus.java +@@ -153,6 +153,7 @@ public class ChunkStatus { + return ChunkStatus.q.size(); + } + ++ public static int getTicketLevelOffset(ChunkStatus status) { return ChunkStatus.a(status); } // Paper - OBFHELPER + public static int a(ChunkStatus chunkstatus) { + return ChunkStatus.r.getInt(chunkstatus.c()); + } +diff --git a/src/main/java/net/minecraft/server/IChunkLoader.java b/src/main/java/net/minecraft/server/IChunkLoader.java +index 3f14392e6e..cc933ec067 100644 +--- a/src/main/java/net/minecraft/server/IChunkLoader.java ++++ b/src/main/java/net/minecraft/server/IChunkLoader.java +@@ -3,6 +3,10 @@ package net.minecraft.server; + import com.mojang.datafixers.DataFixer; + import java.io.File; + import java.io.IOException; ++// Paper start ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.CompletionException; ++// Paper end + import java.util.function.Supplier; + import javax.annotation.Nullable; + +@@ -10,7 +14,9 @@ public class IChunkLoader extends RegionFileCache { + + protected final DataFixer b; + @Nullable +- private PersistentStructureLegacy a; ++ private volatile PersistentStructureLegacy a; // Paper - async chunk loading ++ ++ private final Object persistentDataLock = new Object(); // Paper + + public IChunkLoader(File file, DataFixer datafixer) { + super(file); +@@ -55,9 +61,26 @@ public class IChunkLoader extends RegionFileCache { + NBTTagCompound level = nbttagcompound.getCompound("Level"); + if (level.getBoolean("TerrainPopulated") && !level.getBoolean("LightPopulated")) { + ChunkProviderServer cps = (generatoraccess == null) ? null : ((WorldServer) generatoraccess).getChunkProvider(); ++ // Paper start - Async chunk loading ++ CompletableFuture future = new CompletableFuture<>(); ++ MCUtil.ensureMain((Runnable)() -> { ++ try { ++ // Paper end + if (check(cps, pos.x - 1, pos.z) && check(cps, pos.x - 1, pos.z - 1) && check(cps, pos.x, pos.z - 1)) { + level.setBoolean("LightPopulated", true); + } ++ // Paper start - Async chunk loading ++ future.complete(null); ++ } catch (IOException ex) { ++ future.completeExceptionally(ex); ++ } ++ }); ++ try { ++ future.join(); ++ } catch (CompletionException ex) { ++ com.destroystokyo.paper.util.SneakyThrow.sneaky(ex.getCause()); ++ } ++ // Paper end + } + } + // CraftBukkit end +@@ -65,11 +88,13 @@ public class IChunkLoader extends RegionFileCache { + if (i < 1493) { + nbttagcompound = GameProfileSerializer.a(this.b, DataFixTypes.CHUNK, nbttagcompound, i, 1493); + if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) { ++ synchronized (this.persistentDataLock) { // Paper - Async chunk loading + if (this.a == null) { + this.a = PersistentStructureLegacy.a(dimensionmanager.getType(), (WorldPersistentData) supplier.get()); // CraftBukkit - getType + } + + nbttagcompound = this.a.a(nbttagcompound); ++ } // Paper - Async chunk loading + } + } + +@@ -89,7 +114,9 @@ public class IChunkLoader extends RegionFileCache { + public void write(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws IOException { + super.write(chunkcoordintpair, nbttagcompound); + if (this.a != null) { ++ synchronized (this.persistentDataLock) { // Paper - Async chunk loading + this.a.a(chunkcoordintpair.pair()); ++ } // Paper - Async chunk loading + } + + } +diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java +index 23d1935dd5..14f8b61042 100644 +--- a/src/main/java/net/minecraft/server/MCUtil.java ++++ b/src/main/java/net/minecraft/server/MCUtil.java +@@ -530,4 +530,9 @@ public final class MCUtil { + out.print(fileData); + } + } ++ ++ public static int getTicketLevelFor(ChunkStatus status) { ++ // TODO make sure the constant `33` is correct on future updates. See getChunkAt(int, int, ChunkStatus, boolean) ++ return 33 + ChunkStatus.getTicketLevelOffset(status); ++ } + } diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 0324a90ca5..430cd70cf5 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1261,7 +1971,7 @@ index 0324a90ca5..430cd70cf5 100644 public String getServerIp() { diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java -index a439277813..6ca98b7ad5 100644 +index a439277813..f25ca782b9 100644 --- a/src/main/java/net/minecraft/server/PlayerChunkMap.java +++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java @@ -57,7 +57,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { @@ -1269,10 +1979,19 @@ index a439277813..6ca98b7ad5 100644 private final IAsyncTaskHandler executor; public final ChunkGenerator chunkGenerator; - private final Supplier m; -+ private final Supplier m; private final Supplier getWorldPersistentDataSupplier() { return this.m; } // Paper - OBFHELPER ++ private final Supplier m; public final Supplier getWorldPersistentDataSupplier() { return this.m; } // Paper - OBFHELPER private final VillagePlace n; public final LongSet unloadQueue; private boolean updatingChunksModified; +@@ -67,7 +67,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + public final WorldLoadListener worldLoadListener; + public final PlayerChunkMap.a u; public final PlayerChunkMap.a getChunkMapDistanceManager() { return this.u; } // Paper - OBFHELPER // CraftBukkit - private -> public // PAIL chunkDistanceManager + private final AtomicInteger v; +- private final DefinedStructureManager definedStructureManager; ++ public final DefinedStructureManager definedStructureManager; // Paper - private -> public + private final File x; + private final PlayerMap playerMap; + public final Int2ObjectMap trackedEntities; @@ -101,7 +101,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { this.lightEngine = new LightEngineThreaded(ilightaccess, this, this.world.getWorldProvider().g(), threadedmailbox2, this.q.a(threadedmailbox2, false)); this.u = new PlayerChunkMap.a(executor, iasynctaskhandler); @@ -1286,7 +2005,7 @@ index a439277813..6ca98b7ad5 100644 @Override public void close() throws IOException { this.q.close(); -+ com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.flush(); // Paper - Required since we're closing regionfiles in the next line ++ this.world.asyncLoadManager.shutdown(true); // Paper - Required since we're closing regionfiles in the next line this.n.close(); super.close(); } @@ -1299,7 +2018,23 @@ index a439277813..6ca98b7ad5 100644 ++savedThisTick; playerchunk.m(); } -@@ -386,7 +387,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { +@@ -328,11 +329,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + protected void unloadChunks(BooleanSupplier booleansupplier) { + GameProfilerFiller gameprofilerfiller = this.world.getMethodProfiler(); + ++ try (Timing ignored = this.world.timings.poiUnload.startTiming()) { // Paper + gameprofilerfiller.enter("poi"); + this.n.a(booleansupplier); ++ } + gameprofilerfiller.exitEnter("chunk_unload"); + if (!this.world.isSavingDisabled()) { ++ try (Timing ignored = this.world.timings.chunkUnload.startTiming()) { // Paper + this.b(booleansupplier); ++ } + } + + gameprofilerfiller.exit(); +@@ -386,7 +391,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { ((Chunk) ichunkaccess).setLoaded(false); } @@ -1308,7 +2043,7 @@ index a439277813..6ca98b7ad5 100644 if (this.loadedChunks.remove(i) && ichunkaccess instanceof Chunk) { Chunk chunk = (Chunk) ichunkaccess; -@@ -462,13 +463,22 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { +@@ -462,26 +467,30 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } } @@ -1321,7 +2056,7 @@ index a439277813..6ca98b7ad5 100644 private CompletableFuture> f(ChunkCoordIntPair chunkcoordintpair) { - return CompletableFuture.supplyAsync(() -> { + // Paper start - Async chunk io -+ final java.util.function.BiFunction> callable = (chunkData, ioThrowable) -> { ++ final java.util.function.BiFunction> callable = (chunkHolder, ioThrowable) -> { try (Timing ignored = this.world.timings.syncChunkLoadTimer.startTimingIfSync()) { // Paper - NBTTagCompound nbttagcompound; // Paper - try (Timing ignored2 = this.world.timings.chunkIOStage1.startTimingIfSync()) { // Paper @@ -1329,13 +2064,32 @@ index a439277813..6ca98b7ad5 100644 + if (ioThrowable != null) { + com.destroystokyo.paper.io.IOUtil.rethrow(ioThrowable); } -+ this.n.loadInData(chunkcoordintpair, chunkData.poiData); -+ NBTTagCompound nbttagcompound = this.completeChunkData(chunkData.chunkData, chunkcoordintpair); ++ this.getVillagePlace().loadInData(chunkcoordintpair, chunkHolder.poiData); ++ chunkHolder.tasks.forEach(Runnable::run); ++ // Paper - async load completes this + // Paper end - if (nbttagcompound != null) { - boolean flag = nbttagcompound.hasKeyOfType("Level", 10) && nbttagcompound.getCompound("Level").hasKeyOfType("Status", 8); -@@ -495,7 +505,24 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { +- if (nbttagcompound != null) { +- boolean flag = nbttagcompound.hasKeyOfType("Level", 10) && nbttagcompound.getCompound("Level").hasKeyOfType("Status", 8); +- +- if (flag) { +- ProtoChunk protochunk = ChunkRegionLoader.loadChunk(this.world, this.definedStructureManager, this.n, chunkcoordintpair, nbttagcompound); +- +- protochunk.setLastSaved(this.world.getTime()); +- return Either.left(protochunk); +- } +- +- PlayerChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", chunkcoordintpair); ++ // Paper start - This is done async ++ if (chunkHolder.protoChunk != null) { ++ chunkHolder.protoChunk.setLastSaved(this.world.getTime()); ++ return Either.left(chunkHolder.protoChunk); + } ++ // Paper end + } catch (ReportedException reportedexception) { + Throwable throwable = reportedexception.getCause(); + +@@ -495,7 +504,17 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } return Either.left(new ProtoChunk(chunkcoordintpair, ChunkConverter.a, this.world)); // Paper - Anti-Xray @@ -1343,36 +2097,30 @@ index a439277813..6ca98b7ad5 100644 + // Paper start - Async chunk io + }; + CompletableFuture> ret = new CompletableFuture<>(); -+ com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.loadChunkDataAsync(this.world, chunkcoordintpair.x, chunkcoordintpair.z, -+ com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, (chunkData) -> { -+ PlayerChunkMap.this.executor.execute(() -> { -+ Throwable throwable = null; -+ -+ if (chunkData.chunkData == com.destroystokyo.paper.io.ConcreteFileIOThread.FAILURE_VALUE || -+ chunkData.poiData == com.destroystokyo.paper.io.ConcreteFileIOThread.FAILURE_VALUE) { -+ throwable = new Throwable("See log from the file io thread above"); -+ } -+ -+ ret.complete(callable.apply(throwable != null ? null : chunkData, throwable)); ++ this.world.asyncLoadManager.scheduleChunkLoad(chunkcoordintpair.x, chunkcoordintpair.z, ++ com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, (ChunkRegionLoader.InProgressChunkHolder holder) -> { ++ PlayerChunkMap.this.executor.addTask(() -> { ++ ret.complete(callable.apply(holder, null)); + }); -+ }, true, true, false); ++ }, false); + return ret; + // Paper end } private CompletableFuture> b(PlayerChunk playerchunk, ChunkStatus chunkstatus) { -@@ -701,18 +728,40 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { +@@ -701,18 +720,42 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { return this.v.get(); } + // Paper start - async chunk io + private boolean writeDataAsync(ChunkCoordIntPair chunkPos, NBTTagCompound poiData, NBTTagCompound chunkData, boolean async) { + com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.scheduleSave(this.world, chunkPos.x, chunkPos.z, -+ poiData, chunkData, !async ? com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY : com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY); ++ poiData, chunkData, !async ? com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY : com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY); + + if (async) { + return true; + } ++ try (co.aikar.timings.Timing ignored = this.world.timings.chunkSaveIOWait.startTiming()) { // Paper + Boolean successPoi = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world, chunkPos.x, chunkPos.z, true, true); + Boolean successChunk = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world, chunkPos.x, chunkPos.z, true, false); + @@ -1383,6 +2131,7 @@ index a439277813..6ca98b7ad5 100644 + // null indicates no task existed, which means our write completed before we waited on it + + return true; ++ } // Paper + } + // Paper end + @@ -1391,8 +2140,8 @@ index a439277813..6ca98b7ad5 100644 + // Paper start - async param + return this.saveChunk(ichunkaccess, false); + } -+ + public boolean saveChunk(IChunkAccess ichunkaccess, boolean async) { ++ try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { + NBTTagCompound poiData = this.getVillagePlace().getData(ichunkaccess.getPos()); // Paper + //this.n.a(ichunkaccess.getPos()); // Delay + // Paper end @@ -1410,12 +2159,13 @@ index a439277813..6ca98b7ad5 100644 ichunkaccess.setLastSaved(this.world.getTime()); ichunkaccess.setNeedsSaving(false); -@@ -724,20 +773,22 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { +@@ -723,27 +766,33 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + NBTTagCompound nbttagcompound; if (chunkstatus.getType() != ChunkStatus.Type.LEVELCHUNK) { ++ try (co.aikar.timings.Timing ignored1 = this.world.timings.chunkSaveOverwriteCheck.startTiming()) { // Paper // Paper start - Optimize save by using status cache -- ChunkStatus statusOnDisk = this.getChunkStatusOnDisk(chunkcoordintpair); -+ ChunkStatus statusOnDisk = this.getChunkStatus(chunkcoordintpair, true); // Paper - Async chunk io + ChunkStatus statusOnDisk = this.getChunkStatusOnDisk(chunkcoordintpair); if (statusOnDisk != null && statusOnDisk.getType() == ChunkStatus.Type.LEVELCHUNK) { // Paper end + this.writeDataAsync(ichunkaccess.getPos(), poiData, null, async); // Paper - Async chunk io @@ -1427,16 +2177,26 @@ index a439277813..6ca98b7ad5 100644 return false; } } - +- ++ } // Paper ++ try (co.aikar.timings.Timing ignored1 = this.world.timings.chunkSaveDataSerialization.startTiming()) { // Paper nbttagcompound = ChunkRegionLoader.saveChunk(this.world, ichunkaccess); - this.write(chunkcoordintpair, nbttagcompound); - return true; ++ } // Paper + return this.writeDataAsync(ichunkaccess.getPos(), poiData, nbttagcompound, async); // Paper - Async chunk io + //return true; // Paper } catch (Exception exception) { PlayerChunkMap.LOGGER.error("Failed to save chunk {},{}", chunkcoordintpair.x, chunkcoordintpair.z, exception); com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper -@@ -808,6 +859,62 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + return false; + } + } ++ } // Paper + } + + protected void setViewDistance(int i) { +@@ -808,6 +857,42 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { return Iterables.unmodifiableIterable(this.visibleChunks.values()); } @@ -1446,8 +2206,8 @@ index a439277813..6ca98b7ad5 100644 + public NBTTagCompound read(ChunkCoordIntPair chunkcoordintpair) throws IOException { + if (Thread.currentThread() != com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE) { + NBTTagCompound ret = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE -+ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(), -+ false, true, true).join().chunkData; ++ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(), ++ false, true, true).join().chunkData; + + if (ret == com.destroystokyo.paper.io.ConcreteFileIOThread.FAILURE_VALUE) { + throw new IOException("See logs for further detail"); @@ -1461,11 +2221,11 @@ index a439277813..6ca98b7ad5 100644 + public void write(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws IOException { + if (Thread.currentThread() != com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE) { + com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.scheduleSave( -+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, null, nbttagcompound, -+ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread()); ++ this.world, chunkcoordintpair.x, chunkcoordintpair.z, null, nbttagcompound, ++ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread()); + + Boolean ret = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world, -+ chunkcoordintpair.x, chunkcoordintpair.z, true, false); ++ chunkcoordintpair.x, chunkcoordintpair.z, true, false); + + if (ret == Boolean.FALSE) { + throw new IOException("See logs for further detail"); @@ -1474,43 +2234,95 @@ index a439277813..6ca98b7ad5 100644 + } + super.write(chunkcoordintpair, nbttagcompound); + } -+ -+ public ChunkStatus getChunkStatus(ChunkCoordIntPair chunkPos, boolean load) throws IOException { -+ synchronized (this) { -+ // we enter a synchronized block here so that we do not potentially use a closed regionfile -+ RegionFile regionFile = this.getRegionFile(chunkPos, false); -+ ChunkStatus status = regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); -+ -+ if (!load || status != null || !regionFile.chunkExists(chunkPos)) { -+ return status; -+ } -+ -+ // at this stage we need to load chunk data, however it's best we do that outside of the synchronized block -+ } -+ -+ NBTTagCompound compound = this.readChunkData(chunkPos); -+ -+ // In order to avoid a race condition where a regionfile is re-loaded concurrently we directly use the status in -+ // the returned compound. readChunkData will update the regionfile -+ return compound == null ? null : ChunkRegionLoader.getStatus(compound); -+ } + // Paper end + @Nullable public NBTTagCompound readChunkData(ChunkCoordIntPair chunkcoordintpair) throws IOException { // Paper - private -> public NBTTagCompound nbttagcompound = this.read(chunkcoordintpair); -@@ -822,7 +929,9 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { - return null; - } +@@ -830,12 +915,30 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { -- this.updateChunkStatusOnDisk(chunkcoordintpair, nbttagcompound); -+ synchronized (this) { // Async chunk io - Synchronize so we do not potentially get and use a closed region file -+ this.getRegionFile(chunkcoordintpair, false).setStatus(chunkcoordintpair.x, chunkcoordintpair.z, ChunkRegionLoader.getStatus(nbttagcompound)); + // Paper start - chunk status cache "api" + public ChunkStatus getChunkStatusOnDiskIfCached(ChunkCoordIntPair chunkPos) { ++ // Paper start - async io ++ NBTTagCompound inProgressWrite = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE ++ .getPendingWrite(this.world, chunkPos.x, chunkPos.z, false); ++ ++ if (inProgressWrite != null) { ++ return ChunkRegionLoader.getStatus(inProgressWrite); ++ } ++ // Paper end ++ + RegionFile regionFile = this.getRegionFileIfLoaded(chunkPos); + + return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); + } + + public ChunkStatus getChunkStatusOnDisk(ChunkCoordIntPair chunkPos) throws IOException { ++ // Paper start - async io ++ NBTTagCompound inProgressWrite = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE ++ .getPendingWrite(this.world, chunkPos.x, chunkPos.z, false); ++ ++ if (inProgressWrite != null) { ++ return ChunkRegionLoader.getStatus(inProgressWrite); ++ } ++ // Paper end ++ synchronized (this) { // Paper - async io + RegionFile regionFile = this.getRegionFile(chunkPos, false); + + if (!regionFile.chunkExists(chunkPos)) { +@@ -847,17 +950,49 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { + if (status != null) { + return status; + } ++ // Paper start - async io + } - return nbttagcompound; - // Paper end -@@ -1197,6 +1306,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { +- this.readChunkData(chunkPos); ++ NBTTagCompound compound = this.readChunkData(chunkPos); + +- return regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); ++ return ChunkRegionLoader.getStatus(compound); ++ // Paper end + } + + public void updateChunkStatusOnDisk(ChunkCoordIntPair chunkPos, @Nullable NBTTagCompound compound) throws IOException { ++ synchronized (this) { // Paper - async io + RegionFile regionFile = this.getRegionFile(chunkPos, false); + + regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkRegionLoader.getStatus(compound)); ++ } // Paper - async io + } ++ ++ // Paper start - async io ++ // this function will not load chunk data off disk to check for status ++ // ret null for unknown, empty for empty status on disk or absent from disk ++ public ChunkStatus getStatusOnDiskNoLoad(int x, int z) { ++ // Paper start - async io ++ net.minecraft.server.NBTTagCompound inProgressWrite = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE ++ .getPendingWrite(this.world, x, z, false); ++ ++ if (inProgressWrite != null) { ++ return net.minecraft.server.ChunkRegionLoader.getStatus(inProgressWrite); ++ } ++ // Paper end ++ // variant of PlayerChunkMap#getChunkStatusOnDisk that does not load data off disk, but loads the region file ++ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z); ++ synchronized (world.getChunkProvider().playerChunkMap) { ++ net.minecraft.server.RegionFile file; ++ try { ++ file = world.getChunkProvider().playerChunkMap.getRegionFile(chunkPos, false); ++ } catch (IOException ex) { ++ throw new RuntimeException(ex); ++ } ++ ++ return !file.chunkExists(chunkPos) ? ChunkStatus.EMPTY : file.getStatusIfCached(x, z); ++ } ++ } ++ // Paper end + // Paper end + + // Spigot Start +@@ -1197,6 +1332,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } @@ -1519,21 +2331,10 @@ index a439277813..6ca98b7ad5 100644 return this.n; } diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java -index 3e80f6c53e..2f10152404 100644 +index 66c8b0307f..2ee4b88f09 100644 --- a/src/main/java/net/minecraft/server/RegionFile.java +++ b/src/main/java/net/minecraft/server/RegionFile.java -@@ -53,6 +53,10 @@ public class RegionFile implements AutoCloseable { - final int location = this.getChunkLocation(new ChunkCoordIntPair(x, z)); - return this.statuses[location]; - } -+ -+ public ChunkStatus getStatus(int x, int z, PlayerChunkMap playerChunkMap) throws IOException { -+ return playerChunkMap.getChunkStatus(new ChunkCoordIntPair(x, z), true); -+ } - // Paper end - - public RegionFile(File file) throws IOException { -@@ -337,7 +341,7 @@ public class RegionFile implements AutoCloseable { +@@ -337,7 +337,7 @@ public class RegionFile implements AutoCloseable { this.writeInt(i); // Paper - Avoid 3 io write calls } @@ -1572,7 +2373,7 @@ index e0fdf5f90f..d9283b36b6 100644 RegionFile regionfile = a(pos, true); diff --git a/src/main/java/net/minecraft/server/RegionFileSection.java b/src/main/java/net/minecraft/server/RegionFileSection.java -index a343a7b31d..570b01a6cb 100644 +index a343a7b31d..7584174eb7 100644 --- a/src/main/java/net/minecraft/server/RegionFileSection.java +++ b/src/main/java/net/minecraft/server/RegionFileSection.java @@ -24,7 +24,7 @@ public class RegionFileSection extends RegionFi @@ -1603,11 +2404,11 @@ index a343a7b31d..570b01a6cb 100644 + return optional.get(); // Paper - decompile fix } else { - R r0 = (MinecraftSerializable) this.f.apply(() -> { -+ R r0 = this.f.apply(() -> { // Paper - decompile fix ++ R r0 = this.f.apply(() -> { // Paper - decompile fix this.a(i); }); -@@ -94,7 +94,13 @@ public class RegionFileSection extends RegionFi +@@ -94,7 +94,12 @@ public class RegionFileSection extends RegionFi } private void b(ChunkCoordIntPair chunkcoordintpair) { @@ -1615,14 +2416,13 @@ index a343a7b31d..570b01a6cb 100644 + // Paper start - load data in function + this.loadInData(chunkcoordintpair, this.c(chunkcoordintpair)); + } -+ + public void loadInData(ChunkCoordIntPair chunkPos, NBTTagCompound compound) { + this.a(chunkPos, DynamicOpsNBT.a, compound); + // Paper end } @Nullable -@@ -123,7 +129,7 @@ public class RegionFileSection extends RegionFi +@@ -123,7 +128,7 @@ public class RegionFileSection extends RegionFi for (int l = 0; l < 16; ++l) { long i1 = SectionPosition.a(chunkcoordintpair, l).v(); Optional optional = optionaldynamic.get(Integer.toString(l)).get().map((dynamic2) -> { @@ -1631,7 +2431,7 @@ index a343a7b31d..570b01a6cb 100644 this.a(i1); }, dynamic2); }); -@@ -142,7 +148,7 @@ public class RegionFileSection extends RegionFi +@@ -142,7 +147,7 @@ public class RegionFileSection extends RegionFi } private void d(ChunkCoordIntPair chunkcoordintpair) { @@ -1640,7 +2440,7 @@ index a343a7b31d..570b01a6cb 100644 NBTBase nbtbase = (NBTBase) dynamic.getValue(); if (nbtbase instanceof NBTTagCompound) { -@@ -157,6 +163,20 @@ public class RegionFileSection extends RegionFi +@@ -157,6 +162,20 @@ public class RegionFileSection extends RegionFi } @@ -1661,7 +2461,7 @@ index a343a7b31d..570b01a6cb 100644 private Dynamic a(ChunkCoordIntPair chunkcoordintpair, DynamicOps dynamicops) { Map map = Maps.newHashMap(); -@@ -193,9 +213,9 @@ public class RegionFileSection extends RegionFi +@@ -193,9 +212,9 @@ public class RegionFileSection extends RegionFi public void a(ChunkCoordIntPair chunkcoordintpair) { if (!this.d.isEmpty()) { for (int i = 0; i < 16; ++i) { @@ -1673,7 +2473,7 @@ index a343a7b31d..570b01a6cb 100644 this.d(chunkcoordintpair); return; } -@@ -203,4 +223,21 @@ public class RegionFileSection extends RegionFi +@@ -203,4 +222,21 @@ public class RegionFileSection extends RegionFi } } @@ -1695,11 +2495,23 @@ index a343a7b31d..570b01a6cb 100644 + } + // Paper end } +diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java +index 5acb0732c3..0ed2d2fbf9 100644 +--- a/src/main/java/net/minecraft/server/TicketType.java ++++ b/src/main/java/net/minecraft/server/TicketType.java +@@ -22,6 +22,7 @@ public class TicketType { + public static final TicketType PLUGIN = a("plugin", (a, b) -> 0); // CraftBukkit + public static final TicketType PLUGIN_TICKET = a("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // Craftbukkit + public static final TicketType ANTIXRAY = a("antixray", Integer::compareTo); // Paper - Anti-Xray ++ public static final TicketType ASYNC_LOAD = a("async_load", Long::compareTo); // Paper + + public static TicketType a(String s, Comparator comparator) { + return new TicketType<>(s, comparator, 0L); diff --git a/src/main/java/net/minecraft/server/VillagePlace.java b/src/main/java/net/minecraft/server/VillagePlace.java -index 7bc473e1ef..9f4b1b4c49 100644 +index 7bc473e1ef..8e82326c47 100644 --- a/src/main/java/net/minecraft/server/VillagePlace.java +++ b/src/main/java/net/minecraft/server/VillagePlace.java -@@ -20,8 +20,17 @@ public class VillagePlace extends RegionFileSection { +@@ -20,8 +20,16 @@ public class VillagePlace extends RegionFileSection { private final VillagePlace.a a = new VillagePlace.a(); @@ -1709,7 +2521,6 @@ index 7bc473e1ef..9f4b1b4c49 100644 + // Paper start + this(file, datafixer, null); + } -+ + public VillagePlace(File file, DataFixer datafixer, WorldServer world) { + // Paper end super(file, VillagePlaceSection::new, VillagePlaceSection::new, datafixer, DataFixTypes.POI_CHUNK); @@ -1717,7 +2528,7 @@ index 7bc473e1ef..9f4b1b4c49 100644 } public void a(BlockPosition blockposition, VillagePlaceType villageplacetype) { -@@ -128,7 +137,20 @@ public class VillagePlace extends RegionFileSection { +@@ -128,7 +136,23 @@ public class VillagePlace extends RegionFileSection { @Override public void a(BooleanSupplier booleansupplier) { @@ -1730,25 +2541,28 @@ index 7bc473e1ef..9f4b1b4c49 100644 + while (!((RegionFileSection)this).d.isEmpty() && booleansupplier.getAsBoolean()) { + ChunkCoordIntPair chunkcoordintpair = SectionPosition.a(((RegionFileSection)this).d.firstLong()).u(); + -+ NBTTagCompound data = this.getData(chunkcoordintpair); ++ NBTTagCompound data; ++ try (co.aikar.timings.Timing ignored1 = this.world.timings.poiSaveDataSerialization.startTiming()) { ++ data = this.getData(chunkcoordintpair); ++ } + com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.scheduleSave(this.world, -+ chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY); ++ chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY); + } + } + // Paper end this.a.a(); } -@@ -164,7 +186,7 @@ public class VillagePlace extends RegionFileSection { +@@ -164,7 +188,7 @@ public class VillagePlace extends RegionFileSection { } private static boolean a(ChunkSection chunksection) { - Stream stream = VillagePlaceType.e(); -+ Stream stream = VillagePlaceType.e(); // Paper - decompile fix ++ Stream stream = VillagePlaceType.e(); // Paper - decompile fix chunksection.getClass(); return stream.anyMatch(chunksection::a); -@@ -214,6 +236,42 @@ public class VillagePlace extends RegionFileSection { +@@ -214,6 +238,42 @@ public class VillagePlace extends RegionFileSection { } } @@ -1758,8 +2572,8 @@ index 7bc473e1ef..9f4b1b4c49 100644 + public NBTTagCompound read(ChunkCoordIntPair chunkcoordintpair) throws java.io.IOException { + if (this.world != null && Thread.currentThread() != com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE) { + NBTTagCompound ret = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE -+ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(), -+ true, false, true).join().poiData; ++ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(), ++ true, false, true).join().poiData; + + if (ret == com.destroystokyo.paper.io.ConcreteFileIOThread.FAILURE_VALUE) { + throw new java.io.IOException("See logs for further detail"); @@ -1773,11 +2587,11 @@ index 7bc473e1ef..9f4b1b4c49 100644 + public void write(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws java.io.IOException { + if (this.world != null && Thread.currentThread() != com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE) { + com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.scheduleSave( -+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, null, -+ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread()); ++ this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, null, ++ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread()); + + Boolean ret = com.destroystokyo.paper.io.ConcreteFileIOThread.Holder.INSTANCE.waitForIOToComplete(this.world, -+ chunkcoordintpair.x, chunkcoordintpair.z, true, true); ++ chunkcoordintpair.x, chunkcoordintpair.z, true, true); + + if (ret == Boolean.FALSE) { + throw new java.io.IOException("See logs for further detail"); @@ -1791,7 +2605,7 @@ index 7bc473e1ef..9f4b1b4c49 100644 public static enum Occupancy { HAS_SPACE(VillagePlaceRecord::d), IS_OCCUPIED(VillagePlaceRecord::e), ANY((villageplacerecord) -> { -@@ -222,7 +280,7 @@ public class VillagePlace extends RegionFileSection { +@@ -222,7 +282,7 @@ public class VillagePlace extends RegionFileSection { private final Predicate d; @@ -1801,10 +2615,10 @@ index 7bc473e1ef..9f4b1b4c49 100644 } diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java -index 47005dcfdc..f0380c5df4 100644 +index 47005dcfdc..f7597d499f 100644 --- a/src/main/java/net/minecraft/server/WorldServer.java +++ b/src/main/java/net/minecraft/server/WorldServer.java -@@ -75,6 +75,78 @@ public class WorldServer extends World { +@@ -75,6 +75,79 @@ public class WorldServer extends World { return new Throwable(entity + " Added to world at " + new java.util.Date()); } @@ -1878,11 +2692,83 @@ index 47005dcfdc..f0380c5df4 100644 + } + } + }; ++ public final com.destroystokyo.paper.io.chunk.ChunkLoadTaskManager asyncLoadManager; + // Paper end + // Add env and gen to constructor public WorldServer(MinecraftServer minecraftserver, Executor executor, WorldNBTStorage worldnbtstorage, WorldData worlddata, DimensionManager dimensionmanager, GameProfilerFiller gameprofilerfiller, WorldLoadListener worldloadlistener, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) { super(worlddata, dimensionmanager, (world, worldprovider) -> { +@@ -119,6 +192,7 @@ public class WorldServer extends World { + + this.mobSpawnerTrader = this.worldProvider.getDimensionManager().getType() == DimensionManager.OVERWORLD ? new MobSpawnerTrader(this) : null; // CraftBukkit - getType() + this.getServer().addWorld(this.getWorld()); // CraftBukkit ++ this.asyncLoadManager = new com.destroystokyo.paper.io.chunk.ChunkLoadTaskManager(this, 4); // todo CONFIGURABLE // Paper + } + + public void doTick(BooleanSupplier booleansupplier) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index c5321c5076..d3d43f3b77 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -531,22 +531,23 @@ public class CraftWorld implements World { + } + + if (!generate) { +- net.minecraft.server.RegionFile file; +- try { +- file = world.getChunkProvider().playerChunkMap.getRegionFile(chunkPos, false); +- } catch (IOException ex) { +- throw new RuntimeException(ex); +- } ++ ChunkStatus status = world.getChunkProvider().playerChunkMap.getStatusOnDiskNoLoad(x, z); // Paper - async io - move to own method + +- ChunkStatus status = file.getStatusIfCached(x, z); +- if (!file.chunkExists(chunkPos) || (status != null && status != ChunkStatus.FULL)) { ++ // Paper start - async io ++ if (status == ChunkStatus.EMPTY) { ++ // does not exist on disk + return false; + } + ++ if (status == null) { // at this stage we don't know what it is on disk + IChunkAccess chunk = world.getChunkProvider().getChunkAt(x, z, ChunkStatus.EMPTY, true); + if (!(chunk instanceof ProtoChunkExtension) && !(chunk instanceof net.minecraft.server.Chunk)) { + return false; + } ++ } else if (status != ChunkStatus.FULL) { ++ return false; // not full status on disk ++ } ++ // Paper end + + // fall through to load + // we do this so we do not re-read the chunk data on disk +@@ -2323,16 +2324,17 @@ public class CraftWorld implements World { + + @Override + public CompletableFuture getChunkAtAsync(int x, int z, boolean gen) { +- // TODO placeholder +- if (Bukkit.isPrimaryThread()) { +- return CompletableFuture.completedFuture(getChunkAtGen(x, z, gen)); +- } else { +- CompletableFuture ret = new CompletableFuture<>(); +- net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { +- ret.complete(getChunkAtGen(x, z, gen)); +- }); +- return ret; ++ net.minecraft.server.Chunk immediate = this.world.getChunkProvider().getChunkAtIfLoadedImmediately(x, z); ++ if (immediate != null) { ++ return CompletableFuture.completedFuture(immediate.bukkitChunk); + } ++ ++ CompletableFuture ret = new CompletableFuture<>(); ++ this.world.getChunkProvider().getChunkAtAsynchronously(x, z, gen, (net.minecraft.server.Chunk chunk) -> { ++ ret.complete(chunk == null ? null : chunk.bukkitChunk); ++ }); ++ ++ return ret; + } + // Paper end + -- 2.20.1 diff --git a/patches/server/0052-Asynchronous-chunk-loading-api.patch b/patches/server/0052-Asynchronous-chunk-loading-api.patch deleted file mode 100644 index c8e4b1d05..000000000 --- a/patches/server/0052-Asynchronous-chunk-loading-api.patch +++ /dev/null @@ -1,244 +0,0 @@ -From d7a653379c6a6446a42907cd03e9d1348844f893 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Mon, 8 Jul 2019 03:24:59 -0700 -Subject: [PATCH] Asynchronous chunk loading api - ---- - .../minecraft/server/ChunkProviderServer.java | 134 ++++++++++++++++++ - .../net/minecraft/server/ChunkStatus.java | 1 + - .../java/net/minecraft/server/MCUtil.java | 5 + - .../java/net/minecraft/server/RegionFile.java | 2 +- - .../java/net/minecraft/server/TicketType.java | 1 + - .../org/bukkit/craftbukkit/CraftWorld.java | 19 +-- - 6 files changed, 152 insertions(+), 10 deletions(-) - -diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java -index db9113994e..b46285ecdc 100644 ---- a/src/main/java/net/minecraft/server/ChunkProviderServer.java -+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java -@@ -147,6 +147,140 @@ public class ChunkProviderServer extends IChunkProvider { - return playerChunk.getAvailableChunkNow(); - - } -+ -+ private long asyncLoadSeqCounter; -+ -+ public void getChunkAtAsynchronously(int x, int z, boolean gen, java.util.function.Consumer onComplete) { -+ if (Thread.currentThread() != this.serverThread) { -+ this.serverThreadQueue.execute(() -> { -+ this.getChunkAtAsynchronously(x, z, gen, onComplete); -+ }); -+ return; -+ } -+ -+ long k = ChunkCoordIntPair.pair(x, z); -+ ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z); -+ -+ IChunkAccess ichunkaccess; -+ -+ // try cache -+ for (int l = 0; l < 4; ++l) { -+ if (k == this.cachePos[l] && ChunkStatus.FULL == this.cacheStatus[l]) { -+ ichunkaccess = this.cacheChunk[l]; -+ if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime -+ -+ // move to first in cache -+ -+ for (int i1 = 3; i1 > 0; --i1) { -+ this.cachePos[i1] = this.cachePos[i1 - 1]; -+ this.cacheStatus[i1] = this.cacheStatus[i1 - 1]; -+ this.cacheChunk[i1] = this.cacheChunk[i1 - 1]; -+ } -+ -+ this.cachePos[0] = k; -+ this.cacheStatus[0] = ChunkStatus.FULL; -+ this.cacheChunk[0] = ichunkaccess; -+ -+ onComplete.accept((Chunk)ichunkaccess); -+ -+ return; -+ } -+ } -+ } -+ -+ if (gen) { -+ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); -+ return; -+ } -+ -+ IChunkAccess current = this.getChunkAtImmediately(x, z); // we want to bypass ticket restrictions -+ if (current != null) { -+ if (!(current instanceof ProtoChunkExtension) && !(current instanceof net.minecraft.server.Chunk)) { -+ onComplete.accept(null); // the chunk is not gen'd -+ return; -+ } -+ // we know the chunk is at full status here (either in read-only mode or the real thing) -+ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); -+ return; -+ } else { -+ RegionFile file; -+ -+ try { -+ file = this.world.getChunkProvider().playerChunkMap.getRegionFile(chunkPos, false); -+ } catch (IOException ex) { -+ throw new RuntimeException(ex); -+ } -+ -+ ChunkStatus status; -+ if (!file.chunkExists(chunkPos) || ((status = file.getStatusIfCached(x, z)) != null && status != ChunkStatus.FULL)) { -+ onComplete.accept(null); // cached status says not generated, or data does not exist on disk -+ return; -+ } -+ -+ -+ if (status == ChunkStatus.FULL) { -+ // at this stage we know it is fully generated, but is on disk -+ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); -+ return; -+ } -+ -+ // at this stage we don't know what status the chunk is in -+ } -+ -+ // here we don't know what status it is and we're not supposed to generate -+ // so we asynchronously load empty status -+ -+ this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.EMPTY, (IChunkAccess chunk) -> { -+ if (!(chunk instanceof ProtoChunkExtension) && !(chunk instanceof net.minecraft.server.Chunk)) { -+ // the chunk on disk was not a full status chunk -+ onComplete.accept(null); -+ return; -+ } -+ this.bringToFullStatusAsync(x, z, chunkPos, onComplete); // bring to full status if required -+ }); -+ } -+ -+ private void bringToFullStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, java.util.function.Consumer onComplete) { -+ this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.FULL, (java.util.function.Consumer)onComplete); -+ } -+ -+ -+ private void bringToStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, ChunkStatus status, java.util.function.Consumer onComplete) { -+ CompletableFuture> future = this.getChunkFutureMainThread(x, z, status, true); -+ long identifier = this.asyncLoadSeqCounter++; -+ int ticketLevel = MCUtil.getTicketLevelFor(status); -+ this.addTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier); -+ -+ future.whenCompleteAsync((Either either, Throwable throwable) -> { -+ // either left -> success -+ // either right -> failure -+ -+ if (throwable != null) { -+ throw new RuntimeException(throwable); -+ } -+ -+ this.removeTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier); -+ this.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); // allow unloading -+ -+ Optional failure = either.right(); -+ -+ if (failure.isPresent()) { -+ // failure -+ throw new IllegalStateException("Chunk failed to load: " + failure.get().toString()); -+ } -+ -+ onComplete.accept(either.left().get()); -+ -+ }, this.serverThreadQueue); -+ } -+ -+ public void addTicketAtLevel(TicketType ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) { -+ this.chunkMapDistance.addTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier); -+ } -+ -+ public void removeTicketAtLevel(TicketType ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) { -+ this.chunkMapDistance.removeTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier); -+ } - // Paper end - - @Nullable -diff --git a/src/main/java/net/minecraft/server/ChunkStatus.java b/src/main/java/net/minecraft/server/ChunkStatus.java -index e324989b46..abb0d69d2f 100644 ---- a/src/main/java/net/minecraft/server/ChunkStatus.java -+++ b/src/main/java/net/minecraft/server/ChunkStatus.java -@@ -153,6 +153,7 @@ public class ChunkStatus { - return ChunkStatus.q.size(); - } - -+ public static int getTicketLevelOffset(ChunkStatus status) { return ChunkStatus.a(status); } // Paper - OBFHELPER - public static int a(ChunkStatus chunkstatus) { - return ChunkStatus.r.getInt(chunkstatus.c()); - } -diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java -index 23d1935dd5..14f8b61042 100644 ---- a/src/main/java/net/minecraft/server/MCUtil.java -+++ b/src/main/java/net/minecraft/server/MCUtil.java -@@ -530,4 +530,9 @@ public final class MCUtil { - out.print(fileData); - } - } -+ -+ public static int getTicketLevelFor(ChunkStatus status) { -+ // TODO make sure the constant `33` is correct on future updates. See getChunkAt(int, int, ChunkStatus, boolean) -+ return 33 + ChunkStatus.getTicketLevelOffset(status); -+ } - } -diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java -index 66c8b0307f..3e80f6c53e 100644 ---- a/src/main/java/net/minecraft/server/RegionFile.java -+++ b/src/main/java/net/minecraft/server/RegionFile.java -@@ -310,7 +310,7 @@ public class RegionFile implements AutoCloseable { - return this.c[this.f(chunkcoordintpair)]; - } - -- public final boolean chunkExists(ChunkCoordIntPair chunkPos) { return this.d(chunkPos); } // Paper - OBFHELPER -+ public boolean chunkExists(ChunkCoordIntPair chunkPos) { return this.d(chunkPos); } // Paper - OBFHELPER - public boolean d(ChunkCoordIntPair chunkcoordintpair) { - return this.getOffset(chunkcoordintpair) != 0; - } -diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java -index 5acb0732c3..0ed2d2fbf9 100644 ---- a/src/main/java/net/minecraft/server/TicketType.java -+++ b/src/main/java/net/minecraft/server/TicketType.java -@@ -22,6 +22,7 @@ public class TicketType { - public static final TicketType PLUGIN = a("plugin", (a, b) -> 0); // CraftBukkit - public static final TicketType PLUGIN_TICKET = a("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // Craftbukkit - public static final TicketType ANTIXRAY = a("antixray", Integer::compareTo); // Paper - Anti-Xray -+ public static final TicketType ASYNC_LOAD = a("async_load", Long::compareTo); // Paper - - public static TicketType a(String s, Comparator comparator) { - return new TicketType<>(s, comparator, 0L); -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index c5321c5076..7691f23316 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -2323,16 +2323,17 @@ public class CraftWorld implements World { - - @Override - public CompletableFuture getChunkAtAsync(int x, int z, boolean gen) { -- // TODO placeholder -- if (Bukkit.isPrimaryThread()) { -- return CompletableFuture.completedFuture(getChunkAtGen(x, z, gen)); -- } else { -- CompletableFuture ret = new CompletableFuture<>(); -- net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { -- ret.complete(getChunkAtGen(x, z, gen)); -- }); -- return ret; -+ net.minecraft.server.Chunk immediate = this.world.getChunkProvider().getChunkAtIfLoadedImmediately(x, z); -+ if (immediate != null) { -+ return CompletableFuture.completedFuture(immediate.bukkitChunk); - } -+ -+ CompletableFuture ret = new CompletableFuture<>(); -+ this.world.getChunkProvider().getChunkAtAsynchronously(x, z, gen, (net.minecraft.server.Chunk chunk) -> { -+ ret.complete(chunk == null ? null : chunk.bukkitChunk); -+ }); -+ -+ return ret; - } - // Paper end - --- -2.20.1 - diff --git a/patches/server/0054-Implement-optional-per-player-mob-spawns.patch b/patches/server/0053-Implement-optional-per-player-mob-spawns.patch similarity index 96% rename from patches/server/0054-Implement-optional-per-player-mob-spawns.patch rename to patches/server/0053-Implement-optional-per-player-mob-spawns.patch index 1893abeb1..1e74bd275 100644 --- a/patches/server/0054-Implement-optional-per-player-mob-spawns.patch +++ b/patches/server/0053-Implement-optional-per-player-mob-spawns.patch @@ -1,4 +1,4 @@ -From 854429ab6cd52dfd88f48b40206055b8790085f0 Mon Sep 17 00:00:00 2001 +From 3ff085046cf1d3efc53b2dec884c4d5b75cd0df3 Mon Sep 17 00:00:00 2001 From: kickash32 Date: Tue, 11 Jun 2019 22:22:16 -0400 Subject: [PATCH] Implement optional per player mob spawns @@ -29,10 +29,10 @@ index ff520d9e86..5ed02f6485 100644 public boolean asynchronous; public EngineMode engineMode; diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java -index f6a6421140..770ee018fe 100644 +index 4f7c442264..9f6c362dd1 100644 --- a/src/main/java/net/minecraft/server/ChunkProviderServer.java +++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java -@@ -586,9 +586,21 @@ public class ChunkProviderServer extends IChunkProvider { +@@ -582,9 +582,21 @@ public class ChunkProviderServer extends IChunkProvider { // Paper start - only allow spawns upto the limit per chunk and update count afterwards int currEntityCount = object2intmap.getInt(enumcreaturetype); int difference = k1 - currEntityCount; @@ -57,7 +57,7 @@ index f6a6421140..770ee018fe 100644 } } diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java -index 6ca98b7ad5..56e60e0ce1 100644 +index f25ca782b9..a235df4185 100644 --- a/src/main/java/net/minecraft/server/PlayerChunkMap.java +++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java @@ -105,6 +105,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { @@ -68,7 +68,7 @@ index 6ca98b7ad5..56e60e0ce1 100644 private static double a(ChunkCoordIntPair chunkcoordintpair, Entity entity) { double d0 = (double) (chunkcoordintpair.x * 16 + 8); double d1 = (double) (chunkcoordintpair.z * 16 + 8); -@@ -1131,6 +1132,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { +@@ -1157,6 +1158,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } @@ -167,7 +167,7 @@ index af397dd1f7..5e001733a9 100644 @Nullable diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java -index f0380c5df4..0c4fd5ca4d 100644 +index f7597d499f..2410db3353 100644 --- a/src/main/java/net/minecraft/server/WorldServer.java +++ b/src/main/java/net/minecraft/server/WorldServer.java @@ -17,6 +17,9 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap; @@ -188,7 +188,7 @@ index f0380c5df4..0c4fd5ca4d 100644 // CraftBukkit start private int tickPosition; -@@ -930,6 +934,7 @@ public class WorldServer extends World { +@@ -932,6 +936,7 @@ public class WorldServer extends World { } public Object2IntMap l() { @@ -196,7 +196,7 @@ index f0380c5df4..0c4fd5ca4d 100644 Object2IntMap object2intmap = new Object2IntOpenHashMap(); ObjectIterator objectiterator = this.entitiesById.values().iterator(); -@@ -957,13 +962,47 @@ public class WorldServer extends World { +@@ -959,13 +964,47 @@ public class WorldServer extends World { } // Paper end object2intmap.mergeInt(enumcreaturetype, 1, Integer::sum); diff --git a/patches/server/0055-Tulips-change-fox-type.patch b/patches/server/0054-Tulips-change-fox-type.patch similarity index 98% rename from patches/server/0055-Tulips-change-fox-type.patch rename to patches/server/0054-Tulips-change-fox-type.patch index 270d74073..3a3895508 100644 --- a/patches/server/0055-Tulips-change-fox-type.patch +++ b/patches/server/0054-Tulips-change-fox-type.patch @@ -1,4 +1,4 @@ -From 4c6e788cb3b1158e8955dccd8434939ebe1a026a Mon Sep 17 00:00:00 2001 +From dc255f32785831cfa041105a377ea880926762f8 Mon Sep 17 00:00:00 2001 From: William Blake Galbreath Date: Sat, 13 Jul 2019 15:56:22 -0500 Subject: [PATCH] Tulips change fox type