From ed4b36f94fb2be421408a13e9c55177e5a4d5b08 Mon Sep 17 00:00:00 2001 From: hayanesuru Date: Sat, 31 Jan 2026 16:12:33 +0900 Subject: [PATCH] async save file --- .../0160-Nitori-Async-playerdata-saving.patch | 241 +++++++++--------- ...p-dirty-stats-copy-when-requesting-p.patch | 6 +- ...-SparklyPaper-Parallel-world-ticking.patch | 10 +- .../0294-fix-async-save-profile-cache.patch | 44 ++++ .../0047-Async-playerdata-saving.patch | 4 +- .../leaf/async/AsyncPlayerDataSaving.java | 127 ++++++--- .../modules/async/AsyncPlayerDataSave.java | 31 ++- 7 files changed, 297 insertions(+), 166 deletions(-) create mode 100644 leaf-server/minecraft-patches/features/0294-fix-async-save-profile-cache.patch diff --git a/leaf-server/minecraft-patches/features/0160-Nitori-Async-playerdata-saving.patch b/leaf-server/minecraft-patches/features/0160-Nitori-Async-playerdata-saving.patch index dccf17e1e8..3f6b186037 100644 --- a/leaf-server/minecraft-patches/features/0160-Nitori-Async-playerdata-saving.patch +++ b/leaf-server/minecraft-patches/features/0160-Nitori-Async-playerdata-saving.patch @@ -3,157 +3,162 @@ From: Dreeam <61569423+Dreeam-qwq@users.noreply.github.com> Date: Fri, 23 Aug 2024 22:04:20 -0400 Subject: [PATCH] Nitori: Async playerdata saving -Original license: GPL v3 +Original license: GPL-3.0 Original project: https://github.com/Gensokyo-Reimagined/Nitori +diff --git a/net/minecraft/server/PlayerAdvancements.java b/net/minecraft/server/PlayerAdvancements.java +index 78135cf45c8900eb142933d216744f4a73127965..14e33218bb9bd3e1f8484c88114900297ec65c1e 100644 +--- a/net/minecraft/server/PlayerAdvancements.java ++++ b/net/minecraft/server/PlayerAdvancements.java +@@ -111,6 +111,7 @@ public class PlayerAdvancements { + + private void load(ServerAdvancementManager manager) { + if (Files.isRegularFile(this.playerSavePath)) { ++ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(playerSavePath, org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave.advancements); // Leaf - Async playerdata saving + try (Reader bufferedReader = Files.newBufferedReader(this.playerSavePath, StandardCharsets.UTF_8)) { + JsonElement jsonElement = StrictJsonParser.parse(bufferedReader); + PlayerAdvancements.Data data = this.codec.parse(JsonOps.INSTANCE, jsonElement).getOrThrow(JsonParseException::new); +@@ -128,17 +129,18 @@ public class PlayerAdvancements { + + public void save() { + if (org.spigotmc.SpigotConfig.disableAdvancementSaving) return; // Spigot ++ // Leaf start - Async playerdata saving + JsonElement jsonElement = this.codec.encodeStart(JsonOps.INSTANCE, this.asData()).getOrThrow(); +- +- try { +- FileUtil.createDirectoriesSafe(this.playerSavePath.getParent()); +- +- try (Writer bufferedWriter = Files.newBufferedWriter(this.playerSavePath, StandardCharsets.UTF_8)) { +- GSON.toJson(jsonElement, GSON.newJsonWriter(bufferedWriter)); +- } +- } catch (JsonIOException | IOException var7) { +- LOGGER.error("Couldn't save player advancements to {}", this.playerSavePath, var7); +- } ++ Path path = this.playerSavePath; ++ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(() -> { ++ FileUtil.createDirectoriesSafe(path.getParent()); ++ String content = GSON.toJson(jsonElement); ++ Path temp = org.dreeam.leaf.async.AsyncPlayerDataSaving.tempFile(path); ++ org.apache.commons.io.FileUtils.writeStringToFile(temp.toFile(), content, java.nio.charset.StandardCharsets.UTF_8, false); ++ Files.move(temp, path, java.nio.file.StandardCopyOption.REPLACE_EXISTING); ++ return null; ++ }, this.playerSavePath, org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave.advancements); ++ // Leaf end - Async playerdata saving + } + + private void applyFrom(ServerAdvancementManager advancementManager, PlayerAdvancements.Data data) { +diff --git a/net/minecraft/stats/ServerStatsCounter.java b/net/minecraft/stats/ServerStatsCounter.java +index f239f445f83d50a69e7e6ffb97e7cf005c421e3a..13710f2fcceba200b6df8222d2402f33f83be0da 100644 +--- a/net/minecraft/stats/ServerStatsCounter.java ++++ b/net/minecraft/stats/ServerStatsCounter.java +@@ -68,6 +68,7 @@ public class ServerStatsCounter extends StatsCounter { + if (Files.isRegularFile(file)) { + try (Reader bufferedReader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + JsonElement jsonElement = StrictJsonParser.parse(bufferedReader); ++ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(file, org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave.stats); // Leaf - Async playerdata saving + this.parse(server.getFixerUpper(), jsonElement); + } catch (IOException var8) { + LOGGER.error("Couldn't read statistics file {}", file, var8); +@@ -90,15 +91,16 @@ public class ServerStatsCounter extends StatsCounter { + + public void save() { + if (org.spigotmc.SpigotConfig.disableStatSaving) return; // Spigot +- try { +- FileUtil.createDirectoriesSafe(this.file.getParent()); +- +- try (Writer bufferedWriter = Files.newBufferedWriter(this.file, StandardCharsets.UTF_8)) { +- GSON.toJson(this.toJson(), GSON.newJsonWriter(bufferedWriter)); +- } +- } catch (JsonIOException | IOException var6) { +- LOGGER.error("Couldn't save stats to {}", this.file, var6); +- } ++ // Leaf start - Async playerdata saving ++ Path file = this.file; ++ JsonElement data = this.toJson(); ++ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(() -> { ++ java.nio.file.Path temp = org.dreeam.leaf.async.AsyncPlayerDataSaving.tempFile(file); ++ org.apache.commons.io.FileUtils.writeStringToFile(temp.toFile(), GSON.toJson(data), java.nio.charset.StandardCharsets.UTF_8, false); ++ java.nio.file.Files.move(temp, file, java.nio.file.StandardCopyOption.REPLACE_EXISTING); ++ return null; ++ }, file, org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave.stats); ++ // Leaf end - Async playerdata saving + } + + @Override diff --git a/net/minecraft/world/level/storage/LevelStorageSource.java b/net/minecraft/world/level/storage/LevelStorageSource.java -index 140630186c3b0324c248cc2a2f31d3b906557b9c..d5e9190f0a6baffd2b08225f595ff4d7eb662e65 100644 +index 140630186c3b0324c248cc2a2f31d3b906557b9c..75feaa87159f8bf399b6d51f73ccf1c25a50265e 100644 --- a/net/minecraft/world/level/storage/LevelStorageSource.java +++ b/net/minecraft/world/level/storage/LevelStorageSource.java -@@ -516,15 +516,26 @@ public class LevelStorageSource { +@@ -516,15 +516,20 @@ public class LevelStorageSource { private void saveLevelData(CompoundTag tag) { Path path = this.levelDirectory.path(); +- try { + // Leaf start - Async playerdata saving + // Save level.dat asynchronously -+ var nbtBytes = new it.unimi.dsi.fastutil.io.FastByteArrayOutputStream(65536); - try { -- Path path1 = Files.createTempFile(path, "level", ".dat"); ++ ++ Path path2 = this.levelDirectory.oldDataFile(); ++ Path path3 = this.levelDirectory.dataFile(); ++ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(() -> { ++ var nbtBytes = new it.unimi.dsi.fastutil.io.FastByteArrayOutputStream(65536); ++ NbtIo.writeCompressed(tag, nbtBytes); + Path path1 = Files.createTempFile(path, "level", ".dat"); - NbtIo.writeCompressed(tag, path1); - Path path2 = this.levelDirectory.oldDataFile(); - Path path3 = this.levelDirectory.dataFile(); -- Util.safeReplaceFile(path3, path1, path2); -+ NbtIo.writeCompressed(tag, nbtBytes); - } catch (Exception var6) { ++ org.apache.commons.io.FileUtils.writeByteArrayToFile(path1.toFile(), nbtBytes.array, 0, nbtBytes.length, false); + Util.safeReplaceFile(path3, path1, path2); +- } catch (Exception var6) { - LevelStorageSource.LOGGER.error("Failed to save level {}", path, var6); -+ LevelStorageSource.LOGGER.error("Failed to encode level {}", path, var6); - } -+ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(() -> { -+ try { -+ Path path1 = Files.createTempFile(path, "level", ".dat"); -+ org.apache.commons.io.FileUtils.writeByteArrayToFile(path1.toFile(), nbtBytes.array, 0, nbtBytes.length, false); -+ Path path2 = this.levelDirectory.oldDataFile(); -+ Path path3 = this.levelDirectory.dataFile(); -+ Util.safeReplaceFile(path3, path1, path2); -+ } catch (Exception var6) { -+ LevelStorageSource.LOGGER.error("Failed to save level {}", path, var6); -+ } -+ }); +- } ++ return null; ++ }, path3, org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave.levelData); + // Leaf end - Async playerdata saving } public Optional getIconFile() { diff --git a/net/minecraft/world/level/storage/PlayerDataStorage.java b/net/minecraft/world/level/storage/PlayerDataStorage.java -index b8ef50bc3d07890c9da2c98d5f009a3adc52f4b0..4099f4ebf76c6bf74384aa5b9f5e889d53ebcfa9 100644 +index b8ef50bc3d07890c9da2c98d5f009a3adc52f4b0..11055fa067933083af5fca1c9d9e45435684f23b 100644 --- a/net/minecraft/world/level/storage/PlayerDataStorage.java +++ b/net/minecraft/world/level/storage/PlayerDataStorage.java -@@ -23,6 +23,7 @@ public class PlayerDataStorage { - private static final Logger LOGGER = LogUtils.getLogger(); - private final File playerDir; - protected final DataFixer fixerUpper; -+ private final java.util.Map> savingLocks = new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(); // Leaf - Async playerdata saving - - public PlayerDataStorage(LevelStorageSource.LevelStorageAccess levelStorageAccess, DataFixer fixerUpper) { - this.fixerUpper = fixerUpper; -@@ -32,21 +33,84 @@ public class PlayerDataStorage { - - public void save(Player player) { - if (org.spigotmc.SpigotConfig.disablePlayerDataSaving) return; // Spigot -+ // Leaf start - Async playerdata saving -+ CompoundTag compoundTag; - try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), LOGGER)) { - TagValueOutput tagValueOutput = TagValueOutput.createWithContext(scopedCollector, player.registryAccess()); - player.saveWithoutId(tagValueOutput); -- Path path = this.playerDir.toPath(); -- Path path1 = Files.createTempFile(path, player.getStringUUID() + "-", ".dat"); -- CompoundTag compoundTag = tagValueOutput.buildResult(); +@@ -38,15 +38,28 @@ public class PlayerDataStorage { + Path path = this.playerDir.toPath(); + Path path1 = Files.createTempFile(path, player.getStringUUID() + "-", ".dat"); + CompoundTag compoundTag = tagValueOutput.buildResult(); - NbtIo.writeCompressed(compoundTag, path1); - Path path2 = path.resolve(player.getStringUUID() + ".dat"); - Path path3 = path.resolve(player.getStringUUID() + ".dat_old"); - Util.safeReplaceFile(path2, path1, path3); -- } catch (Exception var11) { -- LOGGER.warn("Failed to save player data for {}", player.getPlainTextName(), var11); // Paper - Print exception -+ compoundTag = tagValueOutput.buildResult(); -+ } catch (Exception exception) { -+ LOGGER.warn("Failed to encode player data for {}", player.getPlainTextName(), exception); -+ return; ++ save(player.getStringUUID(), compoundTag); + } catch (Exception var11) { + LOGGER.warn("Failed to save player data for {}", player.getPlainTextName(), var11); // Paper - Print exception } -+ save(player.getScoreboardName(), player.getUUID(), player.getStringUUID(), compoundTag); -+ // Leaf end - Async playerdata saving } + // Leaf start - Async playerdata saving -+ public void save(String playerName, java.util.UUID uniqueId, String stringId, CompoundTag compoundTag) { -+ var nbtBytes = new it.unimi.dsi.fastutil.io.FastByteArrayOutputStream(65536); -+ try { ++ public void save(String stringId, CompoundTag compoundTag) { ++ Path path = this.playerDir.toPath(); ++ Path path2 = path.resolve(stringId + ".dat"); ++ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(() -> { ++ var nbtBytes = new it.unimi.dsi.fastutil.io.FastByteArrayOutputStream(65536); + NbtIo.writeCompressed(compoundTag, nbtBytes); -+ } catch (Exception exception) { -+ LOGGER.warn("Failed to encode player data for {}", stringId, exception); -+ } -+ lockFor(uniqueId, playerName); -+ synchronized (PlayerDataStorage.this) { -+ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(() -> { -+ try { -+ Path path = this.playerDir.toPath(); -+ Path path1 = Files.createTempFile(path, stringId + "-", ".dat"); -+ org.apache.commons.io.FileUtils.writeByteArrayToFile(path1.toFile(), nbtBytes.array, 0, nbtBytes.length, false); -+ Path path2 = path.resolve(stringId + ".dat"); -+ Path path3 = path.resolve(stringId + ".dat_old"); -+ Util.safeReplaceFile(path2, path1, path3); -+ } catch (Exception var7) { -+ LOGGER.warn("Failed to save player data for {}", playerName, var7); -+ } finally { -+ synchronized (PlayerDataStorage.this) { -+ savingLocks.remove(uniqueId); -+ } -+ } -+ }).ifPresent(future -> savingLocks.put(uniqueId, future)); -+ } -+ } -+ -+ private void lockFor(java.util.UUID uniqueId, String playerName) { -+ java.util.concurrent.Future fut; -+ synchronized (this) { -+ fut = savingLocks.get(uniqueId); -+ } -+ if (fut == null) { -+ return; -+ } -+ while (true) { -+ try { -+ fut.get(10_000L, java.util.concurrent.TimeUnit.MILLISECONDS); -+ break; -+ } catch (InterruptedException ignored) { -+ } catch (java.util.concurrent.ExecutionException -+ | java.util.concurrent.TimeoutException exception) { -+ LOGGER.warn("Failed to save player data for {}", playerName, exception); -+ -+ String threadDump = ""; -+ var threadMXBean = java.lang.management.ManagementFactory.getThreadMXBean(); -+ for (var threadInfo : threadMXBean.dumpAllThreads(true, true)) { -+ if (threadInfo.getThreadName().equals("Leaf IO Thread")) { // TODO: We should use instanceOf here -+ threadDump = threadInfo.toString(); -+ break; -+ } -+ } -+ LOGGER.warn(threadDump); -+ fut.cancel(true); -+ break; -+ } finally { -+ savingLocks.remove(uniqueId); -+ } -+ } ++ Path path1 = org.dreeam.leaf.async.AsyncPlayerDataSaving.tempFile(path, stringId, ".dat"); ++ org.apache.commons.io.FileUtils.writeByteArrayToFile(path1.toFile(), nbtBytes.array, 0, nbtBytes.length, false); ++ Path path3 = path.resolve(stringId + ".dat_old"); ++ Util.safeReplaceFile(path2, path1, path3); ++ return null; ++ }, path2, org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave.playerdata); + } + // Leaf end - Async playerdata saving + private void backup(NameAndId nameAndId, String suffix) { Path path = this.playerDir.toPath(); String string = nameAndId.id().toString(); -@@ -62,6 +126,7 @@ public class PlayerDataStorage { - } - - private Optional load(NameAndId nameAndId, String suffix) { -+ lockFor(nameAndId.id(), nameAndId.name()); // Leaf - Async playerdata saving - File file = new File(this.playerDir, nameAndId.id() + suffix); - // Spigot start - boolean usingWrongFile = false; +@@ -76,6 +89,7 @@ public class PlayerDataStorage { + if (file.exists() && file.isFile()) { + try { + // Spigot start ++ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(file.toPath(), org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave.playerdata); // Leaf - Async playerdata saving + Optional optional = Optional.of(NbtIo.readCompressed(file.toPath(), NbtAccounter.unlimitedHeap())); + if (usingWrongFile) { + file.renameTo(new File(file.getPath() + ".offline-read")); diff --git a/leaf-server/minecraft-patches/features/0179-SparklyPaper-Skip-dirty-stats-copy-when-requesting-p.patch b/leaf-server/minecraft-patches/features/0179-SparklyPaper-Skip-dirty-stats-copy-when-requesting-p.patch index a5b4ffd52c..9969e51716 100644 --- a/leaf-server/minecraft-patches/features/0179-SparklyPaper-Skip-dirty-stats-copy-when-requesting-p.patch +++ b/leaf-server/minecraft-patches/features/0179-SparklyPaper-Skip-dirty-stats-copy-when-requesting-p.patch @@ -7,10 +7,10 @@ Subject: [PATCH] SparklyPaper: Skip dirty stats copy when requesting player Original project: https://github.com/SparklyPower/SparklyPaper diff --git a/net/minecraft/stats/ServerStatsCounter.java b/net/minecraft/stats/ServerStatsCounter.java -index f239f445f83d50a69e7e6ffb97e7cf005c421e3a..e8af6a4877d01cc56ef0347013978ba9d583aa7e 100644 +index 13710f2fcceba200b6df8222d2402f33f83be0da..102001d521a925c3754253f1e47c0b6d0b3c4e9d 100644 --- a/net/minecraft/stats/ServerStatsCounter.java +++ b/net/minecraft/stats/ServerStatsCounter.java -@@ -109,11 +109,15 @@ public class ServerStatsCounter extends StatsCounter { +@@ -111,11 +111,15 @@ public class ServerStatsCounter extends StatsCounter { this.dirty.add(stat); } @@ -26,7 +26,7 @@ index f239f445f83d50a69e7e6ffb97e7cf005c421e3a..e8af6a4877d01cc56ef0347013978ba9 public void parse(DataFixer fixerUpper, JsonElement json) { Dynamic dynamic = new Dynamic<>(JsonOps.INSTANCE, json); -@@ -140,10 +144,12 @@ public class ServerStatsCounter extends StatsCounter { +@@ -142,10 +146,12 @@ public class ServerStatsCounter extends StatsCounter { public void sendStats(ServerPlayer player) { Object2IntMap> map = new Object2IntOpenHashMap<>(); diff --git a/leaf-server/minecraft-patches/features/0194-SparklyPaper-Parallel-world-ticking.patch b/leaf-server/minecraft-patches/features/0194-SparklyPaper-Parallel-world-ticking.patch index 649f2326e5..d806f4fce8 100644 --- a/leaf-server/minecraft-patches/features/0194-SparklyPaper-Parallel-world-ticking.patch +++ b/leaf-server/minecraft-patches/features/0194-SparklyPaper-Parallel-world-ticking.patch @@ -294,7 +294,7 @@ index 963e8cb72bb97daf516c72e954bcb02d1c4643a5..4b06d1314846593d38cc2ce07d6e86f5 } // CraftBukkit end diff --git a/net/minecraft/server/PlayerAdvancements.java b/net/minecraft/server/PlayerAdvancements.java -index 78135cf45c8900eb142933d216744f4a73127965..96525c3107801cdb2904e5379cd59b98024141cd 100644 +index 14e33218bb9bd3e1f8484c88114900297ec65c1e..d991c98eb26e3acef33d8125edcd38927fb8ac68 100644 --- a/net/minecraft/server/PlayerAdvancements.java +++ b/net/minecraft/server/PlayerAdvancements.java @@ -53,8 +53,10 @@ public class PlayerAdvancements { @@ -310,7 +310,7 @@ index 78135cf45c8900eb142933d216744f4a73127965..96525c3107801cdb2904e5379cd59b98 private ServerPlayer player; private @Nullable AdvancementHolder lastSelectedTab; private boolean isFirstPacket = true; -@@ -149,7 +151,7 @@ public class PlayerAdvancements { +@@ -151,7 +153,7 @@ public class PlayerAdvancements { if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.ignoredAdvancements) LOGGER.warn("Ignored advancement '{}' in progress file {} - it doesn't exist anymore?", path, this.playerSavePath); // Gale - Purpur - do not log ignored advancements } else { this.startProgress(advancementHolder, progress); @@ -319,7 +319,7 @@ index 78135cf45c8900eb142933d216744f4a73127965..96525c3107801cdb2904e5379cd59b98 this.markForVisibilityUpdate(advancementHolder); } }); -@@ -218,7 +220,7 @@ public class PlayerAdvancements { +@@ -220,7 +222,7 @@ public class PlayerAdvancements { flag = true; } @@ -328,7 +328,7 @@ index 78135cf45c8900eb142933d216744f4a73127965..96525c3107801cdb2904e5379cd59b98 this.markForVisibilityUpdate(advancement); } -@@ -264,6 +266,7 @@ public class PlayerAdvancements { +@@ -266,6 +268,7 @@ public class PlayerAdvancements { } public void flushDirty(ServerPlayer player, boolean showAdvancements) { @@ -336,7 +336,7 @@ index 78135cf45c8900eb142933d216744f4a73127965..96525c3107801cdb2904e5379cd59b98 if (this.isFirstPacket || !this.rootsToUpdate.isEmpty() || !this.progressChanged.isEmpty()) { Map map = new HashMap<>(); Set set = new java.util.TreeSet<>(java.util.Comparator.comparing(adv -> adv.id().toString())); // Paper - Changed from HashSet to TreeSet ordered alphabetically. -@@ -275,13 +278,23 @@ public class PlayerAdvancements { +@@ -277,13 +280,23 @@ public class PlayerAdvancements { this.rootsToUpdate.clear(); diff --git a/leaf-server/minecraft-patches/features/0294-fix-async-save-profile-cache.patch b/leaf-server/minecraft-patches/features/0294-fix-async-save-profile-cache.patch new file mode 100644 index 0000000000..2ef087b34a --- /dev/null +++ b/leaf-server/minecraft-patches/features/0294-fix-async-save-profile-cache.patch @@ -0,0 +1,44 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: hayanesuru +Date: Sat, 31 Jan 2026 16:00:21 +0900 +Subject: [PATCH] fix async save profile cache + + +diff --git a/net/minecraft/server/players/CachedUserNameToIdResolver.java b/net/minecraft/server/players/CachedUserNameToIdResolver.java +index a06275b08bc2a343260245a348958ed57fdd37d4..eba47e7eb856d04ceff87dbc055c7c3e30279e02 100644 +--- a/net/minecraft/server/players/CachedUserNameToIdResolver.java ++++ b/net/minecraft/server/players/CachedUserNameToIdResolver.java +@@ -219,21 +219,26 @@ public class CachedUserNameToIdResolver implements UserNameToIdResolver { + JsonArray jsonArray = new JsonArray(); + DateFormat dateFormat = createDateFormat(); + this.listTopMRUProfiles(org.spigotmc.SpigotConfig.userCacheCap).forEach(gameProfileInfo -> jsonArray.add(writeGameProfile(gameProfileInfo, dateFormat))); // Spigot // Paper - Fix GameProfileCache concurrency +- String string = this.gson.toJson((JsonElement)jsonArray); ++ //String string = this.gson.toJson((JsonElement)jsonArray); // Leaf - fix async save profile cache + +- Runnable save = () -> { // Paper - Perf: Async GameProfileCache saving ++ java.util.concurrent.Callable save = () -> { // Paper - Perf: Async GameProfileCache saving // Leaf - fix async save profile cache ++ String string = this.gson.toJson((JsonElement)jsonArray); // Leaf - fix async save profile cache + try (Writer writer = Files.newWriter(this.file, StandardCharsets.UTF_8)) { + writer.write(string); + } catch (IOException var9) { + } ++ return null; // Leaf - fix async save profile cache + // Paper start - Perf: Async GameProfileCache saving + }; +- if (asyncSave) { +- io.papermc.paper.util.MCUtil.scheduleAsyncTask(save); +- } else { +- save.run(); +- } ++ // Leaf start - fix async save profile cache ++ //if (asyncSave) { ++ // io.papermc.paper.util.MCUtil.scheduleAsyncTask(save); ++ //} else { ++ // save.run(); ++ //} + // Paper end - Perf: Async GameProfileCache saving ++ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit2(save, this.file.toPath(), asyncSave, org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave.profileCache); ++ // Leaf end - fix async save profile cache + } + + private Stream getTopMRUProfiles(int limit) { diff --git a/leaf-server/paper-patches/features/0047-Async-playerdata-saving.patch b/leaf-server/paper-patches/features/0047-Async-playerdata-saving.patch index cccfa45ced..15606ebe86 100644 --- a/leaf-server/paper-patches/features/0047-Async-playerdata-saving.patch +++ b/leaf-server/paper-patches/features/0047-Async-playerdata-saving.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Async playerdata saving diff --git a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java -index 7283eb950a0d1c92c451a0379d7a409d147bb84c..bfc9b34f39c828831b788814dd2a8c37bec93511 100644 +index 7283eb950a0d1c92c451a0379d7a409d147bb84c..174422cb1d8c02e06c84e7f98081cd26e0187f34 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java @@ -752,16 +752,7 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa @@ -22,7 +22,7 @@ index 7283eb950a0d1c92c451a0379d7a409d147bb84c..bfc9b34f39c828831b788814dd2a8c37 - } catch (java.io.IOException e) { - e.printStackTrace(); - } -+ server.console.playerDataStorage.save(this.getName(), this.getUniqueId(), this.getUniqueId().toString(), compoundTag); // Leaf - Async playerdata saving ++ server.console.playerDataStorage.save(this.getUniqueId().toString(), compoundTag); // Leaf - Async playerdata saving } // Purpur end - OfflinePlayer API } diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/AsyncPlayerDataSaving.java b/leaf-server/src/main/java/org/dreeam/leaf/async/AsyncPlayerDataSaving.java index 7b423a4012..ac15ba8947 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/async/AsyncPlayerDataSaving.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/AsyncPlayerDataSaving.java @@ -1,48 +1,113 @@ package org.dreeam.leaf.async; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.util.Util; -import org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave; +import org.apache.commons.io.FilenameUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; -import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.*; -public class AsyncPlayerDataSaving { +@NullMarked +public final class AsyncPlayerDataSaving { - public static ExecutorService IO_POOL = null; + public static final ThreadPoolExecutor IO_POOL; + private static final Object2ObjectMap> TASKS = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), AsyncPlayerDataSaving.class); + private static final Logger LOGGER = LogManager.getLogger("Leaf Async IO"); private AsyncPlayerDataSaving() { } - public static void init() { - if (IO_POOL == null) { - IO_POOL = new ThreadPoolExecutor( - 1, - 1, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(), - new com.google.common.util.concurrent.ThreadFactoryBuilder() - .setPriority(Thread.NORM_PRIORITY - 2) - .setNameFormat("Leaf IO Thread") - .setUncaughtExceptionHandler(Util::onThreadException) - .build(), - new ThreadPoolExecutor.DiscardPolicy() - ); - } else { - // Temp no-op - //throw new IllegalStateException(); + static { + IO_POOL = new ThreadPoolExecutor( + 1, + 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), + new com.google.common.util.concurrent.ThreadFactoryBuilder() + .setPriority(Thread.NORM_PRIORITY - 2) + .setNameFormat("Leaf IO Thread") + .setUncaughtExceptionHandler(Util::onThreadException) + .build(), + new ThreadPoolExecutor.DiscardPolicy() + ); + } + + public static void submit(@Nullable Path name, boolean enabled) { + if (name == null) { + return; + } + if (enabled) { + Future fut = TASKS.get(name); + if (fut != null) { + try { + fut.get(); + TASKS.remove(name, fut); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + LOGGER.error("Failed to save {}", name, e.getCause()); + } + } } } - public static Optional> submit(Runnable runnable) { - if (!AsyncPlayerDataSave.enabled) { - runnable.run(); - return Optional.empty(); + public static void submit(Callable task, Path name, boolean enabled) { + submit2(task, name, true, enabled); + } + + public static void submit2(Callable task, @Nullable Path name, boolean async, boolean enabled) { + submit(name, enabled); + if (name == null) { + return; + } + if (!enabled || !async) { + //FutureTask f = new FutureTask<>(task); + //TASKS.put(name, f); + //return f; + try { + task.call(); + } catch (Exception e) { + LOGGER.error("Failed to execute {}", name, e); + } } else { - return Optional.of(IO_POOL.submit(runnable)); + TASKS.put(name, IO_POOL.submit(new SaveTask(name, task))); + } + } + + private record SaveTask(Path name, Callable task) implements Callable { + @Override + public Void call() { + try { + task.call(); + } catch (Exception e) { + LOGGER.error("Failed to save {}", name, e); + } finally { + TASKS.remove(name); + } + return null; + } + } + + public static Path tempFile(Path file) throws IOException { + String fileName = file.getFileName().toString(); + String prefix = FilenameUtils.getBaseName(fileName); + String suffix = FilenameUtils.getExtension(fileName); + Path dir = file.getParent(); + return tempFile(dir, prefix, suffix); + } + + public static Path tempFile(Path dir, String prefix, String suffix) throws IOException { + if (!dir.toFile().exists()) { + Files.createDirectories(dir); } + return Files.createTempFile(dir, prefix + '-', suffix); } } diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPlayerDataSave.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPlayerDataSave.java index c87908e232..cbc8cd4ae5 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPlayerDataSave.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncPlayerDataSave.java @@ -9,19 +9,36 @@ public String getBasePath() { return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-playerdata-save"; } - public static boolean enabled = false; + public static boolean playerdata = false; + public static boolean advancements = false; + public static boolean stats = false; + public static boolean levelData = false; + public static boolean userList = false; + public static boolean profileCache = true; + private static boolean asyncPlayerDataSavingInitialized; @Override public void onLoaded() { config.addCommentRegionBased(getBasePath(), """ - Make PlayerData saving asynchronously.""", + Save file asynchronously.""", """ - 异步保存玩家数据."""); + 异步保存文件."""); - enabled = config.getBoolean(getBasePath() + ".enabled", enabled); - - if (enabled) { - org.dreeam.leaf.async.AsyncPlayerDataSaving.init(); + if (asyncPlayerDataSavingInitialized) { + config.getConfigSection(getBasePath()); + return; } + asyncPlayerDataSavingInitialized = true; + + advancements = get("advancements", advancements); + playerdata = get("playerdata", playerdata); + stats = get("stats", stats); + levelData = get("level-data", levelData); + userList = get("user-list", userList); + profileCache = get("profile-cache", profileCache); + } + + private boolean get(String s, boolean def) { + return config.getBoolean(getBasePath() + '.' + s, def); } }