Merge pull request #1193 from VolmitSoftware/feat/faster_pregen

Feat/faster pregen
This commit is contained in:
Julian Krings 2025-05-16 12:29:41 +02:00 committed by GitHub
commit 463e3d9658
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 127 additions and 68 deletions

View File

@ -136,11 +136,19 @@ public class IrisSettings {
@Data @Data
public static class IrisSettingsConcurrency { public static class IrisSettingsConcurrency {
public int parallelism = -1; public int parallelism = -1;
public int worldGenParallelism = -1;
public int getWorldGenThreads() {
return getThreadCount(worldGenParallelism);
}
} }
@Data @Data
public static class IrisSettingsPregen { public static class IrisSettingsPregen {
public boolean useCacheByDefault = true;
public boolean useHighPriority = false;
public boolean useVirtualThreads = false; public boolean useVirtualThreads = false;
public boolean useTicketQueue = false;
public int maxConcurrency = 256; public int maxConcurrency = 256;
} }

View File

@ -32,30 +32,32 @@ import io.papermc.lib.PaperLib;
import org.bukkit.Chunk; import org.bukkit.Chunk;
import org.bukkit.World; import org.bukkit.World;
import java.util.ArrayList; import java.lang.reflect.InvocationTargetException;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
public class AsyncPregenMethod implements PregeneratorMethod { public class AsyncPregenMethod implements PregeneratorMethod {
private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
private final World world; private final World world;
private final ExecutorService service; private final Executor executor;
private final Semaphore semaphore; private final Semaphore semaphore;
private final int threads; private final int threads;
private final boolean urgent;
private final Map<Chunk, Long> lastUse; private final Map<Chunk, Long> lastUse;
public AsyncPregenMethod(World world, int threads) { public AsyncPregenMethod(World world, int unusedThreads) {
if (!PaperLib.isPaper()) { if (!PaperLib.isPaper()) {
throw new UnsupportedOperationException("Cannot use PaperAsync on non paper!"); throw new UnsupportedOperationException("Cannot use PaperAsync on non paper!");
} }
this.world = world; this.world = world;
service = IrisSettings.get().getPregen().isUseVirtualThreads() ? this.executor = IrisSettings.get().getPregen().isUseTicketQueue() ? new TicketExecutor() : new ServiceExecutor();
Executors.newVirtualThreadPerTaskExecutor() :
new MultiBurst("Iris Async Pregen", Thread.MIN_PRIORITY);
this.threads = IrisSettings.get().getPregen().getMaxConcurrency(); this.threads = IrisSettings.get().getPregen().getMaxConcurrency();
semaphore = new Semaphore(threads); this.semaphore = new Semaphore(this.threads, true);
this.urgent = IrisSettings.get().getPregen().useHighPriority;
this.lastUse = new KMap<>(); this.lastUse = new KMap<>();
} }
@ -67,13 +69,18 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return; return;
} }
for (Chunk i : new ArrayList<>(lastUse.keySet())) { long minTime = M.ms() - 10_000;
Long lastUseTime = lastUse.get(i); lastUse.entrySet().removeIf(i -> {
if (!i.isLoaded() || (lastUseTime != null && M.ms() - lastUseTime >= 10000)) { final Chunk chunk = i.getKey();
i.unload(); final Long lastUseTime = i.getValue();
lastUse.remove(i); if (!chunk.isLoaded() || lastUseTime == null)
return true;
if (lastUseTime < minTime) {
chunk.unload();
return true;
} }
} return false;
});
world.save(); world.save();
}).get(); }).get();
} catch (Throwable e) { } catch (Throwable e) {
@ -81,24 +88,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
} }
} }
private void completeChunk(int x, int z, PregenListener listener) {
try {
PaperLib.getChunkAtAsync(world, x, z, true).thenAccept((i) -> {
lastUse.put(i, M.ms());
listener.onChunkGenerated(x, z);
listener.onChunkCleaned(x, z);
}).get();
} catch (InterruptedException ignored) {
} catch (Throwable e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
@Override @Override
public void init() { public void init() {
unloadAndSaveAllChunks(); unloadAndSaveAllChunks();
increaseWorkerThreads();
} }
@Override @Override
@ -110,7 +103,8 @@ public class AsyncPregenMethod implements PregeneratorMethod {
public void close() { public void close() {
semaphore.acquireUninterruptibly(threads); semaphore.acquireUninterruptibly(threads);
unloadAndSaveAllChunks(); unloadAndSaveAllChunks();
service.shutdown(); executor.shutdown();
resetWorkerThreads();
} }
@Override @Override
@ -136,7 +130,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
} catch (InterruptedException e) { } catch (InterruptedException e) {
return; return;
} }
service.submit(() -> completeChunk(x, z, listener)); executor.generate(x, z, listener);
} }
@Override @Override
@ -147,4 +141,94 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return null; return null;
} }
public static void increaseWorkerThreads() {
THREAD_COUNT.updateAndGet(i -> {
if (i > 0) return 1;
var adjusted = IrisSettings.get().getConcurrency().getWorldGenThreads();
try {
var field = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon").getDeclaredField("WORKER_POOL");
var pool = field.get(null);
var threads = ((Thread[]) pool.getClass().getDeclaredMethod("getCoreThreads").invoke(pool)).length;
if (threads >= adjusted) return 0;
pool.getClass().getDeclaredMethod("adjustThreadCount", int.class).invoke(pool, adjusted);
return threads;
} catch (Throwable e) {
Iris.warn("Failed to increase worker threads, if you are on paper or a fork of it please increase it manually to " + adjusted);
Iris.warn("For more information see https://docs.papermc.io/paper/reference/global-configuration#chunk_system_worker_threads");
if (e instanceof InvocationTargetException) e.printStackTrace();
}
return 0;
});
}
public static void resetWorkerThreads() {
THREAD_COUNT.updateAndGet(i -> {
if (i == 0) return 0;
try {
var field = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon").getDeclaredField("WORKER_POOL");
var pool = field.get(null);
var method = pool.getClass().getDeclaredMethod("adjustThreadCount", int.class);
method.invoke(pool, i);
return 0;
} catch (Throwable e) {
Iris.error("Failed to reset worker threads");
e.printStackTrace();
}
return i;
});
}
private interface Executor {
void generate(int x, int z, PregenListener listener);
default void shutdown() {}
}
private class ServiceExecutor implements Executor {
private final ExecutorService service = IrisSettings.get().getPregen().isUseVirtualThreads() ?
Executors.newVirtualThreadPerTaskExecutor() :
new MultiBurst("Iris Async Pregen", Thread.MIN_PRIORITY);
public void generate(int x, int z, PregenListener listener) {
service.submit(() -> {
try {
PaperLib.getChunkAtAsync(world, x, z, true, urgent).thenAccept((i) -> {
listener.onChunkGenerated(x, z);
listener.onChunkCleaned(x, z);
if (i == null) return;
lastUse.put(i, M.ms());
}).get();
} catch (InterruptedException ignored) {
} catch (Throwable e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
@Override
public void shutdown() {
service.shutdown();
}
}
private class TicketExecutor implements Executor {
@Override
public void generate(int x, int z, PregenListener listener) {
PaperLib.getChunkAtAsync(world, x, z, true, urgent)
.exceptionally(e -> {
e.printStackTrace();
return null;
})
.thenAccept(i -> {
semaphore.release();
listener.onChunkGenerated(x, z);
listener.onChunkCleaned(x, z);
if (i == null) return;
lastUse.put(i, M.ms());
});
}
}
} }

View File

@ -9,6 +9,7 @@ import com.volmit.iris.util.collection.KList;
import com.volmit.iris.util.collection.KMap; import com.volmit.iris.util.collection.KMap;
import com.volmit.iris.util.exceptions.IrisException; import com.volmit.iris.util.exceptions.IrisException;
import com.volmit.iris.util.format.Form; import com.volmit.iris.util.format.Form;
import com.volmit.iris.util.io.IO;
import com.volmit.iris.util.scheduling.J; import com.volmit.iris.util.scheduling.J;
import com.volmit.iris.util.scheduling.PrecisionStopwatch; import com.volmit.iris.util.scheduling.PrecisionStopwatch;
import lombok.Getter; import lombok.Getter;
@ -17,11 +18,6 @@ import org.bukkit.Bukkit;
import java.io.File; import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Clock; import java.time.Clock;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collections; import java.util.Collections;
@ -50,10 +46,7 @@ public class IrisPackBenchmarking {
.start(() -> { .start(() -> {
Iris.info("Setting up benchmark environment "); Iris.info("Setting up benchmark environment ");
benchmarkInProgress = true; benchmarkInProgress = true;
File file = new File("benchmark"); IO.delete(new File(Bukkit.getWorldContainer(), "benchmark"));
if (file.exists()) {
deleteDirectory(file.toPath());
}
createBenchmark(); createBenchmark();
while (!IrisToolbelt.isIrisWorld(Bukkit.getWorld("benchmark"))) { while (!IrisToolbelt.isIrisWorld(Bukkit.getWorld("benchmark"))) {
J.sleep(1000); J.sleep(1000);
@ -72,7 +65,7 @@ public class IrisPackBenchmarking {
public void finishedBenchmark(KList<Integer> cps) { public void finishedBenchmark(KList<Integer> cps) {
try { try {
String time = Form.duration(stopwatch.getMillis()); String time = Form.duration((long) stopwatch.getMilliseconds());
Engine engine = IrisToolbelt.access(Bukkit.getWorld("benchmark")).getEngine(); Engine engine = IrisToolbelt.access(Bukkit.getWorld("benchmark")).getEngine();
Iris.info("-----------------"); Iris.info("-----------------");
Iris.info("Results:"); Iris.info("Results:");
@ -83,11 +76,7 @@ public class IrisPackBenchmarking {
Iris.info(" - Lowest CPS: " + findLowest(cps)); Iris.info(" - Lowest CPS: " + findLowest(cps));
Iris.info("-----------------"); Iris.info("-----------------");
Iris.info("Creating a report.."); Iris.info("Creating a report..");
File profilers = new File("plugins" + File.separator + "Iris" + File.separator + "packbenchmarks"); File results = Iris.instance.getDataFile("packbenchmarks", dimension.getName() + " " + LocalDateTime.now(Clock.systemDefaultZone()).toString().replace(':', '-') + ".txt");
profilers.mkdir();
File results = new File(profilers, dimension.getName() + " " + LocalDateTime.now(Clock.systemDefaultZone()).toString().replace(':', '-') + ".txt");
results.getParentFile().mkdirs();
KMap<String, Double> metrics = engine.getMetrics().pull(); KMap<String, Double> metrics = engine.getMetrics().pull();
try (FileWriter writer = new FileWriter(results)) { try (FileWriter writer = new FileWriter(results)) {
writer.write("-----------------\n"); writer.write("-----------------\n");
@ -178,26 +167,4 @@ public class IrisPackBenchmarking {
private int findHighest(KList<Integer> list) { private int findHighest(KList<Integer> list) {
return Collections.max(list); return Collections.max(list);
} }
private boolean deleteDirectory(Path dir) {
try {
Files.walkFileTree(dir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
} }

View File

@ -142,7 +142,7 @@ public class IrisToolbelt {
* @return the pregenerator job (already started) * @return the pregenerator job (already started)
*/ */
public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine) { public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine) {
return pregenerate(task, method, engine, true); return pregenerate(task, method, engine, IrisSettings.get().getPregen().useCacheByDefault);
} }
/** /**