2020-05-06 11:48:49 +02:00
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2019-08-19 01:40:04 +02:00
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sat, 13 Jul 2019 09:23:10 -0700
Subject: [PATCH] Asynchronous chunk IO and loading
This patch re-adds a file IO thread as well as shoving de-serializing
chunk NBT data onto worker threads. This patch also will shove
chunk data serialization onto the same worker threads when the chunk
is unloaded - this cannot be done for regular saves since that's unsafe.
The file IO Thread
Unlike 1.13 and below, the file IO thread is prioritized - IO tasks can
be reoredered, however they are "stuck" to a world & coordinate.
Scheduling IO tasks works as follows, given a world & coordinate - location:
The IO thread has been designed to ensure that reads and writes appear to
occur synchronously for a given location, however the implementation also
has the unfortunate side-effect of making every write appear as if
they occur without failure.
The IO thread has also been designed to accomodate Mojang's decision to
store chunk data and POI data separately. It can independently schedule
tasks for each.
However threads can wait for writes to complete and check if:
- The write was overwriten by another scheduler
- The write failed (however it does not indicate whether it was overwritten by another scheduler)
Scheduling reads:
- If a write task is in progress, the task is not scheduled and returns the in-progress write data
This means that readers cannot modify the NBTTagCompound returned and must clone if it they wish to write
- If a write task is not in progress but a read task is in progress, then the read task is simply chained
This means that again, readers cannot modify the NBTTagCompound returned
Scheduling writes:
- If a read task is in progress, ignore the read task and schedule the write
We cannot complete the read task since we assume it wants old data - not current
- If a write task is pending, overwrite the write data
The file IO thread does correctly handle cases where the data is overwritten when it
is writing data (before completing a task it will check if the data was overwritten and
will retry).
When the file IO thread executes a task for a location, the it will
execute the read task first (if it exists), then it will execute the
write task. This ensures that, even when scheduling at different
priorities, that reads/writes for a location act synchronously.
The downside of the file IO thread is that write failure can only be
indicated to the scheduling thread if:
- No other thread decides to schedule another write for the location
concurrently
- The scheduling thread blocks on the write to complete (however the
current implementation can be modified to indicate success
asynchronously)
The file io thread can be modified easily to provide indications
of write failure and write overwriting if needed.
The upside of the file IO thread is that if a write failures, then
chunk data is not lost until server restart. This leaves more room
for spurious failure.
Finally, the io thread will indicate to the console when reads
or writes fail - with relevant detail.
Asynchronous chunk data serialization for unloading chunks
When chunks unload they make a call to PlayerChunkMap#saveChunk(IChunkAccess).
Even if I make the IO asynchronous for this call, the data serialization
still hits pretty hard. And given that now the chunk system will
aggressively unload chunks more often (queued immediately at
ticket level 45 or higher), unloads occur more often, and
combined with our changes to the unload queue to make it
significantly more aggresive - chunk unloads can hit pretty hard.
Especially players running around with elytras and fireworks.
For serializing chunk data off main, there are some tasks which cannot be
done asynchronously. Lighting data must be saved beforehand as well as
potentially some tick lists. These are completed before scheduling the
asynchronous save.
However serializing chunk data off of the main thread is still risky.
Even though this patch schedules the save to occur after ALL references
of the chunk are removed from the world, plugins can still technically
access entities inside the chunks. For this, if the serialization task
fails for any reason, it will be re-scheduled to be serialized on the
main thread - with the hopes that the reason it failed was due to a plugin
and not an error with the save code itself. Like vanilla code - if the
serialization fails, the chunk data is lost.
Asynchronous chunk io/loading
Mojang's current implementation for loading chunk data off disk is
to return a CompletableFuture that will be completed by scheduling a
task to be executed on the world's chunk queue (which is only drained
on the main thread). This task will read the IO off disk and it will
apply data conversions & deserialization synchronously. Obviously
all 3 of these operations are expensive however all can be completed
asynchronously instead.
The solution this patch uses is as follows:
0. If an asynchronous chunk save is in progress (see above), wait
for that task to complete. It will use the serialized NBTTagCompound
created by the task. If the task fails to complete, then we would continue
with step 1. If it does not, we skip step 1. (Note: We actually load
POI data no matter what in this case).
1. Schedule an IO task to read chunk & poi data off disk.
2. The IO task will schedule a chunk load task.
3. The chunk load task executes on the async chunk loader threads
and will apply datafixers & de-serialize the chunk into a ProtoChunk
or ProtoChunkExtension.
4. The in progress chunk is then passed on to the world's chunk queue
to complete the ComletableFuture and execute any of the synchronous
tasks required to be executed by the chunk load task (i.e lighting
and some poi tasks).
diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
2020-06-26 14:04:38 +02:00
index 944fd203e9f39d6c6fc9e270940c76c98067273a..a27dc38d1a29ed1d63d2f44b7984c2b65be487d9 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java
+++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
2020-06-26 14:04:38 +02:00
@@ -58,6 +58,17 @@ public class WorldTimingsHandler {
2020-03-19 07:03:32 +01:00
2020-03-02 20:56:04 +01:00
public final Timing miscMobSpawning;
2019-08-19 01:40:04 +02:00
+ 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 final Timing chunkUnloadPrepareSave;
+ public final Timing chunkUnloadPOISerialization;
+ public final Timing chunkUnloadDataSave;
+
public WorldTimingsHandler(World server) {
2020-06-26 14:04:38 +02:00
String name = ((WorldDataServer) server.getWorldData()).getName() + " - ";
2019-08-19 01:40:04 +02:00
2020-06-26 14:04:38 +02:00
@@ -111,6 +122,17 @@ public class WorldTimingsHandler {
2020-03-19 07:03:32 +01:00
2020-03-02 20:56:04 +01:00
2019-09-29 23:06:03 +02:00
miscMobSpawning = Timings.ofSafe(name + "Mob spawning - Misc");
2019-08-19 01:40:04 +02:00
+
+ 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");
+ chunkUnloadPrepareSave = Timings.ofSafe(name + "Chunk unload - Async Save Prepare");
+ chunkUnloadPOISerialization = Timings.ofSafe(name + "Chunk unload - POI Data Serialization");
+ chunkUnloadDataSave = Timings.ofSafe(name + "Chunk unload - Data Serialization");
}
public static Timing getTickList(WorldServer worldserver, String timingsType) {
2020-05-20 11:11:57 +02:00
diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
index af810987846efcd2bffbd23c31481b2d31c168dd..331493a172f58e71b464d635efdba461082bd27d 100644
--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
@@ -1,5 +1,6 @@
package com.destroystokyo.paper;
+import com.destroystokyo.paper.io.chunk.ChunkTaskManager;
import com.google.common.base.Functions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -28,14 +29,14 @@ public class PaperCommand extends Command {
public PaperCommand(String name) {
super(name);
this.description = "Paper related commands";
- this.usageMessage = "/paper [heap | entity | reload | version | debug | chunkinfo]";
+ this.usageMessage = "/paper [heap | entity | reload | version | debug | dumpwaiting | chunkinfo]";
this.setPermission("bukkit.command.paper");
}
@Override
public List<String> tabComplete(CommandSender sender, String alias, String[] args, Location location) throws IllegalArgumentException {
if (args.length <= 1)
- return getListMatchingLast(args, "heap", "entity", "reload", "version", "debug", "chunkinfo");
+ return getListMatchingLast(args, "heap", "entity", "reload", "version", "debug", "dumpwaiting", "chunkinfo");
switch (args[0].toLowerCase(Locale.ENGLISH))
{
@@ -127,6 +128,9 @@ public class PaperCommand extends Command {
case "debug":
doDebug(sender, args);
break;
+ case "dumpwaiting":
+ ChunkTaskManager.dumpAllChunkLoadInfo();
+ break;
case "chunkinfo":
doChunkInfo(sender, args);
break;
2019-08-19 01:40:04 +02:00
diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java
2020-06-26 08:29:44 +02:00
index f9b1b198299166759fe0bd0a36d8d88c626e06a4..bf86444c479f346e7d56f10a7c0ebefd62f08f59 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java
+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java
@@ -1,5 +1,6 @@
package com.destroystokyo.paper;
+import com.destroystokyo.paper.io.chunk.ChunkTaskManager;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
2020-06-26 08:29:44 +02:00
@@ -347,4 +348,54 @@ public class PaperConfig {
2019-08-19 01:40:04 +02:00
maxBookPageSize = getInt("settings.book-size.page-max", maxBookPageSize);
maxBookTotalSizeMultiplier = getDouble("settings.book-size.total-multiplier", maxBookTotalSizeMultiplier);
}
+
+ public static boolean asyncChunks = false;
+ private static void asyncChunks() {
2020-04-26 08:05:41 +02:00
+ ConfigurationSection section;
2019-08-19 01:40:04 +02:00
+ if (version < 15) {
+ boolean enabled = config.getBoolean("settings.async-chunks", true);
2020-04-26 08:05:41 +02:00
+ section = config.createSection("settings.async-chunks");
2019-08-19 01:40:04 +02:00
+ section.set("enable", enabled);
2020-04-26 08:05:41 +02:00
+ section.set("threads", -1);
+ } else {
+ section = config.getConfigurationSection("settings.async-chunks");
+ if (section == null) {
+ section = config.createSection("settings.async-chunks");
+ }
2019-08-19 01:40:04 +02:00
+ }
2020-04-26 08:05:41 +02:00
+ // Clean up old configs
+ if (section.contains("load-threads")) {
+ if (!section.contains("threads")) {
+ section.set("threads", section.get("load-threads"));
+ }
+ section.set("load-threads", null);
+ }
+ section.set("generation", null);
+ section.set("thread-per-world-generation", null);
2019-08-19 01:40:04 +02:00
+
+ asyncChunks = getBoolean("settings.async-chunks.enable", true);
2020-04-26 08:05:41 +02:00
+ int threads = getInt("settings.async-chunks.threads", -1);
2020-05-19 10:01:53 +02:00
+ int cpus = Runtime.getRuntime().availableProcessors();
2020-04-26 08:05:41 +02:00
+ if (threads <= 0) {
2020-05-19 10:01:53 +02:00
+ threads = (int) Math.min(Integer.getInteger("paper.maxChunkThreads", 8), Math.max(1, cpus - 1));
+ }
+ if (cpus == 1 && !Boolean.getBoolean("Paper.allowAsyncChunksSingleCore")) {
+ asyncChunks = false;
2019-08-19 01:40:04 +02:00
+ }
+
+ // Let Shared Host set some limits
2020-04-26 08:05:41 +02:00
+ String sharedHostThreads = System.getenv("PAPER_ASYNC_CHUNKS_SHARED_HOST_THREADS");
+ if (sharedHostThreads != null) {
2019-08-19 01:40:04 +02:00
+ try {
2020-04-26 08:05:41 +02:00
+ threads = Math.max(1, Math.min(threads, Integer.parseInt(sharedHostThreads)));
2019-08-19 01:40:04 +02:00
+ } catch (NumberFormatException ignored) {}
+ }
+
+ if (!asyncChunks) {
+ log("Async Chunks: Disabled - Chunks will be managed synchronosuly, and will cause tremendous lag.");
+ } else {
2020-04-26 08:05:41 +02:00
+ ChunkTaskManager.initGlobalLoadThreads(threads);
2019-08-19 01:40:04 +02:00
+ log("Async Chunks: Enabled - Chunks will be loaded much faster, without lag.");
+ }
+ }
}
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
2020-05-06 11:48:49 +02:00
index 0000000000000000000000000000000000000000..5af0ac3d9e87c06053e65433060f15779c156c2a
2019-08-19 01:40:04 +02:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/IOUtil.java
@@ -0,0 +1,62 @@
+package com.destroystokyo.paper.io;
+
+import org.bukkit.Bukkit;
+
+public final class IOUtil {
+
+ /* Copied from concrete or concurrentutil */
+
+ public static long getCoordinateKey(final int x, final int z) {
+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
+ }
+
+ public static int getCoordinateX(final long key) {
+ return (int)key;
+ }
+
+ public static int getCoordinateZ(final long key) {
+ return (int)(key >>> 32);
+ }
+
+ public static int getRegionCoordinate(final int chunkCoordinate) {
+ return chunkCoordinate >> 5;
+ }
+
+ public static int getChunkInRegion(final int chunkCoordinate) {
+ return chunkCoordinate & 31;
+ }
+
+ public static String genericToString(final Object object) {
+ return object == null ? "null" : object.getClass().getName() + ":" + object.toString();
+ }
+
+ public static <T> T notNull(final T obj) {
+ if (obj == null) {
+ throw new NullPointerException();
+ }
+ return obj;
+ }
+
+ public static <T> T notNull(final T obj, final String msgIfNull) {
+ if (obj == null) {
+ throw new NullPointerException(msgIfNull);
+ }
+ return obj;
+ }
+
+ public static void arrayBounds(final int off, final int len, final int arrayLength, final String msgPrefix) {
+ if (off < 0 || len < 0 || (arrayLength - off) < len) {
+ throw new ArrayIndexOutOfBoundsException(msgPrefix + ": off: " + off + ", len: " + len + ", array length: " + arrayLength);
+ }
+ }
+
+ public static int getPriorityForCurrentThread() {
+ return Bukkit.isPrimaryThread() ? PrioritizedTaskQueue.HIGHEST_PRIORITY : PrioritizedTaskQueue.NORMAL_PRIORITY;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T extends Throwable> void rethrow(final Throwable throwable) throws T {
+ throw (T)throwable;
+ }
+
+}
diff --git a/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java b/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java
new file mode 100644
2020-06-26 08:29:44 +02:00
index 0000000000000000000000000000000000000000..1917d7be7dfddc93c95b9fd864cf13f9a24c8f96
2019-08-19 01:40:04 +02:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java
2020-06-26 08:29:44 +02:00
@@ -0,0 +1,606 @@
2019-08-19 01:40:04 +02:00
+package com.destroystokyo.paper.io;
+
+import net.minecraft.server.ChunkCoordIntPair;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.NBTTagCompound;
+import net.minecraft.server.RegionFile;
+import net.minecraft.server.WorldServer;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Prioritized singleton thread responsible for all chunk IO that occurs in a minecraft server.
+ *
+ * <p>
+ * Singleton access: {@link Holder#INSTANCE}
+ * </p>
+ *
+ * <p>
+ * All functions provided are MT-Safe, however certain ordering constraints are (but not enforced):
+ * <li>
+ * Chunk saves may not occur for unloaded chunks.
+ * </li>
+ * <li>
+ * Tasks must be scheduled on the main thread.
+ * </li>
+ * </p>
+ *
+ * @see Holder#INSTANCE
+ * @see #scheduleSave(WorldServer, int, int, NBTTagCompound, NBTTagCompound, int)
+ * @see #loadChunkDataAsync(WorldServer, int, int, int, Consumer, boolean, boolean, boolean)
+ */
+public final class PaperFileIOThread extends QueueExecutorThread {
+
+ public static final Logger LOGGER = MinecraftServer.LOGGER;
+ public static final NBTTagCompound FAILURE_VALUE = new NBTTagCompound();
+
+ public static final class Holder {
+
+ public static final PaperFileIOThread INSTANCE = new PaperFileIOThread();
+
+ static {
+ INSTANCE.start();
+ }
+ }
+
+ private final AtomicLong writeCounter = new AtomicLong();
+
+ private PaperFileIOThread() {
+ super(new PrioritizedTaskQueue<>(), (int)(1.0e6)); // 1.0ms spinwait time
+ this.setName("Paper 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) -> {
+ LOGGER.fatal("Uncaught exception thrown from IO thread, report this!", thr);
+ });
+ }
+
+ /* run() is implemented by superclass */
+
+ /*
+ *
+ * IO thread will perform reads before writes
+ *
+ * How reads/writes are scheduled:
+ *
+ * If read in progress while scheduling write, ignore read and schedule write
+ * If read in progress while scheduling read (no write in progress), chain the read task
+ *
+ *
+ * If write in progress while scheduling read, use the pending write data and ret immediately
+ * If write in progress while scheduling write (ignore read in progress), overwrite the write in progress data
+ *
+ * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however
+ * it fails to properly propagate write failures. When writes fail the data is kept so future reads will actually
+ * read the failed write data. This should hopefully act as a way to prevent data loss for spurious fails for writing data.
+ *
+ */
+
+ /**
+ * 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 priority Priority level to try to bump to
+ */
+ public void bumpPriority(final WorldServer world, final int chunkX, final int chunkZ, final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority: " + priority);
+ }
+
+ final Long key = Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ));
+
+ final ChunkDataTask poiTask = world.poiDataController.tasks.get(key);
+ final ChunkDataTask chunkTask = world.chunkDataController.tasks.get(key);
+
+ if (poiTask != null) {
+ poiTask.raisePriority(priority);
+ }
+ if (chunkTask != null) {
+ chunkTask.raisePriority(priority);
+ }
+ }
+
+ 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 priority Priority level to set to
+ */
+ public void setPriority(final WorldServer world, final int chunkX, final int chunkZ, final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority: " + priority);
+ }
+
+ final Long key = Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ));
+
+ final ChunkDataTask poiTask = world.poiDataController.tasks.get(key);
+ final ChunkDataTask chunkTask = world.chunkDataController.tasks.get(key);
+
+ if (poiTask != null) {
+ poiTask.updatePriority(priority);
+ }
+ if (chunkTask != null) {
+ chunkTask.updatePriority(priority);
+ }
+ }
+
+ /**
+ * Schedules the chunk data to be written asynchronously.
+ * <p>
+ * Impl notes:
+ * </p>
+ * <li>
+ * 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.
+ * </li>
+ * <li>
+ * Writes may be called concurrently, although only the "later" write will go through.
+ * </li>
+ * @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}
+ * @throws IllegalArgumentException If both {@code poiData} and {@code chunkData} are {@code null}.
+ * @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,
+ final int priority) throws IllegalArgumentException {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority: " + priority);
+ }
+
+ final long writeCounter = this.writeCounter.getAndIncrement();
+
+ if (poiData != null) {
+ this.scheduleWrite(world.poiDataController, world, chunkX, chunkZ, poiData, priority, writeCounter);
+ }
+ if (chunkData != null) {
+ this.scheduleWrite(world.chunkDataController, world, chunkX, chunkZ, chunkData, priority, writeCounter);
+ }
+ }
+
+ private void scheduleWrite(final ChunkDataController dataController, final WorldServer world,
+ final int chunkX, final int chunkZ, final NBTTagCompound data, final int priority, final long writeCounter) {
+ dataController.tasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkDataTask taskRunning) -> {
+ if (taskRunning == null) {
+ // no task is scheduled
+
+ // create task
+ final ChunkDataTask newTask = new ChunkDataTask(priority, world, chunkX, chunkZ, dataController);
+ newTask.inProgressWrite = new ChunkDataController.InProgressWrite();
+ newTask.inProgressWrite.writeCounter = writeCounter;
+ newTask.inProgressWrite.data = data;
+
+ PaperFileIOThread.this.queueTask(newTask); // schedule
+ return newTask;
+ }
+
+ taskRunning.raisePriority(priority);
+
+ if (taskRunning.inProgressWrite == null) {
+ taskRunning.inProgressWrite = new ChunkDataController.InProgressWrite();
+ }
+
+ boolean reschedule = taskRunning.inProgressWrite.writeCounter == -1L;
+
+ // synchronize for readers
+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
+ synchronized (taskRunning) {
+ taskRunning.inProgressWrite.data = data;
+ taskRunning.inProgressWrite.writeCounter = writeCounter;
+ }
+
+ if (reschedule) {
+ // We need to reschedule this task since the previous one is not currently scheduled since it failed
+ taskRunning.reschedule(priority);
+ }
+
+ return taskRunning;
+ });
+ }
+
+ /**
+ * Same as {@link #loadChunkDataAsync(WorldServer, int, int, int, Consumer, boolean, boolean, boolean)}, except this function returns
+ * a {@link CompletableFuture} which is potentially completed <b>ASYNCHRONOUSLY ON THE FILE IO THREAD</b> when the load task
+ * has completed.
+ * <p>
+ * Note that if the chunk fails to load the returned future is completed with {@code null}.
+ * </p>
+ */
+ public CompletableFuture<ChunkData> loadChunkDataAsyncFuture(final WorldServer world, final int chunkX, final int chunkZ,
+ final int priority, final boolean readPoiData, final boolean readChunkData,
+ final boolean intendingToBlock) {
+ final CompletableFuture<ChunkData> future = new CompletableFuture<>();
+ this.loadChunkDataAsync(world, chunkX, chunkZ, priority, future::complete, readPoiData, readChunkData, intendingToBlock);
+ return future;
+ }
+
+ /**
+ * Schedules a load to be executed asynchronously.
+ * <p>
+ * Impl notes:
+ * </p>
+ * <li>
+ * If a chunk fails to load, the {@code onComplete} parameter is completed with {@code null}.
+ * </li>
+ * <li>
+ * It is possible for the {@code onComplete} parameter to be given {@link ChunkData} containing data
+ * this call did not request.
+ * </li>
+ * <li>
+ * 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.
+ * </li>
+ * @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.
+ */
+ public void loadChunkDataAsync(final WorldServer world, final int chunkX, final int chunkZ,
+ final int priority, final Consumer<ChunkData> onComplete,
+ final boolean readPoiData, final boolean readChunkData,
+ final boolean intendingToBlock) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority: " + priority);
+ }
+
+ if (!(readPoiData | readChunkData)) {
+ throw new IllegalArgumentException("Must read chunk data or poi data");
+ }
+
+ final ChunkData complete = new ChunkData();
+ final boolean[] requireCompletion = new boolean[] { readPoiData, readChunkData };
+
+ if (readPoiData) {
+ this.scheduleRead(world.poiDataController, world, chunkX, chunkZ, (final NBTTagCompound poiData) -> {
+ complete.poiData = poiData;
+
+ final boolean finished;
+
+ // avoid a race condition where the file io thread completes and we complete synchronously
+ // Note: Synchronization can be elided if both of the accesses are volatile
+ synchronized (requireCompletion) {
+ requireCompletion[0] = false; // 0 -> poi data
+ finished = !requireCompletion[1]; // 1 -> chunk data
+ }
+
+ if (finished) {
+ onComplete.accept(complete);
+ }
+ }, priority, intendingToBlock);
+ }
+
+ if (readChunkData) {
+ this.scheduleRead(world.chunkDataController, world, chunkX, chunkZ, (final NBTTagCompound chunkData) -> {
+ complete.chunkData = chunkData;
+
+ final boolean finished;
+
+ // avoid a race condition where the file io thread completes and we complete synchronously
+ // Note: Synchronization can be elided if both of the accesses are volatile
+ synchronized (requireCompletion) {
+ requireCompletion[1] = false; // 1 -> chunk data
+ finished = !requireCompletion[0]; // 0 -> poi data
+ }
+
+ if (finished) {
+ onComplete.accept(complete);
+ }
+ }, priority, intendingToBlock);
+ }
+
+ }
+
+ // Note: the onComplete may be called asynchronously or synchronously here.
+ private void scheduleRead(final ChunkDataController dataController, final WorldServer world,
+ final int chunkX, final int chunkZ, final Consumer<NBTTagCompound> onComplete, final int priority,
+ final boolean intendingToBlock) {
+
+ Function<RegionFile, Boolean> tryLoadFunction = (final RegionFile file) -> {
+ if (file == null) {
+ return Boolean.TRUE;
+ }
+ return Boolean.valueOf(file.chunkExists(new ChunkCoordIntPair(chunkX, chunkZ)));
+ };
+
+ dataController.tasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkDataTask running) -> {
+ if (running == null) {
+ // not scheduled
+
+ final Boolean shouldSchedule = intendingToBlock ? dataController.computeForRegionFile(chunkX, chunkZ, tryLoadFunction) :
+ dataController.computeForRegionFileIfLoaded(chunkX, chunkZ, tryLoadFunction);
+
+ if (shouldSchedule == Boolean.FALSE) {
+ // not on disk
+ onComplete.accept(null);
+ return null;
+ }
+
+ // set up task
+ final ChunkDataTask newTask = new ChunkDataTask(priority, world, chunkX, chunkZ, dataController);
+ newTask.inProgressRead = new ChunkDataController.InProgressRead();
+ newTask.inProgressRead.readFuture.thenAccept(onComplete);
+
+ PaperFileIOThread.this.queueTask(newTask); // schedule task
+ return newTask;
+ }
+
+ running.raisePriority(priority);
+
+ if (running.inProgressWrite == null) {
+ // chain to the read future
+ running.inProgressRead.readFuture.thenAccept(onComplete);
+ return running;
+ }
+
+ // at this stage we have to use the in progress write's data to avoid an order issue
+ // we don't synchronize since all writes to data occur in the compute() call
+ onComplete.accept(running.inProgressWrite.data);
+ return running;
+ });
+ }
+
+ /**
+ * 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,
+ final boolean readPoiData, final boolean readChunkData) {
+ return this.loadChunkDataAsyncFuture(world, chunkX, chunkZ, priority, readPoiData, readChunkData, true).join();
+ }
+
+ /**
+ * Schedules the given task at the specified priority to be executed on the IO thread.
+ * <p>
+ * Internal api. Do not use.
+ * </p>
+ */
+ public void runTask(final int priority, final Runnable runnable) {
+ this.queueTask(new GeneralTask(priority, runnable));
+ }
+
+ static final class GeneralTask extends PrioritizedTaskQueue.PrioritizedTask implements Runnable {
+
+ private final Runnable run;
+
+ public GeneralTask(final int priority, final Runnable run) {
+ super(priority);
+ this.run = IOUtil.notNull(run, "Task may not be null");
+ }
+
+ @Override
+ public void run() {
+ try {
+ this.run.run();
+ } catch (final Throwable throwable) {
+ if (throwable instanceof ThreadDeath) {
+ throw (ThreadDeath)throwable;
+ }
+ LOGGER.fatal("Failed to execute general task on IO thread " + IOUtil.genericToString(this.run), throwable);
+ }
+ }
+ }
+
+ public static final class ChunkData {
+
+ public NBTTagCompound poiData;
+ public NBTTagCompound chunkData;
+
+ public ChunkData() {}
+
+ public ChunkData(final NBTTagCompound poiData, final NBTTagCompound chunkData) {
+ this.poiData = poiData;
+ this.chunkData = chunkData;
+ }
+ }
+
+ public static abstract class ChunkDataController {
+
+ // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding.
+ public final ConcurrentHashMap<Long, ChunkDataTask> 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> T computeForRegionFile(final int chunkX, final int chunkZ, final Function<RegionFile, T> function);
+ public abstract <T> T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function<RegionFile, T> function);
+
+ public static final class InProgressWrite {
+ public long writeCounter;
+ public NBTTagCompound data;
+ }
+
+ public static final class InProgressRead {
+ public final CompletableFuture<NBTTagCompound> readFuture = new CompletableFuture<>();
+ }
+ }
+
+ public static final class ChunkDataTask extends PrioritizedTaskQueue.PrioritizedTask implements Runnable {
+
+ public ChunkDataController.InProgressWrite inProgressWrite;
+ public ChunkDataController.InProgressRead inProgressRead;
+
+ private final WorldServer world;
+ private final int x;
+ private final int z;
+ private final ChunkDataController taskController;
+
+ public ChunkDataTask(final int priority, final WorldServer world, final int x, final int z, final ChunkDataController taskController) {
+ super(priority);
+ this.world = world;
+ this.x = x;
+ this.z = z;
+ this.taskController = taskController;
+ }
+
+ @Override
+ public String toString() {
+ return "Task for world: '" + this.world.getWorld().getName() + "' at " + this.x + "," + this.z +
+ " poi: " + (this.taskController == this.world.poiDataController) + ", hash: " + this.hashCode();
+ }
+
+ /*
+ *
+ * IO thread will perform reads before writes
+ *
+ * How reads/writes are scheduled:
+ *
+ * If read in progress while scheduling write, ignore read and schedule write
+ * If read in progress while scheduling read (no write in progress), chain the read task
+ *
+ *
+ * If write in progress while scheduling read, use the pending write data and ret immediately
+ * If write in progress while scheduling write (ignore read in progress), overwrite the write in progress data
+ *
+ * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however
+ * it fails to properly propagate write failures
+ *
+ */
+
+ void reschedule(final int priority) {
+ // priority is checked before this stage // TODO what
+ this.queue.lazySet(null);
+ this.priority.lazySet(priority);
+ PaperFileIOThread.Holder.INSTANCE.queueTask(this);
+ }
+
+ @Override
+ public void run() {
+ ChunkDataController.InProgressRead read = this.inProgressRead;
+ if (read != null) {
+ NBTTagCompound compound = PaperFileIOThread.FAILURE_VALUE;
+ try {
+ compound = this.taskController.readData(this.x, this.z);
+ } catch (final Throwable thr) {
+ if (thr instanceof ThreadDeath) {
+ throw (ThreadDeath)thr;
+ }
+ LOGGER.fatal("Failed to read chunk data for task: " + this.toString(), thr);
+ // fall through to complete with null data
+ }
+ read.readFuture.complete(compound);
+ }
+
+ final Long chunkKey = Long.valueOf(IOUtil.getCoordinateKey(this.x, this.z));
+
+ ChunkDataController.InProgressWrite write = this.inProgressWrite;
+
+ if (write == null) {
+ // IntelliJ warns this is invalid, however it does not consider that writes to the task map & the inProgress field can occur concurrently.
+ ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final Long keyInMap, final ChunkDataTask valueInMap) -> {
+ if (valueInMap == null) {
+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
+ }
+ if (valueInMap != ChunkDataTask.this) {
+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
+ }
+ return valueInMap.inProgressWrite == null ? null : valueInMap;
+ });
+
+ if (inMap == null) {
+ return; // set the task value to null, indicating we're done
+ }
+
+ // not null, which means there was a concurrent write
+ write = this.inProgressWrite;
+ }
+
+ // check if another process is writing
2020-06-26 08:29:44 +02:00
+ /*try { TODO: Can we restore this?
+ ((WorldServer)this.world).checkSession();
+ } catch (final Exception ex) {
2019-08-19 01:40:04 +02:00
+ LOGGER.fatal("Couldn't save chunk; already in use by another instance of Minecraft?", ex);
+ // we don't need to set the write counter to -1 as we know at this stage there's no point in re-scheduling
+ // writes since they'll fail anyways.
+ return;
+ }
2020-06-26 08:29:44 +02:00
+*/
2019-08-19 01:40:04 +02:00
+ for (;;) {
+ final long writeCounter;
+ final NBTTagCompound data;
+
+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
+ synchronized (write) {
+ writeCounter = write.writeCounter;
+ data = write.data;
+ }
+
+ boolean failedWrite = false;
+
+ try {
+ this.taskController.writeData(this.x, this.z, data);
+ } catch (final Throwable thr) {
+ if (thr instanceof ThreadDeath) {
+ throw (ThreadDeath)thr;
+ }
+ LOGGER.fatal("Failed to write chunk data for task: " + this.toString(), thr);
+ failedWrite = true;
+ }
+
+ boolean finalFailWrite = failedWrite;
+
+ ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final Long keyInMap, final ChunkDataTask valueInMap) -> {
+ if (valueInMap == null) {
+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
+ }
+ if (valueInMap != ChunkDataTask.this) {
+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
+ }
+ if (valueInMap.inProgressWrite.writeCounter == writeCounter) {
+ if (finalFailWrite) {
+ valueInMap.inProgressWrite.writeCounter = -1L;
+ }
+
+ return null;
+ }
+ return valueInMap;
+ // Hack end
+ });
+
+ if (inMap == null) {
+ // write counter matched, so we wrote the most up-to-date pending data, we're done here
+ // or we failed to write and successfully set the write counter to -1
+ return; // we're done here
+ }
+
+ // fetch & write new data
+ continue;
+ }
+ }
+ }
+}
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
2020-05-06 11:48:49 +02:00
index 0000000000000000000000000000000000000000..97f2e433c483f1ebd7500ae142269e144ef5fda4
2019-08-19 01:40:04 +02:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/PrioritizedTaskQueue.java
2020-04-11 10:40:00 +02:00
@@ -0,0 +1,277 @@
2019-08-19 01:40:04 +02:00
+package com.destroystokyo.paper.io;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class PrioritizedTaskQueue<T extends PrioritizedTaskQueue.PrioritizedTask> {
+
+ // lower numbers are a higher priority (except < 0)
+ // higher priorities are always executed before lower priorities
+
+ /**
+ * Priority value indicating the task has completed or is being completed.
+ */
+ 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;
+
+ /**
+ * 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;
+
+ /**
+ * Should be used for scheduling chunk loads/generation that would increase response times to users.
+ */
+ public static final int HIGH_PRIORITY = 2;
+
+ /**
+ * Default priority.
+ */
+ 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;
+
+ /**
+ * Use for tasks that should "eventually" execute.
+ */
+ public static final int LOWEST_PRIORITY = 5;
+
+ private static final int TOTAL_PRIORITIES = 6;
+
+ final ConcurrentLinkedQueue<T>[] queues = (ConcurrentLinkedQueue<T>[])new ConcurrentLinkedQueue[TOTAL_PRIORITIES];
+
+ private final AtomicBoolean shutdown = new AtomicBoolean();
+
+ {
+ for (int i = 0; i < TOTAL_PRIORITIES; ++i) {
+ this.queues[i] = new ConcurrentLinkedQueue<>();
+ }
+ }
+
+ /**
+ * Returns whether the specified priority is valid
+ */
+ public static boolean validPriority(final int priority) {
+ return priority >= 0 && priority < TOTAL_PRIORITIES;
+ }
+
+ /**
+ * 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.
+ */
+ public void add(final T task) throws IllegalStateException {
2020-04-11 10:40:00 +02:00
+ int priority = task.getPriority();
+ if (priority != COMPLETING_PRIORITY) {
+ task.setQueue(this);
+ this.queues[priority].add(task);
+ }
2019-08-19 01:40:04 +02:00
+ if (this.shutdown.get()) {
+ // note: we're not actually sure at this point if our task will go through
+ throw new IllegalStateException("Queue has shutdown, refusing to execute task " + IOUtil.genericToString(task));
+ }
+ }
+
+ /**
+ * Polls the highest priority task currently available. {@code null} if none.
+ */
+ public T poll() {
+ T task;
+ for (int i = 0; i < TOTAL_PRIORITIES; ++i) {
+ final ConcurrentLinkedQueue<T> queue = this.queues[i];
+
+ while ((task = queue.poll()) != null) {
+ final int prevPriority = task.tryComplete(i);
+ if (prevPriority != COMPLETING_PRIORITY && prevPriority <= i) {
+ // if the prev priority was greater-than or equal to our current priority
+ return task;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
2019-09-16 06:02:13 +02:00
+ * Returns whether this queue may have tasks queued.
+ * <p>
+ * This operation is not atomic, but is MT-Safe.
+ * </p>
+ * @return {@code true} if tasks may be queued, {@code false} otherwise
+ */
+ public boolean hasTasks() {
+ for (int i = 0; i < TOTAL_PRIORITIES; ++i) {
+ final ConcurrentLinkedQueue<T> queue = this.queues[i];
+
+ if (queue.peek() != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
2019-08-19 01:40:04 +02:00
+ * Prevent further additions to this queue. Attempts to add after this call has completed (potentially during) will
+ * result in {@link IllegalStateException} being thrown.
+ * <p>
+ * This operation is atomic with respect to other shutdown calls
+ * </p>
+ * <p>
+ * After this call has completed, regardless of return value, this queue will be shutdown.
+ * </p>
+ * @return {@code true} if the queue was shutdown, {@code false} if it has shut down already
+ */
+ public boolean shutdown() {
+ return this.shutdown.getAndSet(false);
+ }
+
+ public abstract static class PrioritizedTask {
+
+ protected final AtomicReference<PrioritizedTaskQueue> queue = new AtomicReference<>();
+
+ protected final AtomicInteger priority;
+
+ protected PrioritizedTask() {
+ this(PrioritizedTaskQueue.NORMAL_PRIORITY);
+ }
+
+ protected PrioritizedTask(final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.priority = new AtomicInteger(priority);
+ }
+
+ /**
+ * Returns the current priority. Note that {@link PrioritizedTaskQueue#COMPLETING_PRIORITY} will be returned
+ * if this task is completing or has completed.
+ */
+ public final int getPriority() {
+ 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();;) {
+ if (curr == COMPLETING_PRIORITY) {
+ return COMPLETING_PRIORITY;
+ }
+ if (curr > minPriority) {
+ // curr is lower priority
+ return curr;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, COMPLETING_PRIORITY))) {
+ return curr;
+ }
+ continue;
+ }
+ }
+
+ /**
+ * 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() {
+ return this.exchangePriorityVolatile(PrioritizedTaskQueue.COMPLETING_PRIORITY) != PrioritizedTaskQueue.COMPLETING_PRIORITY;
+ }
+
+ /**
+ * Attempts to raise the priority to the priority level specified.
+ * @param priority Priority specified
+ * @return {@code true} if successful, {@code false} otherwise.
+ */
+ public boolean raisePriority(final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority");
+ }
+
+ for (int curr = this.getPriorityVolatile();;) {
+ if (curr == COMPLETING_PRIORITY) {
+ return false;
+ }
+ if (priority >= curr) {
+ return true;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority))) {
+ PrioritizedTaskQueue queue = this.queue.get();
+ if (queue != null) {
+ //noinspection unchecked
+ queue.queues[priority].add(this); // silently fail on shutdown
+ }
+ return true;
+ }
+ continue;
+ }
+ }
+
+ /**
+ * 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.
+ */
+ public boolean updatePriority(final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority");
+ }
+
+ for (int curr = this.getPriorityVolatile();;) {
+ if (curr == COMPLETING_PRIORITY) {
+ return false;
+ }
+ if (curr == priority) {
+ return true;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority))) {
+ PrioritizedTaskQueue queue = this.queue.get();
+ if (queue != null) {
+ //noinspection unchecked
+ queue.queues[priority].add(this); // silently fail on shutdown
+ }
+ return true;
+ }
+ continue;
+ }
+ }
+
2020-04-11 10:40:00 +02:00
+ void setQueue(final PrioritizedTaskQueue queue) {
+ this.queue.set(queue);
2019-08-19 01:40:04 +02:00
+ }
+
+ /* priority */
+
+ protected final int getPriorityVolatile() {
+ return this.priority.get();
+ }
+
+ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) {
+ if (this.priority.compareAndSet(expect, update)) {
+ return expect;
+ }
+ return this.priority.get();
+ }
+
+ protected final int exchangePriorityVolatile(final int value) {
+ return this.priority.getAndSet(value);
+ }
+ }
+}
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
2020-05-06 11:48:49 +02:00
index 0000000000000000000000000000000000000000..ee906b594b306906c170180a29a8b61997d05168
2019-08-19 01:40:04 +02:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/QueueExecutorThread.java
2019-09-16 06:02:13 +02:00
@@ -0,0 +1,241 @@
2019-08-19 01:40:04 +02:00
+package com.destroystokyo.paper.io;
+
+import net.minecraft.server.MinecraftServer;
+import org.apache.logging.log4j.Logger;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.LockSupport;
+
+public class QueueExecutorThread<T extends PrioritizedTaskQueue.PrioritizedTask & Runnable> extends Thread {
+
+ private static final Logger LOGGER = MinecraftServer.LOGGER;
+
+ protected final PrioritizedTaskQueue<T> queue;
+ protected final long spinWaitTime;
+
+ protected volatile boolean closed;
+
+ protected final AtomicBoolean parked = new AtomicBoolean();
+
+ protected volatile ConcurrentLinkedQueue<Thread> flushQueue = new ConcurrentLinkedQueue<>();
2019-09-16 06:02:13 +02:00
+ protected volatile long flushCycles;
2019-08-19 01:40:04 +02:00
+
+ public QueueExecutorThread(final PrioritizedTaskQueue<T> queue) {
+ this(queue, (int)(1.e6)); // 1.0ms
+ }
+
+ public QueueExecutorThread(final PrioritizedTaskQueue<T> queue, final long spinWaitTime) { // in ms
+ this.queue = queue;
+ this.spinWaitTime = spinWaitTime;
+ }
+
+ @Override
+ public void run() {
+ final long spinWaitTime = this.spinWaitTime;
+ main_loop:
+ for (;;) {
+ this.pollTasks(true);
+
+ // spinwait
+
+ final long start = System.nanoTime();
+
+ 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
+
+ if (this.pollTasks(true)) {
+ // restart loop, found tasks
+ continue main_loop;
+ }
+
+ if (this.handleClose()) {
+ return; // we're done
+ }
+
+ if ((System.nanoTime() - start) >= spinWaitTime) {
+ break;
+ }
+ }
+
+ if (this.handleClose()) {
+ return;
+ }
+
+ this.parked.set(true);
2019-09-16 06:02:13 +02:00
+
2019-08-19 01:40:04 +02:00
+ // We need to parse here to avoid a race condition where a thread queues a task before we set parked to true
+ // (i.e it will not notify us)
+ if (this.pollTasks(true)) {
+ this.parked.set(false);
+ continue;
+ }
2019-09-16 06:02:13 +02:00
+
2019-08-19 01:40:04 +02:00
+ if (this.handleClose()) {
+ return;
+ }
+
+ // we don't need to check parked before sleeping, but we do need to check parked in a do-while loop
+ // LockSupport.park() can fail for any reason
+ do {
+ Thread.interrupted();
+ LockSupport.park("Waiting on tasks");
+ } while (this.parked.get());
+ }
+ }
+
+ protected boolean handleClose() {
+ if (this.closed) {
+ this.pollTasks(true); // this ensures we've emptied the queue
+ this.handleFlushThreads(true);
+ return true;
+ }
+ return false;
+ }
+
+ protected boolean pollTasks(boolean flushTasks) {
+ Runnable task;
+ boolean ret = false;
+
+ while ((task = this.queue.poll()) != null) {
+ ret = true;
+ try {
+ task.run();
+ } catch (final Throwable throwable) {
+ if (throwable instanceof ThreadDeath) {
+ throw (ThreadDeath)throwable;
+ }
+ LOGGER.fatal("Exception thrown from prioritized runnable task in thread '" + this.getName() + "': " + IOUtil.genericToString(task), throwable);
+ }
+ }
+
+ if (flushTasks) {
+ this.handleFlushThreads(false);
+ }
+
+ return ret;
+ }
+
+ protected void handleFlushThreads(final boolean shutdown) {
2019-09-16 06:02:13 +02:00
+ Thread parking;
+ ConcurrentLinkedQueue<Thread> flushQueue = this.flushQueue;
+ do {
+ ++flushCycles; // may be plain read opaque write
+ while ((parking = flushQueue.poll()) != null) {
+ LockSupport.unpark(parking);
+ }
+ } while (this.pollTasks(false));
2019-08-19 01:40:04 +02:00
+
2019-09-16 06:02:13 +02:00
+ if (shutdown) {
+ this.flushQueue = null;
2019-08-19 01:40:04 +02:00
+
2019-09-16 06:02:13 +02:00
+ // defend against a race condition where a flush thread double-checks right before we set to null
+ while ((parking = flushQueue.poll()) != null) {
+ LockSupport.unpark(parking);
+ }
2019-08-19 01:40:04 +02:00
+ }
+ }
+
+ /**
+ * Notify's this thread that a task has been added to its queue
+ * @return {@code true} if this thread was waiting for tasks, {@code false} if it is executing tasks
+ */
+ public boolean notifyTasks() {
+ if (this.parked.get() && this.parked.getAndSet(false)) {
+ LockSupport.unpark(this);
+ return true;
+ }
+ return false;
+ }
+
+ protected void queueTask(final T task) {
+ this.queue.add(task);
+ this.notifyTasks();
+ }
+
+ /**
+ * Waits until this thread's queue is empty.
+ *
+ * @throws IllegalStateException If the current thread is {@code this} thread.
+ */
+ public void flush() {
+ final Thread currentThread = Thread.currentThread();
+
+ if (currentThread == this) {
+ // avoid deadlock
+ throw new IllegalStateException("Cannot flush the queue executor thread while on the queue executor thread");
+ }
+
+ // order is important
+
2019-09-16 06:02:13 +02:00
+ int successes = 0;
+ long lastCycle = -1L;
2019-08-19 01:40:04 +02:00
+
2019-09-16 06:02:13 +02:00
+ do {
+ final ConcurrentLinkedQueue<Thread> flushQueue = this.flushQueue;
+ if (flushQueue == null) {
+ return;
+ }
2019-08-19 01:40:04 +02:00
+
2019-09-16 06:02:13 +02:00
+ flushQueue.add(currentThread);
2019-08-19 01:40:04 +02:00
+
2019-09-16 06:02:13 +02:00
+ // double check flush queue
+ if (this.flushQueue == null) {
+ return;
+ }
2019-08-19 01:40:04 +02:00
+
2019-09-16 06:02:13 +02:00
+ final long currentCycle = this.flushCycles; // may be opaque read
2019-08-19 01:40:04 +02:00
+
2019-09-16 06:02:13 +02:00
+ if (currentCycle == lastCycle) {
+ Thread.yield();
+ continue;
+ }
2019-08-19 01:40:04 +02:00
+
2019-09-16 06:02:13 +02:00
+ // force response
+ this.parked.set(false);
+ LockSupport.unpark(this);
2019-08-19 01:40:04 +02:00
+
2019-09-16 06:02:13 +02:00
+ LockSupport.park("flushing queue executor thread");
2019-08-19 01:40:04 +02:00
+
2019-09-16 06:02:13 +02:00
+ // returns whether there are tasks queued, does not return whether there are tasks executing
+ // this is why we cycle twice twice through flush (we know a pollTask call is made after a flush cycle)
+ // we really only need to guarantee that the tasks this thread has queued has gone through, and can leave
+ // tasks queued concurrently that are unsychronized with this thread as undefined behavior
+ if (this.queue.hasTasks()) {
+ successes = 0;
+ } else {
+ ++successes;
+ }
2019-08-19 01:40:04 +02:00
+
2019-09-16 06:02:13 +02:00
+ } while (successes != 2);
2019-08-19 01:40:04 +02:00
+
+ }
+
+ /**
+ * Closes this queue executor's queue and optionally waits for it to empty.
+ * <p>
+ * If wait is {@code true}, then the queue will be empty by the time this call completes.
+ * </p>
+ * <p>
+ * This function is MT-Safe.
+ * </p>
+ * @param wait If this call is to wait until the queue is empty
+ * @param killQueue Whether to shutdown this thread's queue
+ * @return whether this thread shut down the queue
+ */
+ public boolean close(final boolean wait, final boolean killQueue) {
+ boolean ret = !killQueue ? false : this.queue.shutdown();
+ this.closed = true;
+
+ // force thread to respond to the shutdown
+ this.parked.set(false);
+ LockSupport.unpark(this);
+
+ if (wait) {
+ this.flush();
+ }
+ return ret;
+ }
+}
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
2020-06-26 14:04:38 +02:00
index 0000000000000000000000000000000000000000..6367092663a213793367ccb65733afc119e4883c
2019-08-19 01:40:04 +02:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkLoadTask.java
2020-05-17 03:38:19 +02:00
@@ -0,0 +1,146 @@
2019-08-19 01:40:04 +02:00
+package com.destroystokyo.paper.io.chunk;
+
+import co.aikar.timings.Timing;
+import com.destroystokyo.paper.io.PaperFileIOThread;
+import com.destroystokyo.paper.io.IOUtil;
+import net.minecraft.server.ChunkCoordIntPair;
+import net.minecraft.server.ChunkRegionLoader;
+import net.minecraft.server.PlayerChunkMap;
+import net.minecraft.server.WorldServer;
+
+import java.util.ArrayDeque;
+import java.util.function.Consumer;
+
+public final class ChunkLoadTask extends ChunkTask {
+
+ public boolean cancelled;
+
+ Consumer<ChunkRegionLoader.InProgressChunkHolder> onComplete;
+ public PaperFileIOThread.ChunkData chunkData;
+
+ private boolean hasCompleted;
+
+ public ChunkLoadTask(final WorldServer world, final int chunkX, final int chunkZ, final int priority,
+ final ChunkTaskManager taskManager,
+ final Consumer<ChunkRegionLoader.InProgressChunkHolder> onComplete) {
+ super(world, chunkX, chunkZ, priority, taskManager);
+ this.onComplete = onComplete;
+ }
+
+ private static final ArrayDeque<Runnable> EMPTY_QUEUE = new ArrayDeque<>();
+
+ private static ChunkRegionLoader.InProgressChunkHolder createEmptyHolder() {
+ return new ChunkRegionLoader.InProgressChunkHolder(null, EMPTY_QUEUE);
+ }
+
+ @Override
+ public void run() {
+ try {
+ this.executeTask();
+ } catch (final Throwable ex) {
+ PaperFileIOThread.LOGGER.error("Failed to execute chunk load task: " + this.toString(), ex);
+ if (!this.hasCompleted) {
+ this.complete(ChunkLoadTask.createEmptyHolder());
+ }
+ }
+ }
+
+ private boolean checkCancelled() {
+ if (this.cancelled) {
+ // IntelliJ does not understand writes may occur to cancelled concurrently.
+ return this.taskManager.chunkLoadTasks.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 + ", current: " + ChunkLoadTask.this);
+ }
+
+ if (valueInMap.cancelled) {
+ return null;
+ }
+ return valueInMap;
+ }) == null;
+ }
+ return false;
+ }
+
+ public void executeTask() {
+ if (this.checkCancelled()) {
+ return;
+ }
+
+ // either executed synchronously or asynchronously
+ final PaperFileIOThread.ChunkData chunkData = this.chunkData;
+
+ if (chunkData.poiData == PaperFileIOThread.FAILURE_VALUE || chunkData.chunkData == PaperFileIOThread.FAILURE_VALUE) {
+ PaperFileIOThread.LOGGER.error("Could not load chunk for task: " + this.toString() + ", file IO thread has dumped the relevant exception above");
+ this.complete(ChunkLoadTask.createEmptyHolder());
+ return;
+ }
+
+ if (chunkData.chunkData == null) {
+ // not on disk
+ this.complete(ChunkLoadTask.createEmptyHolder());
+ return;
+ }
+
+ final ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(this.chunkX, this.chunkZ);
+
+ final PlayerChunkMap chunkManager = this.world.getChunkProvider().playerChunkMap;
+
2020-04-26 08:05:41 +02:00
+ try (Timing ignored = this.world.timings.chunkLoadLevelTimer.startTimingIfSync()) {
2019-08-19 01:40:04 +02:00
+ final ChunkRegionLoader.InProgressChunkHolder chunkHolder;
+
+ // apply fixes
+
+ try {
2020-06-26 14:04:38 +02:00
+ chunkData.chunkData = chunkManager.getChunkData(this.world.getTypeKey(),
2020-05-17 03:38:19 +02:00
+ chunkManager.getWorldPersistentDataSupplier(), chunkData.chunkData, chunkPos, this.world); // clone data for safety, file IO thread does not clone
2019-08-19 01:40:04 +02:00
+ } catch (final Throwable ex) {
+ PaperFileIOThread.LOGGER.error("Could not apply datafixers for chunk task: " + this.toString(), ex);
+ this.complete(ChunkLoadTask.createEmptyHolder());
+ }
+
+ if (this.checkCancelled()) {
+ return;
+ }
+
+ try {
+ this.world.getChunkProvider().playerChunkMap.updateChunkStatusOnDisk(chunkPos, chunkData.chunkData);
+ } catch (final Throwable ex) {
+ PaperFileIOThread.LOGGER.warn("Failed to update chunk status cache for task: " + this.toString(), ex);
+ // non-fatal, continue
+ }
+
+ try {
+ chunkHolder = ChunkRegionLoader.loadChunk(this.world,
+ chunkManager.definedStructureManager, chunkManager.getVillagePlace(), chunkPos,
+ chunkData.chunkData, true);
+ } catch (final Throwable ex) {
+ PaperFileIOThread.LOGGER.error("Could not de-serialize chunk data for task: " + this.toString(), ex);
+ this.complete(ChunkLoadTask.createEmptyHolder());
+ return;
+ }
+
+ this.complete(chunkHolder);
+ }
+ }
+
+ private void complete(final ChunkRegionLoader.InProgressChunkHolder holder) {
+ this.hasCompleted = true;
+ holder.poiData = this.chunkData == null ? null : this.chunkData.poiData;
+
+ this.taskManager.chunkLoadTasks.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 + ", current: " + ChunkLoadTask.this);
+ }
+ if (valueInMap.cancelled) {
+ return null;
+ }
+ try {
+ ChunkLoadTask.this.onComplete.accept(holder);
+ } catch (final Throwable thr) {
+ PaperFileIOThread.LOGGER.error("Failed to complete chunk data for task: " + this.toString(), thr);
+ }
+ return null;
+ });
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkSaveTask.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkSaveTask.java
new file mode 100644
2020-05-06 11:48:49 +02:00
index 0000000000000000000000000000000000000000..60312b85f9e7070378458f29afee0ebb7850d8a5
2019-08-19 01:40:04 +02:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkSaveTask.java
@@ -0,0 +1,112 @@
+package com.destroystokyo.paper.io.chunk;
+
+import co.aikar.timings.Timing;
+import com.destroystokyo.paper.io.PaperFileIOThread;
+import com.destroystokyo.paper.io.IOUtil;
+import com.destroystokyo.paper.io.PrioritizedTaskQueue;
+import net.minecraft.server.ChunkRegionLoader;
+import net.minecraft.server.IAsyncTaskHandler;
+import net.minecraft.server.IChunkAccess;
+import net.minecraft.server.NBTTagCompound;
+import net.minecraft.server.WorldServer;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public final class ChunkSaveTask extends ChunkTask {
+
+ public final ChunkRegionLoader.AsyncSaveData asyncSaveData;
+ public final IChunkAccess chunk;
+ public final CompletableFuture<NBTTagCompound> onComplete = new CompletableFuture<>();
+
+ private final AtomicInteger attemptedPriority;
+
+ public ChunkSaveTask(final WorldServer world, final int chunkX, final int chunkZ, final int priority,
+ final ChunkTaskManager taskManager, final ChunkRegionLoader.AsyncSaveData asyncSaveData,
+ final IChunkAccess chunk) {
+ super(world, chunkX, chunkZ, priority, taskManager);
+ this.chunk = chunk;
+ this.asyncSaveData = asyncSaveData;
+ this.attemptedPriority = new AtomicInteger(priority);
+ }
+
+ @Override
+ public void run() {
+ // can be executed asynchronously or synchronously
+ final NBTTagCompound compound;
+
+ try (Timing ignored = this.world.timings.chunkUnloadDataSave.startTimingIfSync()) {
+ compound = ChunkRegionLoader.saveChunk(this.world, this.chunk, this.asyncSaveData);
+ } catch (final Throwable ex) {
+ // has a plugin modified something it should not have and made us CME?
+ PaperFileIOThread.LOGGER.error("Failed to serialize unloading chunk data for task: " + this.toString() + ", falling back to a synchronous execution", ex);
+
+ // Note: We add to the server thread queue here since this is what the server will drain tasks from
+ // when waiting for chunks
+ ChunkTaskManager.queueChunkWaitTask(() -> {
+ try (Timing ignored = this.world.timings.chunkUnloadDataSave.startTiming()) {
+ NBTTagCompound data = PaperFileIOThread.FAILURE_VALUE;
+
+ try {
+ data = ChunkRegionLoader.saveChunk(this.world, this.chunk, this.asyncSaveData);
+ PaperFileIOThread.LOGGER.info("Successfully serialized chunk data for task: " + this.toString() + " synchronously");
+ } catch (final Throwable ex1) {
+ PaperFileIOThread.LOGGER.fatal("Failed to synchronously serialize unloading chunk data for task: " + this.toString() + "! Chunk data will be lost", ex1);
+ }
+
+ ChunkSaveTask.this.complete(data);
+ }
+ });
+
+ return; // the main thread will now complete the data
+ }
+
+ this.complete(compound);
+ }
+
+ @Override
+ public boolean raisePriority(final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalStateException("Invalid priority: " + priority);
+ }
+
+ // we know priority is valid here
+ for (int curr = this.attemptedPriority.get();;) {
+ if (curr <= priority) {
+ break; // curr is higher/same priority
+ }
+ if (this.attemptedPriority.compareAndSet(curr, priority)) {
+ break;
+ }
+ curr = this.attemptedPriority.get();
+ }
+
+ return super.raisePriority(priority);
+ }
+
+ @Override
+ public boolean updatePriority(final int priority) {
+ if (!PrioritizedTaskQueue.validPriority(priority)) {
+ throw new IllegalStateException("Invalid priority: " + priority);
+ }
+ this.attemptedPriority.set(priority);
+ return super.updatePriority(priority);
+ }
+
+ private void complete(final NBTTagCompound compound) {
+ try {
+ this.onComplete.complete(compound);
+ } catch (final Throwable thr) {
+ PaperFileIOThread.LOGGER.error("Failed to complete chunk data for task: " + this.toString(), thr);
+ }
+ if (compound != PaperFileIOThread.FAILURE_VALUE) {
+ PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world, this.chunkX, this.chunkZ, null, compound, this.attemptedPriority.get());
+ }
+ this.taskManager.chunkSaveTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(this.chunkX, this.chunkZ)), (final Long keyInMap, final ChunkSaveTask valueInMap) -> {
+ if (valueInMap != ChunkSaveTask.this) {
+ throw new IllegalStateException("Expected this task to be scheduled, but another was! Other: " + valueInMap + ", this: " + ChunkSaveTask.this);
+ }
+ return null;
+ });
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTask.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTask.java
new file mode 100644
2020-05-06 11:48:49 +02:00
index 0000000000000000000000000000000000000000..1dfa8abfd869ca97e4cc566d44e509b444dbcdc5
2019-08-19 01:40:04 +02:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTask.java
@@ -0,0 +1,40 @@
+package com.destroystokyo.paper.io.chunk;
+
+import com.destroystokyo.paper.io.PaperFileIOThread;
+import com.destroystokyo.paper.io.PrioritizedTaskQueue;
+import net.minecraft.server.WorldServer;
+
+abstract class ChunkTask extends PrioritizedTaskQueue.PrioritizedTask implements Runnable {
+
+ public final WorldServer world;
+ public final int chunkX;
+ public final int chunkZ;
+ public final ChunkTaskManager taskManager;
+
+ public ChunkTask(final WorldServer world, final int chunkX, final int chunkZ, final int priority,
+ final ChunkTaskManager taskManager) {
+ super(priority);
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.taskManager = taskManager;
+ }
+
+ @Override
+ public String toString() {
+ return "Chunk task: class:" + this.getClass().getName() + ", for world '" + this.world.getWorld().getName() +
+ "', (" + this.chunkX + "," + this.chunkZ + "), hashcode:" + this.hashCode() + ", priority: " + this.getPriority();
+ }
+
+ @Override
+ public boolean raisePriority(final int priority) {
+ PaperFileIOThread.Holder.INSTANCE.bumpPriority(this.world, this.chunkX, this.chunkZ, priority);
+ return super.raisePriority(priority);
+ }
+
+ @Override
+ public boolean updatePriority(final int priority) {
+ PaperFileIOThread.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/ChunkTaskManager.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
new file mode 100644
2020-06-26 14:04:38 +02:00
index 0000000000000000000000000000000000000000..49a594e5f345096cb3b9913b8ee2760795c5f012
2019-08-19 01:40:04 +02:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java
2020-05-23 01:03:48 +02:00
@@ -0,0 +1,511 @@
2019-08-19 01:40:04 +02:00
+package com.destroystokyo.paper.io.chunk;
+
+import com.destroystokyo.paper.io.PaperFileIOThread;
+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.IChunkAccess;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.NBTTagCompound;
2020-05-20 11:11:57 +02:00
+import net.minecraft.server.PlayerChunk;
2019-08-19 01:40:04 +02:00
+import net.minecraft.server.WorldServer;
2020-05-20 11:11:57 +02:00
+import org.apache.commons.lang.StringUtils;
2019-08-19 01:40:04 +02:00
+import org.apache.logging.log4j.Level;
+import org.bukkit.Bukkit;
+import org.spigotmc.AsyncCatcher;
+
+import java.util.ArrayDeque;
2020-05-22 06:46:44 +02:00
+import java.util.HashSet;
+import java.util.Set;
2019-08-19 01:40:04 +02:00
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.function.Consumer;
+
+public final class ChunkTaskManager {
+
+ private final QueueExecutorThread<ChunkTask>[] workers;
+ private final WorldServer world;
+
+ private final PrioritizedTaskQueue<ChunkTask> queue;
+ private final boolean perWorldQueue;
+
+ final ConcurrentHashMap<Long, ChunkLoadTask> chunkLoadTasks = new ConcurrentHashMap<>(64, 0.5f);
+ final ConcurrentHashMap<Long, ChunkSaveTask> chunkSaveTasks = new ConcurrentHashMap<>(64, 0.5f);
+
2019-09-02 03:30:25 +02:00
+ private final PrioritizedTaskQueue<ChunkTask> chunkTasks = new PrioritizedTaskQueue<>(); // used if async chunks are disabled in config
+
2019-08-19 01:40:04 +02:00
+ protected static QueueExecutorThread<ChunkTask>[] globalWorkers;
2020-04-11 10:40:00 +02:00
+ protected static QueueExecutorThread<ChunkTask> globalUrgentWorker;
2019-08-19 01:40:04 +02:00
+ protected static PrioritizedTaskQueue<ChunkTask> globalQueue;
2020-04-11 10:40:00 +02:00
+ protected static PrioritizedTaskQueue<ChunkTask> globalUrgentQueue;
2019-08-19 01:40:04 +02:00
+
+ protected static final ConcurrentLinkedQueue<Runnable> CHUNK_WAIT_QUEUE = new ConcurrentLinkedQueue<>();
+
+ public static final ArrayDeque<ChunkInfo> WAITING_CHUNKS = new ArrayDeque<>(); // stack
+
+ private static final class ChunkInfo {
+
+ public final int chunkX;
+ public final int chunkZ;
+ public final WorldServer world;
+
+ public ChunkInfo(final int chunkX, final int chunkZ, final WorldServer world) {
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.world = world;
+ }
+
+ @Override
+ public String toString() {
+ return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + this.world.getWorld().getName() + "']";
+ }
+ }
+
+ public static void pushChunkWait(final WorldServer world, final int chunkX, final int chunkZ) {
+ synchronized (WAITING_CHUNKS) {
+ WAITING_CHUNKS.push(new ChunkInfo(chunkX, chunkZ, world));
+ }
+ }
+
+ public static void popChunkWait() {
+ synchronized (WAITING_CHUNKS) {
+ WAITING_CHUNKS.pop();
+ }
+ }
+
2020-05-23 01:03:48 +02:00
+ private static ChunkInfo[] getChunkInfos() {
+ ChunkInfo[] chunks;
2019-08-19 01:40:04 +02:00
+ synchronized (WAITING_CHUNKS) {
2020-05-23 01:03:48 +02:00
+ chunks = WAITING_CHUNKS.toArray(new ChunkInfo[0]);
2019-08-19 01:40:04 +02:00
+ }
2020-05-23 01:03:48 +02:00
+ return chunks;
2019-08-19 01:40:04 +02:00
+ }
+
+ public static void dumpAllChunkLoadInfo() {
2020-05-23 01:03:48 +02:00
+ ChunkInfo[] chunks = getChunkInfos();
+ if (chunks.length > 0) {
2019-08-19 01:40:04 +02:00
+ PaperFileIOThread.LOGGER.log(Level.ERROR, "Chunk wait task info below: ");
+
2020-05-23 01:03:48 +02:00
+ for (final ChunkInfo chunkInfo : chunks) {
2019-09-30 03:23:09 +02:00
+ final long key = IOUtil.getCoordinateKey(chunkInfo.chunkX, chunkInfo.chunkZ);
+ final ChunkLoadTask loadTask = chunkInfo.world.asyncChunkTaskManager.chunkLoadTasks.get(key);
+ final ChunkSaveTask saveTask = chunkInfo.world.asyncChunkTaskManager.chunkSaveTasks.get(key);
2019-08-19 01:40:04 +02:00
+
+ PaperFileIOThread.LOGGER.log(Level.ERROR, chunkInfo.chunkX + "," + chunkInfo.chunkZ + " in '" + chunkInfo.world.getWorld().getName() + ":");
+ PaperFileIOThread.LOGGER.log(Level.ERROR, "Load Task - " + (loadTask == null ? "none" : loadTask.toString()));
+ PaperFileIOThread.LOGGER.log(Level.ERROR, "Save Task - " + (saveTask == null ? "none" : saveTask.toString()));
2019-09-30 03:23:09 +02:00
+ // log current status of chunk to indicate whether we're waiting on generation or loading
+ net.minecraft.server.PlayerChunk chunkHolder = chunkInfo.world.getChunkProvider().playerChunkMap.getVisibleChunk(key);
+
2020-05-23 01:03:48 +02:00
+ dumpChunkInfo(new HashSet<>(), chunkHolder, chunkInfo.chunkX, chunkInfo.chunkZ);
2019-08-19 01:40:04 +02:00
+ }
+ }
+ }
+
2020-05-22 06:46:44 +02:00
+ static void dumpChunkInfo(Set<PlayerChunk> seenChunks, PlayerChunk chunkHolder, int x, int z) {
2020-05-23 01:03:48 +02:00
+ dumpChunkInfo(seenChunks, chunkHolder, x, z, 0, 1);
2020-05-20 11:11:57 +02:00
+ }
2020-05-23 01:03:48 +02:00
+
+ static void dumpChunkInfo(Set<PlayerChunk> seenChunks, PlayerChunk chunkHolder, int x, int z, int indent, int maxDepth) {
2020-05-22 06:46:44 +02:00
+ if (seenChunks.contains(chunkHolder)) {
+ return;
+ }
2020-05-23 01:03:48 +02:00
+ if (indent > maxDepth) {
+ return;
+ }
2020-05-22 06:46:44 +02:00
+ seenChunks.add(chunkHolder);
2020-05-20 11:11:57 +02:00
+ String indentStr = StringUtils.repeat(" ", indent);
+ if (chunkHolder == null) {
+ PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Holder - null for (" + x +"," + z +")");
+ } else {
+ IChunkAccess chunk = chunkHolder.getAvailableChunkNow();
+ net.minecraft.server.ChunkStatus holderStatus = chunkHolder.getChunkHolderStatus();
+ PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Holder - non-null");
+ PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Status - " + ((chunk == null) ? "null chunk" : chunk.getChunkStatus().toString()));
+ PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Ticket Status - " + PlayerChunk.getChunkStatus(chunkHolder.getTicketLevel()));
+ PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Holder Status - " + ((holderStatus == null) ? "null" : holderStatus.toString()));
+ }
+ }
+
2019-08-19 01:40:04 +02:00
+ public static void initGlobalLoadThreads(int threads) {
+ if (threads <= 0 || globalWorkers != null) {
+ return;
+ }
+
+ globalWorkers = new QueueExecutorThread[threads];
+ globalQueue = new PrioritizedTaskQueue<>();
2020-04-11 10:40:00 +02:00
+ globalUrgentQueue = new PrioritizedTaskQueue<>();
2019-08-19 01:40:04 +02:00
+
+ for (int i = 0; i < threads; ++i) {
+ globalWorkers[i] = new QueueExecutorThread<>(globalQueue, (long)0.10e6); //0.1ms
+ globalWorkers[i].setName("Paper Async Chunk Task Thread #" + i);
+ globalWorkers[i].setPriority(Thread.NORM_PRIORITY - 1);
+ globalWorkers[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
+ PaperFileIOThread.LOGGER.fatal("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable);
+ });
+
+ globalWorkers[i].start();
+ }
2020-04-11 10:40:00 +02:00
+
+ globalUrgentWorker = new QueueExecutorThread<>(globalUrgentQueue, (long)0.10e6); //0.1ms
+ globalUrgentWorker.setName("Paper Async Chunk Urgent Task Thread");
+ globalUrgentWorker.setPriority(Thread.NORM_PRIORITY+1);
+ globalUrgentWorker.setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
+ PaperFileIOThread.LOGGER.fatal("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable);
+ });
+
+ globalUrgentWorker.start();
2019-08-19 01:40:04 +02:00
+ }
+
+ /**
+ * Creates this chunk task manager to operate off the specified number of threads. If the specified number of threads is
+ * less-than or equal to 0, then this chunk task manager will operate off of the world's chunk task queue.
+ * @param world Specified world.
+ * @param threads Specified number of threads.
+ * @see net.minecraft.server.ChunkProviderServer#serverThreadQueue
+ */
+ public ChunkTaskManager(final WorldServer world, final int threads) {
+ this.world = world;
+ this.workers = threads <= 0 ? null : new QueueExecutorThread[threads];
+ this.queue = new PrioritizedTaskQueue<>();
+ this.perWorldQueue = true;
+
+ for (int i = 0; i < threads; ++i) {
+ this.workers[i] = new QueueExecutorThread<>(this.queue, (long)0.10e6); //0.1ms
2020-06-26 14:04:38 +02:00
+ this.workers[i].setName("Async chunk loader thread #" + i + " for world: " + world.getWorld().getName());
2019-08-19 01:40:04 +02:00
+ this.workers[i].setPriority(Thread.NORM_PRIORITY - 1);
+ this.workers[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
+ PaperFileIOThread.LOGGER.fatal("Thread '" + thread.getName() + "' threw an uncaught exception!", throwable);
+ });
+
+ this.workers[i].start();
+ }
+ }
+
+ /**
2019-09-02 03:30:25 +02:00
+ * Creates the chunk task manager to work from the global workers. When {@link #close(boolean)} is invoked,
+ * the global queue is not shutdown. If the global workers is configured to be disabled or use 0 threads, then
+ * this chunk task manager will operate off of the world's chunk task queue.
+ * @param world The world that this task manager is responsible for
+ * @see net.minecraft.server.ChunkProviderServer#serverThreadQueue
+ */
+ public ChunkTaskManager(final WorldServer world) {
+ this.world = world;
+ this.workers = globalWorkers;
+ this.queue = globalQueue;
+ this.perWorldQueue = false;
+ }
+
+ public boolean pollNextChunkTask() {
+ final ChunkTask task = this.chunkTasks.poll();
+
+ if (task != null) {
+ task.run();
+ return true;
+ }
+ return false;
+ }
+
+ /**
2019-08-19 01:40:04 +02:00
+ * Polls and runs the next available chunk wait queue task. This is to be used when the server is waiting on a chunk queue.
+ * (per-world can cause issues if all the worker threads are blocked waiting for a response from the main thread)
+ */
+ public static boolean pollChunkWaitQueue() {
+ final Runnable run = CHUNK_WAIT_QUEUE.poll();
+ if (run != null) {
+ run.run();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Queues a chunk wait task. Note that this will execute out of order with respect to tasks scheduled on a world's
+ * chunk task queue, since this is the global chunk wait queue.
+ */
+ public static void queueChunkWaitTask(final Runnable runnable) {
+ CHUNK_WAIT_QUEUE.add(runnable);
+ }
+
+ private static void drainChunkWaitQueue() {
+ Runnable run;
+ while ((run = CHUNK_WAIT_QUEUE.poll()) != null) {
+ run.run();
+ }
+ }
+
+ /**
+ * The exact same as {@link #scheduleChunkLoad(int, int, int, Consumer, boolean)}, except that the chunk data is provided as
+ * the {@code data} parameter.
+ */
+ public ChunkLoadTask scheduleChunkLoad(final int chunkX, final int chunkZ, final int priority,
+ final Consumer<ChunkRegionLoader.InProgressChunkHolder> onComplete,
+ final boolean intendingToBlock, final CompletableFuture<NBTTagCompound> dataFuture) {
+ final WorldServer world = this.world;
+
+ return this.chunkLoadTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
+ if (valueInMap != null) {
+ if (!valueInMap.cancelled) {
+ throw new IllegalStateException("Double scheduling chunk load for task: " + valueInMap.toString());
+ }
+ valueInMap.cancelled = false;
+ valueInMap.onComplete = onComplete;
+ return valueInMap;
+ }
+
+ final ChunkLoadTask ret = new ChunkLoadTask(world, chunkX, chunkZ, priority, ChunkTaskManager.this, onComplete);
+
+ dataFuture.thenAccept((final NBTTagCompound data) -> {
+ final boolean failed = data == PaperFileIOThread.FAILURE_VALUE;
+ PaperFileIOThread.Holder.INSTANCE.loadChunkDataAsync(world, chunkX, chunkZ, priority, (final PaperFileIOThread.ChunkData chunkData) -> {
+ ret.chunkData = chunkData;
+ if (!failed) {
+ chunkData.chunkData = data;
+ }
+ ChunkTaskManager.this.internalSchedule(ret); // only schedule to the worker threads here
+ }, true, failed, intendingToBlock); // read data off disk if the future fails
+ });
+
+ return ret;
+ });
+ }
+
+ public void cancelChunkLoad(final int chunkX, final int chunkZ) {
+ this.chunkLoadTasks.compute(IOUtil.getCoordinateKey(chunkX, chunkZ), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
+ if (valueInMap == null) {
+ return null;
+ }
+
+ if (valueInMap.cancelled) {
+ PaperFileIOThread.LOGGER.warn("Task " + valueInMap.toString() + " is already cancelled!");
+ }
+ valueInMap.cancelled = true;
+ if (valueInMap.cancel()) {
+ return null;
+ }
+
+ return valueInMap;
+ });
+ }
+
+ /**
+ * Schedules an asynchronous chunk load for the specified coordinates. The onComplete parameter may be invoked asynchronously
+ * on a worker thread or on the world's chunk executor queue. As such the code that is executed for the parameter should be
+ * carefully chosen.
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param priority Priority for this task
+ * @param onComplete The consumer to invoke with the {@link net.minecraft.server.ChunkRegionLoader.InProgressChunkHolder} object once this task is complete
+ * @param intendingToBlock Whether the caller is intending to block on this task completing (this is a performance tune, and has no adverse side-effects)
+ * @return The {@link ChunkLoadTask} associated with
+ */
+ public ChunkLoadTask scheduleChunkLoad(final int chunkX, final int chunkZ, final int priority,
+ final Consumer<ChunkRegionLoader.InProgressChunkHolder> onComplete,
+ final boolean intendingToBlock) {
+ final WorldServer world = this.world;
+
+ return this.chunkLoadTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkLoadTask valueInMap) -> {
+ if (valueInMap != null) {
+ if (!valueInMap.cancelled) {
+ throw new IllegalStateException("Double scheduling chunk load for task: " + valueInMap.toString());
+ }
+ valueInMap.cancelled = false;
+ valueInMap.onComplete = onComplete;
+ return valueInMap;
+ }
+
+ final ChunkLoadTask ret = new ChunkLoadTask(world, chunkX, chunkZ, priority, ChunkTaskManager.this, onComplete);
+
+ PaperFileIOThread.Holder.INSTANCE.loadChunkDataAsync(world, chunkX, chunkZ, priority, (final PaperFileIOThread.ChunkData chunkData) -> {
+ ret.chunkData = chunkData;
+ ChunkTaskManager.this.internalSchedule(ret); // only schedule to the worker threads here
+ }, true, true, intendingToBlock);
+
+ return ret;
+ });
+ }
+
+ /**
+ * Schedules an async save for the specified chunk. The chunk, at the beginning of this call, must be completely unloaded
+ * from the world.
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @param priority Priority for this task
+ * @param asyncSaveData Async save data. See {@link ChunkRegionLoader#getAsyncSaveData(WorldServer, IChunkAccess)}
+ * @param chunk Chunk to save
+ * @return The {@link ChunkSaveTask} associated with the save task.
+ */
+ public ChunkSaveTask scheduleChunkSave(final int chunkX, final int chunkZ, final int priority,
+ final ChunkRegionLoader.AsyncSaveData asyncSaveData,
+ final IChunkAccess chunk) {
+ AsyncCatcher.catchOp("chunk save schedule");
+
+ final WorldServer world = this.world;
+
+ return this.chunkSaveTasks.compute(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)), (final Long keyInMap, final ChunkSaveTask valueInMap) -> {
+ if (valueInMap != null) {
+ throw new IllegalStateException("Double scheduling chunk save for task: " + valueInMap.toString());
+ }
+
+ final ChunkSaveTask ret = new ChunkSaveTask(world, chunkX, chunkZ, priority, ChunkTaskManager.this, asyncSaveData, chunk);
+
+ ChunkTaskManager.this.internalSchedule(ret);
+
+ return ret;
+ });
+ }
+
+ /**
+ * Returns a completable future which will be completed with the <b>un-copied</b> chunk data for an in progress async save.
+ * Returns {@code null} if no save is in progress.
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ */
+ public CompletableFuture<NBTTagCompound> getChunkSaveFuture(final int chunkX, final int chunkZ) {
+ final ChunkSaveTask chunkSaveTask = this.chunkSaveTasks.get(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)));
+ if (chunkSaveTask == null) {
+ return null;
+ }
+ return chunkSaveTask.onComplete;
+ }
+
+ /**
+ * Returns the chunk object being used to serialize data async for an unloaded chunk. Note that modifying this chunk
+ * is not safe to do as another thread is handling its save. The chunk is also not loaded into the world.
+ * @param chunkX Chunk's x coordinate
+ * @param chunkZ Chunk's z coordinate
+ * @return Chunk object for an in-progress async save, or {@code null} if no save is in progress
+ */
+ public IChunkAccess getChunkInSaveProgress(final int chunkX, final int chunkZ) {
+ final ChunkSaveTask chunkSaveTask = this.chunkSaveTasks.get(Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ)));
+ if (chunkSaveTask == null) {
+ return null;
+ }
+ return chunkSaveTask.chunk;
+ }
+
+ public void flush() {
+ // flush here since we schedule tasks on the IO thread that can schedule tasks here
+ drainChunkWaitQueue();
+ PaperFileIOThread.Holder.INSTANCE.flush();
+ drainChunkWaitQueue();
+
+ if (this.workers == null) {
2020-04-12 21:56:03 +02:00
+ if (Bukkit.isPrimaryThread() || MinecraftServer.getServer().hasStopped()) {
2019-08-19 01:40:04 +02:00
+ ((IAsyncTaskHandler<Runnable>)this.world.getChunkProvider().serverThreadQueue).executeAll();
+ } else {
+ CompletableFuture<Void> wait = new CompletableFuture<>();
+ MinecraftServer.getServer().scheduleOnMain(() -> {
+ ((IAsyncTaskHandler<Runnable>)this.world.getChunkProvider().serverThreadQueue).executeAll();
+ });
+ wait.join();
+ }
2019-09-01 23:18:40 +02:00
+ } else {
+ for (final QueueExecutorThread<ChunkTask> worker : this.workers) {
+ worker.flush();
+ }
2019-08-19 01:40:04 +02:00
+ }
2020-04-12 06:42:28 +02:00
+ if (globalUrgentWorker != null) globalUrgentWorker.flush();
2019-08-19 01:40:04 +02:00
+
+ // flush again since tasks we execute async saves
+ drainChunkWaitQueue();
+ PaperFileIOThread.Holder.INSTANCE.flush();
+ }
+
+ public void close(final boolean wait) {
+ // flush here since we schedule tasks on the IO thread that can schedule tasks to this task manager
+ // we do this regardless of the wait param since after we invoke close no tasks can be queued
+ PaperFileIOThread.Holder.INSTANCE.flush();
+
+ if (this.workers == null) {
+ if (wait) {
+ this.flush();
+ }
+ return;
+ }
+
+ if (this.workers != globalWorkers) {
+ for (final QueueExecutorThread<ChunkTask> worker : this.workers) {
+ worker.close(false, this.perWorldQueue);
+ }
+ }
+
+ if (wait) {
+ this.flush();
+ }
+ }
+
+ public void raisePriority(final int chunkX, final int chunkZ, final int priority) {
+ final Long chunkKey = Long.valueOf(IOUtil.getCoordinateKey(chunkX, chunkZ));
+
2020-04-11 10:40:00 +02:00
+ ChunkTask chunkSaveTask = this.chunkSaveTasks.get(chunkKey);
2019-08-19 01:40:04 +02:00
+ if (chunkSaveTask != null) {
2020-04-11 10:40:00 +02:00
+ // don't bump save into urgent queue
+ raiseTaskPriority(chunkSaveTask, priority != PrioritizedTaskQueue.HIGHEST_PRIORITY ? priority : PrioritizedTaskQueue.HIGH_PRIORITY);
2019-08-19 01:40:04 +02:00
+ }
+
+ ChunkLoadTask chunkLoadTask = this.chunkLoadTasks.get(chunkKey);
+ if (chunkLoadTask != null) {
2020-04-11 10:40:00 +02:00
+ raiseTaskPriority(chunkLoadTask, priority);
+ }
+ }
+
+ private void raiseTaskPriority(ChunkTask task, int priority) {
+ final boolean raised = task.raisePriority(priority);
2020-04-11 23:15:41 +02:00
+ if (task.isScheduled() && raised && this.workers != null) {
2020-04-11 10:40:00 +02:00
+ // only notify if we're in queue to be executed
+ if (priority == PrioritizedTaskQueue.HIGHEST_PRIORITY) {
+ // was in another queue but became urgent later, add to urgent queue and the previous
+ // queue will just have to ignore this task if it has already been started.
+ // Ultimately, we now have 2 potential queues that can pull it out whoever gets it first
+ // but the urgent queue has dedicated thread(s) so it's likely to win....
+ globalUrgentQueue.add(task);
+ this.internalScheduleNotifyUrgent();
+ } else {
2019-08-19 01:40:04 +02:00
+ this.internalScheduleNotify();
+ }
+ }
+ }
+
+ protected void internalSchedule(final ChunkTask task) {
+ if (this.workers == null) {
2019-09-02 03:30:25 +02:00
+ this.chunkTasks.add(task);
2019-08-19 01:40:04 +02:00
+ return;
+ }
+
+ // It's important we order the task to be executed before notifying. Avoid a race condition where the worker thread
+ // wakes up and goes to sleep before we actually schedule (or it's just about to sleep)
2020-04-11 10:40:00 +02:00
+ if (task.getPriority() == PrioritizedTaskQueue.HIGHEST_PRIORITY) {
+ globalUrgentQueue.add(task);
+ this.internalScheduleNotifyUrgent();
+ } else {
+ this.queue.add(task);
+ this.internalScheduleNotify();
+ }
+
2019-08-19 01:40:04 +02:00
+ }
+
+ protected void internalScheduleNotify() {
2019-09-29 22:29:56 +02:00
+ if (this.workers == null) {
+ return;
+ }
2019-08-19 01:40:04 +02:00
+ for (final QueueExecutorThread<ChunkTask> worker : this.workers) {
+ if (worker.notifyTasks()) {
+ // break here since we only want to wake up one worker for scheduling one task
+ break;
+ }
+ }
+ }
+
2020-04-11 10:40:00 +02:00
+
+ protected void internalScheduleNotifyUrgent() {
+ if (globalUrgentWorker == null) {
+ return;
+ }
+ globalUrgentWorker.notifyTasks();
+ }
+
2019-08-19 01:40:04 +02:00
+}
diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
2020-06-26 01:38:24 +02:00
index f2ff1aa915c218bb1fc72467ccbd73ccf135c494..edd901bb53385fa3d189a0057d57f98bf8b7115c 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
2020-06-26 01:38:24 +02:00
@@ -310,11 +310,138 @@ public class ChunkProviderServer extends IChunkProvider {
2019-08-19 01:40:04 +02:00
return playerChunk.getAvailableChunkNow();
}
+
+ private long asyncLoadSeqCounter;
+
2020-05-10 04:30:28 +02:00
+ public CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> getChunkAtAsynchronously(int x, int z, boolean gen, boolean isUrgent) {
2019-08-19 01:40:04 +02:00
+ if (Thread.currentThread() != this.serverThread) {
2020-05-10 04:30:28 +02:00
+ CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> future = new CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>>();
2019-08-19 01:40:04 +02:00
+ this.serverThreadQueue.execute(() -> {
2020-05-10 04:30:28 +02:00
+ this.getChunkAtAsynchronously(x, z, gen, isUrgent).whenComplete((chunk, ex) -> {
+ if (ex != null) {
+ future.completeExceptionally(ex);
+ } else {
+ future.complete(chunk);
+ }
+ });
2019-08-19 01:40:04 +02:00
+ });
2020-05-10 04:30:28 +02:00
+ return future;
+ }
+
+ if (!com.destroystokyo.paper.PaperConfig.asyncChunks) {
2020-05-22 06:46:44 +02:00
+ world.getWorld().loadChunk(x, z, gen);
+ Chunk chunk = getChunkAtIfLoadedMainThread(x, z);
2020-05-14 10:58:47 +02:00
+ return CompletableFuture.completedFuture(chunk != null ? Either.left(chunk) : PlayerChunk.UNLOADED_CHUNK_ACCESS);
2019-08-19 01:40:04 +02:00
+ }
+
+ 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;
+
2020-05-10 04:30:28 +02:00
+ return CompletableFuture.completedFuture(Either.left(ichunkaccess));
2019-08-19 01:40:04 +02:00
+ }
+ }
+ }
+
+ if (gen) {
2020-05-10 04:30:28 +02:00
+ return this.bringToFullStatusAsync(x, z, chunkPos, isUrgent);
2019-08-19 01:40:04 +02:00
+ }
+
+ 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)) {
2020-05-22 06:46:44 +02:00
+ return CompletableFuture.completedFuture(PlayerChunk.UNLOADED_CHUNK_ACCESS);
2019-08-19 01:40:04 +02:00
+ }
+ // we know the chunk is at full status here (either in read-only mode or the real thing)
2020-05-10 04:30:28 +02:00
+ return this.bringToFullStatusAsync(x, z, chunkPos, isUrgent);
2019-08-19 01:40:04 +02:00
+ }
+
+ ChunkStatus status = world.getChunkProvider().playerChunkMap.getStatusOnDiskNoLoad(x, z);
+
+ if (status != null && status != ChunkStatus.FULL) {
+ // does not exist on disk
2020-05-14 10:58:47 +02:00
+ return CompletableFuture.completedFuture(PlayerChunk.UNLOADED_CHUNK_ACCESS);
2019-08-19 01:40:04 +02:00
+ }
+
+ if (status == ChunkStatus.FULL) {
2020-05-10 04:30:28 +02:00
+ return this.bringToFullStatusAsync(x, z, chunkPos, isUrgent);
2019-08-19 01:40:04 +02:00
+ }
+
+ // status is null here
+
+ // here we don't know what status it is and we're not supposed to generate
+ // so we asynchronously load empty status
2020-05-10 04:30:28 +02:00
+ return this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.EMPTY, isUrgent).thenCompose((either) -> {
+ IChunkAccess chunk = either.left().orElse(null);
+ if (!(chunk instanceof ProtoChunkExtension) && !(chunk instanceof Chunk)) {
2019-08-19 01:40:04 +02:00
+ // the chunk on disk was not a full status chunk
2020-05-14 10:58:47 +02:00
+ return CompletableFuture.completedFuture(PlayerChunk.UNLOADED_CHUNK_ACCESS);
2019-08-19 01:40:04 +02:00
+ }
2020-05-10 04:30:28 +02:00
+ ; // bring to full status if required
+ return this.bringToFullStatusAsync(x, z, chunkPos, isUrgent);
2019-08-19 01:40:04 +02:00
+ });
+ }
+
2020-05-10 04:30:28 +02:00
+ private CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> bringToFullStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, boolean isUrgent) {
+ return this.bringToStatusAsync(x, z, chunkPos, ChunkStatus.FULL, isUrgent);
2019-08-19 01:40:04 +02:00
+ }
+
2020-05-10 04:30:28 +02:00
+ private CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> bringToStatusAsync(int x, int z, ChunkCoordIntPair chunkPos, ChunkStatus status, boolean isUrgent) {
2020-05-11 05:31:36 +02:00
+ CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> future = this.getChunkFutureMainThread(x, z, status, true, isUrgent);
2019-08-19 01:40:04 +02:00
+ Long identifier = Long.valueOf(this.asyncLoadSeqCounter++);
+ int ticketLevel = MCUtil.getTicketLevelFor(status);
+ this.addTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier);
+
2020-05-10 04:30:28 +02:00
+ return future.thenComposeAsync((Either<IChunkAccess, PlayerChunk.Failure> either) -> {
2019-08-19 01:40:04 +02:00
+ // either left -> success
+ // either right -> failure
+
+ this.removeTicketAtLevel(TicketType.ASYNC_LOAD, chunkPos, ticketLevel, identifier);
+ this.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); // allow unloading
+
+ Optional<PlayerChunk.Failure> failure = either.right();
+
+ if (failure.isPresent()) {
+ // failure
+ throw new IllegalStateException("Chunk failed to load: " + failure.get().toString());
+ }
+
2020-05-10 04:30:28 +02:00
+ return CompletableFuture.completedFuture(either);
2019-08-19 01:40:04 +02:00
+ }, this.serverThreadQueue);
+ }
+
+ public <T> void addTicketAtLevel(TicketType<T> ticketType, ChunkCoordIntPair chunkPos, int ticketLevel, T identifier) {
+ this.chunkMapDistance.addTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier);
+ }
+
+ public <T> void removeTicketAtLevel(TicketType<T> 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) {
+ final int x = i; final int z = j; // Paper - conflict on variable change
if (Thread.currentThread() != this.serverThread) {
return (IChunkAccess) CompletableFuture.supplyAsync(() -> {
return this.getChunkAt(i, j, chunkstatus, flag);
2020-06-26 01:38:24 +02:00
@@ -337,11 +464,16 @@ public class ChunkProviderServer extends IChunkProvider {
2020-05-11 05:31:36 +02:00
}
gameprofilerfiller.c("getChunkCacheMiss");
- CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> completablefuture = this.getChunkFutureMainThread(i, j, chunkstatus, flag);
+ CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> completablefuture = this.getChunkFutureMainThread(i, j, chunkstatus, flag, true); // Paper
2019-08-19 01:40:04 +02:00
if (!completablefuture.isDone()) { // Paper
+ // Paper start - async chunk io/loading
+ this.world.asyncChunkTaskManager.raisePriority(x, z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY);
+ com.destroystokyo.paper.io.chunk.ChunkTaskManager.pushChunkWait(this.world, x, z);
+ // Paper end
2020-04-26 08:05:41 +02:00
this.world.timings.syncChunkLoad.startTiming(); // Paper
2019-08-19 01:40:04 +02:00
this.serverThreadQueue.awaitTasks(completablefuture::isDone);
+ com.destroystokyo.paper.io.chunk.ChunkTaskManager.popChunkWait(); // Paper - async chunk debug
2020-04-26 08:05:41 +02:00
this.world.timings.syncChunkLoad.stopTiming(); // Paper
2019-08-19 01:40:04 +02:00
} // Paper
ichunkaccess = (IChunkAccess) ((Either) completablefuture.join()).map((ichunkaccess1) -> {
2020-06-26 01:38:24 +02:00
@@ -407,6 +539,11 @@ public class ChunkProviderServer extends IChunkProvider {
2020-05-11 05:31:36 +02:00
}
private CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> getChunkFutureMainThread(int i, int j, ChunkStatus chunkstatus, boolean flag) {
+ // Paper start - add isUrgent - old sig left in place for dirty nms plugins
+ return getChunkFutureMainThread(i, j, chunkstatus, flag, false);
+ }
+ private CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> getChunkFutureMainThread(int i, int j, ChunkStatus chunkstatus, boolean flag, boolean isUrgent) {
+ // Paper end
ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(i, j);
long k = chunkcoordintpair.pair();
int l = 33 + ChunkStatus.a(chunkstatus);
2020-06-26 01:38:24 +02:00
@@ -806,11 +943,12 @@ public class ChunkProviderServer extends IChunkProvider {
2019-08-19 01:40:04 +02:00
protected boolean executeNext() {
// CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
try {
2019-09-02 03:30:25 +02:00
+ boolean execChunkTask = com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ChunkProviderServer.this.world.asyncChunkTaskManager.pollNextChunkTask(); // Paper
2019-08-19 01:40:04 +02:00
if (ChunkProviderServer.this.tickDistanceManager()) {
return true;
} else {
ChunkProviderServer.this.lightEngine.queueUpdate();
- return super.executeNext();
+ return super.executeNext() || execChunkTask; // Paper
}
} finally {
playerChunkMap.callbackExecutor.run();
diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
2020-06-26 01:38:24 +02:00
index be3c7a8697c540d03a143b9306311a184474857f..4d6e8f987233ca6c5f53d004031c022bb2d43e1e 100644
2019-08-19 01:40:04 +02:00
--- 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;
2020-06-26 01:38:24 +02:00
@@ -23,7 +24,29 @@ public class ChunkRegionLoader {
2019-08-19 01:40:04 +02:00
private static final Logger LOGGER = LogManager.getLogger();
+ // Paper start
+ public static final class InProgressChunkHolder {
+
+ public final ProtoChunk protoChunk;
+ public final ArrayDeque<Runnable> tasks;
+
+ public NBTTagCompound poiData;
+
+ public InProgressChunkHolder(final ProtoChunk protoChunk, final ArrayDeque<Runnable> 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<Runnable> tasksToExecuteOnMain = new ArrayDeque<>();
+ // Paper end
2020-06-26 01:38:24 +02:00
ChunkGenerator chunkgenerator = worldserver.getChunkProvider().getChunkGenerator();
2019-08-19 01:40:04 +02:00
WorldChunkManager worldchunkmanager = chunkgenerator.getWorldChunkManager();
NBTTagCompound nbttagcompound1 = nbttagcompound.getCompound("Level");
2020-06-26 01:38:24 +02:00
@@ -50,7 +73,9 @@ public class ChunkRegionLoader {
2019-08-19 01:40:04 +02:00
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
}
2019-12-12 21:58:07 +01:00
for (int i = 0; i < nbttaglist.size(); ++i) {
2020-06-26 01:38:24 +02:00
@@ -66,16 +91,28 @@ public class ChunkRegionLoader {
2019-08-19 01:40:04 +02:00
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)) {
2020-06-26 01:38:24 +02:00
- lightengine.a(EnumSkyBlock.BLOCK, SectionPosition.a(chunkcoordintpair, b0), new NibbleArray(nbttagcompound2.getByteArray("BlockLight")), true);
2019-08-19 01:40:04 +02:00
+ // Paper start - delay this task since we're executing off-main
+ NibbleArray blockLight = new NibbleArray(nbttagcompound2.getByteArray("BlockLight"));
+ tasksToExecuteOnMain.add(() -> {
2020-06-26 01:38:24 +02:00
+ lightengine.a(EnumSkyBlock.BLOCK, SectionPosition.a(chunkcoordintpair, b0), blockLight, true);
2019-08-19 01:40:04 +02:00
+ });
2020-06-26 01:38:24 +02:00
+ // Paper end - delay this task since we're executing off-main
2019-08-19 01:40:04 +02:00
}
if (flag2 && nbttagcompound2.hasKeyOfType("SkyLight", 7)) {
2020-06-26 01:38:24 +02:00
- lightengine.a(EnumSkyBlock.SKY, SectionPosition.a(chunkcoordintpair, b0), new NibbleArray(nbttagcompound2.getByteArray("SkyLight")), true);
2019-08-19 01:40:04 +02:00
+ // Paper start - delay this task since we're executing off-main
+ NibbleArray skyLight = new NibbleArray(nbttagcompound2.getByteArray("SkyLight"));
+ tasksToExecuteOnMain.add(() -> {
2020-06-26 01:38:24 +02:00
+ lightengine.a(EnumSkyBlock.SKY, SectionPosition.a(chunkcoordintpair, b0), skyLight, true);
2019-08-19 01:40:04 +02:00
+ });
2020-06-26 01:38:24 +02:00
+ // Paper end - delay this task since we're executing off-main
2019-08-19 01:40:04 +02:00
}
}
}
2020-06-26 01:38:24 +02:00
@@ -178,7 +215,7 @@ public class ChunkRegionLoader {
2019-08-19 01:40:04 +02:00
}
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;
2020-06-26 01:38:24 +02:00
@@ -217,11 +254,83 @@ public class ChunkRegionLoader {
2019-08-19 01:40:04 +02:00
protochunk1.a(worldgenstage_features, BitSet.valueOf(nbttagcompound5.getByteArray(s1)));
}
- return protochunk1;
+ return new InProgressChunkHolder(protochunk1, tasksToExecuteOnMain); // Paper - Async chunk loading
2020-06-26 01:38:24 +02:00
+ }
+ }
+
2019-08-19 01:40:04 +02:00
+ // Paper start - async chunk save for unload
+ public static final class AsyncSaveData {
+ public final NibbleArray[] blockLight; // null or size of 17 (for indices -1 through 15)
+ public final NibbleArray[] skyLight;
+
+ public final NBTTagList blockTickList; // non-null if we had to go to the server's tick list
+ public final NBTTagList fluidTickList; // non-null if we had to go to the server's tick list
+
+ public final long worldTime;
+
+ public AsyncSaveData(NibbleArray[] blockLight, NibbleArray[] skyLight, NBTTagList blockTickList, NBTTagList fluidTickList,
+ long worldTime) {
+ this.blockLight = blockLight;
+ this.skyLight = skyLight;
+ this.blockTickList = blockTickList;
+ this.fluidTickList = fluidTickList;
+ this.worldTime = worldTime;
2020-06-26 01:38:24 +02:00
}
}
2019-08-19 01:40:04 +02:00
+ // must be called sync
+ public static AsyncSaveData getAsyncSaveData(WorldServer world, IChunkAccess chunk) {
+ org.spigotmc.AsyncCatcher.catchOp("preparation of chunk data for async save");
+ ChunkCoordIntPair chunkPos = chunk.getPos();
+
+ LightEngineThreaded lightenginethreaded = world.getChunkProvider().getLightEngine();
+
+ NibbleArray[] blockLight = new NibbleArray[17 - (-1)];
+ NibbleArray[] skyLight = new NibbleArray[17 - (-1)];
+
+ for (int i = -1; i < 17; ++i) {
+ NibbleArray blockArray = lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkPos, i));
+ NibbleArray skyArray = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkPos, i));
+
+ // copy data for safety
+ if (blockArray != null) {
+ blockArray = blockArray.copy();
+ }
+ if (skyArray != null) {
+ skyArray = skyArray.copy();
+ }
+
+ // apply offset of 1 for -1 starting index
+ blockLight[i + 1] = blockArray;
+ skyLight[i + 1] = skyArray;
+ }
+
+ TickList<Block> blockTickList = chunk.n();
+
+ NBTTagList blockTickListSerialized;
+ if (blockTickList instanceof ProtoChunkTickList || blockTickList instanceof TickListChunk) {
+ blockTickListSerialized = null;
+ } else {
+ blockTickListSerialized = world.getBlockTickList().a(chunkPos);
+ }
+
+ TickList<FluidType> fluidTickList = chunk.o();
+
+ NBTTagList fluidTickListSerialized;
+ if (fluidTickList instanceof ProtoChunkTickList || fluidTickList instanceof TickListChunk) {
+ fluidTickListSerialized = null;
+ } else {
+ fluidTickListSerialized = world.getFluidTickList().a(chunkPos);
+ }
+
+ return new AsyncSaveData(blockLight, skyLight, blockTickListSerialized, fluidTickListSerialized, world.getTime());
+ }
+
public static NBTTagCompound saveChunk(WorldServer worldserver, IChunkAccess ichunkaccess) {
+ return saveChunk(worldserver, ichunkaccess, null);
+ }
+ public static NBTTagCompound saveChunk(WorldServer worldserver, IChunkAccess ichunkaccess, AsyncSaveData asyncsavedata) {
+ // Paper end
ChunkCoordIntPair chunkcoordintpair = ichunkaccess.getPos();
NBTTagCompound nbttagcompound = new NBTTagCompound();
NBTTagCompound nbttagcompound1 = new NBTTagCompound();
2020-06-26 01:38:24 +02:00
@@ -230,7 +339,7 @@ public class ChunkRegionLoader {
2019-08-19 01:40:04 +02:00
nbttagcompound.set("Level", nbttagcompound1);
nbttagcompound1.setInt("xPos", chunkcoordintpair.x);
nbttagcompound1.setInt("zPos", chunkcoordintpair.z);
- nbttagcompound1.setLong("LastUpdate", worldserver.getTime());
+ nbttagcompound1.setLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : worldserver.getTime()); // Paper - async chunk unloading
2019-12-12 21:58:07 +01:00
nbttagcompound1.setLong("InhabitedTime", ichunkaccess.getInhabitedTime());
2019-08-19 01:40:04 +02:00
nbttagcompound1.setString("Status", ichunkaccess.getChunkStatus().d());
ChunkConverter chunkconverter = ichunkaccess.p();
2020-06-26 01:38:24 +02:00
@@ -246,14 +355,22 @@ public class ChunkRegionLoader {
2019-08-19 01:40:04 +02:00
NBTTagCompound nbttagcompound2;
- for (int i = -1; i < 17; ++i) {
+ for (int i = -1; i < 17; ++i) { // Paper - conflict on loop parameter change
int finalI = i;
ChunkSection chunksection = (ChunkSection) Arrays.stream(achunksection).filter((chunksection1) -> {
return chunksection1 != null && chunksection1.getYPosition() >> 4 == finalI;
}).findFirst().orElse(Chunk.a);
- NibbleArray nibblearray = lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, i));
- NibbleArray nibblearray1 = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, i));
-
+ // Paper start - async chunk save for unload
+ NibbleArray nibblearray; // block light
+ NibbleArray nibblearray1; // sky light
+ if (asyncsavedata == null) {
+ nibblearray = lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, i)); /// Paper - diff on method change (see getAsyncSaveData)
+ nibblearray1 = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, i)); // Paper - diff on method change (see getAsyncSaveData)
+ } else {
+ nibblearray = asyncsavedata.blockLight[i + 1]; // +1 to offset the -1 starting index
+ nibblearray1 = asyncsavedata.skyLight[i + 1]; // +1 to offset the -1 starting index
+ }
+ // Paper end
if (chunksection != Chunk.a || nibblearray != null || nibblearray1 != null) {
nbttagcompound2 = new NBTTagCompound();
nbttagcompound2.setByte("Y", (byte) (i & 255));
2020-06-26 01:38:24 +02:00
@@ -314,7 +431,7 @@ public class ChunkRegionLoader {
2020-04-13 00:29:52 +02:00
Entity entity = (Entity) iterator1.next();
NBTTagCompound nbttagcompound4 = new NBTTagCompound();
// Paper start
- if ((int) Math.floor(entity.locX()) >> 4 != chunk.getPos().x || (int) Math.floor(entity.locZ()) >> 4 != chunk.getPos().z) {
+ if (asyncsavedata == null && !entity.dead && (int) Math.floor(entity.locX()) >> 4 != chunk.getPos().x || (int) Math.floor(entity.locZ()) >> 4 != chunk.getPos().z) {
toUpdate.add(entity);
continue;
}
2020-06-26 01:38:24 +02:00
@@ -357,24 +474,32 @@ public class ChunkRegionLoader {
2019-08-19 01:40:04 +02:00
}
nbttagcompound1.set("Entities", nbttaglist2);
- TickList<Block> ticklist = ichunkaccess.n();
+ TickList<Block> ticklist = ichunkaccess.n(); // Paper - diff on method change (see getAsyncSaveData)
if (ticklist instanceof ProtoChunkTickList) {
nbttagcompound1.set("ToBeTicked", ((ProtoChunkTickList) ticklist).b());
} else if (ticklist instanceof TickListChunk) {
2020-06-26 01:38:24 +02:00
nbttagcompound1.set("TileTicks", ((TickListChunk) ticklist).b());
2019-08-19 01:40:04 +02:00
+ // Paper start - async chunk save for unload
+ } else if (asyncsavedata != null) {
+ nbttagcompound1.set("TileTicks", asyncsavedata.blockTickList);
+ // Paper end
} else {
- nbttagcompound1.set("TileTicks", worldserver.getBlockTickList().a(chunkcoordintpair));
+ nbttagcompound1.set("TileTicks", worldserver.getBlockTickList().a(chunkcoordintpair)); // Paper - diff on method change (see getAsyncSaveData)
}
- TickList<FluidType> ticklist1 = ichunkaccess.o();
+ TickList<FluidType> ticklist1 = ichunkaccess.o(); // Paper - diff on method change (see getAsyncSaveData)
if (ticklist1 instanceof ProtoChunkTickList) {
nbttagcompound1.set("LiquidsToBeTicked", ((ProtoChunkTickList) ticklist1).b());
} else if (ticklist1 instanceof TickListChunk) {
2020-06-26 01:38:24 +02:00
nbttagcompound1.set("LiquidTicks", ((TickListChunk) ticklist1).b());
2019-08-19 01:40:04 +02:00
+ // Paper start - async chunk save for unload
+ } else if (asyncsavedata != null) {
+ nbttagcompound1.set("LiquidTicks", asyncsavedata.fluidTickList);
+ // Paper end
} else {
- nbttagcompound1.set("LiquidTicks", worldserver.getFluidTickList().a(chunkcoordintpair));
2020-06-26 01:38:24 +02:00
+ nbttagcompound1.set("LiquidTicks", worldserver.getFluidTickList().a(chunkcoordintpair)); // Paper - diff on method change (see getAsyncSaveData)
2019-08-19 01:40:04 +02:00
}
nbttagcompound1.set("PostProcessing", a(ichunkaccess.l()));
diff --git a/src/main/java/net/minecraft/server/ChunkStatus.java b/src/main/java/net/minecraft/server/ChunkStatus.java
2020-06-30 07:20:29 +02:00
index 0939e015aad7d3fcc7908afcabee0100c4deee40..36c26512297430cf072b549614990b064db94152 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/ChunkStatus.java
+++ b/src/main/java/net/minecraft/server/ChunkStatus.java
2020-06-26 01:38:24 +02:00
@@ -159,6 +159,7 @@ public class ChunkStatus {
2019-08-19 01:40:04 +02:00
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());
}
2020-06-26 01:38:24 +02:00
@@ -174,6 +175,7 @@ public class ChunkStatus {
2020-04-11 10:40:00 +02:00
this.t = chunkstatus == null ? 0 : chunkstatus.c() + 1;
}
+ public int getStatusIndex() { return c(); } // Paper - OBFHELPER
public int c() {
return this.t;
}
2020-06-26 01:38:24 +02:00
@@ -195,6 +197,7 @@ public class ChunkStatus {
2020-04-11 10:40:00 +02:00
return this.w.doWork(this, worldserver, definedstructuremanager, lightenginethreaded, function, ichunkaccess);
}
+ public int getNeighborRadius() { return this.f(); } // Paper - OBFHELPER
public int f() {
return this.x;
}
2020-06-26 01:38:24 +02:00
@@ -222,6 +225,7 @@ public class ChunkStatus {
2020-04-11 10:40:00 +02:00
return this.z;
}
+ public boolean isAtLeastStatus(ChunkStatus chunkstatus) { return b(chunkstatus); } // Paper - OBFHELPER
public boolean b(ChunkStatus chunkstatus) {
return this.c() >= chunkstatus.c();
}
2019-08-19 01:40:04 +02:00
diff --git a/src/main/java/net/minecraft/server/IAsyncTaskHandler.java b/src/main/java/net/minecraft/server/IAsyncTaskHandler.java
2020-06-26 01:38:24 +02:00
index 1ba26ee10f338edbec0f580bb55d083a3d6d2284..63fdae15ccbef0c39718b320dbd096794bcfa3b4 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/IAsyncTaskHandler.java
+++ b/src/main/java/net/minecraft/server/IAsyncTaskHandler.java
@@ -91,7 +91,7 @@ public abstract class IAsyncTaskHandler<R extends Runnable> implements Mailbox<R
}
- protected void executeAll() {
+ public void executeAll() { // Paper - protected -> public
while (this.executeNext()) {
;
}
diff --git a/src/main/java/net/minecraft/server/IChunkLoader.java b/src/main/java/net/minecraft/server/IChunkLoader.java
2020-06-26 01:38:24 +02:00
index fa03834dacacf7ae6a326c88007256a261153c27..c0d2df8ef3b4d0224ede2b7a4ef4e3f930590209 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/IChunkLoader.java
+++ b/src/main/java/net/minecraft/server/IChunkLoader.java
2020-06-26 01:38:24 +02:00
@@ -3,37 +3,50 @@ package net.minecraft.server;
2019-08-19 01:40:04 +02:00
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;
2020-06-26 01:38:24 +02:00
public class IChunkLoader implements AutoCloseable {
2019-12-13 14:46:45 +01:00
- private final IOWorker a; public IOWorker getIOWorker() { return a; } // Paper - OBFHELPER
2020-06-26 01:38:24 +02:00
+ // Paper - OBFHELPER - nuke IOWorker
2019-08-19 01:40:04 +02:00
protected final DataFixer b;
@Nullable
2019-12-12 21:58:07 +01:00
- private PersistentStructureLegacy c;
+ private volatile PersistentStructureLegacy c; // Paper - async chunk loading
2019-08-19 01:40:04 +02:00
+
+ private final Object persistentDataLock = new Object(); // Paper
2020-06-26 01:38:24 +02:00
+ protected final RegionFileCache regionFileCache;
2019-08-19 01:40:04 +02:00
2020-06-26 01:38:24 +02:00
public IChunkLoader(File file, DataFixer datafixer, boolean flag) {
+ this.regionFileCache = new RegionFileCache(file, flag); // Paper - nuke IOWorker
2019-12-12 21:58:07 +01:00
this.b = datafixer;
2020-06-26 01:38:24 +02:00
- this.a = new IOWorker(file, flag, "chunk");
+ // Paper - nuke IOWorker
2019-12-13 14:46:45 +01:00
}
// CraftBukkit start
2019-08-19 01:40:04 +02:00
private boolean check(ChunkProviderServer cps, int x, int z) throws IOException {
ChunkCoordIntPair pos = new ChunkCoordIntPair(x, z);
if (cps != null) {
- com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread");
- if (cps.isLoaded(x, z)) {
+ //com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread"); // Paper - this function is now MT-Safe
+ if (cps.getChunkAtIfCachedImmediately(x, z) != null) { // Paper - isLoaded is a ticket level check, not a chunk loaded check!
return true;
}
}
2019-12-12 21:58:07 +01:00
- NBTTagCompound nbt = read(pos);
- if (nbt != null) {
- NBTTagCompound level = nbt.getCompound("Level");
- if (level.getBoolean("TerrainPopulated")) {
- return true;
- }
+
2019-08-19 01:40:04 +02:00
+ // Paper start - prioritize
+ NBTTagCompound nbt = cps == null ? read(pos) :
+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.loadChunkData((WorldServer)cps.getWorld(), x, z,
+ com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHER_PRIORITY, false, true).chunkData;
+ // Paper end
2019-12-12 21:58:07 +01:00
+ if (nbt != null) {
+ NBTTagCompound level = nbt.getCompound("Level");
+ if (level.getBoolean("TerrainPopulated")) {
+ return true;
+ }
ChunkStatus status = ChunkStatus.a(level.getString("Status"));
if (status != null && status.b(ChunkStatus.FEATURES)) {
2020-06-26 01:38:24 +02:00
@@ -64,11 +77,13 @@ public class IChunkLoader implements AutoCloseable {
2019-08-19 01:40:04 +02:00
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
2019-12-12 21:58:07 +01:00
if (this.c == null) {
2020-06-26 01:38:24 +02:00
this.c = PersistentStructureLegacy.a(resourcekey, (WorldPersistentData) supplier.get());
2019-08-19 01:40:04 +02:00
}
2019-12-12 21:58:07 +01:00
nbttagcompound = this.c.a(nbttagcompound);
2019-08-19 01:40:04 +02:00
+ } // Paper - Async chunk loading
}
}
2020-06-26 01:38:24 +02:00
@@ -86,22 +101,20 @@ public class IChunkLoader implements AutoCloseable {
2019-12-12 21:58:07 +01:00
2020-06-26 01:38:24 +02:00
@Nullable
public NBTTagCompound read(ChunkCoordIntPair chunkcoordintpair) throws IOException {
2019-12-13 14:46:45 +01:00
- return this.a.a(chunkcoordintpair);
2020-06-26 01:38:24 +02:00
+ return this.regionFileCache.read(chunkcoordintpair);
}
2019-12-12 21:58:07 +01:00
- public void a(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) {
2019-12-13 14:46:45 +01:00
- this.a.a(chunkcoordintpair, nbttagcompound);
2019-12-12 21:58:07 +01:00
+ public void a(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws IOException { write(chunkcoordintpair, nbttagcompound); } // Paper OBFHELPER
+ public void write(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws IOException { // Paper - OBFHELPER - (Switched around for safety)
2020-06-26 01:38:24 +02:00
+ this.regionFileCache.write(chunkcoordintpair, nbttagcompound);
2019-12-12 21:58:07 +01:00
if (this.c != null) {
2019-08-19 01:40:04 +02:00
+ synchronized (this.persistentDataLock) { // Paper - Async chunk loading
2020-06-26 01:38:24 +02:00
this.c.a(chunkcoordintpair.pair());
+ } // Paper - Async chunk loading}
2019-08-19 01:40:04 +02:00
}
2019-12-13 14:46:45 +01:00
-
- }
-
2020-06-26 01:38:24 +02:00
- public void i() {
- this.a.a().join();
}
public void close() throws IOException {
2019-12-13 14:46:45 +01:00
- this.a.close();
2020-06-26 01:38:24 +02:00
+ this.regionFileCache.close();
}
2019-12-13 14:46:45 +01:00
}
2019-08-19 01:40:04 +02:00
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
2020-06-26 08:29:44 +02:00
index 99801463d958540b5148e55c1e44f90a1078c371..9615c4d324e42f7f91a7e60b6151c16d20e9c739 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/MCUtil.java
+++ b/src/main/java/net/minecraft/server/MCUtil.java
2020-06-26 01:38:24 +02:00
@@ -696,4 +696,9 @@ public final class MCUtil {
2019-08-19 01:40:04 +02:00
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);
+ }
}
2020-06-26 01:38:24 +02:00
diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
2020-06-26 18:20:03 +02:00
index 55c79f17a25f1bfe80cbc65e8c01ea1703f27538..2f29d7040d37e8be2ba5f8f634011e069a07db2f 100644
2020-06-26 01:38:24 +02:00
--- a/src/main/java/net/minecraft/server/Main.java
+++ b/src/main/java/net/minecraft/server/Main.java
2020-06-26 18:20:03 +02:00
@@ -166,6 +166,7 @@ public class Main {
2020-06-26 01:38:24 +02:00
convertable_conversionsession.a((IRegistryCustom) iregistrycustom_dimension, (SaveData) object);
*/
+ Class.forName("net.minecraft.server.VillagerTrades");// Paper - load this sync so it won't fail later async
final DedicatedServer dedicatedserver = (DedicatedServer) MinecraftServer.a((thread) -> {
DedicatedServer dedicatedserver1 = new DedicatedServer(optionset, datapackconfiguration1, thread, iregistrycustom_dimension, convertable_conversionsession, resourcepackrepository, datapackresources, null, dedicatedserversettings, DataConverterRegistry.a(), minecraftsessionservice, gameprofilerepository, usercache, WorldLoadListenerLogger::new);
2019-08-19 01:40:04 +02:00
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
2020-06-30 07:20:29 +02:00
index 4ffd9c1d7b4fc2f42fa157b2235365cab41e295f..0633944364179ffb9b9fdd0400d82fd5b2ab3892 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
2020-06-26 18:20:03 +02:00
@@ -807,7 +807,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
2019-08-19 01:40:04 +02:00
this.getUserCache().c(false); // Paper
}
// Spigot end
2020-06-26 01:38:24 +02:00
-
2019-08-19 01:40:04 +02:00
+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.close(true, true); // Paper
}
public String getServerIp() {
2019-11-10 19:00:16 +01:00
diff --git a/src/main/java/net/minecraft/server/NextTickListEntry.java b/src/main/java/net/minecraft/server/NextTickListEntry.java
2020-05-06 11:48:49 +02:00
index e9c405fb5376c5733b9b0191cd5309173f4021e8..33cfeabdee03195a294f303f28044a313cb1c4ed 100644
2019-11-10 19:00:16 +01:00
--- a/src/main/java/net/minecraft/server/NextTickListEntry.java
+++ b/src/main/java/net/minecraft/server/NextTickListEntry.java
@@ -4,7 +4,7 @@ import java.util.Comparator;
public class NextTickListEntry<T> {
- private static long d;
+ private static final java.util.concurrent.atomic.AtomicLong COUNTER = new java.util.concurrent.atomic.AtomicLong(); // Paper - async chunk loading
private final T e;
public final BlockPosition a;
public final long b;
@@ -16,7 +16,7 @@ public class NextTickListEntry<T> {
}
public NextTickListEntry(BlockPosition blockposition, T t0, long i, TickListPriority ticklistpriority) {
- this.f = (long) (NextTickListEntry.d++);
+ this.f = (long) (NextTickListEntry.COUNTER.getAndIncrement()); // Paper - async chunk loading
this.a = blockposition.immutableCopy();
this.e = t0;
this.b = i;
2019-08-19 01:40:04 +02:00
diff --git a/src/main/java/net/minecraft/server/NibbleArray.java b/src/main/java/net/minecraft/server/NibbleArray.java
2020-05-06 11:48:49 +02:00
index ed8c4a87b52e6375e354a28316f8f24608082417..996c8326387b5a7fe62db6a76e000144565cb85b 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/NibbleArray.java
+++ b/src/main/java/net/minecraft/server/NibbleArray.java
@@ -71,6 +71,7 @@ public class NibbleArray {
return this.a;
}
+ public NibbleArray copy() { return this.b(); } // Paper - OBFHELPER
public NibbleArray b() {
return this.a == null ? new NibbleArray() : new NibbleArray((byte[]) this.a.clone());
}
2020-05-09 00:22:37 +02:00
diff --git a/src/main/java/net/minecraft/server/PacketPlayInTabComplete.java b/src/main/java/net/minecraft/server/PacketPlayInTabComplete.java
2020-05-14 10:58:47 +02:00
index 4c52c57c02571353f71772e3650932f314da62ca..71daa0cb08d69c16bded510d1a49053477faf600 100644
2020-05-09 00:22:37 +02:00
--- a/src/main/java/net/minecraft/server/PacketPlayInTabComplete.java
+++ b/src/main/java/net/minecraft/server/PacketPlayInTabComplete.java
2020-05-14 10:58:47 +02:00
@@ -12,7 +12,7 @@ public class PacketPlayInTabComplete implements Packet<PacketListenerPlayIn> {
2020-05-09 00:22:37 +02:00
@Override
public void a(PacketDataSerializer packetdataserializer) throws IOException {
this.a = packetdataserializer.i();
- this.b = packetdataserializer.e(32500);
2020-05-14 10:58:47 +02:00
+ this.b = packetdataserializer.e(2048);
2020-05-09 00:22:37 +02:00
}
@Override
2019-08-19 01:40:04 +02:00
diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java
2020-06-26 01:38:24 +02:00
index 3e1c1253ad5e2fa68fd8a0bac100c2e7536ea080..b6868b6b23a09e8e0dfe7a5e378dca22b8d80bad 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/PlayerChunk.java
+++ b/src/main/java/net/minecraft/server/PlayerChunk.java
2020-06-26 01:38:24 +02:00
@@ -127,6 +127,18 @@ public class PlayerChunk {
2020-01-12 22:06:05 +01:00
}
return null;
}
+
+ public ChunkStatus getChunkHolderStatus() {
+ for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getPreviousStatus(); curr != next; curr = next, next = next.getPreviousStatus()) {
+ CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> future = this.getStatusFutureUnchecked(curr);
+ Either<IChunkAccess, PlayerChunk.Failure> either = future.getNow(null);
+ if (either == null || !either.left().isPresent()) {
+ continue;
+ }
+ return curr;
+ }
+ return null;
+ }
// Paper end
public CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> getStatusFutureUnchecked(ChunkStatus chunkstatus) {
2020-06-26 01:38:24 +02:00
@@ -340,7 +352,7 @@ public class PlayerChunk {
2019-08-19 01:40:04 +02:00
ChunkStatus chunkstatus = getChunkStatus(this.oldTicketLevel);
ChunkStatus chunkstatus1 = getChunkStatus(this.ticketLevel);
boolean flag = this.oldTicketLevel <= PlayerChunkMap.GOLDEN_TICKET;
- boolean flag1 = this.ticketLevel <= PlayerChunkMap.GOLDEN_TICKET;
+ boolean flag1 = this.ticketLevel <= PlayerChunkMap.GOLDEN_TICKET; // Paper - diff on change: (flag1 = new ticket level is in loadable range)
PlayerChunk.State playerchunk_state = getChunkState(this.oldTicketLevel);
PlayerChunk.State playerchunk_state1 = getChunkState(this.ticketLevel);
// CraftBukkit start
2020-06-26 01:38:24 +02:00
@@ -376,6 +388,12 @@ public class PlayerChunk {
2019-08-19 01:40:04 +02:00
}
});
+ // Paper start
+ if (!flag1) {
+ playerchunkmap.world.asyncChunkTaskManager.cancelChunkLoad(this.location.x, this.location.z);
+ }
+ // Paper end
+
for (int i = flag1 ? chunkstatus1.c() + 1 : 0; i <= chunkstatus.c(); ++i) {
completablefuture = (CompletableFuture) this.statusFutures.get(i);
if (completablefuture != null) {
diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
2020-06-27 05:05:12 +02:00
index 24f3e8a6866bb416f04aca342514fa5dd3d314c8..a10006efc52f093196e7d67a3281a87d4c5df19b 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
+++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
2020-06-26 01:38:24 +02:00
@@ -64,7 +64,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
private final LightEngineThreaded lightEngine;
private final IAsyncTaskHandler<Runnable> executor;
2020-06-26 01:38:24 +02:00
public final ChunkGenerator chunkGenerator;
2019-12-12 21:58:07 +01:00
- private final Supplier<WorldPersistentData> l;
+ private final Supplier<WorldPersistentData> l; public final Supplier<WorldPersistentData> getWorldPersistentDataSupplier() { return this.l; } // Paper - OBFHELPER
private final VillagePlace m;
2019-08-19 01:40:04 +02:00
public final LongSet unloadQueue;
private boolean updatingChunksModified;
2020-06-26 01:38:24 +02:00
@@ -74,7 +74,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
public final WorldLoadListener worldLoadListener;
public final PlayerChunkMap.a chunkDistanceManager; public final PlayerChunkMap.a getChunkMapDistanceManager() { return this.chunkDistanceManager; } // Paper - OBFHELPER
2019-12-12 21:58:07 +01:00
private final AtomicInteger u;
2019-08-19 01:40:04 +02:00
- private final DefinedStructureManager definedStructureManager;
+ public final DefinedStructureManager definedStructureManager; // Paper - private -> public
2019-12-12 21:58:07 +01:00
private final File w;
2019-08-19 01:40:04 +02:00
private final PlayerMap playerMap;
public final Int2ObjectMap<PlayerChunkMap.EntityTracker> trackedEntities;
2020-06-26 01:38:24 +02:00
@@ -157,7 +157,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
this.lightEngine = new LightEngineThreaded(ilightaccess, this, this.world.getDimensionManager().hasSkyLight(), threadedmailbox1, this.p.a(threadedmailbox1, false));
2019-08-19 01:40:04 +02:00
this.chunkDistanceManager = new PlayerChunkMap.a(executor, iasynctaskhandler);
2019-12-12 21:58:07 +01:00
this.l = supplier;
2020-06-26 01:38:24 +02:00
- this.m = new VillagePlace(new File(this.w, "poi"), datafixer, flag);
+ this.m = new VillagePlace(new File(this.w, "poi"), datafixer, flag, this.world); // Paper
2019-08-19 01:40:04 +02:00
this.setViewDistance(i);
}
2020-06-26 01:38:24 +02:00
@@ -204,7 +204,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-09-30 03:23:09 +02:00
}
@Nullable
- protected PlayerChunk getVisibleChunk(long i) {
+ public PlayerChunk getVisibleChunk(long i) { // Paper - protected -> public
return (PlayerChunk) this.visibleChunks.get(i);
}
2020-06-26 01:38:24 +02:00
@@ -326,6 +326,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
public void close() throws IOException {
2019-12-17 23:39:07 +01:00
try {
this.p.close();
+ this.world.asyncChunkTaskManager.close(true); // Paper - Required since we're closing regionfiles in the next line
this.m.close();
} finally {
super.close();
2020-06-26 01:38:24 +02:00
@@ -417,7 +418,8 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-09-16 05:39:30 +02:00
this.b(() -> {
return true;
});
2019-12-13 14:46:45 +01:00
- this.i();
2019-09-16 05:39:30 +02:00
+ this.world.asyncChunkTaskManager.flush(); // Paper - flush to preserve behavior compat with pre-async behaviour
2019-12-13 14:46:45 +01:00
+// this.i(); // Paper - nuke IOWorker
2019-12-12 21:58:07 +01:00
PlayerChunkMap.LOGGER.info("ThreadedAnvilChunkStorage ({}): All chunks are saved", this.w.getName());
2019-09-16 05:39:30 +02:00
} else {
2019-12-13 14:46:45 +01:00
this.visibleChunks.values().stream().filter(PlayerChunk::hasBeenLoaded).forEach((playerchunk) -> {
2020-06-26 01:38:24 +02:00
@@ -433,16 +435,20 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2020-05-25 17:08:11 +02:00
}
- private static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.96; // Spigot
+ private static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.90; // Spigot // Paper - unload more
2019-08-19 01:40:04 +02:00
protected void unloadChunks(BooleanSupplier booleansupplier) {
GameProfilerFiller gameprofilerfiller = this.world.getMethodProfiler();
+ try (Timing ignored = this.world.timings.poiUnload.startTiming()) { // Paper
gameprofilerfiller.enter("poi");
2019-12-12 21:58:07 +01:00
this.m.a(booleansupplier);
2019-08-19 01:40:04 +02:00
+ } // Paper
gameprofilerfiller.exitEnter("chunk_unload");
if (!this.world.isSavingDisabled()) {
+ try (Timing ignored = this.world.timings.chunkUnload.startTiming()) { // Paper
this.b(booleansupplier);
+ }// Paper
}
gameprofilerfiller.exit();
2020-06-26 01:38:24 +02:00
@@ -463,12 +469,13 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2020-05-25 17:08:11 +02:00
if (playerchunk != null) {
this.pendingUnload.put(j, playerchunk);
this.updatingChunksModified = true;
+ this.a(j, playerchunk); // Paper - Move up - don't leak chunks
// Spigot start
if (!booleansupplier.getAsBoolean() && this.unloadQueue.size() <= targetSize && activityAccountant.activityTimeIsExhausted()) {
break;
}
// Spigot end
- this.a(j, playerchunk);
+ //this.a(j, playerchunk); // Paper - move up because spigot did a dumb
}
}
activityAccountant.endActivity(); // Spigot
2020-06-26 01:38:24 +02:00
@@ -482,6 +489,60 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
}
+ // Paper start - async chunk save for unload
+ // Note: This is very unsafe to call if the chunk is still in use.
+ // This is also modeled after PlayerChunkMap#saveChunk(IChunkAccess, boolean), with the intentional difference being
+ // serializing the chunk is left to a worker thread.
+ private void asyncSave(IChunkAccess chunk) {
+ ChunkCoordIntPair chunkPos = chunk.getPos();
+ NBTTagCompound poiData;
+ try (Timing ignored = this.world.timings.chunkUnloadPOISerialization.startTiming()) {
+ poiData = this.getVillagePlace().getData(chunk.getPos());
+ }
+
+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world, chunkPos.x, chunkPos.z,
+ poiData, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY);
+
+ if (!chunk.isNeedsSaving()) {
+ return;
+ }
+
+ ChunkStatus chunkstatus = chunk.getChunkStatus();
+
+ // Copied from PlayerChunkMap#saveChunk(IChunkAccess, boolean)
+ 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
+ try {
+ ChunkStatus statusOnDisk = this.getChunkStatusOnDisk(chunkPos);
+ if (statusOnDisk != null && statusOnDisk.getType() == ChunkStatus.Type.LEVELCHUNK) {
+ // Paper end
+ return;
+ }
+
+ if (chunkstatus == ChunkStatus.EMPTY && chunk.h().values().stream().noneMatch(StructureStart::e)) {
+ return;
+ }
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ return;
+ }
+ }
+ }
+
+ ChunkRegionLoader.AsyncSaveData asyncSaveData;
+ try (Timing ignored = this.world.timings.chunkUnloadPrepareSave.startTiming()) {
+ asyncSaveData = ChunkRegionLoader.getAsyncSaveData(this.world, chunk);
+ }
+
+ this.world.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY,
+ asyncSaveData, chunk);
+
+ chunk.setLastSaved(this.world.getTime());
+ chunk.setNeedsSaving(false);
+ }
+ // Paper end
+
private void a(long i, PlayerChunk playerchunk) {
CompletableFuture<IChunkAccess> completablefuture = playerchunk.getChunkSave();
Consumer<IChunkAccess> consumer = (ichunkaccess) -> { // CraftBukkit - decompile error
2020-06-26 01:38:24 +02:00
@@ -495,7 +556,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
((Chunk) ichunkaccess).setLoaded(false);
}
- this.saveChunk(ichunkaccess);
+ //this.saveChunk(ichunkaccess);// Paper - delay
if (this.loadedChunks.remove(i) && ichunkaccess instanceof Chunk) {
Chunk chunk = (Chunk) ichunkaccess;
2020-06-26 01:38:24 +02:00
@@ -503,6 +564,13 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
}
2020-01-28 01:16:53 +01:00
this.autoSaveQueue.remove(playerchunk); // Paper
2019-08-19 01:40:04 +02:00
+ try {
+ this.asyncSave(ichunkaccess); // Paper - async chunk saving
+ } catch (Throwable ex) {
+ LOGGER.fatal("Failed to prepare async save, attempting synchronous save", ex);
2020-01-28 01:16:53 +01:00
+ this.saveChunk(ichunkaccess);
2019-08-19 01:40:04 +02:00
+ }
+
this.lightEngine.a(ichunkaccess.getPos());
this.lightEngine.queueUpdate();
this.worldLoadListener.a(ichunkaccess.getPos(), (ChunkStatus) null);
2020-06-27 05:05:12 +02:00
@@ -573,19 +641,21 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
}
private CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> f(ChunkCoordIntPair chunkcoordintpair) {
- return CompletableFuture.supplyAsync(() -> {
+ // Paper start - Async chunk io
+ final java.util.function.BiFunction<ChunkRegionLoader.InProgressChunkHolder, Throwable, Either<IChunkAccess, PlayerChunk.Failure>> syncLoadComplete = (chunkHolder, ioThrowable) -> {
2020-04-26 08:05:41 +02:00
try (Timing ignored = this.world.timings.chunkLoad.startTimingIfSync()) { // Paper
2020-01-22 03:02:07 +01:00
this.world.getMethodProfiler().c("chunkLoad");
2019-08-19 01:40:04 +02:00
- NBTTagCompound nbttagcompound; // Paper
2020-04-26 08:05:41 +02:00
- try (Timing ignored2 = this.world.timings.chunkIO.startTimingIfSync()) { // Paper start - timings
2020-01-22 03:02:07 +01:00
- nbttagcompound = this.readChunkData(chunkcoordintpair);
- } // Paper end
2020-06-26 01:38:24 +02:00
+ // Paper start
2020-01-22 03:02:07 +01:00
+ if (ioThrowable != null) {
2020-06-26 01:38:24 +02:00
+ com.destroystokyo.paper.util.SneakyThrow.sneaky(ioThrowable);
2020-01-22 03:02:07 +01:00
+ }
2020-06-27 05:05:12 +02:00
+ chunkHolder.tasks.forEach(Runnable::run);
2020-06-26 01:38:24 +02:00
+ // Paper end
- if (nbttagcompound != null) {try (Timing ignored2 = this.world.timings.chunkLoadLevelTimer.startTimingIfSync()) { // Paper start - timings
- boolean flag = nbttagcompound.hasKeyOfType("Level", 10) && nbttagcompound.getCompound("Level").hasKeyOfType("Status", 8);
+ if (chunkHolder.protoChunk != null) {try (Timing ignored2 = this.world.timings.chunkLoadLevelTimer.startTimingIfSync()) { // Paper start - timings // Paper - chunk is created async
2020-01-22 03:02:07 +01:00
2020-05-29 01:34:58 +02:00
- if (flag) {
- ProtoChunk protochunk = ChunkRegionLoader.loadChunk(this.world, this.definedStructureManager, this.m, chunkcoordintpair, nbttagcompound);
2020-06-26 01:38:24 +02:00
+ if (true) {
+ ProtoChunk protochunk = chunkHolder.protoChunk;
2019-08-19 01:40:04 +02:00
2020-06-26 01:38:24 +02:00
protochunk.setLastSaved(this.world.getTime());
this.a(chunkcoordintpair, protochunk.getChunkStatus().getType());
2020-06-27 05:05:12 +02:00
@@ -609,7 +679,32 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
2020-06-26 01:38:24 +02:00
this.g(chunkcoordintpair);
2020-05-06 10:35:20 +02:00
return Either.left(new ProtoChunk(chunkcoordintpair, ChunkConverter.a, this.world)); // Paper - Anti-Xray - Add parameter
2019-08-19 01:40:04 +02:00
- }, this.executor);
+ // Paper start - Async chunk io
+ };
+ CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> ret = new CompletableFuture<>();
+
+ Consumer<ChunkRegionLoader.InProgressChunkHolder> chunkHolderConsumer = (ChunkRegionLoader.InProgressChunkHolder holder) -> {
Improve mid tick chunk loading, Fix Oversleep, other improvements
Process loads outside of any canSleep check. Original intent was to
only apply those restrictions to generations but realized I had some
checks higher up the call chain.
Reworked the back off strategy to just run every 1 millisecond per world,
and to apply the per tick limit to generations only.
This guarantees that your chunk will load with at most around 1ms delay.
Additionally, fire midTick processing in a few more places, notably the
oversleep section so we can keep processing loads here too which has
a large up to 50ms window...
Speaking of oversleep, we had a bug in our implementation changes for
Timings that caused oversleep to not sleep the correct amount.
Because we now moved it into the NEXT tick instead of THIS tick, the
value of nextTick had already been increased to +50ms, resulting in
the risk of sleeping more than it should, but, more importantly, this
caused every task that was trying to NOT run during oversleep to actually
run during oversleep.
This is now fixed.
Another small tweak is to the /tps command, to no longer show the star when
TPS is right at 20.
Due to ineffeciencies in the sleep precision, TPS is commonly 20.02.
This causes the star to show up almost constantly, so now only show it if
we actually hit a real "catchup".
This commit also improves the changes to the CallbackExecutor, in that
it now is also recursion safe.
It was possible that the executor could run tasks out of desired order
if the executor task scheduled more executor tasks.
We solve this by ensuring new additions do not enter the currently iterated queue.
Each depth level will have its own queue.
Fixes #3220
2020-04-26 05:47:29 +02:00
+ // Go into the chunk load queue and not server task queue so we can be popped out even faster.
+ com.destroystokyo.paper.io.chunk.ChunkTaskManager.queueChunkWaitTask(() -> {
+ try {
+ ret.complete(syncLoadComplete.apply(holder, null));
+ } catch (Exception e) {
+ ret.completeExceptionally(e);
+ }
2019-08-19 01:40:04 +02:00
+ });
+ };
+
+ CompletableFuture<NBTTagCompound> chunkSaveFuture = this.world.asyncChunkTaskManager.getChunkSaveFuture(chunkcoordintpair.x, chunkcoordintpair.z);
+ if (chunkSaveFuture != null) {
+ this.world.asyncChunkTaskManager.scheduleChunkLoad(chunkcoordintpair.x, chunkcoordintpair.z,
+ com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY, chunkHolderConsumer, false, chunkSaveFuture);
+ this.world.asyncChunkTaskManager.raisePriority(chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY);
+ } else {
+ this.world.asyncChunkTaskManager.scheduleChunkLoad(chunkcoordintpair.x, chunkcoordintpair.z,
+ com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, chunkHolderConsumer, false);
+ }
+ return ret;
+ // Paper end
}
2020-06-26 01:38:24 +02:00
private void g(ChunkCoordIntPair chunkcoordintpair) {
2020-06-27 05:05:12 +02:00
@@ -836,6 +931,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
}
public boolean saveChunk(IChunkAccess ichunkaccess) {
2020-05-29 01:34:58 +02:00
+ try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { // Paper
this.m.a(ichunkaccess.getPos());
2019-08-19 01:40:04 +02:00
if (!ichunkaccess.isNeedsSaving()) {
return false;
2020-06-27 05:05:12 +02:00
@@ -848,6 +944,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2020-06-26 01:38:24 +02:00
ChunkStatus chunkstatus = ichunkaccess.getChunkStatus();
2019-08-19 01:40:04 +02:00
if (chunkstatus.getType() != ChunkStatus.Type.LEVELCHUNK) {
+ try (co.aikar.timings.Timing ignored1 = this.world.timings.chunkSaveOverwriteCheck.startTiming()) { // Paper
2020-06-26 01:38:24 +02:00
if (this.h(chunkcoordintpair)) {
return false;
}
2020-06-27 05:05:12 +02:00
@@ -855,12 +952,20 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2020-06-26 01:38:24 +02:00
if (chunkstatus == ChunkStatus.EMPTY && ichunkaccess.h().values().stream().noneMatch(StructureStart::e)) {
return false;
2019-08-19 01:40:04 +02:00
}
2020-06-26 01:38:24 +02:00
+ } // Paper
2019-08-19 01:40:04 +02:00
}
2020-01-22 03:02:07 +01:00
2020-05-29 01:34:58 +02:00
this.world.getMethodProfiler().c("chunkSave");
2020-06-26 01:38:24 +02:00
- NBTTagCompound nbttagcompound = ChunkRegionLoader.saveChunk(this.world, ichunkaccess);
+ NBTTagCompound nbttagcompound;
2019-08-19 01:40:04 +02:00
+ try (co.aikar.timings.Timing ignored1 = this.world.timings.chunkSaveDataSerialization.startTiming()) { // Paper
2020-06-26 01:38:24 +02:00
+ nbttagcompound = ChunkRegionLoader.saveChunk(this.world, ichunkaccess);
2019-08-19 01:40:04 +02:00
+ } // Paper
2020-06-26 01:38:24 +02:00
+
- this.a(chunkcoordintpair, nbttagcompound);
2020-05-29 01:34:58 +02:00
+ // Paper start - async chunk io
+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world, chunkcoordintpair.x, chunkcoordintpair.z,
+ null, nbttagcompound, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY);
+ // Paper end - async chunk io
2020-06-26 01:38:24 +02:00
this.a(chunkcoordintpair, chunkstatus.getType());
2020-05-29 01:34:58 +02:00
return true;
2019-08-19 01:40:04 +02:00
} catch (Exception exception) {
2020-06-27 05:05:12 +02:00
@@ -869,6 +974,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
return false;
}
}
+ } // Paper
}
2020-06-26 01:38:24 +02:00
private boolean h(ChunkCoordIntPair chunkcoordintpair) {
2020-06-27 05:05:12 +02:00
@@ -998,6 +1104,35 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
}
}
+ // Paper start - Asynchronous chunk io
+ @Nullable
+ @Override
+ public NBTTagCompound read(ChunkCoordIntPair chunkcoordintpair) throws IOException {
+ if (Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) {
+ NBTTagCompound ret = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE
+ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(),
+ false, true, true).join().chunkData;
+
+ if (ret == com.destroystokyo.paper.io.PaperFileIOThread.FAILURE_VALUE) {
+ throw new IOException("See logs for further detail");
+ }
+ return ret;
+ }
+ return super.read(chunkcoordintpair);
+ }
+
+ @Override
+ public void write(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws IOException {
+ if (Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) {
+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(
+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, null, nbttagcompound,
+ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread());
+ return;
+ }
+ super.write(chunkcoordintpair, nbttagcompound);
+ }
+ // Paper end
+
@Nullable
public NBTTagCompound readChunkData(ChunkCoordIntPair chunkcoordintpair) throws IOException { // Paper - private -> public
NBTTagCompound nbttagcompound = this.read(chunkcoordintpair);
2020-06-27 05:05:12 +02:00
@@ -1019,33 +1154,55 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-12-13 14:46:45 +01:00
// Paper start - chunk status cache "api"
public ChunkStatus getChunkStatusOnDiskIfCached(ChunkCoordIntPair chunkPos) {
- RegionFile regionFile = this.getIOWorker().getRegionFileCache().getRegionFileIfLoaded(chunkPos);
2020-01-10 02:42:33 +01:00
+ synchronized (this) { // Paper
2020-06-26 01:38:24 +02:00
+ RegionFile regionFile = this.regionFileCache.getRegionFileIfLoaded(chunkPos);
2019-12-13 14:46:45 +01:00
return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
2020-01-10 02:42:33 +01:00
+ } // Paper
2019-08-19 01:40:04 +02:00
}
public ChunkStatus getChunkStatusOnDisk(ChunkCoordIntPair chunkPos) throws IOException {
2020-06-26 01:38:24 +02:00
- RegionFile regionFile = this.getIOWorker().getRegionFileCache().getFile(chunkPos, true);
2019-08-19 01:40:04 +02:00
+ // Paper start - async chunk save for unload
+ IChunkAccess unloadingChunk = this.world.asyncChunkTaskManager.getChunkInSaveProgress(chunkPos.x, chunkPos.z);
+ if (unloadingChunk != null) {
+ return unloadingChunk.getChunkStatus();
+ }
+ // Paper end
+ // Paper start - async io
+ NBTTagCompound inProgressWrite = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE
2019-12-12 21:58:07 +01:00
+ .getPendingWrite(this.world, chunkPos.x, chunkPos.z, false);
2020-06-26 01:38:24 +02:00
- if (regionFile == null || !regionFile.chunkExists(chunkPos)) {
2019-12-12 21:58:07 +01:00
- return null;
2019-08-19 01:40:04 +02:00
+ if (inProgressWrite != null) {
+ return ChunkRegionLoader.getStatus(inProgressWrite);
2019-12-12 21:58:07 +01:00
}
2019-08-19 01:40:04 +02:00
+ // Paper end
+ synchronized (this) { // Paper - async io
2020-06-26 01:38:24 +02:00
+ RegionFile regionFile = this.regionFileCache.getFile(chunkPos, true);
2019-12-12 21:58:07 +01:00
- ChunkStatus status = regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
2020-06-26 01:38:24 +02:00
+ if (regionFile == null || !regionFile.chunkExists(chunkPos)) {
+ return null;
+ }
+
2019-12-12 21:58:07 +01:00
+ ChunkStatus status = regionFile.getStatusIfCached(chunkPos.x, chunkPos.z);
2019-08-19 01:40:04 +02:00
2019-12-12 21:58:07 +01:00
- if (status != null) {
- return status;
+ if (status != null) {
+ return status;
+ }
+ // Paper start - async io
2019-08-19 01:40:04 +02:00
}
- 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 {
2019-12-12 21:58:07 +01:00
- RegionFile regionFile = this.getIOWorker().getRegionFileCache().getFile(chunkPos, false);
+ synchronized (this) {
2020-06-26 01:38:24 +02:00
+ RegionFile regionFile = this.regionFileCache.getFile(chunkPos, false);
2019-12-12 21:58:07 +01:00
- regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkRegionLoader.getStatus(compound));
+ regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkRegionLoader.getStatus(compound));
+ }
}
2019-08-19 01:40:04 +02:00
2019-12-12 21:58:07 +01:00
public IChunkAccess getUnloadingChunk(int chunkX, int chunkZ) {
2020-06-27 05:05:12 +02:00
@@ -1054,6 +1211,39 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
}
2019-12-12 21:58:07 +01:00
// Paper end
2019-08-19 01:40:04 +02:00
2019-12-12 21:58:07 +01:00
+
2019-08-19 01:40:04 +02:00
+ // 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 chunk save for unload
+ IChunkAccess unloadingChunk = this.world.asyncChunkTaskManager.getChunkInSaveProgress(x, z);
+ if (unloadingChunk != null) {
+ return unloadingChunk.getChunkStatus();
+ }
+ // Paper end
+ // Paper start - async io
+ net.minecraft.server.NBTTagCompound inProgressWrite = com.destroystokyo.paper.io.PaperFileIOThread.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 {
2020-06-26 01:38:24 +02:00
+ file = world.getChunkProvider().playerChunkMap.regionFileCache.getFile(chunkPos, false);
2019-08-19 01:40:04 +02:00
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ return !file.chunkExists(chunkPos) ? ChunkStatus.EMPTY : file.getStatusIfCached(x, z);
+ }
+ }
+
2019-12-12 21:58:07 +01:00
boolean isOutsideOfRange(ChunkCoordIntPair chunkcoordintpair) {
// Spigot start
return isOutsideOfRange(chunkcoordintpair, false);
2020-06-27 05:05:12 +02:00
@@ -1399,6 +1589,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
2019-08-19 01:40:04 +02:00
}
+ public VillagePlace getVillagePlace() { return this.h(); } // Paper - OBFHELPER
protected VillagePlace h() {
2019-12-12 21:58:07 +01:00
return this.m;
2019-08-19 01:40:04 +02:00
}
2020-05-14 10:58:47 +02:00
diff --git a/src/main/java/net/minecraft/server/PlayerConnection.java b/src/main/java/net/minecraft/server/PlayerConnection.java
2020-06-30 07:20:29 +02:00
index 62a0db6c08047be4ab28f5c23f4ae12694cf003f..70d3b429bfe08f1d2e5a3ac8368d8221770d5307 100644
2020-05-14 10:58:47 +02:00
--- a/src/main/java/net/minecraft/server/PlayerConnection.java
+++ b/src/main/java/net/minecraft/server/PlayerConnection.java
2020-06-26 01:38:24 +02:00
@@ -544,6 +544,13 @@ public class PlayerConnection implements PacketListenerPlayIn {
2020-05-14 10:58:47 +02:00
minecraftServer.scheduleOnMain(() -> this.disconnect(new ChatMessage("disconnect.spam", new Object[0]))); // Paper
return;
}
+ // Paper start
+ String str = packetplayintabcomplete.c(); int index = -1;
+ if (str.length() > 64 && ((index = str.indexOf(' ')) == -1 || index >= 64)) {
+ minecraftServer.scheduleOnMain(() -> this.disconnect(new ChatMessage("disconnect.spam", new Object[0]))); // Paper
+ return;
+ }
+ // Paper end
// CraftBukkit end
StringReader stringreader = new StringReader(packetplayintabcomplete.c());
2019-08-19 01:40:04 +02:00
diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java
2020-06-26 01:38:24 +02:00
index e1730709fff5dfee68621d0aaed70a00bab97948..93797395c3a710d228bd790771ac18b4baa3b1e2 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/RegionFile.java
+++ b/src/main/java/net/minecraft/server/RegionFile.java
2020-01-10 02:42:33 +01:00
@@ -36,6 +36,8 @@ public class RegionFile implements AutoCloseable {
private final RegionFileBitSet freeSectors;
public final File file;
+ public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper
+
// Paper start - Cache chunk status
private final ChunkStatus[] statuses = new ChunkStatus[32 * 32];
2020-06-26 01:38:24 +02:00
@@ -229,7 +231,7 @@ public class RegionFile implements AutoCloseable {
2019-12-13 14:46:45 +01:00
return (i + 4096 - 1) / 4096;
}
- public boolean b(ChunkCoordIntPair chunkcoordintpair) {
+ public synchronized boolean b(ChunkCoordIntPair chunkcoordintpair) { // Paper - synchronized
int i = this.getOffset(chunkcoordintpair);
if (i == 0) {
2020-06-26 01:38:24 +02:00
@@ -389,6 +391,11 @@ public class RegionFile implements AutoCloseable {
2019-08-19 01:40:04 +02:00
}
2020-01-10 02:42:33 +01:00
public void close() throws IOException {
+ // Paper start - Prevent regionfiles from being closed during use
+ this.fileLock.lock();
+ synchronized (this) {
+ try {
+ // Paper end
2019-08-19 01:40:04 +02:00
this.closed = true; // Paper
2019-12-12 21:58:07 +01:00
try {
2020-06-26 01:38:24 +02:00
this.d();
@@ -399,6 +406,10 @@ public class RegionFile implements AutoCloseable {
this.dataFile.close();
2020-01-10 02:42:33 +01:00
}
}
+ } finally { // Paper start - Prevent regionfiles from being closed during use
+ this.fileLock.unlock();
+ }
+ } // Paper end
}
2019-08-19 01:40:04 +02:00
diff --git a/src/main/java/net/minecraft/server/RegionFileCache.java b/src/main/java/net/minecraft/server/RegionFileCache.java
2020-06-26 01:38:24 +02:00
index 341689ac996164b7b53e095495b92b6e85ab991a..867dc074bc57b27486de6ce742971d5db945b0fc 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/RegionFileCache.java
+++ b/src/main/java/net/minecraft/server/RegionFileCache.java
2019-12-13 14:46:45 +01:00
@@ -9,7 +9,7 @@ import java.io.File;
import java.io.IOException;
import javax.annotation.Nullable;
-public final class RegionFileCache implements AutoCloseable {
+public class RegionFileCache implements AutoCloseable { // Paper - no final
public final Long2ObjectLinkedOpenHashMap<RegionFile> cache = new Long2ObjectLinkedOpenHashMap();
private final File b;
2020-06-26 01:38:24 +02:00
@@ -22,16 +22,27 @@ public final class RegionFileCache implements AutoCloseable {
2019-12-12 21:58:07 +01:00
2019-08-19 01:40:04 +02:00
// Paper start
- public RegionFile getRegionFileIfLoaded(ChunkCoordIntPair chunkcoordintpair) {
+ public synchronized RegionFile getRegionFileIfLoaded(ChunkCoordIntPair chunkcoordintpair) { // Paper - synchronize for async io
return this.cache.getAndMoveToFirst(ChunkCoordIntPair.pair(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ()));
}
2019-12-13 14:46:45 +01:00
// Paper end
- public RegionFile getFile(ChunkCoordIntPair chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - private > public
+ public synchronized RegionFile getFile(ChunkCoordIntPair chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - private > public, synchronize
2020-01-10 02:42:33 +01:00
+ // Paper start - add lock parameter
+ return this.getFile(chunkcoordintpair, existingOnly, false);
+ }
+ public synchronized RegionFile getFile(ChunkCoordIntPair chunkcoordintpair, boolean existingOnly, boolean lock) throws IOException {
+ // Paper end
2019-12-13 14:46:45 +01:00
long i = ChunkCoordIntPair.pair(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ());
RegionFile regionfile = (RegionFile) this.cache.getAndMoveToFirst(i);
2020-01-10 02:42:33 +01:00
if (regionfile != null) {
+ // Paper start
+ if (lock) {
+ // must be in this synchronized block
+ regionfile.fileLock.lock();
+ }
+ // Paper end
return regionfile;
} else {
if (this.cache.size() >= com.destroystokyo.paper.PaperConfig.regionFileCacheSize) { // Paper - configurable
2020-06-26 01:38:24 +02:00
@@ -47,6 +58,12 @@ public final class RegionFileCache implements AutoCloseable {
RegionFile regionfile1 = new RegionFile(file, this.b, this.c);
2020-01-10 02:42:33 +01:00
this.cache.putAndMoveToFirst(i, regionfile1);
+ // Paper start
+ if (lock) {
+ // must be in this synchronized block
+ regionfile1.fileLock.lock();
+ }
+ // Paper end
return regionfile1;
}
}
2020-06-26 01:38:24 +02:00
@@ -122,11 +139,12 @@ public final class RegionFileCache implements AutoCloseable {
2020-01-10 02:42:33 +01:00
@Nullable
public NBTTagCompound read(ChunkCoordIntPair chunkcoordintpair) throws IOException {
2020-04-27 09:34:45 +02:00
// CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing
- RegionFile regionfile = this.getFile(chunkcoordintpair, true);
2020-05-09 00:22:37 +02:00
+ RegionFile regionfile = this.getFile(chunkcoordintpair, true, true); // Paper
2020-04-27 09:34:45 +02:00
if (regionfile == null) {
return null;
}
// CraftBukkit end
2020-01-10 02:42:33 +01:00
+ try { // Paper
DataInputStream datainputstream = regionfile.a(chunkcoordintpair);
// Paper start
if (regionfile.isOversized(chunkcoordintpair.x, chunkcoordintpair.z)) {
2020-06-26 01:38:24 +02:00
@@ -164,10 +182,14 @@ public final class RegionFileCache implements AutoCloseable {
2020-01-10 02:42:33 +01:00
}
return nbttagcompound;
+ } finally { // Paper start
+ regionfile.fileLock.unlock();
+ } // Paper end
}
protected void write(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws IOException {
- RegionFile regionfile = this.getFile(chunkcoordintpair, false); // CraftBukkit
+ RegionFile regionfile = this.getFile(chunkcoordintpair, false, true); // CraftBukkit // Paper
+ try { // Paper
int attempts = 0; Exception laste = null; while (attempts++ < 5) { try { // Paper
DataOutputStream dataoutputstream = regionfile.c(chunkcoordintpair);
Throwable throwable = null;
2020-06-26 01:38:24 +02:00
@@ -206,9 +228,12 @@ public final class RegionFileCache implements AutoCloseable {
2020-01-10 02:42:33 +01:00
MinecraftServer.LOGGER.error("Failed to save chunk", laste);
}
2019-12-13 14:46:45 +01:00
// Paper end
2020-01-10 02:42:33 +01:00
+ } finally { // Paper start
+ regionfile.fileLock.unlock();
+ } // Paper end
2019-12-13 14:46:45 +01:00
}
- public void close() throws IOException {
+ public synchronized void close() throws IOException { // Paper -> synchronized
2020-06-26 01:38:24 +02:00
ExceptionSuppressor<IOException> exceptionsuppressor = new ExceptionSuppressor<>();
2019-12-13 14:46:45 +01:00
ObjectIterator objectiterator = this.cache.values().iterator();
2020-06-26 01:38:24 +02:00
@@ -235,4 +260,12 @@ public final class RegionFileCache implements AutoCloseable {
2020-04-27 09:34:45 +02:00
}
2019-08-19 01:40:04 +02:00
2020-04-27 09:34:45 +02:00
}
+
+ // CraftBukkit start
2019-08-19 01:40:04 +02:00
+ public synchronized boolean chunkExists(ChunkCoordIntPair pos) throws IOException { // Paper - synchronize
2020-04-27 09:34:45 +02:00
+ RegionFile regionfile = getFile(pos, true);
+
+ return regionfile != null ? regionfile.chunkExists(pos) : false;
+ }
+ // CraftBukkit end
}
2019-08-19 01:40:04 +02:00
diff --git a/src/main/java/net/minecraft/server/RegionFileSection.java b/src/main/java/net/minecraft/server/RegionFileSection.java
2020-06-26 01:38:24 +02:00
index a39075c68bb04e7183941c5eb80eb7cdde235045..bd0ff1e43a07a3332f9ade49fec2f76275a25c7f 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/RegionFileSection.java
+++ b/src/main/java/net/minecraft/server/RegionFileSection.java
2020-06-26 01:38:24 +02:00
@@ -21,28 +21,29 @@ import javax.annotation.Nullable;
2019-12-13 14:46:45 +01:00
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
2020-06-26 01:38:24 +02:00
-public class RegionFileSection<R> implements AutoCloseable {
+public class RegionFileSection<R> extends RegionFileCache implements AutoCloseable { // Paper - nuke IOWorker
2019-12-13 14:46:45 +01:00
2019-08-19 01:40:04 +02:00
private static final Logger LOGGER = LogManager.getLogger();
2019-12-13 14:46:45 +01:00
- private final IOWorker b;
2020-06-26 01:38:24 +02:00
+ // Paper - nuke IOWorker
2019-12-12 21:58:07 +01:00
private final Long2ObjectMap<Optional<R>> c = new Long2ObjectOpenHashMap();
2019-08-19 01:40:04 +02:00
- private final LongLinkedOpenHashSet d = new LongLinkedOpenHashSet();
+ protected final LongLinkedOpenHashSet d = new LongLinkedOpenHashSet(); // Paper - private -> protected
2020-06-26 01:38:24 +02:00
private final Function<Runnable, Codec<R>> e;
2019-08-19 01:40:04 +02:00
private final Function<Runnable, R> f;
private final DataFixer g;
2019-12-13 14:46:45 +01:00
private final DataFixTypes h;
2020-06-26 01:38:24 +02:00
public RegionFileSection(File file, Function<Runnable, Codec<R>> function, Function<Runnable, R> function1, DataFixer datafixer, DataFixTypes datafixtypes, boolean flag) {
+ super(file, flag); // Paper - nuke IOWorker
this.e = function;
this.f = function1;
2019-12-13 14:46:45 +01:00
this.g = datafixer;
this.h = datafixtypes;
2020-06-26 01:38:24 +02:00
- this.b = new IOWorker(file, flag, file.getName());
+ //this.b = new IOWorker(file, flag, file.getName()); // Paper - nuke IOWorker
2019-08-19 01:40:04 +02:00
}
protected void a(BooleanSupplier booleansupplier) {
2020-06-26 01:38:24 +02:00
while (!this.d.isEmpty() && booleansupplier.getAsBoolean()) {
- ChunkCoordIntPair chunkcoordintpair = SectionPosition.a(this.d.firstLong()).r();
+ ChunkCoordIntPair chunkcoordintpair = SectionPosition.a(this.d.firstLong()).r(); // Paper - conflict here to avoid obfhelpers
2019-08-19 01:40:04 +02:00
this.d(chunkcoordintpair);
}
2020-06-26 01:38:24 +02:00
@@ -96,13 +97,18 @@ public class RegionFileSection<R> implements AutoCloseable {
2019-08-19 01:40:04 +02:00
}
private void b(ChunkCoordIntPair chunkcoordintpair) {
- this.a(chunkcoordintpair, DynamicOpsNBT.a, this.c(chunkcoordintpair));
+ // 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
2019-12-13 14:46:45 +01:00
private NBTTagCompound c(ChunkCoordIntPair chunkcoordintpair) {
try {
- return this.b.a(chunkcoordintpair);
+ return this.read(chunkcoordintpair); // Paper - nuke IOWorker
} catch (IOException ioexception) {
RegionFileSection.LOGGER.error("Error reading chunk {} data from disk", chunkcoordintpair, ioexception);
return null;
2020-06-26 01:38:24 +02:00
@@ -148,17 +154,31 @@ public class RegionFileSection<R> implements AutoCloseable {
2019-08-19 01:40:04 +02:00
}
private void d(ChunkCoordIntPair chunkcoordintpair) {
- Dynamic<NBTBase> dynamic = this.a(chunkcoordintpair, DynamicOpsNBT.a);
+ Dynamic<NBTBase> dynamic = this.a(chunkcoordintpair, DynamicOpsNBT.a); // Paper - conflict here to avoid adding obfhelpers :)
NBTBase nbtbase = (NBTBase) dynamic.getValue();
if (nbtbase instanceof NBTTagCompound) {
2019-12-13 14:46:45 +01:00
- this.b.a(chunkcoordintpair, (NBTTagCompound) nbtbase);
+ try { this.write(chunkcoordintpair, (NBTTagCompound) nbtbase); } catch (IOException ioexception) { RegionFileSection.LOGGER.error("Error writing data to disk", ioexception); } // Paper - nuke IOWorker // TODO make this write async
} else {
RegionFileSection.LOGGER.error("Expected compound tag, got {}", nbtbase);
}
2019-08-19 01:40:04 +02:00
}
+ // Paper start - internal get data function, copied from above
+ private NBTTagCompound getDataInternal(ChunkCoordIntPair chunkcoordintpair) {
+ Dynamic<NBTBase> dynamic = this.a(chunkcoordintpair, DynamicOpsNBT.a);
+ NBTBase nbtbase = (NBTBase) dynamic.getValue();
+
+ if (nbtbase instanceof NBTTagCompound) {
+ return (NBTTagCompound)nbtbase;
+ } else {
+ RegionFileSection.LOGGER.error("Expected compound tag, got {}", nbtbase);
+ }
+ return null;
+ }
+ // Paper end
+
private <T> Dynamic<T> a(ChunkCoordIntPair chunkcoordintpair, DynamicOps<T> dynamicops) {
Map<T, T> map = Maps.newHashMap();
2020-06-26 01:38:24 +02:00
@@ -204,9 +224,9 @@ public class RegionFileSection<R> implements AutoCloseable {
2019-08-19 01:40:04 +02:00
public void a(ChunkCoordIntPair chunkcoordintpair) {
if (!this.d.isEmpty()) {
for (int i = 0; i < 16; ++i) {
2020-06-26 01:38:24 +02:00
- long j = SectionPosition.a(chunkcoordintpair, i).s();
+ long j = SectionPosition.a(chunkcoordintpair, i).s(); // Paper - conflict here to avoid obfhelpers
2019-08-19 01:40:04 +02:00
- if (this.d.contains(j)) {
+ if (this.d.contains(j)) { // Paper - conflict here to avoid obfhelpers
this.d(chunkcoordintpair);
return;
}
2020-06-26 01:38:24 +02:00
@@ -215,7 +235,26 @@ public class RegionFileSection<R> implements AutoCloseable {
2019-12-13 14:46:45 +01:00
2019-08-19 01:40:04 +02:00
}
2019-12-13 14:46:45 +01:00
- public void close() throws IOException {
- this.b.close();
+// Paper start - nuke IOWorker
+// public void close() throws IOException {
+// this.b.close();
+// }
+// Paper end
2019-08-19 01:40:04 +02:00
+
+ // Paper start - get data function
+ public NBTTagCompound getData(ChunkCoordIntPair chunkcoordintpair) {
+ // Note: Copied from above
+ // This is checking if the data exists, then it builds it later in getDataInternal(ChunkCoordIntPair)
+ if (!this.d.isEmpty()) {
+ for (int i = 0; i < 16; ++i) {
2020-06-26 01:38:24 +02:00
+ long j = SectionPosition.a(chunkcoordintpair, i).s();
2019-08-19 01:40:04 +02:00
+
+ if (this.d.contains(j)) {
+ return this.getDataInternal(chunkcoordintpair);
+ }
+ }
+ }
+ return null;
2019-12-13 14:46:45 +01:00
}
2019-08-19 01:40:04 +02:00
+ // Paper end
}
diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java
2020-05-06 11:48:49 +02:00
index 75ab9f185b3231113dfa387c956a707b403bb2db..8055f5998213ab1c6c10d03d88d2b14d220a5e40 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/TicketType.java
+++ b/src/main/java/net/minecraft/server/TicketType.java
2020-05-06 10:35:20 +02:00
@@ -22,6 +22,7 @@ public class TicketType<T> {
public static final TicketType<Unit> PLUGIN = a("plugin", (a, b) -> 0); // CraftBukkit
2020-02-02 12:53:48 +01:00
public static final TicketType<org.bukkit.plugin.Plugin> PLUGIN_TICKET = a("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit
2020-04-19 19:58:02 +02:00
public static final TicketType<Long> FUTURE_AWAIT = a("future_await", Long::compareTo); // Paper
2019-08-19 01:40:04 +02:00
+ public static final TicketType<Long> ASYNC_LOAD = a("async_load", Long::compareTo); // Paper
public static <T> TicketType<T> a(String s, Comparator<T> 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
2020-06-26 01:38:24 +02:00
index b8c15047771bd4527b86e514a3b950b2ffc6eef0..303f6b0953ff3c29bd31ec5e02386a92b11d114a 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/VillagePlace.java
+++ b/src/main/java/net/minecraft/server/VillagePlace.java
2020-06-26 01:38:24 +02:00
@@ -25,8 +25,16 @@ public class VillagePlace extends RegionFileSection<VillagePlaceSection> {
2019-08-19 01:40:04 +02:00
private final VillagePlace.a a = new VillagePlace.a();
2019-12-12 21:58:07 +01:00
private final LongSet b = new LongOpenHashSet();
2019-08-19 01:40:04 +02:00
+ private final WorldServer world; // Paper
+
2020-06-26 01:38:24 +02:00
public VillagePlace(File file, DataFixer datafixer, boolean flag) {
+ // Paper start - add world parameter
+ this(file, datafixer, flag, null);
2019-08-19 01:40:04 +02:00
+ }
2020-06-26 01:38:24 +02:00
+ public VillagePlace(File file, DataFixer datafixer, boolean flag, WorldServer world) {
super(file, VillagePlaceSection::a, VillagePlaceSection::new, datafixer, DataFixTypes.POI_CHUNK, flag);
+ this.world = world;
+ // Paper end - add world parameter
2019-08-19 01:40:04 +02:00
}
public void a(BlockPosition blockposition, VillagePlaceType villageplacetype) {
2020-06-26 01:38:24 +02:00
@@ -134,7 +142,23 @@ public class VillagePlace extends RegionFileSection<VillagePlaceSection> {
2019-08-19 01:40:04 +02:00
@Override
public void a(BooleanSupplier booleansupplier) {
- super.a(booleansupplier);
+ // Paper start - async chunk io
+ if (this.world == null) {
+ super.a(booleansupplier);
+ } else {
+ //super.a(booleansupplier); // re-implement below
+ while (!((RegionFileSection)this).d.isEmpty() && booleansupplier.getAsBoolean()) {
2020-06-26 01:38:24 +02:00
+ ChunkCoordIntPair chunkcoordintpair = SectionPosition.a(((RegionFileSection)this).d.firstLong()).r();
2019-08-19 01:40:04 +02:00
+
+ NBTTagCompound data;
+ try (co.aikar.timings.Timing ignored1 = this.world.timings.poiSaveDataSerialization.startTiming()) {
+ data = this.getData(chunkcoordintpair);
+ }
+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world,
+ chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY);
+ }
+ }
+ // Paper end
this.a.a();
}
2020-06-26 01:38:24 +02:00
@@ -234,6 +258,35 @@ public class VillagePlace extends RegionFileSection<VillagePlaceSection> {
2019-08-19 01:40:04 +02:00
}
}
+ // Paper start - Asynchronous chunk io
+ @javax.annotation.Nullable
+ @Override
+ public NBTTagCompound read(ChunkCoordIntPair chunkcoordintpair) throws java.io.IOException {
+ if (this.world != null && Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) {
+ NBTTagCompound ret = com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE
+ .loadChunkDataAsyncFuture(this.world, chunkcoordintpair.x, chunkcoordintpair.z, com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread(),
+ true, false, true).join().poiData;
+
+ if (ret == com.destroystokyo.paper.io.PaperFileIOThread.FAILURE_VALUE) {
+ throw new java.io.IOException("See logs for further detail");
+ }
+ return ret;
+ }
+ return super.read(chunkcoordintpair);
+ }
+
+ @Override
+ public void write(ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound) throws java.io.IOException {
+ if (this.world != null && Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) {
+ com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(
+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, null,
+ com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread());
+ return;
+ }
+ super.write(chunkcoordintpair, nbttagcompound);
+ }
+ // Paper end
+
public static enum Occupancy {
HAS_SPACE(VillagePlaceRecord::d), IS_OCCUPIED(VillagePlaceRecord::e), ANY((villageplacerecord) -> {
diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java
2020-06-30 07:20:29 +02:00
index 4529ccc02f53fadc6ac2df97682b21913487bc22..85b73e65a4c1af052e75473bc239d91d5d1fd108 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/net/minecraft/server/WorldServer.java
+++ b/src/main/java/net/minecraft/server/WorldServer.java
2020-06-26 08:29:44 +02:00
@@ -93,6 +93,79 @@ public class WorldServer extends World implements GeneratorAccessSeed {
2020-06-26 01:38:24 +02:00
return this.chunkProvider.getChunkAt(x, z, false);
2019-08-19 01:40:04 +02:00
}
+ // Paper start - Asynchronous IO
+ public final com.destroystokyo.paper.io.PaperFileIOThread.ChunkDataController poiDataController = new com.destroystokyo.paper.io.PaperFileIOThread.ChunkDataController() {
+ @Override
+ public void writeData(int x, int z, NBTTagCompound compound) throws java.io.IOException {
+ WorldServer.this.getChunkProvider().playerChunkMap.getVillagePlace().write(new ChunkCoordIntPair(x, z), compound);
+ }
+
+ @Override
+ public NBTTagCompound readData(int x, int z) throws java.io.IOException {
+ return WorldServer.this.getChunkProvider().playerChunkMap.getVillagePlace().read(new ChunkCoordIntPair(x, z));
+ }
+
+ @Override
+ public <T> T computeForRegionFile(int chunkX, int chunkZ, java.util.function.Function<RegionFile, T> function) {
+ synchronized (WorldServer.this.getChunkProvider().playerChunkMap.getVillagePlace()) {
+ RegionFile file;
+
+ try {
2019-12-13 14:46:45 +01:00
+ file = WorldServer.this.getChunkProvider().playerChunkMap.getVillagePlace().getFile(new ChunkCoordIntPair(chunkX, chunkZ), false);
2019-08-19 01:40:04 +02:00
+ } catch (java.io.IOException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ return function.apply(file);
+ }
+ }
+
+ @Override
+ public <T> T computeForRegionFileIfLoaded(int chunkX, int chunkZ, java.util.function.Function<RegionFile, T> function) {
+ synchronized (WorldServer.this.getChunkProvider().playerChunkMap.getVillagePlace()) {
+ RegionFile file = WorldServer.this.getChunkProvider().playerChunkMap.getVillagePlace().getRegionFileIfLoaded(new ChunkCoordIntPair(chunkX, chunkZ));
+ return function.apply(file);
+ }
+ }
+ };
+
+ public final com.destroystokyo.paper.io.PaperFileIOThread.ChunkDataController chunkDataController = new com.destroystokyo.paper.io.PaperFileIOThread.ChunkDataController() {
+ @Override
+ public void writeData(int x, int z, NBTTagCompound compound) throws java.io.IOException {
+ WorldServer.this.getChunkProvider().playerChunkMap.write(new ChunkCoordIntPair(x, z), compound);
+ }
+
+ @Override
+ public NBTTagCompound readData(int x, int z) throws java.io.IOException {
+ return WorldServer.this.getChunkProvider().playerChunkMap.read(new ChunkCoordIntPair(x, z));
+ }
+
+ @Override
+ public <T> T computeForRegionFile(int chunkX, int chunkZ, java.util.function.Function<RegionFile, T> function) {
+ synchronized (WorldServer.this.getChunkProvider().playerChunkMap) {
+ RegionFile file;
+
+ try {
2020-06-26 01:38:24 +02:00
+ file = WorldServer.this.getChunkProvider().playerChunkMap.regionFileCache.getFile(new ChunkCoordIntPair(chunkX, chunkZ), false);
2019-08-19 01:40:04 +02:00
+ } catch (java.io.IOException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ return function.apply(file);
+ }
+ }
+
+ @Override
+ public <T> T computeForRegionFileIfLoaded(int chunkX, int chunkZ, java.util.function.Function<RegionFile, T> function) {
+ synchronized (WorldServer.this.getChunkProvider().playerChunkMap) {
2020-06-26 01:38:24 +02:00
+ RegionFile file = WorldServer.this.getChunkProvider().playerChunkMap.regionFileCache.getRegionFileIfLoaded(new ChunkCoordIntPair(chunkX, chunkZ));
2019-08-19 01:40:04 +02:00
+ return function.apply(file);
+ }
+ }
+ };
+ public final com.destroystokyo.paper.io.chunk.ChunkTaskManager asyncChunkTaskManager;
+ // Paper end
+
2020-06-26 01:38:24 +02:00
// Add env and gen to constructor, WorldData -> WorldDataServer
public WorldServer(MinecraftServer minecraftserver, Executor executor, Convertable.ConversionSession convertable_conversionsession, IWorldDataServer iworlddataserver, ResourceKey<World> resourcekey, ResourceKey<DimensionManager> resourcekey1, DimensionManager dimensionmanager, WorldLoadListener worldloadlistener, ChunkGenerator chunkgenerator, boolean flag, long i, List<MobSpawner> list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) {
super(iworlddataserver, resourcekey, resourcekey1, dimensionmanager, minecraftserver::getMethodProfiler, false, flag, i, gen, env, executor); // Paper pass executor
2020-06-26 08:29:44 +02:00
@@ -140,6 +213,8 @@ public class WorldServer extends World implements GeneratorAccessSeed {
2020-06-26 01:38:24 +02:00
this.dragonBattle = null;
}
2019-08-19 01:40:04 +02:00
this.getServer().addWorld(this.getWorld()); // CraftBukkit
+
+ this.asyncChunkTaskManager = new com.destroystokyo.paper.io.chunk.ChunkTaskManager(this); // Paper
}
2019-10-18 18:31:01 +02:00
// CraftBukkit start
2020-06-26 08:29:44 +02:00
@@ -1569,7 +1644,10 @@ public class WorldServer extends World implements GeneratorAccessSeed {
2020-04-19 12:01:07 +02:00
}
MCUtil.getSpiralOutChunks(spawn, radiusInBlocks >> 4).forEach(pair -> {
- getChunkProvider().getChunkAtMainThread(pair.x, pair.z);
2020-05-10 04:30:28 +02:00
+ getChunkProvider().getChunkAtAsynchronously(pair.x, pair.z, true, false).exceptionally((ex) -> {
+ ex.printStackTrace();
+ return null;
+ });
2020-04-19 12:01:07 +02:00
});
}
public void removeTicketsForSpawn(int radiusInBlocks, BlockPosition spawn) {
2019-08-19 01:40:04 +02:00
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
2020-07-01 02:07:16 +02:00
index 412dc3865dfee606c37c06daefeacc6ed14ced7b..310c4446734a7ebfadf0cbd74d1cfcee20ef3668 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
2020-06-26 01:38:24 +02:00
@@ -74,6 +74,7 @@ import net.minecraft.server.GroupDataEntity;
2020-05-10 04:30:28 +02:00
import net.minecraft.server.IBlockData;
import net.minecraft.server.IChunkAccess;
import net.minecraft.server.MinecraftKey;
+import net.minecraft.server.MinecraftServer;
import net.minecraft.server.MovingObjectPosition;
import net.minecraft.server.PacketPlayOutCustomSoundEffect;
import net.minecraft.server.PacketPlayOutUpdateTime;
2020-07-01 02:07:16 +02:00
@@ -558,22 +559,23 @@ public class CraftWorld implements World {
2019-08-19 01:40:04 +02:00
return true;
}
- net.minecraft.server.RegionFile file;
- try {
2019-12-12 21:58:07 +01:00
- file = world.getChunkProvider().playerChunkMap.getIOWorker().getRegionFileCache().getFile(chunkPos, false);
2019-08-19 01:40:04 +02:00
- } 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
2020-07-01 02:07:16 +02:00
@@ -2479,6 +2481,34 @@ public class CraftWorld implements World {
2020-06-26 01:38:24 +02:00
public DragonBattle getEnderDragonBattle() {
return (getHandle().getDragonBattle() == null) ? null : new CraftDragonBattle(getHandle().getDragonBattle());
2020-03-26 03:37:20 +01:00
}
2019-08-19 01:40:04 +02:00
+ // Paper start
+ @Override
2020-05-10 04:30:28 +02:00
+ public CompletableFuture<Chunk> getChunkAtAsync(int x, int z, boolean gen, boolean urgent) {
2019-08-19 01:40:04 +02:00
+ if (Bukkit.isPrimaryThread()) {
+ net.minecraft.server.Chunk immediate = this.world.getChunkProvider().getChunkAtIfLoadedImmediately(x, z);
+ if (immediate != null) {
2020-05-10 04:30:28 +02:00
+ return CompletableFuture.completedFuture(immediate.getBukkitChunk());
2019-08-19 01:40:04 +02:00
+ }
2020-05-24 00:38:29 +02:00
+ } else {
+ CompletableFuture<Chunk> future = new CompletableFuture<Chunk>();
+ world.getMinecraftServer().execute(() -> {
+ getChunkAtAsync(x, z, gen, urgent).whenComplete((chunk, err) -> {
+ if (err != null) {
+ future.completeExceptionally(err);
+ } else {
+ future.complete(chunk);
+ }
+ });
+ });
+ return future;
2019-08-19 01:40:04 +02:00
+ }
+
2020-05-10 04:30:28 +02:00
+ return this.world.getChunkProvider().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> {
+ net.minecraft.server.Chunk chunk = (net.minecraft.server.Chunk) either.left().orElse(null);
+ return CompletableFuture.completedFuture(chunk == null ? null : chunk.getBukkitChunk());
+ }, MinecraftServer.getServer());
2019-08-19 01:40:04 +02:00
+ }
+ // Paper end
2020-03-26 03:37:20 +01:00
2019-08-19 01:40:04 +02:00
// Spigot start
@Override
diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
2020-05-06 11:48:49 +02:00
index 07936eeba2a1aa68d52f4183f663ce362c816a54..5bdcdcf9e85b73086722783bff26321d03382bb9 100644
2019-08-19 01:40:04 +02:00
--- a/src/main/java/org/spigotmc/WatchdogThread.java
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
@@ -6,6 +6,7 @@ import java.lang.management.ThreadInfo;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.destroystokyo.paper.PaperConfig;
+import com.destroystokyo.paper.io.chunk.ChunkTaskManager; // Paper
import net.minecraft.server.MinecraftServer;
import org.bukkit.Bukkit;
2020-01-18 18:50:23 +01:00
@@ -112,6 +113,7 @@ public class WatchdogThread extends Thread
2019-08-19 01:40:04 +02:00
// Paper end - Different message for short timeout
log.log( Level.SEVERE, "------------------------------" );
log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
2020-01-18 18:50:23 +01:00
+ ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper
2019-08-19 01:40:04 +02:00
dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
log.log( Level.SEVERE, "------------------------------" );
//