mirror of
https://github.com/PaperMC/Paper.git
synced 2025-07-31 12:12:08 -07:00
Rewrite chunk system (#8177)
Patch documentation to come Issues with the old system that are fixed now: - World generation does not scale with cpu cores effectively. - Relies on the main thread for scheduling and maintaining chunk state, dropping chunk load/generate rates at lower tps. - Unreliable prioritisation of chunk gen/load calls that block the main thread. - Shutdown logic is utterly unreliable, as it has to wait for all chunks to unload - is it guaranteed that the chunk system is in a state on shutdown that it can reliably do this? Watchdog shutdown also typically failed due to thread checks, which is now resolved. - Saving of data is not unified (i.e can save chunk data without saving entity data, poses problems for desync if shutdown is really abnormal. - Entities are not loaded with chunks. This caused quite a bit of headache for Chunk#getEntities API, but now the new chunk system loads entities with chunks so that they are ready whenever the chunk loads in. Effectively brings the behavior back to 1.16 era, but still storing entities in their own separate regionfiles. The above list is not complete. The patch documentation will complete it. New chunk system hard relies on starlight and dataconverter, and most importantly the new concurrent utilities in ConcurrentUtil. Some of the old async chunk i/o interface (i.e the old file io thread reroutes _some_ calls to the new file io thread) is kept for plugin compat reasons. It will be removed in the next major version of minecraft. The old legacy chunk system patches have been moved to the removed folder in case we need them again.
This commit is contained in:
@@ -52,188 +52,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
|
||||
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 {
|
||||
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> playersInMobSpawnRange;
|
||||
com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> playersInChunkTickRange;
|
||||
// Paper end - optimise anyPlayerCloseEnoughForSpawning
|
||||
+ 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().chunks.autoSaveInterval.value();
|
||||
+ }
|
||||
+ 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().chunks.autoSaveInterval.value();
|
||||
+ }
|
||||
+ 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) {
|
||||
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.StructureTemp
|
||||
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.slf4j.Logger;
|
||||
@@ -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().chunks.maxAutoSaveChunksPerTick);
|
||||
+ long currentTick = this.level.getGameTime();
|
||||
+ long maxSaveTime = currentTick - this.level.paperConfig().chunks.autoSaveInterval.value();
|
||||
+
|
||||
+ 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().chunks.maxAutoSaveChunksPerTick) {
|
||||
+ break;
|
||||
+ }
|
||||
+ continue;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ reschedule.add(playerchunk);
|
||||
+
|
||||
+ if (savedThisTick >= this.level.paperConfig().chunks.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) {
|
||||
// Paper start - do not overload I/O threads with too much work when saving
|
||||
int[] saved = new int[1];
|
||||
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
||||
}
|
||||
|
||||
int l = 0;
|
||||
- Iterator objectiterator = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
|
||||
-
|
||||
- while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) {
|
||||
- if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) {
|
||||
- ++l;
|
||||
- }
|
||||
- }
|
||||
+ // Paper - incremental chunk and player saving
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -294,8 +112,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
|
||||
+ // Paper end
|
||||
+
|
||||
public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) {
|
||||
ServerChunkCache chunkproviderserver = this.getChunkSource();
|
||||
|
||||
// Paper start - rewrite chunk system - add close param
|
||||
this.save(progressListener, flush, savingDisabled, false);
|
||||
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
|
||||
@@ -344,32 +162,3 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
|
||||
}
|
||||
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
|
||||
|
||||
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);
|
||||
|
Reference in New Issue
Block a user