From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sat, 15 Jun 2019 08:54:33 -0700 Subject: [PATCH] Fix World#isChunkGenerated calls Optimize World#loadChunk() too This patch also adds a chunk status cache on region files (note that its only purpose is to cache the status on DISK) diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java index 6713b7667ae4fe3f1f555a71321832b4a9492162..4c9c8e483974f8869d6711626620cfd7d814d956 100644 --- a/src/main/java/net/minecraft/server/ChunkProviderServer.java +++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java @@ -28,7 +28,7 @@ public class ChunkProviderServer extends IChunkProvider { private final WorldServer world; public final Thread serverThread; // Paper - private -> public private final LightEngineThreaded lightEngine; - private final ChunkProviderServer.a serverThreadQueue; + public final ChunkProviderServer.a serverThreadQueue; // Paper private -> public public final PlayerChunkMap playerChunkMap; private final WorldPersistentData worldPersistentData; private long lastTickTime; @@ -284,6 +284,21 @@ public class ChunkProviderServer extends IChunkProvider { return ret; } + + @Nullable + public IChunkAccess getChunkAtImmediately(int x, int z) { + long k = ChunkCoordIntPair.pair(x, z); + + // Note: Bypass cache to make this MT-Safe + + PlayerChunk playerChunk = this.getChunk(k); + if (playerChunk == null) { + return null; + } + + return playerChunk.getAvailableChunkNow(); + + } // Paper end @Nullable diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java index f54572773c25b6716a68166efa8d3fc8b22b6258..8e4b3e52cbc95e24b1d72aed9ec8c32b94a91561 100644 --- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java +++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java @@ -403,6 +403,17 @@ public class ChunkRegionLoader { } // Paper end + // Paper start + public static ChunkStatus getStatus(NBTTagCompound compound) { + if (compound == null) { + return null; + } + + // Note: Copied from below + return ChunkStatus.getStatus(compound.getCompound("Level").getString("Status")); + } + // Paper end + public static ChunkStatus.Type a(@Nullable NBTTagCompound nbttagcompound) { if (nbttagcompound != null) { ChunkStatus chunkstatus = ChunkStatus.a(nbttagcompound.getCompound("Level").getString("Status")); diff --git a/src/main/java/net/minecraft/server/ChunkStatus.java b/src/main/java/net/minecraft/server/ChunkStatus.java index efdf611e66ffd782291de749d8a48f3bf08f2129..134a4f0b7d254b5dd8ca26a9c5874532826978c4 100644 --- a/src/main/java/net/minecraft/server/ChunkStatus.java +++ b/src/main/java/net/minecraft/server/ChunkStatus.java @@ -176,6 +176,7 @@ public class ChunkStatus { return this.s; } + public ChunkStatus getPreviousStatus() { return this.e(); } // Paper - OBFHELPER public ChunkStatus e() { return this.u; } @@ -196,6 +197,17 @@ public class ChunkStatus { return this.y; } + // Paper start + public static ChunkStatus getStatus(String name) { + try { + // We need this otherwise we return EMPTY for invalid names + MinecraftKey key = new MinecraftKey(name); + return IRegistry.CHUNK_STATUS.getOptional(key).orElse(null); + } catch (Exception ex) { + return null; // invalid name + } + } + // Paper end public static ChunkStatus a(String s) { return (ChunkStatus) IRegistry.CHUNK_STATUS.get(MinecraftKey.a(s)); } diff --git a/src/main/java/net/minecraft/server/IChunkLoader.java b/src/main/java/net/minecraft/server/IChunkLoader.java index f0a052eec2fb72b11dc70bf62a5e57f599bbc190..2f95174fcc467908808ed3f2dc956bdcafdc3558 100644 --- a/src/main/java/net/minecraft/server/IChunkLoader.java +++ b/src/main/java/net/minecraft/server/IChunkLoader.java @@ -8,7 +8,7 @@ import javax.annotation.Nullable; public class IChunkLoader implements AutoCloseable { - private final IOWorker a; + private final IOWorker a; public IOWorker getIOWorker() { return a; } // Paper - OBFHELPER protected final DataFixer b; @Nullable private PersistentStructureLegacy c; diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java index ed9ada49c7cc1131691bd6e005b2380274ef23e3..52ea4f05a0c7f29f62f31bb032a5ceb905107e60 100644 --- a/src/main/java/net/minecraft/server/PlayerChunk.java +++ b/src/main/java/net/minecraft/server/PlayerChunk.java @@ -115,6 +115,19 @@ public class PlayerChunk { Either either = (Either) statusFuture.getNow(null); return either == null ? null : (Chunk) either.left().orElse(null); } + + public IChunkAccess getAvailableChunkNow() { + // TODO can we just getStatusFuture(EMPTY)? + for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getPreviousStatus(); curr != next; curr = next, next = next.getPreviousStatus()) { + CompletableFuture> future = this.getStatusFutureUnchecked(curr); + Either either = future.getNow(null); + if (either == null || !either.left().isPresent()) { + continue; + } + return either.left().get(); + } + return null; + } // Paper end public CompletableFuture> getStatusFutureUnchecked(ChunkStatus chunkstatus) { diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java index 4f5b516144829a7ae11f21a56789ac7a1f256250..1d517fd1aea90edf470388fd857a41f2be149327 100644 --- a/src/main/java/net/minecraft/server/PlayerChunkMap.java +++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java @@ -969,12 +969,62 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } @Nullable - private NBTTagCompound readChunkData(ChunkCoordIntPair chunkcoordintpair) throws IOException { + public NBTTagCompound readChunkData(ChunkCoordIntPair chunkcoordintpair) throws IOException { // Paper - private -> public NBTTagCompound nbttagcompound = this.read(chunkcoordintpair); - return nbttagcompound == null ? null : this.getChunkData(this.world.getWorldProvider().getDimensionManager(), this.l, nbttagcompound, chunkcoordintpair, world); // CraftBukkit + // Paper start - Cache chunk status on disk + if (nbttagcompound == null) { + return null; + } + + nbttagcompound = this.getChunkData(this.world.getWorldProvider().getDimensionManager(), this.l, nbttagcompound, chunkcoordintpair, world); // CraftBukkit + if (nbttagcompound == null) { + return null; + } + + this.updateChunkStatusOnDisk(chunkcoordintpair, nbttagcompound); + + return nbttagcompound; + // Paper end + } + + // Paper start - chunk status cache "api" + public ChunkStatus getChunkStatusOnDiskIfCached(ChunkCoordIntPair chunkPos) { + RegionFile regionFile = this.getIOWorker().getRegionFileCache().getRegionFileIfLoaded(chunkPos); + + return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); } + public ChunkStatus getChunkStatusOnDisk(ChunkCoordIntPair chunkPos) throws IOException { + RegionFile regionFile = this.getIOWorker().getRegionFileCache().getFile(chunkPos, false); + + if (!regionFile.chunkExists(chunkPos)) { + return null; + } + + ChunkStatus status = regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); + + if (status != null) { + return status; + } + + this.readChunkData(chunkPos); + + return regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); + } + + public void updateChunkStatusOnDisk(ChunkCoordIntPair chunkPos, @Nullable NBTTagCompound compound) throws IOException { + RegionFile regionFile = this.getIOWorker().getRegionFileCache().getFile(chunkPos, false); + + regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkRegionLoader.getStatus(compound)); + } + + public IChunkAccess getUnloadingChunk(int chunkX, int chunkZ) { + PlayerChunk chunkHolder = this.pendingUnload.get(ChunkCoordIntPair.pair(chunkX, chunkZ)); + return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow(); + } + // Paper end + boolean isOutsideOfRange(ChunkCoordIntPair chunkcoordintpair) { // Spigot start return isOutsideOfRange(chunkcoordintpair, false); diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java index 6b543f89d4e32fb79bfe9aa7b815e4117dbea602..d37abf2cf304f81405e570588c8accbc44a629f4 100644 --- a/src/main/java/net/minecraft/server/RegionFile.java +++ b/src/main/java/net/minecraft/server/RegionFile.java @@ -36,6 +36,30 @@ public class RegionFile implements AutoCloseable { private final RegionFileBitSet freeSectors; public final File file; + // Paper start - Cache chunk status + private final ChunkStatus[] statuses = new ChunkStatus[32 * 32]; + + private boolean closed; + + // invoked on write/read + public void setStatus(int x, int z, ChunkStatus status) { + if (this.closed) { + // We've used an invalid region file. + throw new IllegalStateException("RegionFile is closed"); + } + this.statuses[this.getChunkLocation(new ChunkCoordIntPair(x, z))] = status; + } + + public ChunkStatus getStatusIfCached(int x, int z) { + if (this.closed) { + // We've used an invalid region file. + throw new IllegalStateException("RegionFile is closed"); + } + final int location = this.getChunkLocation(new ChunkCoordIntPair(x, z)); + return this.statuses[location]; + } + // Paper end + public RegionFile(File file, File file1) throws IOException { this(file.toPath(), file1.toPath(), RegionFileCompression.b); } @@ -350,11 +374,13 @@ public class RegionFile implements AutoCloseable { return this.getOffset(chunkcoordintpair) != 0; } + private final int getChunkLocation(ChunkCoordIntPair chunkcoordintpair) { return this.g(chunkcoordintpair); } // Paper - OBFHELPER private static int g(ChunkCoordIntPair chunkcoordintpair) { return chunkcoordintpair.j() + chunkcoordintpair.k() * 32; } public void close() throws IOException { + this.closed = true; // Paper try { this.c(); } finally { diff --git a/src/main/java/net/minecraft/server/RegionFileCache.java b/src/main/java/net/minecraft/server/RegionFileCache.java index 942b7d323936a872d83c536c1901b4b436aa7e7c..2f8af42e2aadeb1b11db94fdb54ec0ba9e30f095 100644 --- a/src/main/java/net/minecraft/server/RegionFileCache.java +++ b/src/main/java/net/minecraft/server/RegionFileCache.java @@ -18,7 +18,14 @@ public final class RegionFileCache implements AutoCloseable { this.b = file; } - private RegionFile getFile(ChunkCoordIntPair chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit + + // Paper start + public RegionFile getRegionFileIfLoaded(ChunkCoordIntPair chunkcoordintpair) { + return this.cache.getAndMoveToFirst(ChunkCoordIntPair.pair(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ())); + } + + // Paper end + public RegionFile getFile(ChunkCoordIntPair chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - private > public long i = ChunkCoordIntPair.pair(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ()); RegionFile regionfile = (RegionFile) this.cache.getAndMoveToFirst(i); @@ -165,7 +172,8 @@ public final class RegionFileCache implements AutoCloseable { try { NBTCompressedStreamTools.a(nbttagcompound, (DataOutput) dataoutputstream); - regionfile.setOversized(chunkcoordintpair.x, chunkcoordintpair.z, false); // We don't do this anymore + regionfile.setStatus(chunkcoordintpair.x, chunkcoordintpair.z, ChunkRegionLoader.getStatus(nbttagcompound)); // Paper - cache status on disk + regionfile.setOversized(chunkcoordintpair.x, chunkcoordintpair.z, false); } catch (Throwable throwable1) { throwable = throwable1; throw throwable1; diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 8f8c18c5a4f483acee0d61a8007577ac8193bef9..50467656df0b54c2dcba8696b5677a2fc975b178 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -19,6 +19,7 @@ import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; import java.util.stream.Collectors; import net.minecraft.server.ArraySetSorted; @@ -410,8 +411,22 @@ public class CraftWorld implements World { @Override public boolean isChunkGenerated(int x, int z) { + // Paper start - Fix this method + if (!Bukkit.isPrimaryThread()) { + return CompletableFuture.supplyAsync(() -> { + return CraftWorld.this.isChunkGenerated(x, z); + }, world.getChunkProvider().serverThreadQueue).join(); + } + IChunkAccess chunk = world.getChunkProvider().getChunkAtImmediately(x, z); + if (chunk == null) { + chunk = world.getChunkProvider().playerChunkMap.getUnloadingChunk(x, z); + } + if (chunk != null) { + return chunk instanceof ProtoChunkExtension || chunk instanceof net.minecraft.server.Chunk; + } try { - return world.getChunkProvider().getChunkAtIfCachedImmediately(x, z) != null || world.getChunkProvider().playerChunkMap.read(new ChunkCoordIntPair(x, z)) != null; // Paper (TODO check if the first part can be removed) + return world.getChunkProvider().playerChunkMap.getChunkStatusOnDisk(new ChunkCoordIntPair(x, z)) == ChunkStatus.FULL; + // Paper end } catch (IOException ex) { throw new RuntimeException(ex); } @@ -523,20 +538,49 @@ public class CraftWorld implements World { @Override public boolean loadChunk(int x, int z, boolean generate) { org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot - IChunkAccess chunk = world.getChunkProvider().getChunkAt(x, z, generate || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); // Paper + // Paper start - Optimize this method + ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z); - // If generate = false, but the chunk already exists, we will get this back. - if (chunk instanceof ProtoChunkExtension) { - // We then cycle through again to get the full chunk immediately, rather than after the ticket addition - chunk = world.getChunkProvider().getChunkAt(x, z, ChunkStatus.FULL, true); - } + if (!generate) { - if (chunk instanceof net.minecraft.server.Chunk) { - world.getChunkProvider().addTicket(TicketType.PLUGIN, new ChunkCoordIntPair(x, z), 1, Unit.INSTANCE); - return true; + IChunkAccess immediate = world.getChunkProvider().getChunkAtImmediately(x, z); + if (immediate == null) { + immediate = world.getChunkProvider().playerChunkMap.getUnloadingChunk(x, z); + } + if (immediate != null) { + if (!(immediate instanceof ProtoChunkExtension) && !(immediate instanceof net.minecraft.server.Chunk)) { + return false; // not full status + } + world.getChunkProvider().addTicket(TicketType.PLUGIN, chunkPos, 1, Unit.INSTANCE); + world.getChunkAt(x, z); // make sure we're at ticket level 32 or lower + return true; + } + + net.minecraft.server.RegionFile file; + try { + file = world.getChunkProvider().playerChunkMap.getIOWorker().getRegionFileCache().getFile(chunkPos, false); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + + ChunkStatus status = file.getStatusIfCached(x, z); + if (!file.chunkExists(chunkPos) || (status != null && status != ChunkStatus.FULL)) { + return false; + } + + IChunkAccess chunk = world.getChunkProvider().getChunkAt(x, z, ChunkStatus.EMPTY, true); + if (!(chunk instanceof ProtoChunkExtension) && !(chunk instanceof net.minecraft.server.Chunk)) { + return false; + } + + // fall through to load + // we do this so we do not re-read the chunk data on disk } - return false; + world.getChunkProvider().addTicket(TicketType.PLUGIN, chunkPos, 1, Unit.INSTANCE); + world.getChunkProvider().getChunkAt(x, z, ChunkStatus.FULL, true); + return true; + // Paper end } @Override