Skip to content

Instantly share code, notes, and snippets.

@aikar
Created August 31, 2018 03:19
Show Gist options
  • Save aikar/66d632096d7cd1fea7a74d13e96076c0 to your computer and use it in GitHub Desktop.
Save aikar/66d632096d7cd1fea7a74d13e96076c0 to your computer and use it in GitHub Desktop.
commit b2a65ead8b8c7aa5728568a5944131e7bfd135bd
Author: Aikar <[email protected]>
Date: Thu Aug 30 22:38:31 2018 -0400
Async Chunk Loading and Generation
diff --git a/src/main/java/net/minecraft/server/ChunkMap.java b/src/main/java/net/minecraft/server/ChunkMap.java
index b941676829..76a449b1fe 100644
--- a/src/main/java/net/minecraft/server/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/ChunkMap.java
@@ -17,6 +17,7 @@ public class ChunkMap extends Long2ObjectOpenHashMap<Chunk> {
chunk.world.timings.syncChunkLoadPostTimer.startTiming(); // Paper
lastChunkByPos = chunk; // Paper
Chunk chunk1 = (Chunk) super.put(i, chunk);
+
ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(i);
for (int j = chunkcoordintpair.x - 1; j <= chunkcoordintpair.x + 1; ++j) {
@@ -49,7 +50,7 @@ public class ChunkMap extends Long2ObjectOpenHashMap<Chunk> {
}
}
chunk.world.timings.syncChunkLoadPostTimer.stopTiming(); // Paper
-
+ ((ChunkProviderServer) chunk.world.getChunkProvider()).asyncTaskHandler.postToMainThread(() -> {// Paper
if (chunk.newChunk) {
chunk.world.timings.syncChunkLoadPopulateTimer.startTiming(); // Paper
BlockSand.instaFall = true;
@@ -73,7 +74,7 @@ public class ChunkMap extends Long2ObjectOpenHashMap<Chunk> {
BlockSand.instaFall = false;
chunk.world.getServer().getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(chunk.bukkitChunk));
chunk.world.timings.syncChunkLoadPopulateTimer.stopTiming(); // Paper
- }
+ } }); // Paper
// CraftBukkit end
return chunk1;
diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
index c0ec896eea..93b61c1274 100644
--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
@@ -43,15 +43,17 @@ public class ChunkProviderServer implements IChunkProvider {
private final ChunkTaskScheduler chunkScheduler;
private final SchedulerBatch<ChunkCoordIntPair, ChunkStatus, ProtoChunk> batchScheduler;
public final WorldServer world;
- private final IAsyncTaskHandler asyncTaskHandler;
+ final IAsyncTaskHandler asyncTaskHandler; // Paper
+ private final PaperAsyncChunkLoader asyncChunkLoader; // Paper
public ChunkProviderServer(WorldServer worldserver, IChunkLoader ichunkloader, ChunkGenerator<?> chunkgenerator, IAsyncTaskHandler iasynctaskhandler) {
this.world = worldserver;
this.chunkLoader = ichunkloader;
this.chunkGenerator = chunkgenerator;
this.asyncTaskHandler = iasynctaskhandler;
- this.chunkScheduler = new ChunkTaskScheduler(0, worldserver, chunkgenerator, ichunkloader, iasynctaskhandler); // CraftBukkit - very buggy, broken in lots of __subtle__ ways. Same goes for async chunk loading. Also Bukkit API / plugins can't handle async events at all anyway.
+ this.chunkScheduler = new ChunkTaskScheduler(4, worldserver, chunkgenerator, ichunkloader, iasynctaskhandler); // Paper - bump to 4
this.batchScheduler = new SchedulerBatch(this.chunkScheduler);
+ this.asyncChunkLoader = new PaperAsyncChunkLoader(world, this, asyncTaskHandler, chunkLoader, batchScheduler); // Paper - Async Chunks
}
public Collection<Chunk> a() {
@@ -81,7 +83,24 @@ public class ChunkProviderServer implements IChunkProvider {
}
@Nullable
- public Chunk getChunkAt(int i, int j, boolean flag, boolean flag1) {
+ public final Chunk getChunkAt(int i, int j, boolean flag, boolean flag1) {
+ // Paper start - Async Chunks
+ return getChunkAt(i, j, flag, flag1, null);
+ }
+
+ public final Chunk getChunkAt(int x, int z, boolean load, boolean gen, Consumer<Chunk> consumer) {
+ long key = ChunkCoordIntPair.a(x, z);
+ Chunk chunk = this.chunks.get(key);
+ System.out.println("GET" + x + "," + z + " - " + load + ":" + gen);
+ if (chunk != null || !load) { // return null if we aren't loading
+ System.out.println("RETURN" + x + "," + z + " : " + (chunk != null ? "has" : "do not"));
+ return chunk;
+ }
+ return asyncChunkLoader.loadOrGenerateChunk(x, z, gen, consumer);
+ }
+ @Nullable
+ public Chunk getChunkAtOriginal(int i, int j, boolean flag, boolean flag1) { // Paper - rename to disable
+ // Paper end - Async Chunks
IChunkLoader ichunkloader = this.chunkLoader;
Chunk chunk;
@@ -134,6 +153,7 @@ public class ChunkProviderServer implements IChunkProvider {
// CraftBukkit start
public Chunk generateChunk(int x, int z) {
try {
+ synchronized (this.batchScheduler) { // Paper - synchronized
this.batchScheduler.b();
ChunkCoordIntPair pos = new ChunkCoordIntPair(x, z);
this.chunkScheduler.forcePolluteCache(pos);
@@ -141,6 +161,7 @@ public class ChunkProviderServer implements IChunkProvider {
CompletableFuture<ProtoChunk> completablefuture = this.batchScheduler.c();
return (Chunk) completablefuture.thenApply(this::a).join();
+ } // Paper - synchronized
} catch (RuntimeException runtimeexception) {
throw this.a(x, z, (Throwable) runtimeexception);
}
@@ -154,6 +175,7 @@ public class ChunkProviderServer implements IChunkProvider {
}
public CompletableFuture<ProtoChunk> a(Iterable<ChunkCoordIntPair> iterable, Consumer<Chunk> consumer) {
+ synchronized (this.batchScheduler) { // Paper - synchronized
this.batchScheduler.b();
Iterator iterator = iterable.iterator();
@@ -169,8 +191,10 @@ public class ChunkProviderServer implements IChunkProvider {
}
return this.batchScheduler.c();
+ } // Paper - synchronized
}
+ ReportedException generateChunkError(int i, int j, Throwable throwable) { return a(i, j, throwable); } // Paper - OBFHELPER
private ReportedException a(int i, int j, Throwable throwable) {
CrashReport crashreport = CrashReport.a(throwable, "Exception generating new chunk");
CrashReportSystemDetails crashreportsystemdetails = crashreport.a("Chunk to be generated");
diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
index 06968974c5..0f216c39bd 100644
--- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java
+++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
@@ -110,7 +110,7 @@ public class ChunkRegionLoader implements IChunkLoader, IAsyncChunkSaver {
// CraftBukkit start
private boolean check(ChunkProviderServer cps, int x, int z) throws IOException {
if (cps != null) {
- com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread");
+ //com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread"); // Paper - this is safe
if (cps.isLoaded(x, z)) {
return true;
}
diff --git a/src/main/java/net/minecraft/server/IChunkLoader.java b/src/main/java/net/minecraft/server/IChunkLoader.java
index 4698ee99f8..33b30bb7e3 100644
--- a/src/main/java/net/minecraft/server/IChunkLoader.java
+++ b/src/main/java/net/minecraft/server/IChunkLoader.java
@@ -6,6 +6,7 @@ import javax.annotation.Nullable;
public interface IChunkLoader {
+ default Chunk loadChunkAt(GeneratorAccess generatoraccess, int i, int j, Consumer<Chunk> consumer) throws IOException { return a(generatoraccess, i, j, consumer); } // Paper - OBFHELPER
@Nullable
Chunk a(GeneratorAccess generatoraccess, int i, int j, Consumer<Chunk> consumer) throws IOException;
diff --git a/src/main/java/net/minecraft/server/PaperAsyncChunkLoader.java b/src/main/java/net/minecraft/server/PaperAsyncChunkLoader.java
new file mode 100644
index 0000000000..adaae7bfed
--- /dev/null
+++ b/src/main/java/net/minecraft/server/PaperAsyncChunkLoader.java
@@ -0,0 +1,249 @@
+/*
+ * This file is licensed under the MIT License (MIT).
+ *
+ * Copyright (c) 2018 Daniel Ennis <http://aikar.co>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package net.minecraft.server;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import org.bukkit.Bukkit;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+public class PaperAsyncChunkLoader {
+
+ private static final ExecutorService chunkExecutor = Executors.newCachedThreadPool(new NamedIncrementingThreadFactory("Paper Chunk Executor "));
+ private static final ExecutorService directChunkExecutor = MoreExecutors.newDirectExecutorService();
+
+ private final IAsyncTaskHandler asyncTaskHandler;
+ private final ChunkProviderServer server;
+ private final WorldServer world;
+ private final IChunkLoader chunkLoader;
+ private final SchedulerBatch<ChunkCoordIntPair, ChunkStatus, ProtoChunk> batchScheduler;
+
+ private final Long2ObjectMap<PendingChunk> pendingChunks = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
+
+
+ PaperAsyncChunkLoader(WorldServer world, ChunkProviderServer server, IAsyncTaskHandler asynctaskhandler, IChunkLoader chunkloader, SchedulerBatch<ChunkCoordIntPair, ChunkStatus, ProtoChunk> batchScheduler) {
+ this.server = server;
+ this.world = world;
+ this.asyncTaskHandler = asynctaskhandler;
+ this.chunkLoader = chunkloader;
+ this.batchScheduler = batchScheduler;
+ }
+
+ Chunk loadOrGenerateChunk(int x, int z, boolean gen, Consumer<Chunk> consumer) {
+ long key = ChunkCoordIntPair.a(x, z);
+ AtomicBoolean hadPending = new AtomicBoolean(false);
+
+ final CompletableFuture<Chunk> future = new CompletableFuture<>();
+ final PendingChunk pendingChunk = pendingChunks.compute(key, (prevKey, pending) -> {
+ if (pending == null) {
+ pending = new PendingChunk(x, z, key, gen);
+ } else {
+ hadPending.set(true);
+ }
+
+ if (!gen && pending.generating) {
+ future.complete(null);
+ } else {
+ pending.addListener(future, gen);
+ }
+ return pending;
+ });
+
+
+ if (!hadPending.get()) {
+ final boolean useDirect = !Bukkit.isPrimaryThread() || consumer == null;
+ System.out.println("EXECUTE" + x + "," + z + " - " + (useDirect ? "1" : "0"));
+ if (useDirect) {
+ directChunkExecutor.submit(pendingChunk);
+ } else {
+ chunkExecutor.submit(pendingChunk);
+ }
+ }
+
+ if (consumer == null) {
+ System.out.println("JOINWAIT " + x +"," + z);
+ try (co.aikar.timings.Timing timing = world.timings.syncChunkLoadTimer.startTiming()) {
+ return future.join();
+ }
+ } else {
+ System.out.println("CONSUMEASYNC " + x +"," + z);
+ consumeOnMain(future, consumer);
+ }
+ System.out.println("RETNULL " + x +"," + z);
+ return null;
+ }
+
+ private Chunk loadChunk(int x, int z) {
+ try {
+ return this.chunkLoader.loadChunkAt(this.world, x, z, this::doNothingChunkConsumer);
+ } catch (IOException e) {
+ MinecraftServer.LOGGER.error("Couldn't load chunk", e);
+ return null;
+ }
+ }
+ private Chunk generateChunk0(int x, int z) {
+ try (co.aikar.timings.Timing timing = world.timings.chunkGeneration.startTiming()) {
+ CompletableFuture<ProtoChunk> pending;
+ synchronized (this.batchScheduler) {
+ this.batchScheduler.startBatch();
+ this.batchScheduler.add(new ChunkCoordIntPair(x, z));
+ pending = this.batchScheduler.executeBatch();
+ }
+
+ return new Chunk(this.world, pending.join(), x, z);
+ } catch (RuntimeException runtimeexception) {
+ throw this.server.generateChunkError(x, z, runtimeexception);
+ }
+ }
+
+ private void consumeOnMain(CompletableFuture<Chunk> future, Consumer<Chunk> consumer) {
+ future.thenAccept((c) -> this.asyncTaskHandler.postToMainThread(() -> consumer.accept(c)));
+ }
+
+ private void doNothingChunkConsumer(Chunk chunk) {}
+
+
+ static AtomicInteger idpool = new AtomicInteger(1);
+ private class PendingChunk implements Runnable {
+ private final int x;
+ private final int z;
+ private final long key;
+
+ volatile boolean generating;
+ volatile boolean canGenerate;
+
+ final CompletableFuture<Chunk> loadOnly = new CompletableFuture<>();
+ final CompletableFuture<Chunk> generate = new CompletableFuture<>();
+ private String debug;
+
+
+
+ PendingChunk(int x, int z, long key, boolean canGenerate) {
+ this.x = x;
+ this.z = z;
+ this.key = key;
+ this.canGenerate = canGenerate;
+ debug = "(" + idpool.getAndIncrement() + ") " + x + "," + z;
+ }
+ boolean loadFinished(Chunk chunk) {
+ Chunk other = checkAddChunk(chunk);
+ boolean shouldGenerate;
+ synchronized (pendingChunks) {
+ shouldGenerate = chunk == null && other == null && canGenerate;
+ System.out.println("COMPLETINGLOAD" + " " + debug + " - " + (chunk != null ? "1" : "0"));
+ if (!shouldGenerate) {
+ pendingChunks.remove(key);
+ } else {
+ generating = true;
+ }
+ }
+ if (other != null) {
+ System.out.println("OTHER WAS LOADED POST LOAD " + debug);
+ loadOnly.complete(other);
+ generate.complete(other);
+ return false;
+ } else if (chunk != null) {
+ loadOnly.complete(chunk);
+ generate.complete(chunk);
+ addEntities(chunk);
+ } else {
+ // this is nullable for anyone who doesn't need generation
+ loadOnly.complete(null);
+ if (!shouldGenerate) {
+ generate.complete(null);
+ }
+ }
+
+ return shouldGenerate;
+ }
+
+ private void addEntities(Chunk chunk) {
+ System.out.println("ADDENTITIES " + debug);
+ PaperAsyncChunkLoader.this.asyncTaskHandler.postToMainThread(chunk::addEntities);
+ }
+
+ private Chunk checkAddChunk(Chunk chunk) {
+ synchronized (server.chunks) {
+ final Chunk other = server.chunks.get(key);
+ if (other == null && chunk != null) {
+ server.chunks.put(key, chunk);
+ }
+ return other;
+ }
+ }
+
+ void generateFinished(Chunk chunk) {
+ Chunk other = checkAddChunk(chunk);
+ synchronized (pendingChunks) {
+ System.out.println("GENERATE FINISHED " + " " + debug + " - " + (chunk != null ? "1" : "0"));
+ pendingChunks.remove(key);
+ }
+ if (other != null) {
+ System.out.println("OTHER WAS LOADED POST GEN " + debug);
+ chunk = other;
+ } else if (chunk != null) {
+ addEntities(chunk);
+ }
+ generate.complete(chunk);
+ }
+ void addListener(CompletableFuture<Chunk> future, boolean gen) {
+ if (gen) {
+ canGenerate = true;
+ generate.thenAccept(future::complete);
+ } else {
+ loadOnly.thenAccept(future::complete);
+ }
+ }
+
+ @Override
+ public void run() {
+ System.out.println("LOAD" + debug);
+ Chunk chunk = loadChunk(x, z);
+ System.out.println("LOAD DONE " + debug + " - " + (chunk != null ? "1" : "0"));
+ if (!loadFinished(chunk)) {
+ return;
+ }
+
+ System.out.println("GENERATE" + debug);
+ try {
+ chunk = generateChunk0(x, z);
+ System.out.println("GENERATE DONE " + debug);
+ generateFinished(chunk);
+ } catch (Exception e) {
+ generateFinished(null);
+ }
+
+ }
+ }
+
+}
diff --git a/src/main/java/net/minecraft/server/SchedulerBatch.java b/src/main/java/net/minecraft/server/SchedulerBatch.java
index 8c88fc9c38..23965dc882 100644
--- a/src/main/java/net/minecraft/server/SchedulerBatch.java
+++ b/src/main/java/net/minecraft/server/SchedulerBatch.java
@@ -19,6 +19,7 @@ public class SchedulerBatch<K, T extends SchedulerTask<K, T>, R> {
this.b.b();
}
+ public void startBatch() { b(); } // Paper - OBFHELPER
public void b() {
if (this.c) {
throw new RuntimeException("Batch already started.");
@@ -28,6 +29,7 @@ public class SchedulerBatch<K, T extends SchedulerTask<K, T>, R> {
}
}
+ public CompletableFuture<R> add(K k0) { return a(k0); } // Paper - OBFHELPER
public CompletableFuture<R> a(K k0) {
if (!this.c) {
throw new RuntimeException("Batch not properly started. Please use startBatch to create a new batch.");
@@ -44,6 +46,7 @@ public class SchedulerBatch<K, T extends SchedulerTask<K, T>, R> {
}
}
+ public CompletableFuture<R> executeBatch() { return c(); } // Paper - OBFHELPER
public CompletableFuture<R> c() {
if (!this.c) {
throw new RuntimeException("Batch not properly started. Please use startBatch to create a new batch.");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment