From d166381efa45df8c1b2777656fdba13a64ec2ef5 Mon Sep 17 00:00:00 2001 From: Nassim Jahnke <nassim@njahnke.dev> Date: Fri, 26 Nov 2021 17:25:35 +0100 Subject: [PATCH] Readd incremental chunk saving, merge with player saving --- .../server/Do-not-copy-visible-chunks.patch | 2 +- ...I-O-threads-with-chunk-data-while-fl.patch | 2 +- ...mprove-Chunk-Status-Transition-Speed.patch | 4 +- .../server/Incremental-player-saving.patch | 126 ------ .../Make-item-validations-configurable.patch | 4 +- .../Optimise-nearby-player-lookups.patch | 2 +- ...-headless-pistons-from-being-created.patch | 5 +- patches/server/incremental-chunk-saving.patch | 414 ++++++++++++++++++ 8 files changed, 424 insertions(+), 135 deletions(-) delete mode 100644 patches/server/Incremental-player-saving.patch create mode 100644 patches/server/incremental-chunk-saving.patch diff --git a/patches/server/Do-not-copy-visible-chunks.patch b/patches/server/Do-not-copy-visible-chunks.patch index 827595fc4e..e9dee5f7b6 100644 --- a/patches/server/Do-not-copy-visible-chunks.patch +++ b/patches/server/Do-not-copy-visible-chunks.patch @@ -124,7 +124,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 - ObjectIterator objectiterator = this.visibleChunkMap.values().iterator(); + Iterator objectiterator = this.updatingChunks.getVisibleValuesCopy().iterator(); // Paper - while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { + while (false && l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { // Paper - incremental chunk and player saving if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) { @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (!this.modified) { diff --git a/patches/server/Do-not-overload-I-O-threads-with-chunk-data-while-fl.patch b/patches/server/Do-not-overload-I-O-threads-with-chunk-data-while-fl.patch index a25fbb4d68..a30e922275 100644 --- a/patches/server/Do-not-overload-I-O-threads-with-chunk-data-while-fl.patch +++ b/patches/server/Do-not-overload-I-O-threads-with-chunk-data-while-fl.patch @@ -16,7 +16,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } + // Paper end protected void saveAllChunks(boolean flush) { + // Paper start - do not overload I/O threads with too much work when saving diff --git a/patches/server/Improve-Chunk-Status-Transition-Speed.patch b/patches/server/Improve-Chunk-Status-Transition-Speed.patch index 98a4256fff..8606e368f0 100644 --- a/patches/server/Improve-Chunk-Status-Transition-Speed.patch +++ b/patches/server/Improve-Chunk-Status-Transition-Speed.patch @@ -40,9 +40,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/net/minecraft/server/level/ChunkHolder.java +++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java @@ -0,0 +0,0 @@ public class ChunkHolder { - this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key); - } // Paper end - optimise isOutsideOfRange + long lastAutoSaveTime; // Paper - incremental autosave + long inactiveTimeStart; // Paper - incremental autosave + // Paper start - optimize chunk status progression without jumping through thread pool + public boolean canAdvanceStatus() { + ChunkStatus status = getChunkHolderStatus(); diff --git a/patches/server/Incremental-player-saving.patch b/patches/server/Incremental-player-saving.patch deleted file mode 100644 index c96e60b017..0000000000 --- a/patches/server/Incremental-player-saving.patch +++ /dev/null @@ -1,126 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar <aikar@aikar.co> -Date: Sun, 9 Aug 2020 08:59:25 +0300 -Subject: [PATCH] Incremental player saving - - -diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/com/destroystokyo/paper/PaperConfig.java -+++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java -@@ -0,0 +0,0 @@ public class PaperConfig { - config.set("settings.unsupported-settings.allow-headless-pistons-readme", "This setting controls if players should be able to create headless pistons."); - allowHeadlessPistons = getBoolean("settings.unsupported-settings.allow-headless-pistons", false); - } -+ -+ public static int playerAutoSaveRate = -1; -+ public static int maxPlayerAutoSavePerTick = 10; -+ private static void playerAutoSaveRate() { -+ playerAutoSaveRate = getInt("settings.player-auto-save-rate", -1); -+ maxPlayerAutoSavePerTick = getInt("settings.max-player-auto-save-per-tick", -1); -+ if (maxPlayerAutoSavePerTick == -1) { // -1 Automatic / "Recommended" -+ // 10 should be safe for everyone unless you mass spamming player auto save -+ maxPlayerAutoSavePerTick = (playerAutoSaveRate == -1 || playerAutoSaveRate > 100) ? 10 : 20; -+ } -+ } - } -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - return flag3; - } - -+ // Paper start - public boolean saveEverything(boolean suppressLogs, boolean flush, boolean force) { -+ return saveEverything(suppressLogs, flush, force, -1); -+ } -+ public boolean saveEverything(boolean suppressLogs, boolean flush, boolean force, int playerSaveInterval) { -+ // Paper end - boolean flag3; - - try { - this.isSaving = true; -- this.getPlayerList().saveAll(); -+ this.getPlayerList().saveAll(playerSaveInterval); // Paper - flag3 = this.saveAllChunks(suppressLogs, flush, force); - } finally { - this.isSaving = false; -@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - this.isSaving = true; - if (this.playerList != null) { - MinecraftServer.LOGGER.info("Saving players"); -- this.playerList.saveAll(); - this.playerList.removeAll(this.isRestarting); // Paper - try { Thread.sleep(100); } catch (InterruptedException ex) {} // CraftBukkit - SPIGOT-625 - give server at least a chance to send packets - } -@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - } - } - -+ // Paper start -+ int playerSaveInterval = com.destroystokyo.paper.PaperConfig.playerAutoSaveRate; -+ if (playerSaveInterval < 0) { -+ playerSaveInterval = autosavePeriod; -+ } -+ // Paper end - if (this.autosavePeriod > 0 && this.tickCount % this.autosavePeriod == 0) { // CraftBukkit - MinecraftServer.LOGGER.debug("Autosave started"); - this.profiler.push("save"); -- this.saveEverything(true, false, false); -+ this.saveEverything(true, false, false, playerSaveInterval); // Paper - this.profiler.pop(); - MinecraftServer.LOGGER.debug("Autosave finished"); -- } -+ } else this.getPlayerList().saveAll(playerSaveInterval); // Paper - io.papermc.paper.util.CachedLists.reset(); // Paper - // Paper start - move executeAll() into full server tick timing - try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) { -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -0,0 +0,0 @@ public class ServerPlayer extends Player { - public final int getViewDistance() { return this.getLevel().getChunkSource().chunkMap.viewDistance - 1; } // Paper - placeholder - - private static final Logger LOGGER = LogManager.getLogger(); -+ public long lastSave = MinecraftServer.currentTick; // Paper - private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; - private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; - public ServerGamePacketListenerImpl connection; -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -0,0 +0,0 @@ public abstract class PlayerList { - protected void save(ServerPlayer player) { - if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit - if (!player.didPlayerJoinEvent) return; // Paper - If we never fired PJE, we disconnected during login. Data has not changed, and additionally, our saved vehicle is not loaded! If we save now, we will lose our vehicle (CraftBukkit bug) -+ player.lastSave = MinecraftServer.currentTick; // Paper - this.playerIo.save(player); - ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit - -@@ -0,0 +0,0 @@ public abstract class PlayerList { - } - - public void saveAll() { -+ // Paper start - incremental player saving -+ saveAll(-1); -+ } -+ public void saveAll(int interval) { - net.minecraft.server.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main - MinecraftTimings.savePlayers.startTiming(); // Paper -+ int numSaved = 0; -+ long now = MinecraftServer.currentTick; - for (int i = 0; i < this.players.size(); ++i) { -- this.save(this.players.get(i)); -+ ServerPlayer entityplayer = this.players.get(i); -+ if (interval == -1 || now - entityplayer.lastSave >= interval) { -+ this.save(entityplayer); -+ if (interval != -1 && ++numSaved <= com.destroystokyo.paper.PaperConfig.maxPlayerAutoSavePerTick) { break; } -+ } -+ // Paper end - } - MinecraftTimings.savePlayers.stopTiming(); // Paper - return null; }); // Paper - ensure main diff --git a/patches/server/Make-item-validations-configurable.patch b/patches/server/Make-item-validations-configurable.patch index 75ca412237..ea9b4d0284 100644 --- a/patches/server/Make-item-validations-configurable.patch +++ b/patches/server/Make-item-validations-configurable.patch @@ -9,8 +9,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/com/destroystokyo/paper/PaperConfig.java +++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java @@ -0,0 +0,0 @@ public class PaperConfig { - maxPlayerAutoSavePerTick = (playerAutoSaveRate == -1 || playerAutoSaveRate > 100) ? 10 : 20; - } + config.set("settings.unsupported-settings.allow-headless-pistons-readme", "This setting controls if players should be able to create headless pistons."); + allowHeadlessPistons = getBoolean("settings.unsupported-settings.allow-headless-pistons", false); } + + public static int itemValidationDisplayNameLength = 8192; diff --git a/patches/server/Optimise-nearby-player-lookups.patch b/patches/server/Optimise-nearby-player-lookups.patch index 131025b266..33efdb0d39 100644 --- a/patches/server/Optimise-nearby-player-lookups.patch +++ b/patches/server/Optimise-nearby-player-lookups.patch @@ -24,7 +24,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 + // Paper end - optimise checkDespawn } // Paper end - optimise isOutsideOfRange - // Paper start - optimize chunk status progression without jumping through thread pool + long lastAutoSaveTime; // Paper - incremental autosave diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java diff --git a/patches/server/Prevent-headless-pistons-from-being-created.patch b/patches/server/Prevent-headless-pistons-from-being-created.patch index 701e0cfe37..9c1e1ebe00 100644 --- a/patches/server/Prevent-headless-pistons-from-being-created.patch +++ b/patches/server/Prevent-headless-pistons-from-being-created.patch @@ -10,9 +10,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000 --- a/src/main/java/com/destroystokyo/paper/PaperConfig.java +++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java @@ -0,0 +0,0 @@ public class PaperConfig { - set("settings.unsupported-settings.allow-tnt-duplication", null); + maxPlayerAutoSavePerTick = (playerAutoSaveRate == -1 || playerAutoSaveRate > 100) ? 10 : 20; + } } - ++ + public static boolean allowHeadlessPistons; + private static void allowHeadlessPistons() { + config.set("settings.unsupported-settings.allow-headless-pistons-readme", "This setting controls if players should be able to create headless pistons."); diff --git a/patches/server/incremental-chunk-saving.patch b/patches/server/incremental-chunk-saving.patch new file mode 100644 index 0000000000..2c51852aeb --- /dev/null +++ b/patches/server/incremental-chunk-saving.patch @@ -0,0 +1,414 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Shane Freeder <theboyetronic@gmail.com> +Date: Sun, 9 Jun 2019 03:53:22 +0100 +Subject: [PATCH] incremental chunk saving + + +diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java ++++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java +@@ -0,0 +0,0 @@ public class PaperConfig { + set("settings.unsupported-settings.allow-tnt-duplication", null); + } + ++ public static int playerAutoSaveRate = -1; ++ public static int maxPlayerAutoSavePerTick = 10; ++ private static void playerAutoSaveRate() { ++ playerAutoSaveRate = getInt("settings.player-auto-save-rate", -1); ++ maxPlayerAutoSavePerTick = getInt("settings.max-player-auto-save-per-tick", -1); ++ if (maxPlayerAutoSavePerTick == -1) { // -1 Automatic / "Recommended" ++ // 10 should be safe for everyone unless you mass spamming player auto save ++ maxPlayerAutoSavePerTick = (playerAutoSaveRate == -1 || playerAutoSaveRate > 100) ? 10 : 20; ++ } ++ } + } +diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java ++++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +@@ -0,0 +0,0 @@ public class PaperWorldConfig { + log( "Keep Spawn Loaded Range: " + (keepLoadedRange/16)); + } + ++ public int autoSavePeriod = -1; ++ private void autoSavePeriod() { ++ autoSavePeriod = getInt("auto-save-interval", -1); ++ if (autoSavePeriod > 0) { ++ log("Auto Save Interval: " +autoSavePeriod + " (" + (autoSavePeriod / 20) + "s)"); ++ } else if (autoSavePeriod < 0) { ++ autoSavePeriod = net.minecraft.server.MinecraftServer.getServer().autosavePeriod; ++ } ++ } ++ ++ public int maxAutoSaveChunksPerTick = 24; ++ private void maxAutoSaveChunksPerTick() { ++ maxAutoSaveChunksPerTick = getInt("max-auto-save-chunks-per-tick", 24); ++ } ++ + private boolean getBoolean(String path, boolean def) { + config.addDefault("world-settings.default." + path, def); + return config.getBoolean("world-settings." + worldName + "." + path, config.getBoolean("world-settings.default." + path)); +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + + try { + this.isSaving = true; +- this.getPlayerList().saveAll(); ++ this.getPlayerList().saveAll(); // Diff on change + flag3 = this.saveAllChunks(suppressLogs, flush, force); + } finally { + this.isSaving = false; +@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa + } + } + +- if (this.autosavePeriod > 0 && this.tickCount % this.autosavePeriod == 0) { // CraftBukkit +- MinecraftServer.LOGGER.debug("Autosave started"); +- this.profiler.push("save"); +- this.saveEverything(true, false, false); +- this.profiler.pop(); +- MinecraftServer.LOGGER.debug("Autosave finished"); ++ // Paper start - incremental chunk and player saving ++ int playerSaveInterval = com.destroystokyo.paper.PaperConfig.playerAutoSaveRate; ++ if (playerSaveInterval < 0) { ++ playerSaveInterval = autosavePeriod; + } ++ this.profiler.push("save"); ++ try { ++ this.isSaving = true; ++ if (playerSaveInterval > 0) { ++ this.playerList.saveAll(playerSaveInterval); ++ } ++ for (ServerLevel level : this.getAllLevels()) { ++ if (level.paperConfig.autoSavePeriod > 0) { ++ level.saveIncrementally(autosavePeriod > 0 && this.tickCount % autosavePeriod == 0); ++ } ++ } ++ } finally { ++ this.isSaving = false; ++ } ++ this.profiler.pop(); ++ // Paper end + io.papermc.paper.util.CachedLists.reset(); // Paper + // Paper start - move executeAll() into full server tick timing + try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) { +diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java ++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java +@@ -0,0 +0,0 @@ public class ChunkHolder { + this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key); + } + // Paper end - optimise isOutsideOfRange ++ long lastAutoSaveTime; // Paper - incremental autosave ++ long inactiveTimeStart; // Paper - incremental autosave + + public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) { + this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size()); +@@ -0,0 +0,0 @@ public class ChunkHolder { + boolean flag2 = playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER); + boolean flag3 = playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER); + ++ boolean prevHasBeenLoaded = this.wasAccessibleSinceLastSave; // Paper + this.wasAccessibleSinceLastSave |= flag3; ++ // Paper start - incremental autosave ++ if (this.wasAccessibleSinceLastSave & !prevHasBeenLoaded) { ++ long timeSinceAutoSave = this.inactiveTimeStart - this.lastAutoSaveTime; ++ if (timeSinceAutoSave < 0) { ++ // safest bet is to assume autosave is needed here ++ timeSinceAutoSave = this.chunkMap.level.paperConfig.autoSavePeriod; ++ } ++ this.lastAutoSaveTime = this.chunkMap.level.getGameTime() - timeSinceAutoSave; ++ this.chunkMap.autoSaveQueue.add(this); ++ } ++ // Paper end + if (!flag2 && flag3) { + int expectCreateCount = ++this.fullChunkCreateCount; // Paper + this.fullChunkFuture = chunkStorage.prepareAccessibleChunk(this); +@@ -0,0 +0,0 @@ public class ChunkHolder { + } + + public void refreshAccessibility() { ++ boolean prev = this.wasAccessibleSinceLastSave; // Paper + this.wasAccessibleSinceLastSave = ChunkHolder.getFullChunkStatus(this.ticketLevel).isOrAfter(ChunkHolder.FullChunkStatus.BORDER); ++ // Paper start - incremental autosave ++ if (prev != this.wasAccessibleSinceLastSave) { ++ if (this.wasAccessibleSinceLastSave) { ++ long timeSinceAutoSave = this.inactiveTimeStart - this.lastAutoSaveTime; ++ if (timeSinceAutoSave < 0) { ++ // safest bet is to assume autosave is needed here ++ timeSinceAutoSave = this.chunkMap.level.paperConfig.autoSavePeriod; ++ } ++ this.lastAutoSaveTime = this.chunkMap.level.getGameTime() - timeSinceAutoSave; ++ this.chunkMap.autoSaveQueue.add(this); ++ } else { ++ this.inactiveTimeStart = this.chunkMap.level.getGameTime(); ++ this.chunkMap.autoSaveQueue.remove(this); ++ } ++ } ++ // Paper end + } + ++ // Paper start - incremental autosave ++ public boolean setHasBeenLoaded() { ++ this.wasAccessibleSinceLastSave = getFullChunkStatus(this.ticketLevel).isOrAfter(ChunkHolder.FullChunkStatus.BORDER); ++ return this.wasAccessibleSinceLastSave; ++ } ++ // Paper end ++ + public void replaceProtoChunk(ImposterProtoChunk chunk) { + for (int i = 0; i < this.futures.length(); ++i) { + CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completablefuture = (CompletableFuture) this.futures.get(i); +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -0,0 +0,0 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureMana + import net.minecraft.world.level.storage.DimensionDataStorage; + import net.minecraft.world.level.storage.LevelStorageSource; + import net.minecraft.world.phys.Vec3; ++import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; // Paper + import org.apache.commons.lang3.mutable.MutableBoolean; + import org.apache.commons.lang3.mutable.MutableObject; + import org.apache.logging.log4j.LogManager; +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + } + ++ // Paper start - incremental autosave ++ final ObjectRBTreeSet<ChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((playerchunk1, playerchunk2) -> { ++ int timeCompare = Long.compare(playerchunk1.lastAutoSaveTime, playerchunk2.lastAutoSaveTime); ++ if (timeCompare != 0) { ++ return timeCompare; ++ } ++ ++ return Long.compare(MCUtil.getCoordinateKey(playerchunk1.pos), MCUtil.getCoordinateKey(playerchunk2.pos)); ++ }); ++ ++ protected void saveIncrementally() { ++ int savedThisTick = 0; ++ // optimized since we search far less chunks to hit ones that need to be saved ++ List<ChunkHolder> reschedule = new java.util.ArrayList<>(this.level.paperConfig.maxAutoSaveChunksPerTick); ++ long currentTick = this.level.getGameTime(); ++ long maxSaveTime = currentTick - this.level.paperConfig.autoSavePeriod; ++ ++ for (Iterator<ChunkHolder> iterator = this.autoSaveQueue.iterator(); iterator.hasNext();) { ++ ChunkHolder playerchunk = iterator.next(); ++ if (playerchunk.lastAutoSaveTime > maxSaveTime) { ++ break; ++ } ++ ++ iterator.remove(); ++ ++ ChunkAccess ichunkaccess = playerchunk.getChunkToSave().getNow(null); ++ if (ichunkaccess instanceof LevelChunk) { ++ boolean shouldSave = ((LevelChunk)ichunkaccess).lastSaveTime <= maxSaveTime; ++ ++ if (shouldSave && this.save(ichunkaccess) && this.level.entityManager.storeChunkSections(playerchunk.pos.toLong(), entity -> {})) { ++ ++savedThisTick; ++ ++ if (!playerchunk.setHasBeenLoaded()) { ++ // do not fall through to reschedule logic ++ playerchunk.inactiveTimeStart = currentTick; ++ if (savedThisTick >= this.level.paperConfig.maxAutoSaveChunksPerTick) { ++ break; ++ } ++ continue; ++ } ++ } ++ } ++ ++ reschedule.add(playerchunk); ++ ++ if (savedThisTick >= this.level.paperConfig.maxAutoSaveChunksPerTick) { ++ break; ++ } ++ } ++ ++ for (int i = 0, len = reschedule.size(); i < len; ++i) { ++ ChunkHolder playerchunk = reschedule.get(i); ++ playerchunk.lastAutoSaveTime = this.level.getGameTime(); ++ this.autoSaveQueue.add(playerchunk); ++ } ++ } ++ // Paper end ++ + protected void saveAllChunks(boolean flush) { + if (flush) { + List<ChunkHolder> list = (List) this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList()); +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + int l = 0; + ObjectIterator objectiterator = this.visibleChunkMap.values().iterator(); + +- while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { ++ while (false && l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { // Paper - incremental chunk and player saving + if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) { + ++l; + } +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + this.level.unload(chunk); + } ++ this.autoSaveQueue.remove(holder); // Paper + + this.lightEngine.updateChunkStatus(ichunkaccess.getPos()); + this.lightEngine.tryScheduleUpdate(); +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + asyncSaveData, chunk); + + chunk.setUnsaved(false); ++ chunk.setLastSaved(this.level.getGameTime()); // Paper - track last saved time + } + // Paper end + +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + if (!chunk.isUnsaved()) { + return false; + } else { ++ chunk.setLastSaved(this.level.getGameTime()); // Paper - track save time + chunk.setUnsaved(false); + ChunkPos chunkcoordintpair = chunk.getPos(); + +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 { + } // Paper - Timings + } + ++ // Paper start - duplicate save, but call incremental ++ public void saveIncrementally() { ++ this.runDistanceManagerUpdates(); ++ try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings ++ this.chunkMap.saveIncrementally(); ++ } // Paper - Timings ++ } ++ // Paper end ++ + @Override + public void close() throws IOException { + // CraftBukkit start +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 { + return !this.server.isUnderSpawnProtection(this, pos, player) && this.getWorldBorder().isWithinBounds(pos); + } + ++ // Paper start - derived from below ++ public void saveIncrementally(boolean doFull) { ++ ServerChunkCache chunkproviderserver = this.getChunkSource(); ++ ++ if (doFull) { ++ org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld())); ++ } ++ ++ try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) { ++ if (doFull) { ++ this.saveLevelData(); ++ } ++ ++ this.timings.worldSaveChunks.startTiming(); // Paper ++ if (!this.noSave()) chunkproviderserver.saveIncrementally(); ++ this.timings.worldSaveChunks.stopTiming(); // Paper ++ ++ // Copied from save() ++ // CraftBukkit start - moved from MinecraftServer.saveChunks ++ if (doFull) { // Paper ++ ServerLevel worldserver1 = this; ++ ++ this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings()); ++ this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save()); ++ this.convertable.saveDataTag(this.server.registryHolder, this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); ++ } ++ // CraftBukkit end ++ } ++ } ++ // Paper end ++ + public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) { + ServerChunkCache chunkproviderserver = this.getChunkSource(); + +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -0,0 +0,0 @@ public class ServerPlayer extends Player { + public final int getViewDistance() { return this.getLevel().getChunkSource().chunkMap.viewDistance - 1; } // Paper - placeholder + + private static final Logger LOGGER = LogManager.getLogger(); ++ public long lastSave = MinecraftServer.currentTick; // Paper + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; + public ServerGamePacketListenerImpl connection; +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -0,0 +0,0 @@ public abstract class PlayerList { + protected void save(ServerPlayer player) { + if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit + if (!player.didPlayerJoinEvent) return; // Paper - If we never fired PJE, we disconnected during login. Data has not changed, and additionally, our saved vehicle is not loaded! If we save now, we will lose our vehicle (CraftBukkit bug) ++ player.lastSave = MinecraftServer.currentTick; // Paper + this.playerIo.save(player); + ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit + +@@ -0,0 +0,0 @@ public abstract class PlayerList { + } + + public void saveAll() { ++ // Paper start - incremental player saving ++ this.saveAll(-1); ++ } ++ ++ public void saveAll(int interval) { + net.minecraft.server.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main + MinecraftTimings.savePlayers.startTiming(); // Paper ++ int numSaved = 0; ++ long now = MinecraftServer.currentTick; + for (int i = 0; i < this.players.size(); ++i) { +- this.save(this.players.get(i)); ++ ServerPlayer entityplayer = this.players.get(i); ++ if (interval == -1 || now - entityplayer.lastSave >= interval) { ++ this.save(entityplayer); ++ if (interval != -1 && ++numSaved <= com.destroystokyo.paper.PaperConfig.maxPlayerAutoSavePerTick) { break; } ++ } ++ // Paper end + } + MinecraftTimings.savePlayers.stopTiming(); // Paper + return null; }); // Paper - ensure main +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +@@ -0,0 +0,0 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + public LevelHeightAccessor getHeightAccessorForGeneration() { + return this; + } ++ public void setLastSaved(long ticks) {} // Paper + + // CraftBukkit start - decompile error + public static record TicksToSave(SerializableTickContainer<Block> blocks, SerializableTickContainer<Fluid> fluids) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -0,0 +0,0 @@ public class LevelChunk extends ChunkAccess { + private final Int2ObjectMap<GameEventDispatcher> gameEventDispatcherSections; + private final LevelChunkTicks<Block> blockTicks; + private final LevelChunkTicks<Fluid> fluidTicks; ++ // Paper start - track last save time ++ public long lastSaveTime; ++ public void setLastSaved(long ticks) { ++ this.lastSaveTime = ticks; ++ } ++ // Paper end + + public LevelChunk(Level world, ChunkPos pos) { + this(world, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, (LevelChunkSection[]) null, (LevelChunk.PostLoadProcessor) null, (BlendingData) null);