From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Cryptite <cryptite@gmail.com>
Date: Tue, 27 Jun 2023 11:35:52 -0500
Subject: [PATCH] Write SavedData IO async

Co-Authored-By: Shane Freeder <theboyetronic@gmail.com>

diff --git a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java
+++ b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java
@@ -0,0 +0,0 @@ public class ThreadedWorldUpgrader {
             }
 
             this.threadPool.execute(new ConvertTask(info, regionPos.x >> 5, regionPos.z >> 5));
+            // Paper start - Write SavedData IO async
+            this.threadPool.execute(() -> {
+                try {
+                    worldPersistentData.close();
+                } catch (IOException exception) {
+                    LOGGER.error("Failed to close persistent world data", exception);
+                }
+            });
+            // Paper end - Write SavedData IO async
         }
         this.threadPool.shutdown();
 
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource {
 
     public void close(boolean save) { // Paper - rewrite chunk system
         this.level.chunkTaskScheduler.chunkHolderManager.close(save, true); // Paper - rewrite chunk system
+        // Paper start - Write SavedData IO async
+        try {
+            this.dataStorage.close();
+        } catch (IOException exception) {
+            LOGGER.error("Failed to close persistent world data", exception);
+        }
+        // Paper end - Write SavedData IO async
     }
 
     // CraftBukkit start - modelled on below
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel {
 
         try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) {
             if (doFull) {
-                this.saveLevelData();
+                this.saveLevelData(true); // Paper - Write SavedData IO async
             }
 
             this.timings.worldSaveChunks.startTiming(); // Paper
@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel {
                 progressListener.progressStartNoAbort(Component.translatable("menu.savingLevel"));
             }
 
-            this.saveLevelData();
+            this.saveLevelData(!close); // Paper - Write SavedData IO async
             if (progressListener != null) {
                 progressListener.progressStage(Component.translatable("menu.savingChunks"));
             }
@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel {
         // CraftBukkit end
     }
 
-    private void saveLevelData() {
+    private void saveLevelData(boolean async) { // Paper - Write SavedData IO async
         if (this.dragonFight != null) {
             this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit
         }
 
-        this.getChunkSource().getDataStorage().save();
+        this.getChunkSource().getDataStorage().save(async); // Paper - Write SavedData IO async
     }
 
     public <T extends Entity> List<? extends T> getEntities(EntityTypeTest<Entity, T> filter, Predicate<? super T> predicate) {
diff --git a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
+++ b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
@@ -0,0 +0,0 @@ public class WorldUpgrader {
         (new WorldUpgrader.PoiUpgrader(this)).upgrade();
         WorldUpgrader.LOGGER.info("Upgrading blocks");
         (new WorldUpgrader.ChunkUpgrader()).upgrade();
-        this.overworldDataStorage.save();
+        // Paper start - Write SavedData IO async
+        try {
+            this.overworldDataStorage.close();
+        } catch (final IOException e) {
+            LOGGER.error("Failed to close persistent world data", e);
+        }
+        // Paper end - Write SavedData IO async
         i = Util.getMillis() - i;
         WorldUpgrader.LOGGER.info("World optimizaton finished after {} seconds", i / 1000L);
         this.finished = true;
diff --git a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java
+++ b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java
@@ -0,0 +0,0 @@ public abstract class SavedData {
         return this.dirty;
     }
 
+    // Paper start - Write SavedData IO async - joining is evil, but we assume the old blocking behavior here just for safety
+    @io.papermc.paper.annotation.DoNotUse
     public void save(File file, HolderLookup.Provider registryLookup) {
+        save(file, registryLookup, null).join();
+    }
+
+    public java.util.concurrent.CompletableFuture<Void> save(File file, HolderLookup.Provider registryLookup, @org.jetbrains.annotations.Nullable java.util.concurrent.ExecutorService ioExecutor) {
+        // Paper end - Write SavedData IO async
         if (this.isDirty()) {
             CompoundTag compoundTag = new CompoundTag();
             compoundTag.put("data", this.save(new CompoundTag(), registryLookup));
             NbtUtils.addCurrentDataVersion(compoundTag);
 
+            Runnable writeRunnable = () -> { // Paper - Write SavedData IO async
             try {
                 NbtIo.writeCompressed(compoundTag, file.toPath());
             } catch (IOException var5) {
                 LOGGER.error("Could not save data {}", this, var5);
             }
+            }; // Paper - Write SavedData IO async
 
             this.setDirty(false);
+            // Paper start - Write SavedData IO async
+            if (ioExecutor == null) {
+                return java.util.concurrent.CompletableFuture.runAsync(writeRunnable); // No executor, just use common pool
+            }
+            return java.util.concurrent.CompletableFuture.runAsync(writeRunnable, ioExecutor);
         }
+        return java.util.concurrent.CompletableFuture.completedFuture(null);
+        // Paper end - Write SavedData IO async
     }
 
     public static record Factory<T extends SavedData>(
diff --git a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java
+++ b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java
@@ -0,0 +0,0 @@ import net.minecraft.util.datafix.DataFixTypes;
 import net.minecraft.world.level.saveddata.SavedData;
 import org.slf4j.Logger;
 
-public class DimensionDataStorage {
+public class DimensionDataStorage implements java.io.Closeable { // Paper - Write SavedData IO async
     private static final Logger LOGGER = LogUtils.getLogger();
     public final Map<String, SavedData> cache = Maps.newHashMap();
     private final DataFixer fixerUpper;
     private final HolderLookup.Provider registries;
     private final File dataFolder;
+    protected final java.util.concurrent.ExecutorService ioExecutor; // Paper - Write SavedData IO async
 
     public DimensionDataStorage(File directory, DataFixer dataFixer, HolderLookup.Provider registryLookup) {
         this.fixerUpper = dataFixer;
         this.dataFolder = directory;
         this.registries = registryLookup;
+        this.ioExecutor = java.util.concurrent.Executors.newSingleThreadExecutor(new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat("DimensionDataIO - " + dataFolder.getParent() + " - %d").setDaemon(true).build()); // Paper - Write SavedData IO async
     }
 
     private File getDataFile(String id) {
@@ -0,0 +0,0 @@ public class DimensionDataStorage {
         return bl;
     }
 
-    public void save() {
+    // Paper start - Write SavedData IO async
+    @Override
+    public void close() throws IOException {
+        save(false);
+        this.ioExecutor.shutdown();
+    }
+    // Paper end - Write SavedData IO async
+
+    public void save(boolean async) { // Paper - Write SavedData IO async
         this.cache.forEach((id, state) -> {
             if (state != null) {
-                state.save(this.getDataFile(id), this.registries);
+                // Paper start - Write SavedData IO async
+                final java.util.concurrent.CompletableFuture<Void> save = state.save(this.getDataFile(id), this.registries, this.ioExecutor);
+                if (!async) {
+                    save.join();
+                }
+                // Paper end - Write SavedData IO async
             }
         });
     }